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
|
rev: 21.5b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3.7
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|
|
@ -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
|
# 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,18 +772,7 @@ class PGCli:
|
||||||
click.secho(str(e), err=True, fg="red")
|
click.secho(str(e), err=True, fg="red")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Initialize default metaquery in case execution fails
|
self.handle_watch_command(text)
|
||||||
self.watch_command, timing = special.get_watch_command(text)
|
|
||||||
if self.watch_command:
|
|
||||||
while self.watch_command:
|
|
||||||
try:
|
|
||||||
query = self.execute_command(self.watch_command)
|
|
||||||
click.echo(f"Waiting for {timing} seconds before repeating")
|
|
||||||
sleep(timing)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self.watch_command = None
|
|
||||||
else:
|
|
||||||
query = self.execute_command(text)
|
|
||||||
|
|
||||||
self.now = dt.datetime.today()
|
self.now = dt.datetime.today()
|
||||||
|
|
||||||
|
@ -779,12 +780,40 @@ class PGCli:
|
||||||
with self._completer_lock:
|
with self._completer_lock:
|
||||||
self.completer.extend_query_history(text)
|
self.completer.extend_query_history(text)
|
||||||
|
|
||||||
self.query_history.append(query)
|
|
||||||
|
|
||||||
except (PgCliQuitError, EOFError):
|
except (PgCliQuitError, EOFError):
|
||||||
if not self.less_chatty:
|
if not self.less_chatty:
|
||||||
print("Goodbye!")
|
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:
|
||||||
|
query = self.execute_command(self.watch_command)
|
||||||
|
click.echo(f"Waiting for {timing} seconds before repeating")
|
||||||
|
sleep(timing)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.watch_command = None
|
||||||
|
|
||||||
|
# Otherwise, execute it as a regular command.
|
||||||
|
else:
|
||||||
|
query = self.execute_command(text)
|
||||||
|
|
||||||
|
self.query_history.append(query)
|
||||||
|
|
||||||
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,)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue