summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.pre-commit-config.yaml1
-rw-r--r--changelog.rst21
-rw-r--r--pgcli/__init__.py2
-rw-r--r--pgcli/main.py61
-rw-r--r--pgcli/pgclirc6
-rw-r--r--pgcli/pgexecute.py2
-rw-r--r--setup.py2
-rw-r--r--tests/features/steps/auto_vertical.py10
-rw-r--r--tests/features/steps/basic_commands.py10
-rw-r--r--tests/features/steps/expanded.py10
-rw-r--r--tests/test_main.py120
11 files changed, 193 insertions, 52 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9e27ab8..ca4e36b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,5 +3,4 @@ repos:
rev: 21.5b0
hooks:
- id: black
- language_version: python3.7
diff --git a/changelog.rst b/changelog.rst
index d732088..7ce5132 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -1,13 +1,30 @@
TBD
-=====
+================
Features:
---------
+
Bug fixes:
----------
-3.2.0
+
+
+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
=====
Release date: 2021/08/23
diff --git a/pgcli/__init__.py b/pgcli/__init__.py
index 1173108..88c513e 100644
--- a/pgcli/__init__.py
+++ b/pgcli/__init__.py
@@ -1 +1 @@
-__version__ = "3.2.0"
+__version__ = "3.3.0"
diff --git a/pgcli/main.py b/pgcli/main.py
index 5135f6f..5395f67 100644
--- a/pgcli/main.py
+++ b/pgcli/main.py
@@ -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,18 +772,7 @@ class PGCli:
click.secho(str(e), err=True, fg="red")
continue
- # Initialize default metaquery in case execution fails
- 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.handle_watch_command(text)
self.now = dt.datetime.today()
@@ -779,12 +780,40 @@ class PGCli:
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 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):
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,)
diff --git a/pgcli/pgclirc b/pgcli/pgclirc
index 15c10f5..6654ce9 100644
--- a/pgcli/pgclirc
+++ b/pgcli/pgclirc
@@ -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
diff --git a/pgcli/pgexecute.py b/pgcli/pgexecute.py
index a013b55..22e9ea4 100644
--- a/pgcli/pgexecute.py
+++ b/pgcli/pgexecute.py
@@ -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:
diff --git a/setup.py b/setup.py
index fc21032..68260e0 100644
--- a/setup.py
+++ b/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",
diff --git a/tests/features/steps/auto_vertical.py b/tests/features/steps/auto_vertical.py
index 1643ea5..d7cdccd 100644
--- a/tests/features/steps/auto_vertical.py
+++ b/tests/features/steps/auto_vertical.py
@@ -24,11 +24,11 @@ def step_see_small_results(context):
context,
dedent(
"""\
- +------------+\r
- | ?column? |\r
- |------------|\r
- | 1 |\r
- +------------+\r
+ +----------+\r
+ | ?column? |\r
+ |----------|\r
+ | 1 |\r
+ +----------+\r
SELECT 1\r
"""
),
diff --git a/tests/features/steps/basic_commands.py b/tests/features/steps/basic_commands.py
index 07e9ec1..7ca20f0 100644
--- a/tests/features/steps/basic_commands.py
+++ b/tests/features/steps/basic_commands.py
@@ -118,11 +118,11 @@ def step_see_found(context):
+ "\r"
+ dedent(
"""
- +------------+\r
- | ?column? |\r
- |------------|\r
- | found |\r
- +------------+\r
+ +----------+\r
+ | ?column? |\r
+ |----------|\r
+ | found |\r
+ +----------+\r
SELECT 1\r
"""
)
diff --git a/tests/features/steps/expanded.py b/tests/features/steps/expanded.py
index 265ea39..ac84c41 100644
--- a/tests/features/steps/expanded.py
+++ b/tests/features/steps/expanded.py
@@ -58,11 +58,11 @@ def step_see_data(context, which):
context,
dedent(
"""\
- +-----+-----+--------+\r
- | x | y | z |\r
- |-----+-----+--------|\r
- | 1 | 1.0 | 1.0000 |\r
- +-----+-----+--------+\r
+ +---+-----+--------+\r
+ | x | y | z |\r
+ |---+-----+--------|\r
+ | 1 | 1.0 | 1.0000 |\r
+ +---+-----+--------+\r
SELECT 1\r
"""
),
diff --git a/tests/test_main.py b/tests/test_main.py
index c48accb..9b3a84b 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -61,16 +61,47 @@ def test_format_output():
)
expected = [
"Title",
- "+---------+---------+",
- "| head1 | head2 |",
- "|---------+---------|",
- "| abc | def |",
- "+---------+---------+",
+ "+-------+-------+",
+ "| 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>} |",
- "+----------------+------------------------+--------------+",
+ "+--------------+----------------------+--------------+",
+ "| 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 |",
- "+---------+---------+",
+ "+-------+-------+",
+ "| 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"))