Merging upstream version 3.3.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
80a5571dc6
commit
0a3024bc6c
11 changed files with 193 additions and 52 deletions
|
@ -3,5 +3,4 @@ repos:
|
|||
rev: 21.5b0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.7
|
||||
|
||||
|
|
|
@ -1,12 +1,29 @@
|
|||
TBD
|
||||
=====
|
||||
================
|
||||
|
||||
Features:
|
||||
---------
|
||||
|
||||
|
||||
Bug fixes:
|
||||
----------
|
||||
|
||||
|
||||
|
||||
3.3.0 (2022/01/11)
|
||||
================
|
||||
|
||||
Features:
|
||||
---------
|
||||
|
||||
* Add `max_field_width` setting to config, to enable more control over field truncation ([related issue](https://github.com/dbcli/pgcli/issues/1250)).
|
||||
* Re-run last query via bare `\watch`. (Thanks: `Saif Hakim`_)
|
||||
|
||||
Bug fixes:
|
||||
----------
|
||||
|
||||
* Pin the version of pygments to prevent breaking change
|
||||
|
||||
3.2.0
|
||||
=====
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "3.2.0"
|
||||
__version__ = "3.3.0"
|
||||
|
|
|
@ -86,6 +86,7 @@ from textwrap import dedent
|
|||
|
||||
# Ref: https://stackoverflow.com/questions/30425105/filter-special-chars-such-as-color-codes-from-shell-output
|
||||
COLOR_CODE_REGEX = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
|
||||
DEFAULT_MAX_FIELD_WIDTH = 500
|
||||
|
||||
# Query tuples are used for maintaining history
|
||||
MetaQuery = namedtuple(
|
||||
|
@ -106,7 +107,7 @@ MetaQuery.__new__.__defaults__ = ("", False, 0, 0, False, False, False, False)
|
|||
|
||||
OutputSettings = namedtuple(
|
||||
"OutputSettings",
|
||||
"table_format dcmlfmt floatfmt missingval expanded max_width case_function style_output",
|
||||
"table_format dcmlfmt floatfmt missingval expanded max_width case_function style_output max_field_width",
|
||||
)
|
||||
OutputSettings.__new__.__defaults__ = (
|
||||
None,
|
||||
|
@ -117,6 +118,7 @@ OutputSettings.__new__.__defaults__ = (
|
|||
None,
|
||||
lambda x: x,
|
||||
None,
|
||||
DEFAULT_MAX_FIELD_WIDTH,
|
||||
)
|
||||
|
||||
|
||||
|
@ -201,6 +203,16 @@ class PGCli:
|
|||
else:
|
||||
self.row_limit = c["main"].as_int("row_limit")
|
||||
|
||||
# if not specified, set to DEFAULT_MAX_FIELD_WIDTH
|
||||
# if specified but empty, set to None to disable truncation
|
||||
# ellipsis will take at least 3 symbols, so this can't be less than 3 if specified and > 0
|
||||
max_field_width = c["main"].get("max_field_width", DEFAULT_MAX_FIELD_WIDTH)
|
||||
if max_field_width and max_field_width.lower() != "none":
|
||||
max_field_width = max(3, abs(int(max_field_width)))
|
||||
else:
|
||||
max_field_width = None
|
||||
self.max_field_width = max_field_width
|
||||
|
||||
self.min_num_menu_lines = c["main"].as_int("min_num_menu_lines")
|
||||
self.multiline_continuation_char = c["main"]["multiline_continuation_char"]
|
||||
self.table_format = c["main"]["table_format"]
|
||||
|
@ -760,8 +772,33 @@ class PGCli:
|
|||
click.secho(str(e), err=True, fg="red")
|
||||
continue
|
||||
|
||||
self.handle_watch_command(text)
|
||||
|
||||
self.now = dt.datetime.today()
|
||||
|
||||
# Allow PGCompleter to learn user's preferred keywords, etc.
|
||||
with self._completer_lock:
|
||||
self.completer.extend_query_history(text)
|
||||
|
||||
except (PgCliQuitError, EOFError):
|
||||
if not self.less_chatty:
|
||||
print("Goodbye!")
|
||||
|
||||
def handle_watch_command(self, text):
|
||||
# Initialize default metaquery in case execution fails
|
||||
self.watch_command, timing = special.get_watch_command(text)
|
||||
|
||||
# If we run \watch without a command, apply it to the last query run.
|
||||
if self.watch_command is not None and not self.watch_command.strip():
|
||||
try:
|
||||
self.watch_command = self.query_history[-1].query
|
||||
except IndexError:
|
||||
click.secho(
|
||||
"\\watch cannot be used with an empty query", err=True, fg="red"
|
||||
)
|
||||
self.watch_command = None
|
||||
|
||||
# If there's a command to \watch, run it in a loop.
|
||||
if self.watch_command:
|
||||
while self.watch_command:
|
||||
try:
|
||||
|
@ -770,21 +807,13 @@ class PGCli:
|
|||
sleep(timing)
|
||||
except KeyboardInterrupt:
|
||||
self.watch_command = None
|
||||
|
||||
# Otherwise, execute it as a regular command.
|
||||
else:
|
||||
query = self.execute_command(text)
|
||||
|
||||
self.now = dt.datetime.today()
|
||||
|
||||
# Allow PGCompleter to learn user's preferred keywords, etc.
|
||||
with self._completer_lock:
|
||||
self.completer.extend_query_history(text)
|
||||
|
||||
self.query_history.append(query)
|
||||
|
||||
except (PgCliQuitError, EOFError):
|
||||
if not self.less_chatty:
|
||||
print("Goodbye!")
|
||||
|
||||
def _build_cli(self, history):
|
||||
key_bindings = pgcli_bindings(self)
|
||||
|
||||
|
@ -934,6 +963,7 @@ class PGCli:
|
|||
else lambda x: x
|
||||
),
|
||||
style_output=self.style_output,
|
||||
max_field_width=self.max_field_width,
|
||||
)
|
||||
execution = time() - start
|
||||
formatted = format_output(title, cur, headers, status, settings)
|
||||
|
@ -1444,6 +1474,7 @@ def format_output(title, cur, headers, status, settings):
|
|||
"disable_numparse": True,
|
||||
"preserve_whitespace": True,
|
||||
"style": settings.style_output,
|
||||
"max_field_width": settings.max_field_width,
|
||||
}
|
||||
if not settings.floatfmt:
|
||||
output_kwargs["preprocessors"] = (align_decimals,)
|
||||
|
|
|
@ -119,6 +119,12 @@ on_error = STOP
|
|||
# Set threshold for row limit. Use 0 to disable limiting.
|
||||
row_limit = 1000
|
||||
|
||||
# Truncate long text fields to this value for tabular display (does not apply to csv).
|
||||
# Leave unset to disable truncation. Example: "max_field_width = "
|
||||
# Be aware that formatting might get slow with values larger than 500 and tables with
|
||||
# lots of records.
|
||||
max_field_width = 500
|
||||
|
||||
# Skip intro on startup and goodbye on exit
|
||||
less_chatty = False
|
||||
|
||||
|
|
|
@ -430,7 +430,7 @@ class PGExecute:
|
|||
for sql in sqlparse.split(statement):
|
||||
# Remove spaces, eol and semi-colons.
|
||||
sql = sql.rstrip(";")
|
||||
sql = sqlparse.format(sql, strip_comments=True).strip()
|
||||
sql = sqlparse.format(sql, strip_comments=False).strip()
|
||||
if not sql:
|
||||
continue
|
||||
try:
|
||||
|
|
2
setup.py
2
setup.py
|
@ -8,7 +8,7 @@ description = "CLI for Postgres Database. With auto-completion and syntax highli
|
|||
install_requirements = [
|
||||
"pgspecial>=1.11.8",
|
||||
"click >= 4.1",
|
||||
"Pygments >= 2.0", # Pygments has to be Capitalcased. WTF?
|
||||
"Pygments>=2.0,<=2.11.1", # Pygments has to be Capitalcased. WTF?
|
||||
# We still need to use pt-2 unless pt-3 released on Fedora32
|
||||
# see: https://github.com/dbcli/pgcli/pull/1197
|
||||
"prompt_toolkit>=2.0.6,<4.0.0",
|
||||
|
|
|
@ -24,11 +24,11 @@ def step_see_small_results(context):
|
|||
context,
|
||||
dedent(
|
||||
"""\
|
||||
+------------+\r
|
||||
+----------+\r
|
||||
| ?column? |\r
|
||||
|------------|\r
|
||||
|----------|\r
|
||||
| 1 |\r
|
||||
+------------+\r
|
||||
+----------+\r
|
||||
SELECT 1\r
|
||||
"""
|
||||
),
|
||||
|
|
|
@ -118,11 +118,11 @@ def step_see_found(context):
|
|||
+ "\r"
|
||||
+ dedent(
|
||||
"""
|
||||
+------------+\r
|
||||
+----------+\r
|
||||
| ?column? |\r
|
||||
|------------|\r
|
||||
|----------|\r
|
||||
| found |\r
|
||||
+------------+\r
|
||||
+----------+\r
|
||||
SELECT 1\r
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -58,11 +58,11 @@ def step_see_data(context, which):
|
|||
context,
|
||||
dedent(
|
||||
"""\
|
||||
+-----+-----+--------+\r
|
||||
+---+-----+--------+\r
|
||||
| x | y | z |\r
|
||||
|-----+-----+--------|\r
|
||||
|---+-----+--------|\r
|
||||
| 1 | 1.0 | 1.0000 |\r
|
||||
+-----+-----+--------+\r
|
||||
+---+-----+--------+\r
|
||||
SELECT 1\r
|
||||
"""
|
||||
),
|
||||
|
|
|
@ -61,16 +61,47 @@ def test_format_output():
|
|||
)
|
||||
expected = [
|
||||
"Title",
|
||||
"+---------+---------+",
|
||||
"+-------+-------+",
|
||||
"| head1 | head2 |",
|
||||
"|---------+---------|",
|
||||
"|-------+-------|",
|
||||
"| abc | def |",
|
||||
"+---------+---------+",
|
||||
"+-------+-------+",
|
||||
"test status",
|
||||
]
|
||||
assert list(results) == expected
|
||||
|
||||
|
||||
def test_format_output_truncate_on():
|
||||
settings = OutputSettings(
|
||||
table_format="psql", dcmlfmt="d", floatfmt="g", max_field_width=10
|
||||
)
|
||||
results = format_output(
|
||||
None,
|
||||
[("first field value", "second field value")],
|
||||
["head1", "head2"],
|
||||
None,
|
||||
settings,
|
||||
)
|
||||
expected = [
|
||||
"+------------+------------+",
|
||||
"| head1 | head2 |",
|
||||
"|------------+------------|",
|
||||
"| first f... | second ... |",
|
||||
"+------------+------------+",
|
||||
]
|
||||
assert list(results) == expected
|
||||
|
||||
|
||||
def test_format_output_truncate_off():
|
||||
settings = OutputSettings(
|
||||
table_format="psql", dcmlfmt="d", floatfmt="g", max_field_width=None
|
||||
)
|
||||
long_field_value = ("first field " * 100).strip()
|
||||
results = format_output(None, [(long_field_value,)], ["head1"], None, settings)
|
||||
lines = list(results)
|
||||
assert lines[3] == f"| {long_field_value} |"
|
||||
|
||||
|
||||
@dbtest
|
||||
def test_format_array_output(executor):
|
||||
statement = """
|
||||
|
@ -83,12 +114,12 @@ def test_format_array_output(executor):
|
|||
"""
|
||||
results = run(executor, statement)
|
||||
expected = [
|
||||
"+----------------+------------------------+--------------+",
|
||||
"+--------------+----------------------+--------------+",
|
||||
"| bigint_array | nested_numeric_array | 配列 |",
|
||||
"|----------------+------------------------+--------------|",
|
||||
"|--------------+----------------------+--------------|",
|
||||
"| {1,2,3} | {{1,2},{3,4}} | {å,魚,текст} |",
|
||||
"| {} | <null> | {<null>} |",
|
||||
"+----------------+------------------------+--------------+",
|
||||
"+--------------+----------------------+--------------+",
|
||||
"SELECT 2",
|
||||
]
|
||||
assert list(results) == expected
|
||||
|
@ -128,11 +159,11 @@ def test_format_output_auto_expand():
|
|||
)
|
||||
table = [
|
||||
"Title",
|
||||
"+---------+---------+",
|
||||
"+-------+-------+",
|
||||
"| head1 | head2 |",
|
||||
"|---------+---------|",
|
||||
"|-------+-------|",
|
||||
"| abc | def |",
|
||||
"+---------+---------+",
|
||||
"+-------+-------+",
|
||||
"test status",
|
||||
]
|
||||
assert list(table_results) == table
|
||||
|
@ -266,6 +297,63 @@ def test_i_works(tmpdir, executor):
|
|||
run(executor, statement, pgspecial=cli.pgspecial)
|
||||
|
||||
|
||||
@dbtest
|
||||
def test_watch_works(executor):
|
||||
cli = PGCli(pgexecute=executor)
|
||||
|
||||
def run_with_watch(
|
||||
query, target_call_count=1, expected_output="", expected_timing=None
|
||||
):
|
||||
"""
|
||||
:param query: Input to the CLI
|
||||
:param target_call_count: Number of times the user lets the command run before Ctrl-C
|
||||
:param expected_output: Substring expected to be found for each executed query
|
||||
:param expected_timing: value `time.sleep` expected to be called with on every invocation
|
||||
"""
|
||||
with mock.patch.object(cli, "echo_via_pager") as mock_echo, mock.patch(
|
||||
"pgcli.main.sleep"
|
||||
) as mock_sleep:
|
||||
mock_sleep.side_effect = [None] * (target_call_count - 1) + [
|
||||
KeyboardInterrupt
|
||||
]
|
||||
cli.handle_watch_command(query)
|
||||
# Validate that sleep was called with the right timing
|
||||
for i in range(target_call_count - 1):
|
||||
assert mock_sleep.call_args_list[i][0][0] == expected_timing
|
||||
# Validate that the output of the query was expected
|
||||
assert mock_echo.call_count == target_call_count
|
||||
for i in range(target_call_count):
|
||||
assert expected_output in mock_echo.call_args_list[i][0][0]
|
||||
|
||||
# With no history, it errors.
|
||||
with mock.patch("pgcli.main.click.secho") as mock_secho:
|
||||
cli.handle_watch_command(r"\watch 2")
|
||||
mock_secho.assert_called()
|
||||
assert (
|
||||
r"\watch cannot be used with an empty query"
|
||||
in mock_secho.call_args_list[0][0][0]
|
||||
)
|
||||
|
||||
# Usage 1: Run a query and then re-run it with \watch across two prompts.
|
||||
run_with_watch("SELECT 111", expected_output="111")
|
||||
run_with_watch(
|
||||
"\\watch 10", target_call_count=2, expected_output="111", expected_timing=10
|
||||
)
|
||||
|
||||
# Usage 2: Run a query and \watch via the same prompt.
|
||||
run_with_watch(
|
||||
"SELECT 222; \\watch 4",
|
||||
target_call_count=3,
|
||||
expected_output="222",
|
||||
expected_timing=4,
|
||||
)
|
||||
|
||||
# Usage 3: Re-run the last watched command with a new timing
|
||||
run_with_watch(
|
||||
"\\watch 5", target_call_count=4, expected_output="222", expected_timing=5
|
||||
)
|
||||
|
||||
|
||||
def test_missing_rc_dir(tmpdir):
|
||||
rcfile = str(tmpdir.join("subdir").join("rcfile"))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue