Skip to content

Commit 8cc22b9

Browse files
authored
Add verbose_errors config and special command (#1455)
* Add verbose_errors config * Update changelog * Add special command * Blackify
1 parent a7a70fd commit 8cc22b9

File tree

5 files changed

+144
-6
lines changed

5 files changed

+144
-6
lines changed

changelog.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ Upcoming
44
Features:
55
---------
66
* Support `PGAPPNAME` as an environment variable and `--application-name` as a command line argument.
7-
* Show Postgres notifications
7+
* Add `verbose_errors` config and `\v` special command which enable the
8+
displaying of all Postgres error fields received.
9+
* Show Postgres notifications.
810

911
Bug fixes:
1012
----------

pgcli/main.py

+90-3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575

7676
from psycopg import OperationalError, InterfaceError, Notify
7777
from psycopg.conninfo import make_conninfo, conninfo_to_dict
78+
from psycopg.errors import Diagnostic
7879

7980
from collections import namedtuple
8081

@@ -248,6 +249,9 @@ def __init__(
248249
)
249250

250251
self.less_chatty = bool(less_chatty) or c["main"].as_bool("less_chatty")
252+
self.verbose_errors = "verbose_errors" in c["main"] and c["main"].as_bool(
253+
"verbose_errors"
254+
)
251255
self.null_string = c["main"].get("null_string", "<null>")
252256
self.prompt_format = (
253257
prompt
@@ -389,6 +393,26 @@ def register_special_commands(self):
389393
"Echo a string to the query output channel.",
390394
)
391395

396+
self.pgspecial.register(
397+
self.toggle_verbose_errors,
398+
"\\v",
399+
"\\v [on|off]",
400+
"Toggle verbose errors.",
401+
)
402+
403+
def toggle_verbose_errors(self, pattern, **_):
404+
flag = pattern.strip()
405+
406+
if flag == "on":
407+
self.verbose_errors = True
408+
elif flag == "off":
409+
self.verbose_errors = False
410+
else:
411+
self.verbose_errors = not self.verbose_errors
412+
413+
message = "Verbose errors " + "on." if self.verbose_errors else "off."
414+
return [(None, None, None, message)]
415+
392416
def echo(self, pattern, **_):
393417
return [(None, None, None, pattern)]
394418

@@ -1080,7 +1104,7 @@ def _evaluate_command(self, text):
10801104
res = self.pgexecute.run(
10811105
text,
10821106
self.pgspecial,
1083-
exception_formatter,
1107+
lambda x: exception_formatter(x, self.verbose_errors),
10841108
on_error_resume,
10851109
explain_mode=self.explain_mode,
10861110
)
@@ -1618,8 +1642,71 @@ def is_select(status):
16181642
return status.split(None, 1)[0].lower() == "select"
16191643

16201644

1621-
def exception_formatter(e):
1622-
return click.style(str(e), fg="red")
1645+
def diagnostic_output(diagnostic: Diagnostic) -> str:
1646+
fields = []
1647+
1648+
if diagnostic.severity is not None:
1649+
fields.append("Severity: " + diagnostic.severity)
1650+
1651+
if diagnostic.severity_nonlocalized is not None:
1652+
fields.append("Severity (non-localized): " + diagnostic.severity_nonlocalized)
1653+
1654+
if diagnostic.sqlstate is not None:
1655+
fields.append("SQLSTATE code: " + diagnostic.sqlstate)
1656+
1657+
if diagnostic.message_primary is not None:
1658+
fields.append("Message: " + diagnostic.message_primary)
1659+
1660+
if diagnostic.message_detail is not None:
1661+
fields.append("Detail: " + diagnostic.message_detail)
1662+
1663+
if diagnostic.message_hint is not None:
1664+
fields.append("Hint: " + diagnostic.message_hint)
1665+
1666+
if diagnostic.statement_position is not None:
1667+
fields.append("Position: " + diagnostic.statement_position)
1668+
1669+
if diagnostic.internal_position is not None:
1670+
fields.append("Internal position: " + diagnostic.internal_position)
1671+
1672+
if diagnostic.internal_query is not None:
1673+
fields.append("Internal query: " + diagnostic.internal_query)
1674+
1675+
if diagnostic.context is not None:
1676+
fields.append("Where: " + diagnostic.context)
1677+
1678+
if diagnostic.schema_name is not None:
1679+
fields.append("Schema name: " + diagnostic.schema_name)
1680+
1681+
if diagnostic.table_name is not None:
1682+
fields.append("Table name: " + diagnostic.table_name)
1683+
1684+
if diagnostic.column_name is not None:
1685+
fields.append("Column name: " + diagnostic.column_name)
1686+
1687+
if diagnostic.datatype_name is not None:
1688+
fields.append("Data type name: " + diagnostic.datatype_name)
1689+
1690+
if diagnostic.constraint_name is not None:
1691+
fields.append("Constraint name: " + diagnostic.constraint_name)
1692+
1693+
if diagnostic.source_file is not None:
1694+
fields.append("File: " + diagnostic.source_file)
1695+
1696+
if diagnostic.source_line is not None:
1697+
fields.append("Line: " + diagnostic.source_line)
1698+
1699+
if diagnostic.source_function is not None:
1700+
fields.append("Routine: " + diagnostic.source_function)
1701+
1702+
return "\n".join(fields)
1703+
1704+
1705+
def exception_formatter(e, verbose_errors: bool = False):
1706+
s = str(e)
1707+
if verbose_errors:
1708+
s += "\n" + diagnostic_output(e.diag)
1709+
return click.style(s, fg="red")
16231710

16241711

16251712
def format_output(title, cur, headers, status, settings, explain_mode=False):

pgcli/pgclirc

+5
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ max_field_width = 500
156156
# Skip intro on startup and goodbye on exit
157157
less_chatty = False
158158

159+
# Show all Postgres error fields (as listed in
160+
# https://www.postgresql.org/docs/current/protocol-error-fields.html).
161+
# Can be toggled with \v.
162+
verbose_errors = False
163+
159164
# Postgres prompt
160165
# \t - Current date and time
161166
# \u - Username

tests/test_main.py

+18
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,24 @@ def test_i_works(tmpdir, executor):
299299
run(executor, statement, pgspecial=cli.pgspecial)
300300

301301

302+
@dbtest
303+
def test_toggle_verbose_errors(executor):
304+
cli = PGCli(pgexecute=executor)
305+
306+
cli._evaluate_command("\\v on")
307+
assert cli.verbose_errors
308+
output, _ = cli._evaluate_command("SELECT 1/0")
309+
assert "SQLSTATE" in output[0]
310+
311+
cli._evaluate_command("\\v off")
312+
assert not cli.verbose_errors
313+
output, _ = cli._evaluate_command("SELECT 1/0")
314+
assert "SQLSTATE" not in output[0]
315+
316+
cli._evaluate_command("\\v")
317+
assert cli.verbose_errors
318+
319+
302320
@dbtest
303321
def test_echo_works(executor):
304322
cli = PGCli(pgexecute=executor)

tests/test_pgexecute.py

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from textwrap import dedent
23

34
import psycopg
@@ -6,7 +7,7 @@
67
from pgspecial.main import PGSpecial, NO_QUERY
78
from utils import run, dbtest, requires_json, requires_jsonb
89

9-
from pgcli.main import PGCli
10+
from pgcli.main import PGCli, exception_formatter as main_exception_formatter
1011
from pgcli.packages.parseutils.meta import FunctionMetadata
1112

1213

@@ -219,8 +220,33 @@ def test_database_list(executor):
219220

220221
@dbtest
221222
def test_invalid_syntax(executor, exception_formatter):
222-
result = run(executor, "invalid syntax!", exception_formatter=exception_formatter)
223+
result = run(
224+
executor,
225+
"invalid syntax!",
226+
exception_formatter=lambda x: main_exception_formatter(x, verbose_errors=False),
227+
)
223228
assert 'syntax error at or near "invalid"' in result[0]
229+
assert "SQLSTATE" not in result[0]
230+
231+
232+
@dbtest
233+
def test_invalid_syntax_verbose(executor):
234+
result = run(
235+
executor,
236+
"invalid syntax!",
237+
exception_formatter=lambda x: main_exception_formatter(x, verbose_errors=True),
238+
)
239+
fields = r"""
240+
Severity: ERROR
241+
Severity \(non-localized\): ERROR
242+
SQLSTATE code: 42601
243+
Message: syntax error at or near "invalid"
244+
Position: 1
245+
File: scan\.l
246+
Line: \d+
247+
Routine: scanner_yyerror
248+
""".strip()
249+
assert re.search(fields, result[0])
224250

225251

226252
@dbtest

0 commit comments

Comments
 (0)