]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
fix: don't raise a warning using a builtin for JSON dumps/loads
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 10 Sep 2025 10:07:46 +0000 (12:07 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 10 Sep 2025 10:08:56 +0000 (12:08 +0200)
These types are stable so will not leak. However they have no
code/closure dunder attr so they will raise a warning.

Fix #1165.

docs/news.rst
psycopg/psycopg/types/json.py
tests/types/test_json.py

index 9b20c9e921f3280a62905ab22fe0423a47872c67..0a804f3a7b71dc38e424e448d76d3385cbf6d499 100644 (file)
@@ -43,6 +43,13 @@ Psycopg 3.3.0 (unreleased)
 - Drop support for Python 3.8 (:ticket:`#976`) and 3.9 (:ticket:`#1056`).
 
 
+Psycopg 3.2.11 (unreleased)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+- Don't raise warning, and don't leak resources, if a builtin function is used
+  as JSON dumper/loader function (:ticket:`#1165`).
+
+
 Current release
 ---------------
 
index cb61aaab7acd8ef8b90abea102753c52727da0fe..5faa32943daa7d9ee51208d22479fc09fb28dbf7 100644 (file)
@@ -8,7 +8,7 @@ from __future__ import annotations
 
 import json
 import logging
-from types import CodeType
+from types import BuiltinFunctionType, CodeType
 from typing import Any, TypeAlias
 from warnings import warn
 from threading import Lock
@@ -22,7 +22,7 @@ from ..errors import DataError
 
 JsonDumpsFunction: TypeAlias = Callable[[Any], str | bytes]
 JsonLoadsFunction: TypeAlias = Callable[[str | bytes], Any]
-_AdapterKey: TypeAlias = tuple[type, CodeType]
+_AdapterKey: TypeAlias = tuple[type, CodeType | BuiltinFunctionType | type]
 
 logger = logging.getLogger("psycopg")
 
@@ -155,8 +155,12 @@ def _get_adapter_key(t: type, f: Callable[..., Any]) -> _AdapterKey | None:
     the same if a lambda if defined in a function, so we can use it as a more
     stable hash key.
     """
+    # A builtin is stable and has no cache so it's a good candidate as hash key.
+    if isinstance(f, (BuiltinFunctionType, type)):
+        return (t, f)
+
     # Check if there's an unexpected Python implementation that doesn't define
-    # these dunder attributes. If thta's the case, raise a warning, which will
+    # these dunder attributes. If that's the case, raise a warning, which will
     # crash our test suite and/or hopefully will be detected by the user.
     try:
         f.__code__
index faae9e66201b4067b1ba45f40f6c1b94a6c42502..f649ebdf35963379c5458e864263804f634011c6 100644 (file)
@@ -256,6 +256,33 @@ def test_dump_leak_with_local_functions(dsn, binary, pgtype, caplog):
     assert caplog.records
 
 
+@pytest.mark.parametrize("binary", [True, False])
+@pytest.mark.parametrize("pgtype", ["json", "jsonb"])
+@pytest.mark.parametrize("dumps", [str, len])
+def test_dumper_warning_builtin(dsn, binary, pgtype, dumps, caplog, recwarn):
+    caplog.set_level(logging.WARNING, logger="psycopg")
+    recwarn.clear()
+
+    # Note: private implementation, it might change
+    from psycopg.types.json import _dumpers_cache
+
+    # A function with no closure is cached on the code, so lambdas are not
+    # different items.
+
+    with psycopg.connect(dsn) as conn1:
+        set_json_dumps(dumps, conn1)
+    assert not recwarn
+    assert (size1 := len(_dumpers_cache))
+
+    with psycopg.connect(dsn) as conn2:
+        set_json_dumps(dumps, conn2)
+    size2 = len(_dumpers_cache)
+
+    assert size1 == size2
+    assert not caplog.records
+    assert not recwarn
+
+
 @pytest.mark.parametrize("binary", [True, False])
 @pytest.mark.parametrize("pgtype", ["json", "jsonb"])
 def test_load_leak_with_local_functions(dsn, binary, pgtype, caplog):
@@ -298,6 +325,33 @@ def test_load_leak_with_local_functions(dsn, binary, pgtype, caplog):
     assert caplog.records
 
 
+@pytest.mark.parametrize("binary", [True, False])
+@pytest.mark.parametrize("pgtype", ["json", "jsonb"])
+@pytest.mark.parametrize("loads", [str, len])
+def test_loader_warning_builtin(dsn, binary, pgtype, loads, caplog, recwarn):
+    caplog.set_level(logging.WARNING, logger="psycopg")
+    recwarn.clear()
+
+    # Note: private implementation, it might change
+    from psycopg.types.json import _loaders_cache
+
+    # A function with no closure is cached on the code, so lambdas are not
+    # different items.
+
+    with psycopg.connect(dsn) as conn1:
+        set_json_loads(loads, conn1)
+    assert not recwarn
+    assert (size1 := len(_loaders_cache))
+
+    with psycopg.connect(dsn) as conn2:
+        set_json_loads(loads, conn2)
+    size2 = len(_loaders_cache)
+
+    assert size1 == size2
+    assert not caplog.records
+    assert not recwarn
+
+
 def my_dumps(obj):
     obj = deepcopy(obj)
     obj["baz"] = "qux"