Skip to content

Commit

Permalink
Cythonize reify (#2995)
Browse files Browse the repository at this point in the history
* Cleanup

* Implement cythonized reify decorator

* Add changelog

* reify more props

* Fix spelling

* Use bash version of codecov uploader
  • Loading branch information
asvetlov authored May 14, 2018
1 parent 926ff9e commit 97f3d2c
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ script:
- make cov-ci-run

after_success:
- codecov
# - codecov
- bash <(curl -s https://codecov.io/bash)

_helpers:
- &_mainstream_python_base
Expand Down
1 change: 1 addition & 0 deletions CHANGES/2995.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Cythonize ``@helpers.reify``, 5% boost on macro benchmark
35 changes: 35 additions & 0 deletions aiohttp/_helpers.pyx
Original file line number Diff line number Diff line change
@@ -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")
16 changes: 11 additions & 5 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 = (
Expand Down
34 changes: 17 additions & 17 deletions aiohttp/web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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'
Expand Down Expand Up @@ -289,15 +289,15 @@ def scheme(self):
else:
return 'http'

@property
@reify
def method(self):
"""Read only property for getting HTTP method.
The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
"""
return self._method

@property
@reify
def version(self):
"""Read only property for getting HTTP version of request.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -368,25 +368,25 @@ 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.
E.g., id=10
"""
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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ctor
Ctrl
Cython
cythonized
Cythonize
de
deduplicate
# de-facto:
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
),
Extension('aiohttp._frozenlist',
['aiohttp/_frozenlist' + ext]),
Extension('aiohttp._helpers',
['aiohttp/_helpers' + ext]),
Extension('aiohttp._http_writer',
['aiohttp/_http_writer' + ext])]

Expand Down
20 changes: 15 additions & 5 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -260,20 +262,20 @@ 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):
class A:
def __init__(self):
self._cache = {}

@helpers.reify
@self.reify
def prop(self):
return 1

Expand All @@ -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() ----------------------


Expand Down

0 comments on commit 97f3d2c

Please sign in to comment.