From 97f3d2c66adb712d64d1a781daddc6aa7d1ff355 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 14 May 2018 08:51:45 +0300 Subject: [PATCH] Cythonize reify (#2995) * Cleanup * Implement cythonized reify decorator * Add changelog * reify more props * Fix spelling * Use bash version of codecov uploader --- .gitignore | 2 ++ .travis.yml | 3 ++- CHANGES/2995.feature | 1 + aiohttp/_helpers.pyx | 35 +++++++++++++++++++++++++++++++++++ aiohttp/helpers.py | 16 +++++++++++----- aiohttp/web_request.py | 34 +++++++++++++++++----------------- docs/spelling_wordlist.txt | 1 + setup.py | 2 ++ tests/test_helpers.py | 20 +++++++++++++++----- 9 files changed, 86 insertions(+), 28 deletions(-) create mode 100644 CHANGES/2995.feature create mode 100644 aiohttp/_helpers.pyx diff --git a/.gitignore b/.gitignore index c2f46f18bfe..83a9abd5646 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ .vimrc aiohttp/_frozenlist.c aiohttp/_frozenlist.html +aiohttp/_helpers.c +aiohttp/_helpers.html aiohttp/_websocket.c aiohttp/_websocket.html aiohttp/_http_parser.c diff --git a/.travis.yml b/.travis.yml index e78a4292f20..ee872faaed4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,8 @@ script: - make cov-ci-run after_success: -- codecov + # - codecov + - bash <(curl -s https://codecov.io/bash) _helpers: - &_mainstream_python_base diff --git a/CHANGES/2995.feature b/CHANGES/2995.feature new file mode 100644 index 00000000000..e37b4ea26f0 --- /dev/null +++ b/CHANGES/2995.feature @@ -0,0 +1 @@ +Cythonize ``@helpers.reify``, 5% boost on macro benchmark \ No newline at end of file diff --git a/aiohttp/_helpers.pyx b/aiohttp/_helpers.pyx new file mode 100644 index 00000000000..665f367c5de --- /dev/null +++ b/aiohttp/_helpers.pyx @@ -0,0 +1,35 @@ +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): + 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, value): + raise AttributeError("reified property is read-only") diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index a81204fc02c..8670e568fb3 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -490,13 +490,10 @@ class reify: def __init__(self, wrapped): self.wrapped = wrapped - try: - self.__doc__ = wrapped.__doc__ - except Exception: # pragma: no cover - self.__doc__ = "" + self.__doc__ = wrapped.__doc__ self.name = wrapped.__name__ - def __get__(self, inst, owner, _sentinel=sentinel): + def __get__(self, inst, owner): try: try: return inst._cache[self.name] @@ -513,6 +510,15 @@ def __set__(self, inst, value): 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 +except ImportError: + pass + _ipv4_pattern = (r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}' r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$') _ipv6_pattern = ( diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index 39e8de1b11c..17e9e43771d 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -174,18 +174,18 @@ def transport(self): def writer(self): return self._payload_writer - @property + @reify def message(self): warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3) return self._message - @property + @reify def rel_url(self): return self._rel_url - @property + @reify def loop(self): return self._loop @@ -208,7 +208,7 @@ def __iter__(self): ######## - @property + @reify def secure(self): """A bool indicating if the request is handled with SSL.""" return self.scheme == 'https' @@ -289,7 +289,7 @@ def scheme(self): else: return 'http' - @property + @reify def method(self): """Read only property for getting HTTP method. @@ -297,7 +297,7 @@ def method(self): """ return self._method - @property + @reify def version(self): """Read only property for getting HTTP version of request. @@ -343,7 +343,7 @@ def url(self): url = URL.build(scheme=self.scheme, host=self.host) return url.join(self._rel_url) - @property + @reify def path(self): """The URL including *PATH INFO* without the host or scheme. @@ -359,7 +359,7 @@ def path_qs(self): """ return str(self._rel_url) - @property + @reify def raw_path(self): """ The URL including raw *PATH INFO* without the host or scheme. Warning, the path is unquoted and may contains non valid URL characters @@ -368,12 +368,12 @@ def raw_path(self): """ return self._message.path - @property + @reify def query(self): """A multidict with all the variables in the query string.""" return self._rel_url.query - @property + @reify def query_string(self): """The query string in the URL. @@ -381,12 +381,12 @@ def query_string(self): """ return self._rel_url.query_string - @property + @reify def headers(self): """A case-insensitive multidict proxy with all headers.""" return self._headers - @property + @reify def raw_headers(self): """A sequence of pars for all headers.""" return self._message.raw_headers @@ -427,7 +427,7 @@ def if_range(self, _IF_RANGE=hdrs.IF_RANGE): """ return self._http_date(self.headers.get(_IF_RANGE)) - @property + @reify def keep_alive(self): """Is keepalive enabled by client?""" return not self._message.should_close @@ -443,7 +443,7 @@ def cookies(self): return MappingProxyType( {key: val.value for key, val in parsed.items()}) - @property + @reify def http_range(self, *, _RANGE=hdrs.RANGE): """The content of Range HTTP header. @@ -479,7 +479,7 @@ def http_range(self, *, _RANGE=hdrs.RANGE): return slice(start, end, 1) - @property + @reify def content(self): """Return raw payload stream.""" return self._payload @@ -497,7 +497,7 @@ def can_read_body(self): """Return True if request's HTTP BODY can be read, False otherwise.""" return not self._payload.at_eof() - @property + @reify def body_exists(self): """Return True if request has HTTP BODY, False otherwise.""" return type(self._payload) is not EmptyStreamReader @@ -657,7 +657,7 @@ def clone(self, *, method=sentinel, rel_url=sentinel, ret._match_info = self._match_info return ret - @property + @reify def match_info(self): """Result of route resolving.""" return self._match_info diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 66accdc9120..7dfccd36a56 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -66,6 +66,7 @@ ctor Ctrl Cython cythonized +Cythonize de deduplicate # de-facto: diff --git a/setup.py b/setup.py index e32fe5e52f7..3263977ff2c 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,8 @@ ), Extension('aiohttp._frozenlist', ['aiohttp/_frozenlist' + ext]), + Extension('aiohttp._helpers', + ['aiohttp/_helpers' + ext]), Extension('aiohttp._http_writer', ['aiohttp/_http_writer' + ext])] diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 310eac6509d..afeb5da0cf3 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -241,14 +241,16 @@ def log(self, request, response, time): mock_logger.info.assert_called_with('request response 1') -class TestReify: +class ReifyMixin: + + reify = NotImplemented def test_reify(self): class A: def __init__(self): self._cache = {} - @helpers.reify + @self.reify def prop(self): return 1 @@ -260,12 +262,12 @@ class A: def __init__(self): self._cache = {} - @helpers.reify + @self.reify def prop(self): """Docstring.""" return 1 - assert isinstance(A.prop, helpers.reify) + assert isinstance(A.prop, self.reify) assert 'Docstring.' == A.prop.__doc__ def test_reify_assignment(self): @@ -273,7 +275,7 @@ class A: def __init__(self): self._cache = {} - @helpers.reify + @self.reify def prop(self): return 1 @@ -282,6 +284,14 @@ def prop(self): with pytest.raises(AttributeError): a.prop = 123 + +class TestPyReify(ReifyMixin): + reify = helpers.reify_py + + +class TestCReify(ReifyMixin): + reify = helpers.reify_c + # ----------------------------------- is_ip_address() ----------------------