psycopg 3.3.6__tar.gz → 3.3.7__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.6 → psycopg-3.3.7}/PKG-INFO +3 -3
  2. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_dns.py +10 -8
  3. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/adapt.py +6 -2
  4. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/generators.py +6 -8
  5. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/transaction.py +25 -3
  6. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/array.py +91 -59
  7. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/composite.py +22 -12
  8. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/datetime.py +6 -3
  9. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/json.py +1 -1
  10. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/PKG-INFO +3 -3
  11. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/requires.txt +2 -2
  12. {psycopg-3.3.6 → psycopg-3.3.7}/pyproject.toml +3 -3
  13. {psycopg-3.3.6 → psycopg-3.3.7}/LICENSE.txt +0 -0
  14. {psycopg-3.3.6 → psycopg-3.3.7}/README.rst +0 -0
  15. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/__init__.py +0 -0
  16. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_acompat.py +0 -0
  17. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_adapters_map.py +0 -0
  18. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_capabilities.py +0 -0
  19. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_cmodule.py +0 -0
  20. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_column.py +0 -0
  21. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_compat.py +0 -0
  22. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_connection_base.py +0 -0
  23. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_connection_info.py +0 -0
  24. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_conninfo_attempts.py +0 -0
  25. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_conninfo_attempts_async.py +0 -0
  26. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_conninfo_utils.py +0 -0
  27. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_copy.py +0 -0
  28. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_copy_async.py +0 -0
  29. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_copy_base.py +0 -0
  30. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_cursor_base.py +0 -0
  31. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_encodings.py +0 -0
  32. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_enums.py +0 -0
  33. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_oids.py +0 -0
  34. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_pipeline.py +0 -0
  35. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_pipeline_async.py +0 -0
  36. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_pipeline_base.py +0 -0
  37. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_preparing.py +0 -0
  38. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_py_transformer.py +0 -0
  39. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_queries.py +0 -0
  40. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_server_cursor.py +0 -0
  41. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_server_cursor_async.py +0 -0
  42. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_server_cursor_base.py +0 -0
  43. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_struct.py +0 -0
  44. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_tpc.py +0 -0
  45. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_transformer.py +0 -0
  46. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_tstrings.py +0 -0
  47. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_typeinfo.py +0 -0
  48. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_typemod.py +0 -0
  49. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_tz.py +0 -0
  50. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_wrappers.py +0 -0
  51. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/abc.py +0 -0
  52. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/client_cursor.py +0 -0
  53. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/connection.py +0 -0
  54. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/connection_async.py +0 -0
  55. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/conninfo.py +0 -0
  56. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/copy.py +0 -0
  57. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/crdb/__init__.py +0 -0
  58. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/crdb/_types.py +0 -0
  59. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/crdb/connection.py +0 -0
  60. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/cursor.py +0 -0
  61. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/cursor_async.py +0 -0
  62. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/dbapi20.py +0 -0
  63. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/errors.py +0 -0
  64. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/postgres.py +0 -0
  65. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/__init__.py +0 -0
  66. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_debug.py +0 -0
  67. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_enums.py +0 -0
  68. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_pq_ctypes.py +0 -0
  69. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_pq_ctypes.pyi +0 -0
  70. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/abc.py +0 -0
  71. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/misc.py +0 -0
  72. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/pq_ctypes.py +0 -0
  73. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/py.typed +0 -0
  74. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/raw_cursor.py +0 -0
  75. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/rows.py +0 -0
  76. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/sql.py +0 -0
  77. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/__init__.py +0 -0
  78. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/bool.py +0 -0
  79. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/enum.py +0 -0
  80. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/hstore.py +0 -0
  81. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/multirange.py +0 -0
  82. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/net.py +0 -0
  83. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/none.py +0 -0
  84. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/numeric.py +0 -0
  85. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/numpy.py +0 -0
  86. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/range.py +0 -0
  87. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/shapely.py +0 -0
  88. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/string.py +0 -0
  89. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/uuid.py +0 -0
  90. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/version.py +0 -0
  91. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/waiting.py +0 -0
  92. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/SOURCES.txt +0 -0
  93. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/dependency_links.txt +0 -0
  94. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/not-zip-safe +0 -0
  95. {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/top_level.txt +0 -0
  96. {psycopg-3.3.6 → psycopg-3.3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psycopg
3
- Version: 3.3.6
3
+ Version: 3.3.7
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
@@ -34,9 +34,9 @@ License-File: LICENSE.txt
34
34
  Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
35
35
  Requires-Dist: tzdata; sys_platform == "win32"
36
36
  Provides-Extra: c
37
- Requires-Dist: psycopg-c==3.3.6; implementation_name != "pypy" and extra == "c"
37
+ Requires-Dist: psycopg-c==3.3.7; implementation_name != "pypy" and extra == "c"
38
38
  Provides-Extra: binary
39
- Requires-Dist: psycopg-binary==3.3.6; implementation_name != "pypy" and extra == "binary"
39
+ Requires-Dist: psycopg-binary==3.3.7; implementation_name != "pypy" and extra == "binary"
40
40
  Provides-Extra: pool
41
41
  Requires-Dist: psycopg-pool; extra == "pool"
42
42
  Provides-Extra: test
@@ -1,4 +1,3 @@
1
- # type: ignore # dnspython is currently optional and mypy fails if missing
2
1
  """
3
2
  DNS query support
4
3
  """
@@ -13,12 +12,13 @@ import warnings
13
12
  from random import randint
14
13
  from typing import TYPE_CHECKING, Any, DefaultDict, NamedTuple
15
14
  from collections import defaultdict
16
- from collections.abc import Sequence
15
+ from collections.abc import Iterable, Sequence
17
16
 
18
17
  try:
19
18
  from dns.resolver import Cache, Resolver
20
19
  from dns.exception import DNSException
21
20
  from dns.asyncresolver import Resolver as AsyncResolver
21
+ from dns.rdtypes.IN.SRV import SRV
22
22
  except ImportError:
23
23
  raise ImportError(
24
24
  "the module psycopg._dns requires the package 'dnspython' installed"
@@ -28,7 +28,7 @@ from . import conninfo
28
28
  from . import errors as e
29
29
 
30
30
  if TYPE_CHECKING:
31
- from dns.rdtypes.IN.SRV import SRV
31
+ from dns.rdata import Rdata
32
32
 
33
33
  resolver = Resolver()
34
34
  resolver.cache = Cache()
@@ -56,9 +56,9 @@ async def resolve_hostaddr_async(params: dict[str, Any]) -> dict[str, Any]:
56
56
 
57
57
  for attempt in await conninfo.conninfo_attempts_async(params):
58
58
  if attempt.get("host") is not None:
59
- hosts.append(attempt["host"])
59
+ hosts.append(str(attempt["host"]))
60
60
  if attempt.get("hostaddr") is not None:
61
- hostaddrs.append(attempt["hostaddr"])
61
+ hostaddrs.append(str(attempt["hostaddr"]))
62
62
  if attempt.get("port") is not None:
63
63
  ports.append(str(attempt["port"]))
64
64
 
@@ -165,6 +165,7 @@ class Rfc2782Resolver:
165
165
  return out if srv_found else []
166
166
 
167
167
  def _resolve_srv(self, hp: HostPort) -> list[HostPort]:
168
+ ans: Iterable[Rdata]
168
169
  try:
169
170
  ans = resolver.resolve(hp.host, "SRV")
170
171
  except DNSException:
@@ -172,15 +173,16 @@ class Rfc2782Resolver:
172
173
  return self._get_solved_entries(hp, ans)
173
174
 
174
175
  async def _resolve_srv_async(self, hp: HostPort) -> list[HostPort]:
176
+ ans: Iterable[Rdata]
175
177
  try:
176
178
  ans = await async_resolver.resolve(hp.host, "SRV")
177
179
  except DNSException:
178
180
  ans = ()
179
181
  return self._get_solved_entries(hp, ans)
180
182
 
181
- def _get_solved_entries(
182
- self, hp: HostPort, entries: Sequence[SRV]
183
- ) -> list[HostPort]:
183
+ def _get_solved_entries(self, hp: HostPort, ans: Iterable[Rdata]) -> list[HostPort]:
184
+ # A SRV query only returns SRV records: this only narrows the type.
185
+ entries = [rr for rr in ans if isinstance(rr, SRV)]
184
186
  if not entries:
185
187
  # No SRV entry found. Delegate the libpq a QNAME=target lookup
186
188
  if hp.target and hp.port.lower() != "srv":
@@ -141,7 +141,9 @@ class RecursiveDumper(Dumper):
141
141
 
142
142
  def __init__(self, cls: type, context: abc.AdaptContext | None = None):
143
143
  super().__init__(cls, context)
144
- self._tx = Transformer.from_context(context)
144
+ # Don't use from_context(): if the context is a transformer, it will
145
+ # cache this adapter, creating a reference loop.
146
+ self._tx = Transformer(context)
145
147
 
146
148
 
147
149
  class RecursiveLoader(Loader):
@@ -149,4 +151,6 @@ class RecursiveLoader(Loader):
149
151
 
150
152
  def __init__(self, oid: int, context: abc.AdaptContext | None = None):
151
153
  super().__init__(oid, context)
152
- self._tx = Transformer.from_context(context)
154
+ # Don't use from_context(): if the context is a transformer, it will
155
+ # cache this adapter, creating a reference loop.
156
+ self._tx = Transformer(context)
@@ -83,10 +83,8 @@ def _connect(conninfo: str) -> PQGenConn[PGconn]:
83
83
 
84
84
  if status == POLL_READING or status == POLL_WRITING:
85
85
  wait = WAIT_R if status == POLL_READING else WAIT_W
86
- while True:
87
- ready = yield conn.socket, wait
88
- if ready:
89
- break
86
+ while not (yield conn.socket, wait):
87
+ pass
90
88
 
91
89
  elif status == POLL_OK:
92
90
  break
@@ -109,10 +107,10 @@ def _cancel(cancel_conn: PGcancelConn) -> PQGenConn[None]:
109
107
  while True:
110
108
  if (status := cancel_conn.poll()) == POLL_OK:
111
109
  break
112
- elif status == POLL_READING:
113
- yield cancel_conn.socket, WAIT_R
114
- elif status == POLL_WRITING:
115
- yield cancel_conn.socket, WAIT_W
110
+ elif status == POLL_READING or status == POLL_WRITING:
111
+ wait = WAIT_R if status == POLL_READING else WAIT_W
112
+ while not (yield cancel_conn.socket, wait):
113
+ pass
116
114
  elif status == POLL_FAILED:
117
115
  raise e.OperationalError(
118
116
  f"cancellation failed: {cancel_conn.get_error_message()}"
@@ -96,9 +96,11 @@ class BaseTransaction(Generic[ConnectionType]):
96
96
  sp = f"{self.savepoint_name!r} " if self.savepoint_name else ""
97
97
  return f"<{cls} {sp}({self.status.value}) {info} at 0x{id(self):x}>"
98
98
 
99
- def _enter_gen(self) -> PQGen[None]:
99
+ def _check_enter(self) -> None:
100
100
  if self.status != self.Status.NOT_STARTED:
101
101
  raise TypeError("transaction blocks can be used only once")
102
+
103
+ def _enter_gen(self) -> PQGen[None]:
102
104
  self.status = self.Status.ACTIVE
103
105
 
104
106
  self._push_savepoint()
@@ -257,7 +259,17 @@ class Transaction(BaseTransaction["Connection[Any]"]):
257
259
 
258
260
  def __enter__(self) -> Self:
259
261
  with self._conn.lock:
260
- self._conn.wait(self._enter_gen())
262
+ self._check_enter()
263
+ try:
264
+ self._conn.wait(self._enter_gen())
265
+ except BaseException as ex:
266
+ # Undo the enter if it was interrupted (e.g. by Ctrl-C)
267
+ # as __exit__ won't be called.
268
+ if self.pgconn.status == OK:
269
+ self._conn.wait(self._exit_gen(type(ex), ex, None))
270
+ else:
271
+ self.status = self.Status.FAILED
272
+ raise
261
273
  return self
262
274
 
263
275
  def __exit__(
@@ -291,7 +303,17 @@ class AsyncTransaction(BaseTransaction["AsyncConnection[Any]"]):
291
303
 
292
304
  async def __aenter__(self) -> Self:
293
305
  async with self._conn.lock:
294
- await self._conn.wait(self._enter_gen())
306
+ self._check_enter()
307
+ try:
308
+ await self._conn.wait(self._enter_gen())
309
+ except BaseException as ex:
310
+ # Undo the enter if it was interrupted (e.g. by a cancellation)
311
+ # as __aexit__ won't be called.
312
+ if self.pgconn.status == OK:
313
+ await self._conn.wait(self._exit_gen(type(ex), ex, None))
314
+ else:
315
+ self.status = self.Status.FAILED
316
+ raise
295
317
  return self
296
318
 
297
319
  async def __aexit__(
@@ -157,33 +157,39 @@ class ListDumper(BaseListDumper):
157
157
  def dump(self, obj: list[Any]) -> Buffer | None:
158
158
  tokens: list[Buffer] = []
159
159
  needs_quotes = _get_needs_quotes_regexp(self.delimiter).search
160
+ self._dump_list(obj, tokens, needs_quotes)
161
+ return b"".join(tokens)
160
162
 
161
- def dump_list(obj: list[Any]) -> None:
162
- if not obj:
163
- tokens.append(b"{}")
164
- return
165
-
166
- tokens.append(b"{")
167
- for item in obj:
168
- if isinstance(item, list):
169
- dump_list(item)
170
- elif item is not None:
171
- if (ad := self._dump_item(item)) is None:
172
- tokens.append(b"NULL")
173
- else:
174
- if needs_quotes(ad):
175
- ad = b'"' + self._re_esc.sub(rb"\\\1", ad) + b'"'
176
- tokens.append(ad)
177
- else:
178
- tokens.append(b"NULL")
179
-
180
- tokens.append(self.delimiter)
163
+ def _dump_list(
164
+ self,
165
+ obj: list[Any],
166
+ tokens: list[Buffer],
167
+ needs_quotes: Callable[[Buffer], object],
168
+ ) -> None:
169
+ """
170
+ Append the tokens of the text representation of a list to *tokens*.
171
+ """
172
+ if not obj:
173
+ tokens.append(b"{}")
174
+ return
181
175
 
182
- tokens[-1] = b"}"
176
+ tokens.append(b"{")
177
+ for item in obj:
178
+ if isinstance(item, list):
179
+ self._dump_list(item, tokens, needs_quotes)
180
+ elif item is not None:
181
+ if (ad := self._dump_item(item)) is None:
182
+ tokens.append(b"NULL")
183
+ else:
184
+ if needs_quotes(ad):
185
+ ad = b'"' + self._re_esc.sub(rb"\\\1", ad) + b'"'
186
+ tokens.append(ad)
187
+ else:
188
+ tokens.append(b"NULL")
183
189
 
184
- dump_list(obj)
190
+ tokens.append(self.delimiter)
185
191
 
186
- return b"".join(tokens)
192
+ tokens[-1] = b"}"
187
193
 
188
194
  def _dump_item(self, item: Any) -> Buffer | None:
189
195
  if self.sub_dumper:
@@ -201,11 +207,19 @@ def _get_needs_quotes_regexp(delimiter: bytes) -> re.Pattern[bytes]:
201
207
  The array output routine will put double quotes around element values if
202
208
  they are empty strings, contain curly braces, delimiter characters,
203
209
  double quotes, backslashes, or white space, or match the word NULL.
210
+
211
+ Also quote values containing Unicode white space: Postgres doesn't strip
212
+ it from unquoted elements, but CockroachDB does (see
213
+ https://github.com/cockroachdb/cockroach/issues/176211).
204
214
  """
205
215
  return re.compile(rb"""(?xi)
206
216
  ^$ # the empty string
207
217
  | ["{}%s\\\s] # or a char to escape
208
218
  | ^null$ # or the word NULL
219
+ | \xe1\x9a\x80 # or UTF-8 Unicode spaces stripped by CockroachDB:
220
+ | \xe2\x80[\x80-\x8a\xa8\xa9\xaf] # U+1680, U+2000-U+200A,
221
+ | \xe2\x81\x9f # U+2028, U+2029, U+202F, U+205F, U+3000
222
+ | \xe3\x80\x80
209
223
  """ % delimiter)
210
224
 
211
225
 
@@ -247,58 +261,76 @@ class ListBinaryDumper(BaseListDumper):
247
261
 
248
262
  data: list[Buffer] = [b"", b""] # placeholders to avoid a resize
249
263
  dims: list[int] = []
250
- hasnull = 0
251
-
252
- def calc_dims(L: list[Any]) -> None:
253
- if isinstance(L, self.cls):
254
- if not L:
255
- raise e.DataError("lists cannot contain empty lists")
256
- dims.append(len(L))
257
- calc_dims(L[0])
258
-
259
- calc_dims(obj)
260
-
261
- def dump_list(L: list[Any], dim: int) -> None:
262
- nonlocal hasnull
263
- if len(L) != dims[dim]:
264
- raise e.DataError("nested lists have inconsistent lengths")
265
-
266
- if dim == len(dims) - 1:
267
- for item in L:
268
- if item is not None:
269
- # If we get here, the sub_dumper must have been set
270
- item = self.sub_dumper.dump(item) # type: ignore[union-attr]
271
- if item is not None:
272
- data.append(pack_len(len(item)))
273
- data.append(item)
274
- else:
275
- hasnull = 1
276
- data.append(ARRAY_NULL)
277
- else:
278
- for item in L:
279
- if not isinstance(item, self.cls):
280
- raise e.DataError("nested lists have inconsistent depths")
281
- dump_list(item, dim + 1) # type: ignore
282
-
283
- dump_list(obj, 0)
264
+ self._calc_dims(obj, dims)
265
+ hasnull = self._dump_list(obj, 0, dims, data)
284
266
 
285
267
  data[0] = _pack_head(len(dims), hasnull, sub_oid)
286
268
  data[1] = b"".join(_pack_dim(dim, 1) for dim in dims)
287
269
  return b"".join(data)
288
270
 
271
+ def _calc_dims(self, L: list[Any], dims: list[int]) -> None:
272
+ if isinstance(L, self.cls):
273
+ if not L:
274
+ raise e.DataError("lists cannot contain empty lists")
275
+ dims.append(len(L))
276
+ self._calc_dims(L[0], dims)
277
+
278
+ def _dump_list(
279
+ self, L: list[Any], dim: int, dims: list[int], data: list[Buffer]
280
+ ) -> int:
281
+ """
282
+ Append the dumped items of a list to *data*.
283
+
284
+ Return 1 if any item is null, else 0.
285
+ """
286
+ if len(L) != dims[dim]:
287
+ raise e.DataError("nested lists have inconsistent lengths")
288
+
289
+ hasnull = 0
290
+ if dim == len(dims) - 1:
291
+ for item in L:
292
+ if item is not None:
293
+ # If we get here, the sub_dumper must have been set
294
+ item = self.sub_dumper.dump(item) # type: ignore[union-attr]
295
+ if item is not None:
296
+ data.append(pack_len(len(item)))
297
+ data.append(item)
298
+ else:
299
+ hasnull = 1
300
+ data.append(ARRAY_NULL)
301
+ else:
302
+ for item in L:
303
+ if not isinstance(item, self.cls):
304
+ raise e.DataError("nested lists have inconsistent depths")
305
+ hasnull |= self._dump_list(item, dim + 1, dims, data) # type: ignore
306
+
307
+ return hasnull
308
+
289
309
 
290
310
  class ArrayLoader(RecursiveLoader):
291
311
  delimiter = b","
292
312
  base_oid: int
293
313
 
314
+ def __init__(self, oid: int, context: AdaptContext | None = None):
315
+ super().__init__(oid, context)
316
+ # Create the element loader now: the connection might be closed
317
+ # by the time data is loaded (#1428).
318
+ self._loader = self._tx.get_loader(self.base_oid, self.format)
319
+
294
320
  def load(self, data: Buffer) -> list[Any]:
295
- loader = self._tx.get_loader(self.base_oid, self.format)
296
- return _load_text(data, loader, self.delimiter)
321
+ return _load_text(data, self._loader, self.delimiter)
297
322
 
298
323
 
299
324
  class ArrayBinaryLoader(RecursiveLoader):
300
325
  format = pq.Format.BINARY
301
326
 
327
+ def __init__(self, oid: int, context: AdaptContext | None = None):
328
+ super().__init__(oid, context)
329
+ # The element oid is in the data, but if the array type is known,
330
+ # create the element loader now, while the connection is still open.
331
+ if (info := self._tx.adapters.types.get(oid)) and info.array_oid == oid:
332
+ self._tx.get_loader(info.oid, self.format)
333
+
302
334
  def load(self, data: Buffer) -> list[Any]:
303
335
  return _load_binary(data, self._tx)
304
336
 
@@ -324,9 +324,7 @@ def register_composite(
324
324
  factory = cast("Callable[..., T]", _nt_from_info(info))
325
325
 
326
326
  if not make_object:
327
-
328
- def make_object(values: Sequence[Any], info: CompositeInfo) -> T:
329
- return factory(*values)
327
+ make_object = _make_object_from_factory(factory)
330
328
 
331
329
  adapters = context.adapters if context else postgres.adapters
332
330
 
@@ -353,17 +351,10 @@ def register_composite(
353
351
  " argument in the `register_composite()` call",
354
352
  factory.__name__,
355
353
  )
356
-
357
- def make_sequence(obj: T, into: CompositeInfo) -> Sequence[Any]:
358
- raise TypeError(
359
- f"{type(obj).__name__!r} objects cannot be dumped without"
360
- " specifying 'make_sequence' in 'register_composite()'"
361
- )
354
+ make_sequence = _make_sequence_unsupported
362
355
 
363
356
  else:
364
-
365
- def make_sequence(obj: T, info: CompositeInfo) -> Sequence[Any]:
366
- return obj # type: ignore[return-value] # it's a sequence
357
+ make_sequence = _make_sequence_from_sequence
367
358
 
368
359
  type_name = factory.__name__
369
360
  dumper: type[Dumper] = _make_binary_dumper(type_name, info, make_sequence)
@@ -514,6 +505,25 @@ def _make_nt(name: str, fields: tuple[str, ...]) -> type[NamedTuple]:
514
505
  return namedtuple(name, fields) # type: ignore[return-value]
515
506
 
516
507
 
508
+ @cache
509
+ def _make_object_from_factory(factory: Callable[..., T]) -> ObjectMaker[T]:
510
+ def make_object(values: Sequence[Any], info: CompositeInfo) -> T:
511
+ return factory(*values)
512
+
513
+ return make_object
514
+
515
+
516
+ def _make_sequence_from_sequence(obj: Any, info: CompositeInfo) -> Sequence[Any]:
517
+ return obj # type: ignore[no-any-return] # it's a sequence
518
+
519
+
520
+ def _make_sequence_unsupported(obj: Any, info: CompositeInfo) -> Sequence[Any]:
521
+ raise TypeError(
522
+ f"{type(obj).__name__!r} objects cannot be dumped without"
523
+ " specifying 'make_sequence' in 'register_composite()'"
524
+ )
525
+
526
+
517
527
  @cache
518
528
  def _make_loader(
519
529
  info: CompositeInfo, make_object: ObjectMaker[T]
@@ -613,7 +613,8 @@ class IntervalLoader(Loader):
613
613
 
614
614
  ye, mo, da, sgn, ho, mi, se = m.groups()
615
615
  days = 0
616
- seconds = 0.0
616
+ seconds = 0
617
+ micros = 0
617
618
 
618
619
  if ye:
619
620
  days += 365 * int(ye)
@@ -623,12 +624,14 @@ class IntervalLoader(Loader):
623
624
  days += int(da)
624
625
 
625
626
  if ho:
626
- seconds = 3600 * int(ho) + 60 * int(mi) + float(se)
627
+ seconds = 3600 * int(ho) + 60 * int(mi)
628
+ micros = round(float(se) * 1_000_000)
627
629
  if sgn == b"-":
628
630
  seconds = -seconds
631
+ micros = -micros
629
632
 
630
633
  try:
631
- return timedelta(days=days, seconds=seconds)
634
+ return timedelta(days=days, seconds=seconds, microseconds=micros)
632
635
  except OverflowError as e:
633
636
  s = bytes(data).decode("utf8", "replace")
634
637
  raise DataError(f"can't parse interval {s!r}: {e}") from None
@@ -296,7 +296,7 @@ _default_dumpers: dict[tuple[type[_JsonWrapper], PyFormat], type[Dumper]] = {
296
296
  (Json, PyFormat.BINARY): JsonBinaryDumper,
297
297
  (Json, PyFormat.TEXT): JsonDumper,
298
298
  (Jsonb, PyFormat.BINARY): JsonbBinaryDumper,
299
- (Jsonb, PyFormat.TEXT): JsonDumper,
299
+ (Jsonb, PyFormat.TEXT): JsonbDumper,
300
300
  }
301
301
 
302
302
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: psycopg
3
- Version: 3.3.6
3
+ Version: 3.3.7
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
@@ -34,9 +34,9 @@ License-File: LICENSE.txt
34
34
  Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
35
35
  Requires-Dist: tzdata; sys_platform == "win32"
36
36
  Provides-Extra: c
37
- Requires-Dist: psycopg-c==3.3.6; implementation_name != "pypy" and extra == "c"
37
+ Requires-Dist: psycopg-c==3.3.7; implementation_name != "pypy" and extra == "c"
38
38
  Provides-Extra: binary
39
- Requires-Dist: psycopg-binary==3.3.6; implementation_name != "pypy" and extra == "binary"
39
+ Requires-Dist: psycopg-binary==3.3.7; implementation_name != "pypy" and extra == "binary"
40
40
  Provides-Extra: pool
41
41
  Requires-Dist: psycopg-pool; extra == "pool"
42
42
  Provides-Extra: test
@@ -8,12 +8,12 @@ tzdata
8
8
  [binary]
9
9
 
10
10
  [binary:implementation_name != "pypy"]
11
- psycopg-binary==3.3.6
11
+ psycopg-binary==3.3.7
12
12
 
13
13
  [c]
14
14
 
15
15
  [c:implementation_name != "pypy"]
16
- psycopg-c==3.3.6
16
+ psycopg-c==3.3.7
17
17
 
18
18
  [dev]
19
19
  ast-comments>=1.1.2
@@ -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.6"
10
+ version = "3.3.7"
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;
@@ -61,10 +61,10 @@ content-type = "text/x-rst"
61
61
 
62
62
  [project.optional-dependencies]
63
63
  c = [
64
- "psycopg-c == 3.3.6; implementation_name != \"pypy\"",
64
+ "psycopg-c == 3.3.7; implementation_name != \"pypy\"",
65
65
  ]
66
66
  binary = [
67
- "psycopg-binary == 3.3.6; implementation_name != \"pypy\"",
67
+ "psycopg-binary == 3.3.7; implementation_name != \"pypy\"",
68
68
  ]
69
69
  pool = [
70
70
  "psycopg-pool",
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes