diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 79b20bd6..b2daa054 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -113,6 +113,15 @@ body: $ python -m pip show multidict validations: required: true +- type: textarea + attributes: + label: propcache Version + description: Attach your version of propcache. + render: console + value: | + $ python -m pip show propcache + validations: + required: true - type: textarea attributes: label: yarl Version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 840848e8..60692d07 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -116,6 +116,7 @@ repos: - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict + - propcache >= 0.2.0 - pytest - tomli # requirement of packaging/pep517_backend/ - types-setuptools # requirement of packaging/pep517_backend/ @@ -132,6 +133,7 @@ repos: - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict + - propcache >= 0.2.0 - pytest - tomli # requirement of packaging/pep517_backend/ - types-setuptools # requirement of packaging/pep517_backend/ @@ -148,6 +150,7 @@ repos: - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict + - propcache >= 0.2.0 - pytest - tomli # requirement of packaging/pep517_backend/ - types-setuptools # requirement of packaging/pep517_backend/ @@ -166,6 +169,7 @@ repos: - hypothesis - lxml # dep of `--txt-report`, `--cobertura-xml-report` & `--html-report` - multidict + - propcache >= 0.2.0 - pytest - tomli # requirement of packaging/pep517_backend/ - types-setuptools # requirement of packaging/pep517_backend/ diff --git a/CHANGES/1169.packaging.rst b/CHANGES/1169.packaging.rst new file mode 100644 index 00000000..456ac0f5 --- /dev/null +++ b/CHANGES/1169.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/README.rst b/README.rst index 026655db..c7a286e6 100644 --- a/README.rst +++ b/README.rst @@ -128,7 +128,7 @@ by this variable. Dependencies ------------ -YARL requires multidict_ library. +YARL requires multidict_ and propcache_ libraries. API documentation @@ -203,3 +203,5 @@ It's *Apache 2* licensed and freely available. .. _GitHub: https://github.com/aio-libs/yarl .. _multidict: https://github.com/aio-libs/multidict + +.. _propcache: https://github.com/aio-libs/propcache diff --git a/docs/conf.py b/docs/conf.py index 2b223d40..c6f90778 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -84,6 +84,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "multidict": ("https://multidict.aio-libs.org/en/stable", None), + "propcache": ("https://propcache.aio-libs.org/en/stable", None), } diff --git a/docs/index.rst b/docs/index.rst index f3f43875..1300f7c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -116,7 +116,7 @@ by this variable. Dependencies ------------ -``yarl`` requires :mod:`multidict` library. +``yarl`` requires the :mod:`multidict` and :mod:`propcache ` libraries. It installs it automatically. diff --git a/requirements/test.txt b/requirements/test.txt index 6957cd87..f2f2b94f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -3,6 +3,7 @@ covdefaults hypothesis>=6.0 idna==3.10 multidict==6.1.0 +propcache==0.2.0 pytest==8.3.3 pytest-cov>=2.3.1 pytest-xdist diff --git a/setup.cfg b/setup.cfg index 4dc4cc1a..193fed68 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,6 +68,7 @@ include_package_data = True install_requires = idna >= 2.0 multidict >= 4.0 + propcache >= 0.2.0 [options.package_data] # Ref: diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index cdfff129..00000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,91 +0,0 @@ -import platform - -import pytest - -from yarl import _helpers, _helpers_py - -IS_PYPY = platform.python_implementation() == "PyPy" - - -class CachedPropertyMixin: - cached_property = NotImplemented - - def test_cached_property(self) -> None: - class A: - def __init__(self): - self._cache = {} - - @self.cached_property # type: ignore[misc] - def prop(self): - return 1 - - a = A() - assert a.prop == 1 - - def test_cached_property_class(self) -> None: - class A: - def __init__(self): - """Init.""" - # self._cache not set because its never accessed in this test - - @self.cached_property # type: ignore[misc] - def prop(self): - """Docstring.""" - - assert isinstance(A.prop, self.cached_property) - assert A.prop.__doc__ == "Docstring." - - def test_cached_property_assignment(self) -> None: - class A: - def __init__(self): - self._cache = {} - - @self.cached_property # type: ignore[misc] - def prop(self): - """Mock property.""" - - a = A() - - with pytest.raises(AttributeError): - a.prop = 123 - - def test_cached_property_without_cache(self) -> None: - class A: - def __init__(self): - pass - - @self.cached_property # type: ignore[misc] - def prop(self): - """Mock property.""" - - a = A() - - with pytest.raises(AttributeError): - a.prop = 123 - - def test_cached_property_check_without_cache(self) -> None: - class A: - def __init__(self): - pass - - @self.cached_property # type: ignore[misc] - def prop(self): - """Mock property.""" - - a = A() - with pytest.raises(AttributeError): - assert a.prop == 1 - - -class TestPyCachedProperty(CachedPropertyMixin): - cached_property = _helpers_py.cached_property # type: ignore[assignment] - - -if ( - not _helpers.NO_EXTENSIONS - and not IS_PYPY - and hasattr(_helpers, "cached_property_c") -): - - class TestCCachedProperty(CachedPropertyMixin): - cached_property = _helpers.cached_property_c # type: ignore[assignment, attr-defined, unused-ignore] # noqa: E501 diff --git a/yarl/_helpers.py b/yarl/_helpers.py deleted file mode 100644 index ac01158c..00000000 --- a/yarl/_helpers.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -import sys -from typing import TYPE_CHECKING - -__all__ = ("cached_property",) - - -NO_EXTENSIONS = bool(os.environ.get("YARL_NO_EXTENSIONS")) # type: bool -if sys.implementation.name != "cpython": - NO_EXTENSIONS = True - - -# isort: off -if TYPE_CHECKING: - from ._helpers_py import cached_property as cached_property_py - - cached_property = cached_property_py -elif not NO_EXTENSIONS: # pragma: no branch - try: - from ._helpers_c import cached_property as cached_property_c # type: ignore[attr-defined, unused-ignore] # noqa: E501 - - cached_property = cached_property_c - except ImportError: # pragma: no cover - from ._helpers_py import cached_property as cached_property_py - - cached_property = cached_property_py # type: ignore[assignment, misc] -else: - from ._helpers_py import cached_property as cached_property_py - - cached_property = cached_property_py # type: ignore[assignment, misc] -# isort: on diff --git a/yarl/_helpers_c.pyi b/yarl/_helpers_c.pyi deleted file mode 100644 index 69034921..00000000 --- a/yarl/_helpers_c.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Any - -class cached_property: - 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/yarl/_helpers_c.pyx b/yarl/_helpers_c.pyx deleted file mode 100644 index e6eec375..00000000 --- a/yarl/_helpers_c.pyx +++ /dev/null @@ -1,36 +0,0 @@ -# cython: language_level=3 - -cdef _sentinel = object() - -cdef class cached_property: - """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("cached property is read-only") diff --git a/yarl/_helpers_py.py b/yarl/_helpers_py.py deleted file mode 100644 index 5a18afb5..00000000 --- a/yarl/_helpers_py.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Various helper functions.""" - -from typing import Any, Callable, Dict, Generic, Optional, Protocol, Type, TypeVar - -_T = TypeVar("_T") - - -class _TSelf(Protocol, Generic[_T]): - _cache: Dict[str, _T] - - -class cached_property(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("cached property is read-only") diff --git a/yarl/_url.py b/yarl/_url.py index ac8e143c..dfac1aa1 100644 --- a/yarl/_url.py +++ b/yarl/_url.py @@ -32,8 +32,8 @@ import idna from multidict import MultiDict, MultiDictProxy, istr +from propcache.api import under_cached_property as cached_property -from ._helpers import cached_property from ._quoting import _Quoter, _Unquoter DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443, "ftp": 21}