psycopg 3.3.4__tar.gz → 3.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {psycopg-3.3.4 → psycopg-3.3.6}/PKG-INFO +8 -7
  2. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_adapters_map.py +1 -1
  3. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_compat.py +9 -6
  4. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_connection_base.py +15 -8
  5. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_copy_base.py +9 -0
  6. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_encodings.py +52 -53
  7. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_preparing.py +10 -3
  8. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_py_transformer.py +10 -12
  9. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_tstrings.py +4 -4
  10. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_typeinfo.py +1 -1
  11. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_typemod.py +18 -0
  12. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/abc.py +7 -1
  13. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/connection.py +45 -23
  14. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/connection_async.py +48 -24
  15. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/conninfo.py +1 -1
  16. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/crdb/_types.py +3 -3
  17. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/errors.py +8 -0
  18. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/generators.py +13 -16
  19. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/postgres.py +3 -3
  20. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_debug.py +1 -1
  21. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/misc.py +1 -1
  22. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/rows.py +4 -1
  23. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/sql.py +3 -3
  24. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/array.py +1 -3
  25. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/datetime.py +1 -1
  26. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/json.py +1 -1
  27. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/multirange.py +11 -5
  28. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/net.py +6 -12
  29. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/numeric.py +48 -4
  30. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/string.py +1 -3
  31. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/uuid.py +1 -1
  32. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/waiting.py +184 -84
  33. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/PKG-INFO +8 -7
  34. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/requires.txt +6 -6
  35. {psycopg-3.3.4 → psycopg-3.3.6}/pyproject.toml +8 -7
  36. {psycopg-3.3.4 → psycopg-3.3.6}/LICENSE.txt +0 -0
  37. {psycopg-3.3.4 → psycopg-3.3.6}/README.rst +0 -0
  38. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/__init__.py +0 -0
  39. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_acompat.py +0 -0
  40. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_capabilities.py +0 -0
  41. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_cmodule.py +0 -0
  42. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_column.py +0 -0
  43. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_connection_info.py +0 -0
  44. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_conninfo_attempts.py +0 -0
  45. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_conninfo_attempts_async.py +0 -0
  46. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_conninfo_utils.py +0 -0
  47. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_copy.py +0 -0
  48. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_copy_async.py +0 -0
  49. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_cursor_base.py +0 -0
  50. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_dns.py +0 -0
  51. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_enums.py +0 -0
  52. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_oids.py +0 -0
  53. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_pipeline.py +0 -0
  54. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_pipeline_async.py +0 -0
  55. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_pipeline_base.py +0 -0
  56. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_queries.py +0 -0
  57. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_server_cursor.py +0 -0
  58. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_server_cursor_async.py +0 -0
  59. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_server_cursor_base.py +0 -0
  60. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_struct.py +0 -0
  61. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_tpc.py +0 -0
  62. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_transformer.py +0 -0
  63. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_tz.py +0 -0
  64. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_wrappers.py +0 -0
  65. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/adapt.py +0 -0
  66. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/client_cursor.py +0 -0
  67. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/copy.py +0 -0
  68. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/crdb/__init__.py +0 -0
  69. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/crdb/connection.py +0 -0
  70. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/cursor.py +0 -0
  71. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/cursor_async.py +0 -0
  72. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/dbapi20.py +0 -0
  73. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/__init__.py +0 -0
  74. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_enums.py +0 -0
  75. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_pq_ctypes.py +0 -0
  76. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_pq_ctypes.pyi +0 -0
  77. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/abc.py +0 -0
  78. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/pq_ctypes.py +0 -0
  79. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/py.typed +0 -0
  80. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/raw_cursor.py +0 -0
  81. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/transaction.py +0 -0
  82. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/__init__.py +0 -0
  83. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/bool.py +0 -0
  84. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/composite.py +0 -0
  85. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/enum.py +0 -0
  86. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/hstore.py +0 -0
  87. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/none.py +0 -0
  88. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/numpy.py +0 -0
  89. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/range.py +0 -0
  90. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/shapely.py +0 -0
  91. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/version.py +0 -0
  92. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/SOURCES.txt +0 -0
  93. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/dependency_links.txt +0 -0
  94. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/not-zip-safe +0 -0
  95. {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/top_level.txt +0 -0
  96. {psycopg-3.3.4 → psycopg-3.3.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psycopg
3
- Version: 3.3.4
3
+ Version: 3.3.6
4
4
  Summary: PostgreSQL database adapter for Python
5
5
  Author-email: Daniele Varrazzo <daniele.varrazzo@gmail.com>
6
6
  License-Expression: LGPL-3.0-only
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
24
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
25
26
  Classifier: Programming Language :: Python :: Implementation :: PyPy
26
27
  Classifier: Topic :: Database
@@ -33,14 +34,14 @@ License-File: LICENSE.txt
33
34
  Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
34
35
  Requires-Dist: tzdata; sys_platform == "win32"
35
36
  Provides-Extra: c
36
- Requires-Dist: psycopg-c==3.3.4; implementation_name != "pypy" and extra == "c"
37
+ Requires-Dist: psycopg-c==3.3.6; implementation_name != "pypy" and extra == "c"
37
38
  Provides-Extra: binary
38
- Requires-Dist: psycopg-binary==3.3.4; implementation_name != "pypy" and extra == "binary"
39
+ Requires-Dist: psycopg-binary==3.3.6; implementation_name != "pypy" and extra == "binary"
39
40
  Provides-Extra: pool
40
41
  Requires-Dist: psycopg-pool; extra == "pool"
41
42
  Provides-Extra: test
42
43
  Requires-Dist: anyio>=4.0; extra == "test"
43
- Requires-Dist: mypy>=1.19.0; implementation_name != "pypy" and extra == "test"
44
+ Requires-Dist: mypy>=2.1.0; implementation_name != "pypy" and extra == "test"
44
45
  Requires-Dist: pproxy>=2.7; extra == "test"
45
46
  Requires-Dist: pytest>=6.2.5; extra == "test"
46
47
  Requires-Dist: pytest-cov>=3.0; extra == "test"
@@ -49,12 +50,12 @@ Provides-Extra: dev
49
50
  Requires-Dist: ast-comments>=1.1.2; extra == "dev"
50
51
  Requires-Dist: black>=26.1.0; extra == "dev"
51
52
  Requires-Dist: codespell>=2.2; extra == "dev"
52
- Requires-Dist: cython-lint>=0.16; extra == "dev"
53
+ Requires-Dist: cython-lint>=0.21; extra == "dev"
53
54
  Requires-Dist: dnspython>=2.1; extra == "dev"
54
55
  Requires-Dist: flake8>=4.0; extra == "dev"
55
56
  Requires-Dist: isort[colors]>=6.0; extra == "dev"
56
- Requires-Dist: isort-psycopg; extra == "dev"
57
- Requires-Dist: mypy>=1.19.0; extra == "dev"
57
+ Requires-Dist: isort-psycopg>=0.0.3; extra == "dev"
58
+ Requires-Dist: mypy>=2.1.0; extra == "dev"
58
59
  Requires-Dist: pre-commit>=4.0.1; extra == "dev"
59
60
  Requires-Dist: types-setuptools>=57.4; extra == "dev"
60
61
  Requires-Dist: types-shapely>=2.0; extra == "dev"
@@ -48,7 +48,7 @@ class AdaptersMap:
48
48
  already created are not affected if a wider scope (e.g. the global one) is
49
49
  changed.
50
50
 
51
- The connections adapters are initialised using a global `!AdptersMap`
51
+ The connections adapters are initialised using a global `!AdaptersMap`
52
52
  template, exposed as `psycopg.adapters`: changing such mapping allows to
53
53
  customise the type mapping for every connections created afterwards.
54
54
 
@@ -33,24 +33,27 @@ else:
33
33
  if sys.version_info >= (3, 14):
34
34
  from string.templatelib import Interpolation, Template
35
35
  else:
36
+ from typing import Generic, Literal
36
37
  from dataclasses import dataclass
37
38
 
39
+ T = TypeVar("T")
40
+
38
41
  class Template:
39
42
  strings: tuple[str]
40
- interpolations: tuple[Interpolation]
43
+ interpolations: tuple[Interpolation[Any]]
41
44
 
42
- def __new__(cls, *args: str | Interpolation) -> Self:
45
+ def __new__(cls, *args: str | Interpolation[Any]) -> Self:
43
46
  return cls()
44
47
 
45
- def __iter__(self) -> Iterator[str | Interpolation]:
48
+ def __iter__(self) -> Iterator[str | Interpolation[Any]]:
46
49
  return
47
50
  yield
48
51
 
49
52
  @dataclass
50
- class Interpolation:
51
- value: Any
53
+ class Interpolation(Generic[T]):
54
+ value: T
52
55
  expression: str
53
- conversion: str | None
56
+ conversion: Literal["a", "r", "s"] | None
54
57
  format_spec: str
55
58
 
56
59
 
@@ -314,10 +314,10 @@ class BaseConnection(Generic[Row]):
314
314
  )
315
315
  return True
316
316
 
317
- def _cancel_gen(self, *, timeout: float) -> PQGenConn[None]:
317
+ def _cancel_gen(self) -> PQGenConn[None]:
318
318
  cancel_conn = self.pgconn.cancel_conn()
319
319
  cancel_conn.start()
320
- yield from generators.cancel(cancel_conn, timeout=timeout)
320
+ yield from generators.cancel(cancel_conn)
321
321
 
322
322
  def add_notice_handler(self, callback: NoticeHandler) -> None:
323
323
  """
@@ -434,11 +434,9 @@ class BaseConnection(Generic[Row]):
434
434
  # should have a lock and hold it before calling and consuming them.
435
435
 
436
436
  @classmethod
437
- def _connect_gen(
438
- cls, conninfo: str = "", *, timeout: float = 0.0
439
- ) -> PQGenConn[Self]:
437
+ def _connect_gen(cls, conninfo: str = "") -> PQGenConn[Self]:
440
438
  """Generator to connect to the database and create a new instance."""
441
- pgconn = yield from generators.connect(conninfo, timeout=timeout)
439
+ pgconn = yield from generators.connect(conninfo)
442
440
  conn = cls(pgconn)
443
441
  return conn
444
442
 
@@ -477,7 +475,13 @@ class BaseConnection(Generic[Row]):
477
475
  else:
478
476
  self.pgconn.send_query_params(command, None, result_format=result_format)
479
477
 
480
- result: PGresult = (yield from generators.execute(self.pgconn))[-1]
478
+ results: list[PGresult] = (yield from generators.execute(self.pgconn))
479
+ if len(results) != 1:
480
+ raise e.InternalError(
481
+ f"received {len(results)} results from command {command.decode()!r}"
482
+ )
483
+
484
+ result = results[0]
481
485
  if result.status != COMMAND_OK and result.status != TUPLES_OK:
482
486
  if result.status == FATAL_ERROR:
483
487
  raise e.error_from_result(result, encoding=self.pgconn._encoding)
@@ -514,7 +518,10 @@ class BaseConnection(Generic[Row]):
514
518
 
515
519
  self.pgconn.send_close_prepared(name)
516
520
 
517
- result = (yield from generators.execute(self.pgconn))[-1]
521
+ if not (results := (yield from generators.execute(self.pgconn))):
522
+ raise e.InternalError("no result from deallocate command")
523
+
524
+ result = results[-1]
518
525
  if result.status != COMMAND_OK:
519
526
  if result.status == FATAL_ERROR:
520
527
  raise e.error_from_result(result, encoding=self.pgconn._encoding)
@@ -350,6 +350,8 @@ def _format_row_binary(row: Sequence[Any], tx: Transformer, out: bytearray) -> N
350
350
  def _parse_row_text(data: Buffer, tx: Transformer) -> tuple[Any, ...]:
351
351
  if not isinstance(data, bytes):
352
352
  data = bytes(data)
353
+ if not data.endswith(b"\n"):
354
+ raise e.DataError("bad copy data: field delimiter not found")
353
355
  fields = data.split(b"\t")
354
356
  fields[-1] = fields[-1][:-1] # drop \n
355
357
  row = [None if f == b"\\N" else _load_re.sub(_load_sub, f) for f in fields]
@@ -358,12 +360,19 @@ def _parse_row_text(data: Buffer, tx: Transformer) -> tuple[Any, ...]:
358
360
 
359
361
  def _parse_row_binary(data: Buffer, tx: Transformer) -> tuple[Any, ...]:
360
362
  row: list[Buffer | None] = []
363
+ len_data = len(data)
364
+ if len_data < 2:
365
+ raise e.DataError("bad copy data: fields number truncated")
361
366
  nfields = _unpack_int2(data, 0)[0]
362
367
  pos = 2
363
368
  for i in range(nfields):
369
+ if pos + 4 > len_data:
370
+ raise e.DataError("bad copy data: field length truncated")
364
371
  length = _unpack_int4(data, pos)[0]
365
372
  pos += 4
366
373
  if length >= 0:
374
+ if pos + length > len_data:
375
+ raise e.DataError("bad copy data: length exceeding data")
367
376
  row.append(data[pos : pos + length])
368
377
  pos += length
369
378
  else:
@@ -13,70 +13,69 @@ from typing import TYPE_CHECKING, Any
13
13
  from functools import cache
14
14
 
15
15
  from .errors import NotSupportedError
16
- from .pq._enums import ConnStatus
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from ._connection_base import BaseConnection
20
19
 
21
- OK = ConnStatus.OK
22
-
23
-
24
- _py_codecs = {
25
- "BIG5": "big5",
26
- "EUC_CN": "gb2312",
27
- "EUC_JIS_2004": "euc_jis_2004",
28
- "EUC_JP": "euc_jp",
29
- "EUC_KR": "euc_kr",
30
- # "EUC_TW": not available in Python
31
- "GB18030": "gb18030",
32
- "GBK": "gbk",
33
- "ISO_8859_5": "iso8859-5",
34
- "ISO_8859_6": "iso8859-6",
35
- "ISO_8859_7": "iso8859-7",
36
- "ISO_8859_8": "iso8859-8",
37
- "JOHAB": "johab",
38
- "KOI8R": "koi8-r",
39
- "KOI8U": "koi8-u",
40
- "LATIN1": "iso8859-1",
41
- "LATIN10": "iso8859-16",
42
- "LATIN2": "iso8859-2",
43
- "LATIN3": "iso8859-3",
44
- "LATIN4": "iso8859-4",
45
- "LATIN5": "iso8859-9",
46
- "LATIN6": "iso8859-10",
47
- "LATIN7": "iso8859-13",
48
- "LATIN8": "iso8859-14",
49
- "LATIN9": "iso8859-15",
50
- # "MULE_INTERNAL": not available in Python
51
- "SHIFT_JIS_2004": "shift_jis_2004",
52
- "SJIS": "shift_jis",
20
+ # From: https://www.postgresql.org/docs/current/multibyte.html#CHARSET-TABLE
21
+ _py_codecs = (
22
+ (("BIG5", "WIN950", "Windows950"), "big5"),
23
+ (("EUC_CN",), "gb2312"),
24
+ (("EUC_JIS_2004",), "euc_jis_2004"),
25
+ (("EUC_JP",), "euc_jp"),
26
+ (("EUC_KR",), "euc_kr"),
27
+ # (("EUC_TW",), not available in Python
28
+ (("GB18030",), "gb18030"),
29
+ (("GBK", "WIN936", "Windows936"), "gbk"),
30
+ (("ISO_8859_5",), "iso8859-5"),
31
+ (("ISO_8859_6",), "iso8859-6"),
32
+ (("ISO_8859_7",), "iso8859-7"),
33
+ (("ISO_8859_8",), "iso8859-8"),
34
+ (("JOHAB",), "johab"),
35
+ (("KOI8R", "KOI8"), "koi8-r"),
36
+ (("KOI8U",), "koi8-u"),
37
+ (("LATIN1", "ISO88591"), "iso8859-1"),
38
+ (("LATIN10", "ISO885916"), "iso8859-16"),
39
+ (("LATIN2", "ISO88592"), "iso8859-2"),
40
+ (("LATIN3", "ISO88593"), "iso8859-3"),
41
+ (("LATIN4", "ISO88594"), "iso8859-4"),
42
+ (("LATIN5", "ISO88599"), "iso8859-9"),
43
+ (("LATIN6", "ISO885910"), "iso8859-10"),
44
+ (("LATIN7", "ISO885913"), "iso8859-13"),
45
+ (("LATIN8", "ISO885914"), "iso8859-14"),
46
+ (("LATIN9", "ISO885915"), "iso8859-15"),
47
+ # "MULE_INTERNAL" not available in Python
48
+ (("SHIFT_JIS_2004",), "shift_jis_2004"),
49
+ (("SJIS", "Mskanji", "ShiftJIS", "WIN932", "Windows932"), "shift_jis"),
53
50
  # this actually means no encoding, see PostgreSQL docs
54
51
  # it is special-cased by the text loader.
55
- "SQL_ASCII": "ascii",
56
- "UHC": "cp949",
57
- "UTF8": "utf-8",
58
- "WIN1250": "cp1250",
59
- "WIN1251": "cp1251",
60
- "WIN1252": "cp1252",
61
- "WIN1253": "cp1253",
62
- "WIN1254": "cp1254",
63
- "WIN1255": "cp1255",
64
- "WIN1256": "cp1256",
65
- "WIN1257": "cp1257",
66
- "WIN1258": "cp1258",
67
- "WIN866": "cp866",
68
- "WIN874": "cp874",
69
- }
70
-
71
- py_codecs: dict[bytes, str] = {}
72
- py_codecs.update((k.encode(), v) for k, v in _py_codecs.items())
52
+ (("SQL_ASCII",), "ascii"),
53
+ (("UHC", "WIN949", "Windows949"), "cp949"),
54
+ (("UTF8", "Unicode"), "utf-8"),
55
+ (("WIN1250",), "cp1250"),
56
+ (("WIN1251", "WIN"), "cp1251"),
57
+ (("WIN1252",), "cp1252"),
58
+ (("WIN1253",), "cp1253"),
59
+ (("WIN1254",), "cp1254"),
60
+ (("WIN1255",), "cp1255"),
61
+ (("WIN1256",), "cp1256"),
62
+ (("WIN1257",), "cp1257"),
63
+ (("WIN1258", "ABC", "TCVN", "TCVN5712", "VSCII"), "cp1258"),
64
+ (("WIN866", "ALT"), "cp866"),
65
+ (("WIN874",), "cp874"),
66
+ )
67
+
68
+ py_codecs = {a.upper().encode(): v for aliases, v in _py_codecs for a in aliases}
73
69
 
74
70
  # Add an alias without underscore, for lenient lookups
75
71
  py_codecs.update(
76
- (k.replace("_", "").encode(), v) for k, v in _py_codecs.items() if "_" in k
72
+ (a.replace("_", "").encode(), v)
73
+ for aliases, v in _py_codecs
74
+ for a in aliases
75
+ if "_" in a
77
76
  )
78
77
 
79
- pg_codecs = {v: k.encode() for k, v in _py_codecs.items()}
78
+ pg_codecs = {v: k[0].encode() for k, v in _py_codecs}
80
79
 
81
80
 
82
81
  def conn_encoding(conn: BaseConnection[Any] | None) -> str:
@@ -6,6 +6,7 @@ Support for prepared statements
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import re
9
10
  from enum import IntEnum, auto
10
11
  from typing import TYPE_CHECKING, Any, TypeAlias
11
12
  from collections import OrderedDict, deque
@@ -78,7 +79,14 @@ class PrepareManager:
78
79
  # The query is not to be prepared yet
79
80
  return Prepare.NO, b""
80
81
 
81
- def _should_discard(self, prep: Prepare, results: Sequence[PGresult]) -> bool:
82
+ def _should_discard(
83
+ self,
84
+ prep: Prepare,
85
+ results: Sequence[PGresult],
86
+ __should_clear: Any = re.compile(
87
+ rb"^(?:DROP|ALTER|ROLLBACK|DISCARD ALL|DEALLOCATE ALL)\b"
88
+ ).match,
89
+ ) -> bool:
82
90
  """Check if we need to discard our entire state: it should happen on
83
91
  rollback or on dropping objects, because the same object may get
84
92
  recreated and postgres would fail internal lookups.
@@ -87,8 +95,7 @@ class PrepareManager:
87
95
  for result in results:
88
96
  if result.status != COMMAND_OK:
89
97
  continue
90
- cmdstat = result.command_status
91
- if cmdstat and (cmdstat.startswith(b"DROP ") or cmdstat == b"ROLLBACK"):
98
+ if (cmdstat := result.command_status) and __should_clear(cmdstat):
92
99
  return self.clear()
93
100
  return False
94
101
 
@@ -65,6 +65,7 @@ class Transformer(AdaptContext):
65
65
 
66
66
  def __init__(self, context: AdaptContext | None = None):
67
67
  self._pgresult = self.types = self.formats = None
68
+ self._none_oid = -1
68
69
 
69
70
  # WARNING: don't store context, or you'll create a loop with the Cursor
70
71
  if context:
@@ -187,12 +188,14 @@ class Transformer(AdaptContext):
187
188
  out[i] = self._row_dumpers[i].dump(param)
188
189
  return out
189
190
 
190
- types = [self._get_none_oid()] * nparams
191
+ types = [-1] * nparams
191
192
  pqformats = [TEXT] * nparams
192
193
 
193
194
  for i in range(nparams):
194
195
  if (param := params[i]) is None:
196
+ types[i] = self._get_none_oid()
195
197
  continue
198
+
196
199
  dumper = self.get_dumper(param, formats[i])
197
200
  out[i] = dumper.dump(param)
198
201
  types[i] = dumper.oid
@@ -266,17 +269,12 @@ class Transformer(AdaptContext):
266
269
  return dumper
267
270
 
268
271
  def _get_none_oid(self) -> int:
269
- try:
270
- return self._none_oid
271
- except AttributeError:
272
- pass
273
-
274
- try:
275
- rv = self._none_oid = self._adapters.get_dumper(NoneType, PY_TEXT).oid
276
- except KeyError:
277
- raise e.InterfaceError("None dumper not found")
278
-
279
- return rv
272
+ if self._none_oid < 0:
273
+ try:
274
+ self._none_oid = self._adapters.get_dumper(NoneType, PY_TEXT).oid
275
+ except KeyError:
276
+ raise e.InterfaceError("None dumper not found")
277
+ return self._none_oid
280
278
 
281
279
  def get_dumper_by_oid(self, oid: int, format: pq.Format) -> abc.Dumper:
282
280
  """
@@ -40,7 +40,7 @@ class TemplateProcessor:
40
40
  self._process_template(self.template)
41
41
  self.query = b"".join(self._chunks)
42
42
 
43
- def _check_template_format(self, item: Interpolation, want_fmt: str) -> None:
43
+ def _check_template_format(self, item: Interpolation[Any], want_fmt: str) -> None:
44
44
  if item.format_spec == want_fmt:
45
45
  return
46
46
  fmt = f":{item.format_spec}" if item.format_spec else ""
@@ -94,7 +94,7 @@ class TemplateProcessor:
94
94
  else:
95
95
  self._process_client_variable(item, fmt)
96
96
 
97
- def _process_server_variable(self, item: Interpolation, fmt: str) -> None:
97
+ def _process_server_variable(self, item: Interpolation[Any], fmt: str) -> None:
98
98
  try:
99
99
  pyfmt = PyFormat(fmt)
100
100
  except ValueError:
@@ -107,7 +107,7 @@ class TemplateProcessor:
107
107
  self.params.append(item.value)
108
108
  self._chunks.append(b"$%d" % len(self.params))
109
109
 
110
- def _process_client_variable(self, item: Interpolation, fmt: str) -> None:
110
+ def _process_client_variable(self, item: Interpolation[Any], fmt: str) -> None:
111
111
  try:
112
112
  PyFormat(fmt)
113
113
  except ValueError:
@@ -120,7 +120,7 @@ class TemplateProcessor:
120
120
  self._chunks.append(param)
121
121
  self.params.append(param)
122
122
 
123
- def _process_composable(self, item: Interpolation) -> None:
123
+ def _process_composable(self, item: Interpolation[Any]) -> None:
124
124
  if isinstance(item.value, sql.Identifier):
125
125
  self._check_template_format(item, FMT_IDENT)
126
126
  self._chunks.append(item.value.as_bytes(self._tx))
@@ -303,7 +303,7 @@ class TypesRegistry:
303
303
  """
304
304
  Return the oid of a PostgreSQL type by name.
305
305
 
306
- :param key: the name of the type to look for.
306
+ :param name: the name of the type to look for.
307
307
 
308
308
  Return the array oid if the type ends with "``[]``"
309
309
 
@@ -84,3 +84,21 @@ class TimeTypeModifier(TypeModifier):
84
84
 
85
85
  def get_precision(self, typemod: int) -> int | None:
86
86
  return typemod & 0xFFFF if typemod >= 0 else None
87
+
88
+
89
+ class IntervalTypeModifier(TimeTypeModifier):
90
+ """Handle interval type modifier.
91
+
92
+ Unlike the other time-related types, the interval typmod is a composite:
93
+ the fields restriction is packed in the high bits and the precision in the
94
+ low ones. A typmod written without an explicit precision (e.g. ``interval
95
+ day to second``) stores ``INTERVAL_FULL_PRECISION`` (0xFFFF) there, which
96
+ means "unspecified" rather than a precision of 65535.
97
+ """
98
+
99
+ def get_precision(self, typemod: int) -> int | None:
100
+ if typemod < 0:
101
+ return None
102
+
103
+ precision = typemod & 0xFFFF
104
+ return precision if precision != 0xFFFF else None
@@ -57,7 +57,13 @@ class WaitFunc(Protocol):
57
57
  Wait on the connection which generated `PQgen` and return its final result.
58
58
  """
59
59
 
60
- def __call__(self, gen: PQGen[RV], fileno: int, interval: float = ...) -> RV: ...
60
+ def __call__(
61
+ self,
62
+ gen: PQGen[RV],
63
+ fileno: int,
64
+ interval: float = ...,
65
+ timeout: float | None = ...,
66
+ ) -> RV: ...
61
67
 
62
68
 
63
69
  # Adaptation types
@@ -50,7 +50,7 @@ IDLE = pq.TransactionStatus.IDLE
50
50
  ACTIVE = pq.TransactionStatus.ACTIVE
51
51
  INTRANS = pq.TransactionStatus.INTRANS
52
52
 
53
- _INTERRUPTED = KeyboardInterrupt
53
+ _INTERRUPTED = (KeyboardInterrupt, SystemExit)
54
54
 
55
55
  logger = logging.getLogger("psycopg")
56
56
 
@@ -105,8 +105,12 @@ class Connection(BaseConnection[Row]):
105
105
  logger.debug("connection attempt: %s", descr)
106
106
  try:
107
107
  conninfo = make_conninfo("", **attempt)
108
- gen = cls._connect_gen(conninfo, timeout=timeout)
109
- rv = waiting.wait_conn(gen, interval=_WAIT_INTERVAL)
108
+ gen = cls._connect_gen(conninfo)
109
+ rv = waiting.wait_conn(gen, interval=_WAIT_INTERVAL, timeout=timeout)
110
+ except e._WaitTimeout:
111
+ tex = e.ConnectionTimeout("connection timeout expired")
112
+ logger.debug("connection failed: %s: %s", descr, str(tex))
113
+ conn_errors.append((tex, descr))
110
114
  except e.Error as ex:
111
115
  logger.debug("connection failed: %s: %s", descr, str(ex))
112
116
  conn_errors.append((ex, descr))
@@ -329,9 +333,12 @@ class Connection(BaseConnection[Row]):
329
333
  return
330
334
 
331
335
  if capabilities.has_cancel_safe():
332
- waiting.wait_conn(
333
- self._cancel_gen(timeout=timeout), interval=_WAIT_INTERVAL
334
- )
336
+ try:
337
+ waiting.wait_conn(
338
+ self._cancel_gen(), interval=_WAIT_INTERVAL, timeout=timeout or None
339
+ )
340
+ except e._WaitTimeout:
341
+ raise e.CancellationTimeout("cancellation timeout expired") from None
335
342
  else:
336
343
  self.cancel()
337
344
 
@@ -374,15 +381,7 @@ class Connection(BaseConnection[Row]):
374
381
  You might actually receive more than this number if more than one
375
382
  notifications arrives in the same packet.
376
383
  """
377
- # Allow interrupting the wait with a signal by reducing a long timeout
378
- # into shorter intervals.
379
- if timeout is not None:
380
- deadline = monotonic() + timeout
381
- interval = min(timeout, _WAIT_INTERVAL)
382
- else:
383
- deadline = None
384
- interval = _WAIT_INTERVAL
385
-
384
+ deadline = monotonic() + timeout if timeout is not None else None
386
385
  nreceived = 0
387
386
 
388
387
  if self._notify_handlers:
@@ -407,8 +406,14 @@ class Connection(BaseConnection[Row]):
407
406
  yield d.popleft()
408
407
  nreceived += 1
409
408
  else:
409
+ if deadline is not None:
410
+ remaining = max(0.0, deadline - monotonic())
411
+ else:
412
+ remaining = None
410
413
  try:
411
- pgns = self.wait(notifies(self.pgconn), interval=interval)
414
+ pgns = self.wait(notifies(self.pgconn), timeout=remaining)
415
+ except e._WaitTimeout:
416
+ break
412
417
  except e._NO_TRACEBACK as ex:
413
418
  raise ex.with_traceback(None)
414
419
  # Emit the notifications received.
@@ -426,10 +431,8 @@ class Connection(BaseConnection[Row]):
426
431
 
427
432
  # Check the deadline after the loop to ensure that timeout=0
428
433
  # polls at least once.
429
- if deadline:
430
- interval = min(_WAIT_INTERVAL, deadline - monotonic())
431
- if interval < 0.0:
432
- break
434
+ if deadline is not None and monotonic() > deadline:
435
+ break
433
436
  finally:
434
437
  self._notifies_backlog = d
435
438
 
@@ -473,24 +476,43 @@ class Connection(BaseConnection[Row]):
473
476
  assert pipeline is self._pipeline
474
477
  self._pipeline = None
475
478
 
476
- def wait(self, gen: PQGen[RV], interval: float = _WAIT_INTERVAL) -> RV:
479
+ def wait(
480
+ self,
481
+ gen: PQGen[RV],
482
+ interval: float = _WAIT_INTERVAL,
483
+ timeout: float | None = None,
484
+ ) -> RV:
477
485
  """
478
486
  Consume a generator operating on the connection.
479
487
 
480
488
  The function must be used on generators that don't change connection
481
489
  fd (i.e. not on connect and reset).
490
+
491
+ Raise `~errors._WaitTimeout` if `!gen` doesn't complete within
492
+ `!timeout` seconds.
482
493
  """
483
494
  try:
484
- return waiting.wait(gen, self.pgconn.socket, interval=interval)
495
+ return waiting.wait(
496
+ gen, self.pgconn.socket, interval=interval, timeout=timeout
497
+ )
485
498
  except _INTERRUPTED:
486
499
  if self.pgconn.transaction_status == ACTIVE:
487
500
  # On Ctrl-C, try to cancel the query in the server, otherwise
488
501
  # the connection will remain stuck in ACTIVE state.
489
502
  self._try_cancel(timeout=5.0)
490
503
  try:
491
- waiting.wait(gen, self.pgconn.socket, interval=interval)
504
+ waiting.wait(
505
+ gen, self.pgconn.socket, interval=interval, timeout=5.0
506
+ )
492
507
  except e.QueryCanceled:
493
508
  pass # as expected
509
+ except e._WaitTimeout:
510
+ # The server didn't terminate the query: the connection
511
+ # is in an unknown state, so don't wait forever (#1371).
512
+ logger.warning(
513
+ "query not terminated after cancellation: closing connection"
514
+ )
515
+ self.pgconn.finish()
494
516
  raise
495
517
 
496
518
  def _set_autocommit(self, value: bool) -> None: