1
0
Fork 0

Merging upstream version 3.3.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 19:59:04 +01:00
parent 80a5571dc6
commit 0a3024bc6c
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
11 changed files with 193 additions and 52 deletions

View file

@ -3,5 +3,4 @@ repos:
rev: 21.5b0 rev: 21.5b0
hooks: hooks:
- id: black - id: black
language_version: python3.7

View file

@ -1,12 +1,29 @@
TBD TBD
===== ================
Features: Features:
--------- ---------
Bug fixes: 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 3.2.0
===== =====

View file

@ -1 +1 @@
__version__ = "3.2.0" __version__ = "3.3.0"

View file

@ -86,6 +86,7 @@ from textwrap import dedent
# Ref: https://stackoverflow.com/questions/30425105/filter-special-chars-such-as-color-codes-from-shell-output # 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\\))") COLOR_CODE_REGEX = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
DEFAULT_MAX_FIELD_WIDTH = 500
# Query tuples are used for maintaining history # Query tuples are used for maintaining history
MetaQuery = namedtuple( MetaQuery = namedtuple(
@ -106,7 +107,7 @@ MetaQuery.__new__.__defaults__ = ("", False, 0, 0, False, False, False, False)
OutputSettings = namedtuple( OutputSettings = namedtuple(
"OutputSettings", "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__ = ( OutputSettings.__new__.__defaults__ = (
None, None,
@ -117,6 +118,7 @@ OutputSettings.__new__.__defaults__ = (
None, None,
lambda x: x, lambda x: x,
None, None,
DEFAULT_MAX_FIELD_WIDTH,
) )
@ -201,6 +203,16 @@ class PGCli:
else: else:
self.row_limit = c["main"].as_int("row_limit") 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.min_num_menu_lines = c["main"].as_int("min_num_menu_lines")
self.multiline_continuation_char = c["main"]["multiline_continuation_char"] self.multiline_continuation_char = c["main"]["multiline_continuation_char"]
self.table_format = c["main"]["table_format"] self.table_format = c["main"]["table_format"]
@ -760,8 +772,33 @@ class PGCli:
click.secho(str(e), err=True, fg="red") click.secho(str(e), err=True, fg="red")
continue 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 # Initialize default metaquery in case execution fails
self.watch_command, timing = special.get_watch_command(text) 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: if self.watch_command:
while self.watch_command: while self.watch_command:
try: try:
@ -770,21 +807,13 @@ class PGCli:
sleep(timing) sleep(timing)
except KeyboardInterrupt: except KeyboardInterrupt:
self.watch_command = None self.watch_command = None
# Otherwise, execute it as a regular command.
else: else:
query = self.execute_command(text) 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) self.query_history.append(query)
except (PgCliQuitError, EOFError):
if not self.less_chatty:
print("Goodbye!")
def _build_cli(self, history): def _build_cli(self, history):
key_bindings = pgcli_bindings(self) key_bindings = pgcli_bindings(self)
@ -934,6 +963,7 @@ class PGCli:
else lambda x: x else lambda x: x
), ),
style_output=self.style_output, style_output=self.style_output,
max_field_width=self.max_field_width,
) )
execution = time() - start execution = time() - start
formatted = format_output(title, cur, headers, status, settings) formatted = format_output(title, cur, headers, status, settings)
@ -1444,6 +1474,7 @@ def format_output(title, cur, headers, status, settings):
"disable_numparse": True, "disable_numparse": True,
"preserve_whitespace": True, "preserve_whitespace": True,
"style": settings.style_output, "style": settings.style_output,
"max_field_width": settings.max_field_width,
} }
if not settings.floatfmt: if not settings.floatfmt:
output_kwargs["preprocessors"] = (align_decimals,) output_kwargs["preprocessors"] = (align_decimals,)

View file

@ -119,6 +119,12 @@ on_error = STOP
# Set threshold for row limit. Use 0 to disable limiting. # Set threshold for row limit. Use 0 to disable limiting.
row_limit = 1000 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 # Skip intro on startup and goodbye on exit
less_chatty = False less_chatty = False

View file

