psycopg 3.3.2__tar.gz → 3.3.4__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.2 → psycopg-3.3.4}/PKG-INFO +8 -8
  2. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_adapters_map.py +3 -1
  3. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_capabilities.py +2 -2
  4. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_column.py +2 -5
  5. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_cursor_base.py +14 -9
  6. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_queries.py +4 -6
  7. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_tstrings.py +1 -1
  8. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_typeinfo.py +2 -4
  9. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/connection.py +2 -1
  10. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/connection_async.py +2 -1
  11. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/conninfo.py +1 -1
  12. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/__init__.py +2 -4
  13. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/array.py +4 -10
  14. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/composite.py +7 -9
  15. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/datetime.py +8 -16
  16. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/enum.py +10 -8
  17. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/multirange.py +2 -4
  18. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/range.py +2 -4
  19. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/uuid.py +4 -8
  20. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg.egg-info/PKG-INFO +8 -8
  21. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg.egg-info/requires.txt +7 -7
  22. {psycopg-3.3.2 → psycopg-3.3.4}/pyproject.toml +8 -8
  23. {psycopg-3.3.2 → psycopg-3.3.4}/LICENSE.txt +0 -0
  24. {psycopg-3.3.2 → psycopg-3.3.4}/README.rst +0 -0
  25. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/__init__.py +0 -0
  26. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_acompat.py +0 -0
  27. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_cmodule.py +0 -0
  28. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_compat.py +0 -0
  29. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_connection_base.py +0 -0
  30. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_connection_info.py +0 -0
  31. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_conninfo_attempts.py +0 -0
  32. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_conninfo_attempts_async.py +0 -0
  33. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_conninfo_utils.py +0 -0
  34. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_copy.py +0 -0
  35. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_copy_async.py +0 -0
  36. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_copy_base.py +0 -0
  37. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_dns.py +0 -0
  38. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_encodings.py +0 -0
  39. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_enums.py +0 -0
  40. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_oids.py +0 -0
  41. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_pipeline.py +0 -0
  42. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_pipeline_async.py +0 -0
  43. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_pipeline_base.py +0 -0
  44. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_preparing.py +0 -0
  45. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_py_transformer.py +0 -0
  46. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_server_cursor.py +0 -0
  47. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_server_cursor_async.py +0 -0
  48. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_server_cursor_base.py +0 -0
  49. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_struct.py +0 -0
  50. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_tpc.py +0 -0
  51. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_transformer.py +0 -0
  52. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_typemod.py +0 -0
  53. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_tz.py +0 -0
  54. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/_wrappers.py +0 -0
  55. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/abc.py +0 -0
  56. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/adapt.py +0 -0
  57. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/client_cursor.py +0 -0
  58. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/copy.py +0 -0
  59. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/crdb/__init__.py +0 -0
  60. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/crdb/_types.py +0 -0
  61. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/crdb/connection.py +0 -0
  62. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/cursor.py +0 -0
  63. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/cursor_async.py +0 -0
  64. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/dbapi20.py +0 -0
  65. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/errors.py +0 -0
  66. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/generators.py +0 -0
  67. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/postgres.py +0 -0
  68. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/_debug.py +0 -0
  69. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/_enums.py +0 -0
  70. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/_pq_ctypes.py +0 -0
  71. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/_pq_ctypes.pyi +0 -0
  72. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/abc.py +0 -0
  73. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/misc.py +0 -0
  74. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/pq/pq_ctypes.py +0 -0
  75. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/py.typed +0 -0
  76. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/raw_cursor.py +0 -0
  77. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/rows.py +0 -0
  78. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/sql.py +0 -0
  79. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/transaction.py +0 -0
  80. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/__init__.py +0 -0
  81. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/bool.py +0 -0
  82. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/hstore.py +0 -0
  83. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/json.py +0 -0
  84. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/net.py +0 -0
  85. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/none.py +0 -0
  86. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/numeric.py +0 -0
  87. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/numpy.py +0 -0
  88. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/shapely.py +0 -0
  89. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/types/string.py +0 -0
  90. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/version.py +0 -0
  91. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg/waiting.py +0 -0
  92. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg.egg-info/SOURCES.txt +0 -0
  93. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg.egg-info/dependency_links.txt +0 -0
  94. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg.egg-info/not-zip-safe +0 -0
  95. {psycopg-3.3.2 → psycopg-3.3.4}/psycopg.egg-info/top_level.txt +0 -0
  96. {psycopg-3.3.2 → psycopg-3.3.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psycopg
3
- Version: 3.3.2
3
+ Version: 3.3.4
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
@@ -33,9 +33,9 @@ License-File: LICENSE.txt
33
33
  Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
34
34
  Requires-Dist: tzdata; sys_platform == "win32"
35
35
  Provides-Extra: c
36
- Requires-Dist: psycopg-c==3.3.2; implementation_name != "pypy" and extra == "c"
36
+ Requires-Dist: psycopg-c==3.3.4; implementation_name != "pypy" and extra == "c"
37
37
  Provides-Extra: binary
38
- Requires-Dist: psycopg-binary==3.3.2; implementation_name != "pypy" and extra == "binary"
38
+ Requires-Dist: psycopg-binary==3.3.4; implementation_name != "pypy" and extra == "binary"
39
39
  Provides-Extra: pool
40
40
  Requires-Dist: psycopg-pool; extra == "pool"
41
41
  Provides-Extra: test
@@ -47,7 +47,7 @@ Requires-Dist: pytest-cov>=3.0; extra == "test"
47
47
  Requires-Dist: pytest-randomly>=3.5; extra == "test"
48
48
  Provides-Extra: dev
49
49
  Requires-Dist: ast-comments>=1.1.2; extra == "dev"
50
- Requires-Dist: black>=24.1.0; extra == "dev"
50
+ Requires-Dist: black>=26.1.0; extra == "dev"
51
51
  Requires-Dist: codespell>=2.2; extra == "dev"
52
52
  Requires-Dist: cython-lint>=0.16; extra == "dev"
53
53
  Requires-Dist: dnspython>=2.1; extra == "dev"
@@ -60,10 +60,10 @@ Requires-Dist: types-setuptools>=57.4; extra == "dev"
60
60
  Requires-Dist: types-shapely>=2.0; extra == "dev"
61
61
  Requires-Dist: wheel>=0.37; extra == "dev"
62
62
  Provides-Extra: docs
63
- Requires-Dist: Sphinx>=5.0; extra == "docs"
64
- Requires-Dist: furo==2022.6.21; extra == "docs"
65
- Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "docs"
66
- Requires-Dist: sphinx-autodoc-typehints>=1.12; extra == "docs"
63
+ Requires-Dist: Sphinx>=9.1; extra == "docs"
64
+ Requires-Dist: furo==2025.12.19; extra == "docs"
65
+ Requires-Dist: sphinx-autobuild>=2025.8.25; extra == "docs"
66
+ Requires-Dist: sphinx-autodoc-typehints>=3.10.2; extra == "docs"
67
67
  Dynamic: license-file
68
68
 
69
69
  Psycopg 3: PostgreSQL database adapter for Python
@@ -212,8 +212,10 @@ class AdaptersMap:
212
212
 
213
213
  # Look for the right class, including looking at superclasses
214
214
  for scls in cls.__mro__:
215
- if scls in dmap:
215
+ try:
216
216
  return dmap[scls]
217
+ except KeyError:
218
+ pass
217
219
 
218
220
  # If the adapter is not found, look for its name as a string
219
221
  fqn = scls.__module__ + "." + scls.__qualname__
@@ -98,9 +98,9 @@ class Capabilities:
98
98
 
99
99
  The expletive messages, are left to the user.
100
100
  """
101
- if feature in self._cache:
101
+ try:
102
102
  msg = self._cache[feature]
103
- else:
103
+ except KeyError:
104
104
  msg = self._get_unsupported_message(feature, want_version)
105
105
  self._cache[feature] = msg
106
106
 
@@ -32,12 +32,9 @@ class Column(Sequence[Any]):
32
32
  self._fmod = res.fmod(index)
33
33
  self._fsize = res.fsize(index)
34
34
 
35
- _attrs = tuple(
36
- attrgetter(attr)
37
- for attr in """
35
+ _attrs = tuple(attrgetter(attr) for attr in """
38
36
  name type_code display_size internal_size precision scale null_ok
39
- """.split()
40
- )
37
+ """.split())
41
38
 
42
39
  def __repr__(self) -> str:
43
40
  return (
@@ -49,7 +49,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
49
49
  __slots__ = """
50
50
  _conn format _adapters arraysize _closed _results pgresult _pos
51
51
  _iresult _rowcount _query _tx _last_query _row_factory _make_row
52
- _pgconn _execmany_returning
52
+ _pgconn _execmany_returning _statusmessage
53
53
  __weakref__
54
54
  """.split()
55
55
 
@@ -79,6 +79,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
79
79
  self._pos = 0
80
80
  self._iresult = 0
81
81
  self._rowcount = -1
82
+ self._statusmessage: bytes | None = None
82
83
  self._query: PostgresQuery | None
83
84
  # None if executemany() not executing, True/False according to returning state
84
85
  self._execmany_returning: bool | None = None
@@ -178,8 +179,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
178
179
 
179
180
  `!None` if the cursor doesn't have a result available.
180
181
  """
181
- msg = self.pgresult.command_status if self.pgresult else None
182
- return msg.decode() if msg else None
182
+ return self._statusmessage.decode() if self._statusmessage else None
183
183
 
184
184
  def _make_row_maker(self) -> RowMaker[Row]:
185
185
  raise NotImplementedError
@@ -289,9 +289,10 @@ class BaseCursor(Generic[ConnectionType, Row]):
289
289
  if prep is Prepare.SHOULD:
290
290
  self._send_prepare(name, pgq)
291
291
  if not self._conn._pipeline:
292
- (result,) = yield from execute(self._pgconn)
293
- if result.status == FATAL_ERROR:
294
- raise e.error_from_result(result, encoding=self._encoding)
292
+ results = yield from execute(self._pgconn)
293
+ for result in results:
294
+ if result.status == FATAL_ERROR:
295
+ raise e.error_from_result(result, encoding=self._encoding)
295
296
  # Then execute it.
296
297
  self._send_query_prepared(name, pgq, binary=binary)
297
298
 
@@ -524,6 +525,7 @@ class BaseCursor(Generic[ConnectionType, Row]):
524
525
  nrows = self.pgresult.command_tuples
525
526
  self._rowcount = nrows if nrows is not None else -1
526
527
 
528
+ self._statusmessage = res.command_status
527
529
  self._make_row = self._make_row_maker()
528
530
 
529
531
  def _set_results(self, results: list[PGresult]) -> None:
@@ -539,9 +541,12 @@ class BaseCursor(Generic[ConnectionType, Row]):
539
541
  self._select_current_result(0)
540
542
  else:
541
543
  # In non-returning case, set rowcount to the cumulated number of
542
- # rows of executed queries.
543
- for res in results:
544
- self._rowcount += res.command_tuples or 0
544
+ # rows of executed queries. Keep the last result's command_status
545
+ # so that statusmessage is available after the batch completes.
546
+ if results:
547
+ self._statusmessage = results[-1].command_status
548
+ for res in results:
549
+ self._rowcount += res.command_tuples or 0
545
550
 
546
551
  @classmethod
547
552
  def _loaders_changed(
@@ -82,7 +82,7 @@ class PostgresQuery:
82
82
  else:
83
83
  f = _query2pg_nocache
84
84
 
85
- (self.query, self._want_formats, self._order, self._parts) = f(
85
+ self.query, self._want_formats, self._order, self._parts = f(
86
86
  query, self._tx.encoding
87
87
  )
88
88
  else:
@@ -281,7 +281,7 @@ class PostgresClientQuery(PostgresQuery):
281
281
  else:
282
282
  f = _query2pg_client_nocache
283
283
 
284
- (self.template, self._order, self._parts) = f(query, self._tx.encoding)
284
+ self.template, self._order, self._parts = f(query, self._tx.encoding)
285
285
  else:
286
286
  self.query = query
287
287
  self._order = None
@@ -360,8 +360,7 @@ def _query2pg_client_nocache(
360
360
  _query2pg_client = lru_cache(_query2pg_client_nocache)
361
361
 
362
362
 
363
- _re_placeholder = re.compile(
364
- rb"""(?x)
363
+ _re_placeholder = re.compile(rb"""(?x)
365
364
  % # a literal %
366
365
  (?:
367
366
  (?:
@@ -371,8 +370,7 @@ _re_placeholder = re.compile(
371
370
  |
372
371
  (?:.) # or any char, really
373
372
  )
374
- """
375
- )
373
+ """)
376
374
 
377
375
 
378
376
  def _split_query(
@@ -83,7 +83,7 @@ class TemplateProcessor:
83
83
  elif fmt == FMT_SQL:
84
84
  # It must have been processed already
85
85
  raise e.ProgrammingError(
86
- "sql values must be sql.Composite, sql.SQL, or Template;"
86
+ "sql values must be sql.Composed, sql.SQL, or Template;"
87
87
  f" got {type(item.value).__qualname__}"
88
88
  f" in {{{item.expression}:{fmt}}}"
89
89
  )
@@ -158,16 +158,14 @@ class TypeInfo:
158
158
 
159
159
  @classmethod
160
160
  def _get_info_query(cls, conn: BaseConnection[Any]) -> QueryNoTemplate:
161
- return sql.SQL(
162
- """\
161
+ return sql.SQL("""\
163
162
  SELECT
164
163
  typname AS name, oid, typarray AS array_oid,
165
164
  oid::regtype::text AS regtype, typdelim AS delimiter
166
165
  FROM pg_type t
167
166
  WHERE t.oid = {regtype}
168
167
  ORDER BY t.oid
169
- """
170
- ).format(regtype=cls._to_regtype(conn))
168
+ """).format(regtype=cls._to_regtype(conn))
171
169
 
172
170
  @classmethod
173
171
  def _has_to_regtype_function(cls, conn: BaseConnection[Any]) -> bool:
@@ -126,7 +126,8 @@ class Connection(BaseConnection[Row]):
126
126
  lines = [str(last_ex)]
127
127
  lines.append("Multiple connection attempts failed. All failures were:")
128
128
  lines.extend((f"- {descr}: {error}" for error, descr in conn_errors))
129
- raise type(last_ex)("\n".join(lines)).with_traceback(None)
129
+ new_ex = type(last_ex)("\n".join(lines), pgconn=last_ex.pgconn)
130
+ raise new_ex.with_traceback(None)
130
131
 
131
132
  if (
132
133
  capabilities.has_used_gssapi()
@@ -142,7 +142,8 @@ class AsyncConnection(BaseConnection[Row]):
142
142
  lines = [str(last_ex)]
143
143
  lines.append("Multiple connection attempts failed. All failures were:")
144
144
  lines.extend(f"- {descr}: {error}" for error, descr in conn_errors)
145
- raise type(last_ex)("\n".join(lines)).with_traceback(None)
145
+ new_ex = type(last_ex)("\n".join(lines), pgconn=last_ex.pgconn)
146
+ raise new_ex.with_traceback(None)
146
147
 
147
148
  if (
148
149
  capabilities.has_used_gssapi()
@@ -13,7 +13,7 @@ from . import errors as e
13
13
  from . import pq
14
14
  from .abc import ConnDict, ConnParam
15
15
 
16
- # re-exoprts
16
+ # re-exports
17
17
  conninfo_attempts = _conninfo_attempts.conninfo_attempts
18
18
  conninfo_attempts_async = _conninfo_attempts_async.conninfo_attempts_async
19
19
 
@@ -105,12 +105,10 @@ def import_from_libpq() -> None:
105
105
  raise ImportError(f"requested psycopg implementation '{impl}' unknown")
106
106
  else:
107
107
  sattempts = "\n".join(f"- {attempt}" for attempt in attempts)
108
- raise ImportError(
109
- f"""\
108
+ raise ImportError(f"""\
110
109
  no pq wrapper available.
111
110
  Attempts made:
112
- {sattempts}"""
113
- )
111
+ {sattempts}""")
114
112
 
115
113
 
116
114
  import_from_libpq()
@@ -204,14 +204,11 @@ def _get_needs_quotes_regexp(delimiter: bytes) -> re.Pattern[bytes]:
204
204
  they are empty strings, contain curly braces, delimiter characters,
205
205
  double quotes, backslashes, or white space, or match the word NULL.
206
206
  """
207
- return re.compile(
208
- rb"""(?xi)
207
+ return re.compile(rb"""(?xi)
209
208
  ^$ # the empty string
210
209
  | ["{}%s\\\s] # or a char to escape
211
210
  | ^null$ # or the word NULL
212
- """
213
- % delimiter
214
- )
211
+ """ % delimiter)
215
212
 
216
213
 
217
214
  class ListBinaryDumper(BaseListDumper):
@@ -431,15 +428,12 @@ def _get_array_parse_regexp(delimiter: bytes) -> re.Pattern[bytes]:
431
428
  """
432
429
  Return a regexp to tokenize an array representation into item and brackets
433
430
  """
434
- return re.compile(
435
- rb"""(?xi)
431
+ return re.compile(rb"""(?xi)
436
432
  ( [{}] # open or closed bracket
437
433
  | " (?: [^"\\] | \\. )* " # or a quoted string
438
434
  | [^"{}%s\\]+ # or an unquoted non-empty string
439
435
  ) ,?
440
- """
441
- % delimiter
442
- )
436
+ """ % delimiter)
443
437
 
444
438
 
445
439
  def _load_binary(data: Buffer, tx: Transformer) -> list[Any]:
@@ -63,8 +63,7 @@ class CompositeInfo(TypeInfo):
63
63
 
64
64
  @classmethod
65
65
  def _get_info_query(cls, conn: BaseConnection[Any]) -> abc.QueryNoTemplate:
66
- return sql.SQL(
67
- """\
66
+ return sql.SQL("""\
68
67
  SELECT
69
68
  t.typname AS name, t.oid AS oid, t.typarray AS array_oid,
70
69
  t.oid::regtype::text AS regtype,
@@ -88,8 +87,7 @@ LEFT JOIN (
88
87
  GROUP BY attrelid
89
88
  ) a ON a.attrelid = t.typrelid
90
89
  WHERE t.oid = {regtype}
91
- """
92
- ).format(regtype=cls._to_regtype(conn))
90
+ """).format(regtype=cls._to_regtype(conn))
93
91
 
94
92
 
95
93
  class TupleDumper(RecursiveDumper):
@@ -208,8 +206,10 @@ class RecordBinaryLoader(Loader):
208
206
  return tx.load_sequence(record)
209
207
 
210
208
  def _get_transformer(self, key: tuple[int, ...]) -> abc.Transformer:
211
- if key in self._txs:
209
+ try:
212
210
  return self._txs[key]
211
+ except KeyError:
212
+ pass
213
213
 
214
214
  tx = Transformer(self._ctx)
215
215
  tx.set_loader_types([*key], self.format)
@@ -472,13 +472,11 @@ def _parse_text_record(data: abc.Buffer) -> list[bytes | None]:
472
472
  return record
473
473
 
474
474
 
475
- _re_tokenize = re.compile(
476
- rb"""(?x)
475
+ _re_tokenize = re.compile(rb"""(?x)
477
476
  (,) # an empty token, representing NULL
478
477
  | " ((?: [^"] | "")*) " ,? # or a quoted string
479
478
  | ([^",)]+) ,? # or an unquoted string
480
- """
481
- )
479
+ """)
482
480
  _re_undouble = re.compile(rb'(["\\])\1')
483
481
 
484
482
 
@@ -325,14 +325,12 @@ class TimeBinaryLoader(Loader):
325
325
 
326
326
 
327
327
  class TimetzLoader(Loader):
328
- _re_format = re.compile(
329
- rb"""(?ix)
328
+ _re_format = re.compile(rb"""(?ix)
330
329
  ^
331
330
  (\d+) : (\d+) : (\d+) (?: \. (\d+) )? # Time and micros
332
331
  ([-+]) (\d+) (?: : (\d+) )? (?: : (\d+) )? # Timezone
333
332
  $
334
- """
335
- )
333
+ """)
336
334
 
337
335
  def load(self, data: Buffer) -> time:
338
336
  if not (m := self._re_format.match(data)):
@@ -381,18 +379,15 @@ class TimetzBinaryLoader(Loader):
381
379
 
382
380
 
383
381
  class TimestampLoader(Loader):
384
- _re_format = re.compile(
385
- rb"""(?ix)
382
+ _re_format = re.compile(rb"""(?ix)
386
383
  ^
387
384
  (\d+) [^a-z0-9] (\d+) [^a-z0-9] (\d+) # Date
388
385
  (?: T | [^a-z0-9] ) # Separator, including T
389
386
  (\d+) [^a-z0-9] (\d+) [^a-z0-9] (\d+) # Time
390
387
  (?: \.(\d+) )? # Micros
391
388
  $
392
- """
393
- )
394
- _re_format_pg = re.compile(
395
- rb"""(?ix)
389
+ """)
390
+ _re_format_pg = re.compile(rb"""(?ix)
396
391
  ^
397
392
  [a-z]+ [^a-z0-9] # DoW, separator
398
393
  (\d+|[a-z]+) [^a-z0-9] # Month or day
@@ -401,8 +396,7 @@ class TimestampLoader(Loader):
401
396
  (?: \.(\d+) )? # Micros
402
397
  [^a-z0-9] (\d+) # Year
403
398
  $
404
- """
405
- )
399
+ """)
406
400
 
407
401
  _ORDER_YMD = 0
408
402
  _ORDER_DMY = 1
@@ -477,8 +471,7 @@ class TimestampBinaryLoader(Loader):
477
471
 
478
472
 
479
473
  class TimestamptzLoader(Loader):
480
- _re_format = re.compile(
481
- rb"""(?ix)
474
+ _re_format = re.compile(rb"""(?ix)
482
475
  ^
483
476
  (\d+) [^a-z0-9] (\d+) [^a-z0-9] (\d+) # Date
484
477
  (?: T | [^a-z0-9] ) # Separator, including T
@@ -486,8 +479,7 @@ class TimestamptzLoader(Loader):
486
479
  (?: \.(\d+) )? # Micros
487
480
  ([-+]) (\d+) (?: : (\d+) )? (?: : (\d+) )? # Timezone
488
481
  $
489
- """
490
- )
482
+ """)
491
483
 
492
484
  def __init__(self, oid: int, context: AdaptContext | None = None):
493
485
  super().__init__(oid, context)
@@ -43,31 +43,33 @@ class EnumInfo(TypeInfo):
43
43
  name: str,
44
44
  oid: int,
45
45
  array_oid: int,
46
+ # A bit ugly: this should have been a keyword-only argument, but it has
47
+ # been it the wild accepting a positional argument for too long to fix.
46
48
  labels: Sequence[str],
49
+ *,
50
+ regtype: str = "",
47
51
  ):
48
- super().__init__(name, oid, array_oid)
52
+ super().__init__(name, oid, array_oid, regtype=regtype)
49
53
  self.labels = labels
50
54
  # Will be set by register_enum()
51
55
  self.enum: type[Enum] | None = None
52
56
 
53
57
  @classmethod
54
58
  def _get_info_query(cls, conn: BaseConnection[Any]) -> QueryNoTemplate:
55
- return sql.SQL(
56
- """\
57
- SELECT name, oid, array_oid, array_agg(label) AS labels
59
+ return sql.SQL("""\
60
+ SELECT name, oid, array_oid, regtype, array_agg(label) AS labels
58
61
  FROM (
59
62
  SELECT
60
63
  t.typname AS name, t.oid AS oid, t.typarray AS array_oid,
61
- e.enumlabel AS label
64
+ t.oid::regtype::text AS regtype, e.enumlabel AS label
62
65
  FROM pg_type t
63
66
  LEFT JOIN pg_enum e
64
67
  ON e.enumtypid = t.oid
65
68
  WHERE t.oid = {regtype}
66
69
  ORDER BY e.enumsortorder
67
70
  ) x
68
- GROUP BY name, oid, array_oid
69
- """
70
- ).format(regtype=cls._to_regtype(conn))
71
+ GROUP BY name, oid, array_oid, regtype
72
+ """).format(regtype=cls._to_regtype(conn))
71
73
 
72
74
 
73
75
  class _BaseEnumLoader(Loader, Generic[E]):
@@ -51,16 +51,14 @@ class MultirangeInfo(TypeInfo):
51
51
  raise e.NotSupportedError(
52
52
  "multirange types are only available from PostgreSQL 14"
53
53
  )
54
- return sql.SQL(
55
- """\
54
+ return sql.SQL("""\
56
55
  SELECT t.typname AS name, t.oid AS oid, t.typarray AS array_oid,
57
56
  t.oid::regtype::text AS regtype,
58
57
  r.rngtypid AS range_oid, r.rngsubtype AS subtype_oid
59
58
  FROM pg_type t
60
59
  JOIN pg_range r ON t.oid = r.rngmultitypid
61
60
  WHERE t.oid = {regtype}
62
- """
63
- ).format(regtype=cls._to_regtype(conn))
61
+ """).format(regtype=cls._to_regtype(conn))
64
62
 
65
63
  def _added(self, registry: TypesRegistry) -> None:
66
64
  # Map multiranges ranges and subtypes to info
@@ -55,16 +55,14 @@ class RangeInfo(TypeInfo):
55
55
 
56
56
  @classmethod
57
57
  def _get_info_query(cls, conn: BaseConnection[Any]) -> QueryNoTemplate:
58
- return sql.SQL(
59
- """\
58
+ return sql.SQL("""\
60
59
  SELECT t.typname AS name, t.oid AS oid, t.typarray AS array_oid,
61
60
  t.oid::regtype::text AS regtype,
62
61
  r.rngsubtype AS subtype_oid
63
62
  FROM pg_type t
64
63
  JOIN pg_range r ON t.oid = r.rngtypid
65
64
  WHERE t.oid = {regtype}
66
- """
67
- ).format(regtype=cls._to_regtype(conn))
65
+ """).format(regtype=cls._to_regtype(conn))
68
66
 
69
67
  def _added(self, registry: TypesRegistry) -> None:
70
68
  # Map ranges subtypes to info
@@ -25,14 +25,14 @@ class UUIDDumper(Dumper):
25
25
  oid = _oids.UUID_OID
26
26
 
27
27
  def dump(self, obj: uuid.UUID) -> Buffer | None:
28
- return obj.hex.encode()
28
+ return b"%032x" % obj.int
29
29
 
30
30
 
31
31
  class UUIDBinaryDumper(UUIDDumper):
32
32
  format = Format.BINARY
33
33
 
34
34
  def dump(self, obj: uuid.UUID) -> Buffer | None:
35
- return obj.bytes
35
+ return obj.int.to_bytes(16, "big")
36
36
 
37
37
 
38
38
  class UUIDLoader(Loader):
@@ -43,18 +43,14 @@ class UUIDLoader(Loader):
43
43
  from uuid import UUID
44
44
 
45
45
  def load(self, data: Buffer) -> uuid.UUID:
46
- if isinstance(data, memoryview):
47
- data = bytes(data)
48
- return UUID(data.decode())
46
+ return UUID((bytes(data) if isinstance(data, memoryview) else data).decode())
49
47
 
50
48
 
51
49
  class UUIDBinaryLoader(UUIDLoader):
52
50
  format = Format.BINARY
53
51
 
54
52
  def load(self, data: Buffer) -> uuid.UUID:
55
- if isinstance(data, memoryview):
56
- data = bytes(data)
57
- return UUID(bytes=data)
53
+ return UUID(bytes=(bytes(data) if isinstance(data, memoryview) else data))
58
54
 
59
55
 
60
56
  def register_default_adapters(context: AdaptContext) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psycopg
3
- Version: 3.3.2
3
+ Version: 3.3.4
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
@@ -33,9 +33,9 @@ License-File: LICENSE.txt
33
33
  Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
34
34
  Requires-Dist: tzdata; sys_platform == "win32"
35
35
  Provides-Extra: c
36
- Requires-Dist: psycopg-c==3.3.2; implementation_name != "pypy" and extra == "c"
36
+ Requires-Dist: psycopg-c==3.3.4; implementation_name != "pypy" and extra == "c"
37
37
  Provides-Extra: binary
38
- Requires-Dist: psycopg-binary==3.3.2; implementation_name != "pypy" and extra == "binary"
38
+ Requires-Dist: psycopg-binary==3.3.4; implementation_name != "pypy" and extra == "binary"
39
39
  Provides-Extra: pool
40
40
  Requires-Dist: psycopg-pool; extra == "pool"
41
41
  Provides-Extra: test
@@ -47,7 +47,7 @@ Requires-Dist: pytest-cov>=3.0; extra == "test"
47
47
  Requires-Dist: pytest-randomly>=3.5; extra == "test"
48
48
  Provides-Extra: dev
49
49
  Requires-Dist: ast-comments>=1.1.2; extra == "dev"
50
- Requires-Dist: black>=24.1.0; extra == "dev"
50
+ Requires-Dist: black>=26.1.0; extra == "dev"
51
51
  Requires-Dist: codespell>=2.2; extra == "dev"
52
52
  Requires-Dist: cython-lint>=0.16; extra == "dev"
53
53
  Requires-Dist: dnspython>=2.1; extra == "dev"
@@ -60,10 +60,10 @@ Requires-Dist: types-setuptools>=57.4; extra == "dev"
60
60
  Requires-Dist: types-shapely>=2.0; extra == "dev"
61
61
  Requires-Dist: wheel>=0.37; extra == "dev"
62
62
  Provides-Extra: docs
63
- Requires-Dist: Sphinx>=5.0; extra == "docs"
64
- Requires-Dist: furo==2022.6.21; extra == "docs"
65
- Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "docs"
66
- Requires-Dist: sphinx-autodoc-typehints>=1.12; extra == "docs"
63
+ Requires-Dist: Sphinx>=9.1; extra == "docs"
64
+ Requires-Dist: furo==2025.12.19; extra == "docs"
65
+ Requires-Dist: sphinx-autobuild>=2025.8.25; extra == "docs"
66
+ Requires-Dist: sphinx-autodoc-typehints>=3.10.2; extra == "docs"
67
67
  Dynamic: license-file
68
68
 
69
69
  Psycopg 3: PostgreSQL database adapter for Python
@@ -8,16 +8,16 @@ tzdata
8
8
  [binary]
9
9
 
10
10
  [binary:implementation_name != "pypy"]
11
- psycopg-binary==3.3.2
11
+ psycopg-binary==3.3.4
12
12
 
13
13
  [c]
14
14
 
15
15
  [c:implementation_name != "pypy"]
16
- psycopg-c==3.3.2
16
+ psycopg-c==3.3.4
17
17
 
18
18
  [dev]
19
19
  ast-comments>=1.1.2
20
- black>=24.1.0
20
+ black>=26.1.0
21
21
  codespell>=2.2
22
22
  cython-lint>=0.16
23
23
  dnspython>=2.1
@@ -31,10 +31,10 @@ types-shapely>=2.0
31
31
  wheel>=0.37
32
32
 
33
33
  [docs]
34
- Sphinx>=5.0
35
- furo==2022.6.21
36
- sphinx-autobuild>=2021.3.14
37
- sphinx-autodoc-typehints>=1.12
34
+ Sphinx>=9.1
35
+ furo==2025.12.19
36
+ sphinx-autobuild>=2025.8.25
37
+ sphinx-autodoc-typehints>=3.10.2
38
38
 
39
39
  [pool]
40
40
  psycopg-pool
@@ -7,7 +7,7 @@ name = "psycopg"
7
7
  description = "PostgreSQL database adapter for Python"
8
8
 
9
9
  # STOP AND READ! if you change:
10
- version = "3.3.2"
10
+ version = "3.3.4"
11
11
  # also change:
12
12
  # - `docs/news.rst` to declare this as the current version or an unreleased one;
13
13
  # - `psycopg_c/pyproject.toml` to the same version;
@@ -60,10 +60,10 @@ content-type = "text/x-rst"
60
60
 
61
61
  [project.optional-dependencies]
62
62
  c = [
63
- "psycopg-c == 3.3.2; implementation_name != \"pypy\"",
63
+ "psycopg-c == 3.3.4; implementation_name != \"pypy\"",
64
64
  ]
65
65
  binary = [
66
- "psycopg-binary == 3.3.2; implementation_name != \"pypy\"",
66
+ "psycopg-binary == 3.3.4; implementation_name != \"pypy\"",
67
67
  ]
68
68
  pool = [
69
69
  "psycopg-pool",
@@ -79,7 +79,7 @@ test = [
79
79
  ]
80
80
  dev = [
81
81
  "ast-comments >= 1.1.2",
82
- "black >= 24.1.0",
82
+ "black >= 26.1.0",
83
83
  "codespell >= 2.2",
84
84
  "cython-lint >= 0.16",
85
85
  "dnspython >= 2.1",
@@ -93,10 +93,10 @@ dev = [
93
93
  "wheel >= 0.37",
94
94
  ]
95
95
  docs = [
96
- "Sphinx >= 5.0",
97
- "furo == 2022.6.21",
98
- "sphinx-autobuild >= 2021.3.14",
99
- "sphinx-autodoc-typehints >= 1.12",
96
+ "Sphinx >= 9.1",
97
+ "furo == 2025.12.19",
98
+ "sphinx-autobuild >= 2025.8.25",
99
+ "sphinx-autodoc-typehints >= 3.10.2",
100
100
  ]
101
101
 
102
102
  [tool.setuptools]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes