Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to using propcache for property caching #9394

Merged
merged 15 commits into from
Oct 8, 2024
9 changes: 9 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CHANGES/9394.packaging.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Switched to using the :mod:`propcache <propcache.api>` package for property caching
-- by :user:`bdraco`.

The :mod:`propcache <propcache.api>` package is derived from the property caching
code in :mod:`yarl` and has been broken out to avoid maintaining it for multiple
projects.
6 changes: 0 additions & 6 deletions aiohttp/_helpers.pyi

This file was deleted.

35 changes: 0 additions & 35 deletions aiohttp/_helpers.pyx

This file was deleted.

50 changes: 2 additions & 48 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from urllib.request import getproxies, proxy_bypass

from multidict import CIMultiDict, MultiDict, MultiDictProxy, MultiMapping
from propcache.api import under_cached_property as reify
bdraco marked this conversation as resolved.
Show resolved Hide resolved
from yarl import URL

from . import hdrs
Expand All @@ -60,7 +61,7 @@
else:
import async_timeout

__all__ = ("BasicAuth", "ChainMapProxy", "ETag")
__all__ = ("BasicAuth", "ChainMapProxy", "ETag", "reify")
bdraco marked this conversation as resolved.
Show resolved Hide resolved
bdraco marked this conversation as resolved.
Show resolved Hide resolved

PY_310 = sys.version_info >= (3, 10)

Expand Down Expand Up @@ -440,53 +441,6 @@ def is_expected_content_type(
return expected_content_type in response_content_type


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.

Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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),
Expand Down
2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ multidict==6.1.0
# yarl
packaging==24.1
# via gunicorn
propcache==0.2.0
# via -r requirements/runtime-deps.in
pycares==4.4.0
# via aiodns
pycparser==2.22
Expand Down
2 changes: 2 additions & 0 deletions requirements/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ pluggy==1.5.0
# via pytest
pre-commit==3.5.0
# via -r requirements/lint.in
propcache==0.2.0
# via -r requirements/runtime-deps.in
proxy-py==2.4.8
# via
# -r requirements/lint.in
Expand Down
2 changes: 2 additions & 0 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ pluggy==1.5.0
# via pytest
pre-commit==3.5.0
# via -r requirements/lint.in
propcache==0.2.0
# via -r requirements/runtime-deps.in
proxy-py==2.4.8
# via
# -r requirements/lint.in
Expand Down
1 change: 1 addition & 0 deletions requirements/runtime-deps.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,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
2 changes: 2 additions & 0 deletions requirements/runtime-deps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ multidict==6.1.0
# via
# -r requirements/runtime-deps.in
# yarl
propcache==0.2.0
# via -r requirements/runtime-deps.in
pycares==4.4.0
# via aiodns
pycparser==2.22
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ install_requires =
async-timeout >= 4.0, < 5.0 ; python_version < "3.11"
frozenlist >= 1.1.1
multidict >=4.5, < 7.0
propcache >= 0.2.0
yarl >= 1.13.0, < 2.0

[options.exclude_package_data]
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
]

Expand Down
59 changes: 1 addition & 58 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
import base64
import datetime
import gc
import platform
import sys
import weakref
from math import ceil, modf
from pathlib import Path
from typing import Any, Dict, Iterator, Optional, Type, Union
from typing import Dict, Iterator, Optional, Union
from unittest import mock
from urllib.request import getproxies_environment # type: ignore[attr-defined]

Expand All @@ -24,9 +23,6 @@
should_remove_content_length,
)

IS_PYPY = platform.python_implementation() == "PyPy"


# ------------------- parse_mimetype ----------------------------------


Expand Down Expand Up @@ -227,59 +223,6 @@ def test_basic_auth_from_not_url() -> None:
helpers.BasicAuth.from_url("http://user:pass@example.com") # type: ignore[arg-type]


class ReifyMixin:
reify: Type["helpers.reify[Any]"]

def test_reify(self) -> None:
class A:
def __init__(self) -> None:
self._cache: Dict[str, str] = {}

@self.reify # type: ignore[misc]
def prop(self) -> int:
return 1

a = A()
assert 1 == a.prop

def test_reify_class(self) -> None:
class A:
def __init__(self) -> None:
self._cache: Dict[str, str] = {}

@self.reify # type: ignore[misc]
def prop(self) -> int:
"""Docstring."""
return 1

assert isinstance(A.prop, self.reify) # type: ignore[arg-type]
assert "Docstring." == A.prop.__doc__ # type: ignore[arg-type]

def test_reify_assignment(self) -> None:
class A:
def __init__(self) -> None:
self._cache: Dict[str, str] = {}

@self.reify # type: ignore[misc]
def prop(self) -> int:
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 # type: ignore[attr-defined,assignment]


# ----------------------------------- is_ip_address() ----------------------


Expand Down
Loading