@ -430,7 +430,7 @@ class PGExecute:
for sql in sqlparse.split(statement): for sql in sqlparse.split(statement):
# Remove spaces, eol and semi-colons. # Remove spaces, eol and semi-colons.
sql = sql.rstrip(";") sql = sql.rstrip(";")
sql = sqlparse.format(sql, strip_comments=True).strip() sql = sqlparse.format(sql, strip_comments=False).strip()
if not sql: if not sql:
continue continue
try: try:

View file

@ -8,7 +8,7 @@ description = "CLI for Postgres Database. With auto-completion and syntax highli
install_requirements = [ install_requirements = [
"pgspecial>=1.11.8", "pgspecial>=1.11.8",
"click >= 4.1", "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 # We still need to use pt-2 unless pt-3 released on Fedora32
# see: https://github.com/dbcli/pgcli/pull/1197 # see: https://github.com/dbcli/pgcli/pull/1197
"prompt_toolkit>=2.0.6,<4.0.0", "prompt_toolkit>=2.0.6,<4.0.0",

View file

@ -24,11 +24,11 @@ def step_see_small_results(context):
context, context,
dedent( dedent(
"""\ """\
+------------+\r +----------+\r
| ?column? |\r | ?column? |\r
|------------|\r |----------|\r
| 1 |\r | 1 |\r
+------------+\r +----------+\r
SELECT 1\r SELECT 1\r
""" """
), ),

View file

@ -118,11 +118,11 @@ def step_see_found(context):
+ "\r" + "\r"
+ dedent( + dedent(
""" """
+------------+\r +----------+\r
| ?column? |\r | ?column? |\r
|------------|\r |----------|\r
| found |\r | found |\r
+------------+\r +----------+\r
SELECT 1\r SELECT 1\r
""" """
) )

View file

@ -58,11 +58,11 @@ def step_see_data(context, which):
context, context,
dedent( dedent(
"""\ """\
+-----+-----+--------+\r +---+-----+--------+\r
| x | y | z |\r | x | y | z |\r
|-----+-----+--------|\r |---+-----+--------|\r
| 1 | 1.0 | 1.0000 |\r | 1 | 1.0 | 1.0000 |\r
+-----+-----+--------+\r +---+-----+--------+\r
SELECT 1\r SELECT 1\r
""" """
), ),

View file

@ -61,16 +61,47 @@ def test_format_output():
) )
expected = [ expected = [
"Title", "Title",
"+---------+---------+", "+-------+-------+",
"| head1 | head2 |", "| head1 | head2 |",
"|---------+---------|", "|-------+-------|",
"| abc | def |", "| abc | def |",
"+---------+---------+", "+-------+-------+",
"test status", "test status",
] ]
assert list(results) == expected 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 @dbtest
def test_format_array_output(executor): def test_format_array_output(executor):
statement = """ statement = """
@ -83,12 +114,12 @@ def test_format_array_output(executor):
""" """
results = run(executor, statement) results = run(executor, statement)
expected = [ expected = [
"+----------------+------------------------+--------------+", "+--------------+----------------------+--------------+",
"| bigint_array | nested_numeric_array | 配列 |", "| bigint_array | nested_numeric_array | 配列 |",
"|----------------+------------------------+--------------|", "|--------------+----------------------+--------------|",
"| {1,2,3} | {{1,2},{3,4}} | {å,魚,текст} |", "| {1,2,3} | {{1,2},{3,4}} | {å,魚,текст} |",
"| {} | <null> | {<null>} |", "| {} | <null> | {<null>} |",
"+----------------+------------------------+--------------+", "+--------------+----------------------+--------------+",
"SELECT 2", "SELECT 2",
] ]
assert list(results) == expected assert list(results) == expected
@ -128,11 +159,11 @@ def test_format_output_auto_expand():
) )
table = [ table = [
"Title", "Title",
"+---------+---------+", "+-------+-------+",
"| head1 | head2 |", "| head1 | head2 |",
"|---------+---------|", "|-------+-------|",
"| abc | def |", "| abc | def |",
"+---------+---------+", "+-------+-------+",
"test status", "test status",
] ]
assert list(table_results) == table assert list(table_results) == table
@ -266,6 +297,63 @@ def test_i_works(tmpdir, executor):
run(executor, statement, pgspecial=cli.pgspecial) 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): def test_missing_rc_dir(tmpdir):
rcfile = str(tmpdir.join("subdir").join("rcfile")) rcfile = str(tmpdir.join("subdir").join("rcfile"))