Skip to content

Commit 31905a6

Browse files
committed
fix: more careful stripping of error prefixes
Only strip the known prefixes, both in English and in the currently known localizations. Added script to generate regexp to match every backend localization. The script was executed on PostgreSQL commit f4ad0021af (on master branch, before v17). Close psycopg#752.
1 parent 5e79487 commit 31905a6

File tree

5 files changed

+179
-11
lines changed

5 files changed

+179
-11
lines changed

docs/news.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ Psycopg 3.2 (unreleased)
3535
.. __: https://numpy.org/doc/stable/reference/arrays.scalars.html#built-in-scalar-types
3636

3737

38+
Psycopg 3.1.19
39+
^^^^^^^^^^^^^^
40+
41+
- Fix excessive stripping of error message prefixes (:ticket:`#752`).
42+
43+
3844
Current release
3945
---------------
4046

psycopg/psycopg/pq/misc.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# Copyright (C) 2020 The Psycopg Team
66

7+
import re
78
import os
89
import sys
910
import logging
@@ -90,31 +91,61 @@ def error_message(obj: Union[PGconn, PGresult], encoding: str = "utf8") -> str:
9091
obj = cast(PGresult, obj)
9192
bmsg = obj.error_message
9293

93-
# strip severity and whitespaces
94-
if bmsg:
95-
bmsg = bmsg.split(b":", 1)[-1].strip()
96-
9794
elif hasattr(obj, "error_message"):
9895
# obj is a PGconn
9996
if obj.status == OK:
10097
encoding = pgconn_encoding(obj)
10198
bmsg = obj.error_message
10299

103-
# strip severity and whitespaces
104-
if bmsg:
105-
bmsg = bmsg.split(b":", 1)[-1].strip()
106-
107100
else:
108101
raise TypeError(f"PGconn or PGresult expected, got {type(obj).__name__}")
109102

110103
if bmsg:
111-
msg = bmsg.decode(encoding, "replace")
104+
msg = strip_severity(bmsg.decode(encoding, "replace"))
112105
else:
113106
msg = "no details available"
114107

115108
return msg
116109

117110

