From efa5ad211e6ce464f1974b0880358fffcfdeec6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Oct 2024 14:02:48 +0200 Subject: [PATCH 1/2] Migrate to using propcache for property caching (#9394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) (cherry picked from commit e9edd04475ef6bfb4a9a8535b2617443e7bc71d2) --- .gitignore | 2 -- CHANGES/9394.packaging.rst | 6 ++++ aiohttp/_helpers.pyi | 6 ---- aiohttp/_helpers.pyx | 35 --------------------- aiohttp/helpers.py | 50 ++---------------------------- docs/conf.py | 1 + requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/runtime-deps.in | 1 + requirements/runtime-deps.txt | 2 +- setup.cfg | 1 + setup.py | 1 - tests/test_helpers.py | 57 ----------------------------------- 14 files changed, 15 insertions(+), 153 deletions(-) create mode 100644 CHANGES/9394.packaging.rst delete mode 100644 aiohttp/_helpers.pyi delete mode 100644 aiohttp/_helpers.pyx diff --git a/.gitignore b/.gitignore index 7d38dd91998..62770ddc80a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,6 @@ aiohttp/_find_header.c aiohttp/_headers.html aiohttp/_headers.pxi -aiohttp/_helpers.c -aiohttp/_helpers.html aiohttp/_http_parser.c aiohttp/_http_parser.html aiohttp/_http_writer.c diff --git a/CHANGES/9394.packaging.rst b/CHANGES/9394.packaging.rst new file mode 100644 index 00000000000..456ac0f52c8 --- /dev/null +++ b/CHANGES/9394.packaging.rst @@ -0,0 +1,6 @@ +Switched to using the :mod:`propcache ` package for property caching +-- by :user:`bdraco`. + +The :mod:`propcache ` package is derived from the property caching +code in :mod:`yarl` and has been broken out to avoid maintaining it for multiple +projects. diff --git a/aiohttp/_helpers.pyi b/aiohttp/_helpers.pyi deleted file mode 100644 index 1e358937024..00000000000 --- a/aiohttp/_helpers.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Any - -class reify: - def __init__(self, wrapped: Any) -> None: ... - def __get__(self, inst: Any, owner: Any) -> Any: ... - def __set__(self, inst: Any, value: Any) -> None: ... diff --git a/aiohttp/_helpers.pyx b/aiohttp/_helpers.pyx deleted file mode 100644 index 5f089225dc8..00000000000 --- a/aiohttp/_helpers.pyx +++ /dev/null @@ -1,35 +0,0 @@ - -cdef _sentinel = object() - -cdef class reify: - """Use as a class method decorator. It operates almost exactly like - the Python `@property` decorator, but it puts the result of the - method it decorates into the instance dict after the first call, - effectively replacing the function it decorates with an instance - variable. It is, in Python parlance, a data descriptor. - - """ - - cdef object wrapped - cdef object name - - def __init__(self, wrapped): - self.wrapped = wrapped - self.name = wrapped.__name__ - - @property - def __doc__(self): - return self.wrapped.__doc__ - - def __get__(self, inst, owner): - if inst is None: - return self - cdef dict cache = inst._cache - val = cache.get(self.name, _sentinel) - if val is _sentinel: - val = self.wrapped(inst) - cache[self.name] = val - return val - - def __set__(self, inst, value): - raise AttributeError("reified property is read-only") diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 81b79792dea..895aa3916f6 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -47,6 +47,7 @@ import attr from multidict import MultiDict, MultiDictProxy, MultiMapping +from propcache.api import under_cached_property as reify from yarl import URL from . import hdrs @@ -57,7 +58,7 @@ else: import async_timeout -__all__ = ("BasicAuth", "ChainMapProxy", "ETag") +__all__ = ("BasicAuth", "ChainMapProxy", "ETag", "reify") IS_MACOS = platform.system() == "Darwin" IS_WINDOWS = platform.system() == "Windows" @@ -425,53 +426,6 @@ def content_disposition_header( return value -class _TSelf(Protocol, Generic[_T]): - _cache: Dict[str, _T] - - -class reify(Generic[_T]): - """Use as a class method decorator. - - It operates almost exactly like - the Python `@property` decorator, but it puts the result of the - method it decorates into the instance dict after the first call, - effectively replacing the function it decorates with an instance - variable. It is, in Python parlance, a data descriptor. - """ - - def __init__(self, wrapped: Callable[..., _T]) -> None: - self.wrapped = wrapped - self.__doc__ = wrapped.__doc__ - self.name = wrapped.__name__ - - def __get__(self, inst: _TSelf[_T], owner: Optional[Type[Any]] = None) -> _T: - try: - try: - return inst._cache[self.name] - except KeyError: - val = self.wrapped(inst) - inst._cache[self.name] = val - return val - except AttributeError: - if inst is None: - return self - raise - - def __set__(self, inst: _TSelf[_T], value: _T) -> None: - raise AttributeError("reified property is read-only") - - -reify_py = reify - -try: - from ._helpers import reify as reify_c - - if not NO_EXTENSIONS: - reify = reify_c # type: ignore[misc,assignment] -except ImportError: - pass - - def is_ip_address(host: Optional[str]) -> bool: """Check if host looks like an IP Address. diff --git a/docs/conf.py b/docs/conf.py index 5cbf398e6a9..f60c8ffcf8c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,6 +75,7 @@ "pytest": ("http://docs.pytest.org/en/latest/", None), "python": ("http://docs.python.org/3", None), "multidict": ("https://multidict.readthedocs.io/en/stable/", None), + "propcache": ("https://propcache.aio-libs.org/en/stable", None), "yarl": ("https://yarl.readthedocs.io/en/stable/", None), "aiosignal": ("https://aiosignal.readthedocs.io/en/stable/", None), "aiohttpjinja2": ("https://aiohttp-jinja2.readthedocs.io/en/stable/", None), diff --git a/requirements/base.txt b/requirements/base.txt index a1cf4b90f88..41b6ce6196c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -33,7 +33,7 @@ multidict==6.1.0 packaging==24.1 # via gunicorn propcache==0.2.0 - # via yarl + # via -r requirements/runtime-deps.in pycares==4.4.0 # via aiodns pycparser==2.22 diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 0ca3180f80e..7624ad13d83 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -139,7 +139,7 @@ pluggy==1.5.0 pre-commit==3.5.0 # via -r requirements/lint.in propcache==0.2.0 - # via yarl + # via -r requirements/runtime-deps.in proxy-py==2.4.8 # via -r requirements/test.in pycares==4.4.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index e5037b0d6d5..9040c7ff962 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -136,7 +136,7 @@ pluggy==1.5.0 pre-commit==3.5.0 # via -r requirements/lint.in propcache==0.2.0 - # via yarl + # via -r requirements/runtime-deps.in proxy-py==2.4.8 # via -r requirements/test.in pycares==4.4.0 diff --git a/requirements/runtime-deps.in b/requirements/runtime-deps.in index 4b8ab98dd08..7af9fb50246 100644 --- a/requirements/runtime-deps.in +++ b/requirements/runtime-deps.in @@ -9,4 +9,5 @@ Brotli; platform_python_implementation == 'CPython' brotlicffi; platform_python_implementation != 'CPython' frozenlist >= 1.1.1 multidict >=4.5, < 7.0 +propcache >= 0.2.0 yarl >= 1.13.0, < 2.0 diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index ed709d2dbe6..36da22e0dce 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -29,7 +29,7 @@ multidict==6.1.0 # -r requirements/runtime-deps.in # yarl propcache==0.2.0 - # via yarl + # via -r requirements/runtime-deps.in pycares==4.4.0 # via aiodns pycparser==2.22 diff --git a/setup.cfg b/setup.cfg index 781fc4ca40f..a26d472b22a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,6 +55,7 @@ install_requires = attrs >= 17.3.0 frozenlist >= 1.1.1 multidict >=4.5, < 7.0 + propcache >= 0.2.0 yarl >= 1.13.0, < 2.0 [options.exclude_package_data] diff --git a/setup.py b/setup.py index 808f539d259..3a90ae2e20a 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,6 @@ define_macros=[("LLHTTP_STRICT_MODE", 0)], include_dirs=["vendor/llhttp/build"], ), - Extension("aiohttp._helpers", ["aiohttp/_helpers.c"]), Extension("aiohttp._http_writer", ["aiohttp/_http_writer.c"]), ] diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 6f45ceca0b9..1aba1aae3bd 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -2,7 +2,6 @@ import base64 import datetime import gc -import platform import sys import weakref from math import ceil, modf @@ -22,9 +21,6 @@ should_remove_content_length, ) -IS_PYPY = platform.python_implementation() == "PyPy" - - # ------------------- parse_mimetype ---------------------------------- @@ -208,59 +204,6 @@ def test_basic_auth_from_not_url() -> None: helpers.BasicAuth.from_url("http://user:pass@example.com") -class ReifyMixin: - reify = NotImplemented - - def test_reify(self) -> None: - class A: - def __init__(self): - self._cache = {} - - @self.reify - def prop(self): - return 1 - - a = A() - assert 1 == a.prop - - def test_reify_class(self) -> None: - class A: - def __init__(self): - self._cache = {} - - @self.reify - def prop(self): - """Docstring.""" - return 1 - - assert isinstance(A.prop, self.reify) - assert "Docstring." == A.prop.__doc__ - - def test_reify_assignment(self) -> None: - class A: - def __init__(self): - self._cache = {} - - @self.reify - def prop(self): - return 1 - - a = A() - - with pytest.raises(AttributeError): - a.prop = 123 - - -class TestPyReify(ReifyMixin): - reify = helpers.reify_py - - -if not helpers.NO_EXTENSIONS and not IS_PYPY and hasattr(helpers, "reify_c"): - - class TestCReify(ReifyMixin): - reify = helpers.reify_c - - # ----------------------------------- is_ip_address() ---------------------- From 6eb1bc6f6add0e7244a00b083b24cbf2da277a30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Oct 2024 14:12:48 +0200 Subject: [PATCH 2/2] back merge from master --- requirements/base.txt | 4 +++- requirements/constraints.txt | 4 +++- requirements/dev.txt | 4 +++- requirements/runtime-deps.txt | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 41b6ce6196c..734e543c5f9 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -33,7 +33,9 @@ multidict==6.1.0 packaging==24.1 # via gunicorn propcache==0.2.0 - # via -r requirements/runtime-deps.in + # via + # -r requirements/runtime-deps.in + # yarl pycares==4.4.0 # via aiodns pycparser==2.22 diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 7624ad13d83..554c3af5118 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -139,7 +139,9 @@ pluggy==1.5.0 pre-commit==3.5.0 # via -r requirements/lint.in propcache==0.2.0 - # via -r requirements/runtime-deps.in + # via + # -r requirements/runtime-deps.in + # yarl proxy-py==2.4.8 # via -r requirements/test.in pycares==4.4.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 9040c7ff962..26924314e4e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -136,7 +136,9 @@ pluggy==1.5.0 pre-commit==3.5.0 # via -r requirements/lint.in propcache==0.2.0 - # via -r requirements/runtime-deps.in + # via + # -r requirements/runtime-deps.in + # yarl proxy-py==2.4.8 # via -r requirements/test.in pycares==4.4.0 diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 36da22e0dce..13d1cfac572 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -29,7 +29,9 @@ multidict==6.1.0 # -r requirements/runtime-deps.in # yarl propcache==0.2.0 - # via -r requirements/runtime-deps.in + # via + # -r requirements/runtime-deps.in + # yarl pycares==4.4.0 # via aiodns pycparser==2.22