111+
# Possible prefixes to strip for error messages, in the known localizations.
112+
# This regular expression is generated from PostgreSQL sources using the
113+
# `tools/update_error_prefixes.py` script
114+
PREFIXES = re.compile(
115+
# autogenerated: start
116+
r"""
117+
^ (?:
118+
DEBUG | INFO | HINWEIS | WARNUNG | FEHLER | LOG | FATAL | PANIK # de
119+
| DEBUG | INFO | NOTICE | WARNING | ERROR | LOG | FATAL | PANIC # en
120+
| DEBUG | INFO | NOTICE | WARNING | ERROR | LOG | FATAL | PANIC # es
121+
| DEBUG | INFO | NOTICE | ATTENTION | ERREUR | LOG | FATAL | PANIC # fr
122+
| DEBUG | INFO | NOTICE | PERINGATAN | ERROR | LOG | FATAL | PANIK # id
123+
| DEBUG | INFO | NOTIFICA | ATTENZIONE | ERRORE | LOG | FATALE | PANICO # it
124+
| DEBUG | INFO | NOTICE | WARNING | ERROR | LOG | FATAL | PANIC # ja
125+
| 디버그 | 정보 | 알림 | 경고 | 오류 | 로그 | 치명적오류 | 손상 # ko
126+
| DEBUG | INFORMACJA | UWAGA | OSTRZEŻENIE | BŁĄD | DZIENNIK | KATASTROFALNY | PANIKA # pl
127+
| DEPURAÇÃO | INFO | NOTA | AVISO | ERRO | LOG | FATAL | PÂNICO # pt_BR
128+
| ОТЛАДКА | ИНФОРМАЦИЯ | ЗАМЕЧАНИЕ | ПРЕДУПРЕЖДЕНИЕ | ОШИБКА | СООБЩЕНИЕ | ВАЖНО | ПАНИКА # ru
129+
| DEBUG | INFO | NOTIS | VARNING | FEL | LOGG | FATALT | PANIK # sv
130+
| DEBUG | BİLGİ | NOT | UYARI | HATA | LOG | ÖLÜMCÜL\ \(FATAL\) | KRİTİK # tr
131+
| НАЛАГОДЖЕННЯ | ІНФОРМАЦІЯ | ПОВІДОМЛЕННЯ | ПОПЕРЕДЖЕННЯ | ПОМИЛКА | ЗАПИСУВАННЯ | ФАТАЛЬНО | ПАНІКА # uk
132+
| 调试 | 信息 | 注意 | 警告 | 错误 | 日志 | 致命错误 | 比致命错误还过分的错误 # zh_CN
133+
) : \s+
134+
""", # noqa: E501
135+
# autogenerated: end
136+
re.VERBOSE | re.MULTILINE,
137+
)
138+
139+
140+
def strip_severity(msg: str) -> str:
141+
"""Strip severity and whitespaces from error message."""
142+
m = PREFIXES.match(msg)
143+
if m:
144+
msg = msg[m.span()[1] :]
145+
146+
return msg.strip()
147+
148+
118149
def connection_summary(pgconn: PGconn) -> str:
119150
"""
120151
Return summary information on a connection.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ disallow_untyped_defs = false
5656
disallow_untyped_calls = false
5757

5858
[tool.codespell]
59-
ignore-words-list = 'alot,ans,ba,fo,te'
60-
skip = '_build,.mypy_cache,.venv,pq.c,_psycopg.c,*.html'
59+
ignore-words-list = "alot,ans,ba,fo,te,erro,varning"
60+
skip = "build,_build,.tox,.mypy_cache,.venv,pq.c,_psycopg.c"

tests/test_errors.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,32 @@ def test_pgresult_pickle(conn):
323323

324324
def test_blank_sqlstate(conn):
325325
assert e.get_base_exception("") is e.DatabaseError
326+
327+
328+
@pytest.mark.parametrize(
329+
"msg",
330+
[
331+
'connection to server at "2001:1488:fffe:20::229", port 5432 failed',
332+
"HORROR: foo\n",
333+
],
334+
)
335+
def test_strip_severity_unstripped(msg):
336+
from psycopg.pq.misc import strip_severity
337+
338+
out = strip_severity(msg)
339+
assert out == msg.strip()
340+
341+
342+
@pytest.mark.parametrize(
343+
"msg",
344+
[
345+
"ERROR: foo\n",
346+
"ERRORE: foo\nbar\n",
347+
"오류: foo: bar",
348+
],
349+
)
350+
def test_strip_severity_l10n(msg):
351+
from psycopg.pq.misc import strip_severity
352+
353+
out = strip_severity(msg)
354+
assert out == msg.split(":", 1)[1].strip()

tools/update_error_prefixes.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python
2+
"""Find the error prefixes in various l10n used for precise prefixstripping.
3+
"""
4+
5+
import re
6+
import logging
7+
from pathlib import Path
8+
from argparse import ArgumentParser, Namespace
9+
from collections import defaultdict
10+
11+
import polib
12+
13+
HERE = Path(__file__).parent
14+
15+
logger = logging.getLogger()
16+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
17+
18+
19+
def main() -> None:
20+
args = parse_cmdline()
21+
regexp = make_regexp(args.pgroot)
22+
update_file(args.dest, regexp)
23+
24+
25+
def make_regexp(pgroot: Path) -> str:
26+
logger.info("looking for translations in %s", pgroot)
27+
msgids = "DEBUG INFO NOTICE WARNING ERROR LOG FATAL PANIC".split()
28+
bylang = defaultdict[str, list[str]](list)
29+
bylang["en"].extend(msgids)
30+
for fn in (pgroot / "src/backend/po").glob("*.po"):
31+
lang = fn.name.rsplit(".")[0]
32+
pofile = polib.pofile(str(fn))
33+
for msgid in msgids:
34+
if not (entry := pofile.find(msgid)):
35+
continue
36+
bylang[lang].append(entry.msgstr)
37+
38+
pattern = "\n | ".join(
39+
"%s # %s" % (" | ".join(re.escape(msg) for msg in msgs), lang)
40+
for lang, msgs in sorted(bylang.items())
41+
)
42+
return rf''' r"""
43+
^ (?:
44+
{pattern}
45+
) : \s+
46+
""", # noqa: E501'''
47+
48+
49+
def update_file(fn: Path, content: str) -> None:
50+
logger.info("updating %s", fn)
51+
52+
with open(fn, "r") as f:
53+
lines = f.read().splitlines()
54+
55+
istart, iend = [
56+
i
57+
for i, line in enumerate(lines)
58+
if re.match(r"\s*(#|\.\.)\s*autogenerated:\s+(start|end)", line)
59+
]
60+
61+
lines[istart + 1 : iend] = [content]
62+
63+
with open(fn, "w") as f:
64+
for line in lines:
65+
f.write(line + "\n")
66+
67+
68+
def parse_cmdline() -> Namespace:
69+
for default_pgroot in (
70+
HERE / "../../fs/postgres", # it happens to be my laptop
71+
HERE / "../../postgres", # the last entry is the default if none found
72+
):
73+
if default_pgroot.exists():
74+
break
75+
76+
default_pgroot = default_pgroot.resolve()
77+
default_dest = (HERE / "../psycopg/psycopg/pq/misc.py").resolve()
78+
79+
parser = ArgumentParser(description=__doc__)
80+
parser.add_argument(
81+
"--pgroot",
82+
metavar="DIR",
83+
default=default_pgroot,
84+
type=Path,
85+
help="root PostgreSQL source directory [default: %(default)s]",
86+
)
87+
parser.add_argument(
88+
"--dest",
89+
default=default_dest,
90+
type=Path,
91+
help="the file to change [default: %(default)s]",
92+
)
93+
94+
opt = parser.parse_args()
95+
if not opt.pgroot.is_dir():
96+
parser.error("not a valid directory: {opt.pgroot}")
97+
98+
return opt
99+
100+
101+
if __name__ == "__main__":
102+
main()

0 commit comments

Comments
 (0)