Skip to content

Commit

Permalink
Merge pull request #46 from christopherjwang/christopherjwang/django_…
Browse files Browse the repository at this point in the history
…cached_property_support

Add support for django.utils.functional.cached_property
  • Loading branch information
mpage committed Jan 8, 2018
2 parents a58d6a5 + 40ca2d6 commit 2a35a0c
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 55 deletions.
3 changes: 2 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ pytest-smartcov = "*"
cython = "*"
sphinx = "*"
twine = "*"
django = "*"


[requires]

python_version = "3.6"
python_version = "3.6"
95 changes: 41 additions & 54 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions monkeytype/stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
_Union,
)

try:
from django.utils.functional import cached_property # type: ignore
except ImportError:
cached_property = None


from monkeytype.tracing import (
CallTrace,
Expand All @@ -55,6 +60,7 @@ class FunctionKind(enum.Enum):
STATIC = 3
# Properties are really instance methods, but this is fine for now...
PROPERTY = 4
DJANGO_CACHED_PROPERTY = 5

@classmethod
def from_callable(cls, func: Callable) -> 'FunctionKind':
Expand All @@ -67,6 +73,8 @@ def from_callable(cls, func: Callable) -> 'FunctionKind':
return FunctionKind.STATIC
elif isinstance(func_or_desc, property):
return FunctionKind.PROPERTY
elif cached_property and isinstance(func_or_desc, cached_property):
return FunctionKind.DJANGO_CACHED_PROPERTY
return FunctionKind.INSTANCE


Expand All @@ -75,6 +83,7 @@ class FunctionDefinition:
FunctionKind.CLASS,
FunctionKind.INSTANCE,
FunctionKind.PROPERTY,
FunctionKind.DJANGO_CACHED_PROPERTY,
}

def __init__(
Expand Down Expand Up @@ -466,6 +475,8 @@ def render(self, prefix: str = '') -> str:
s = prefix + "@staticmethod\n" + s
elif self.kind == FunctionKind.PROPERTY:
s = prefix + "@property\n" + s
elif self.kind == FunctionKind.DJANGO_CACHED_PROPERTY:
s = prefix + "@cached_property\n" + s
return s

def __repr__(self) -> str:
Expand Down
7 changes: 7 additions & 0 deletions monkeytype/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
cast,
)

try:
from django.utils.functional import cached_property # type: ignore
except ImportError:
cached_property = None

from monkeytype.typing import get_type
from monkeytype.util import get_func_fqname

Expand Down Expand Up @@ -112,6 +117,8 @@ def get_func_in_mro(obj: Any, code: CodeType) -> Optional[Callable]:
cand = cast(Callable, val.__func__)
elif isinstance(val, property) and (val.fset is None) and (val.fdel is None):
cand = cast(Callable, val.fget)
elif cached_property and isinstance(val, cached_property):
cand = cast(Callable, val.func)
else:
cand = cast(Callable, val)
return _has_code(cand, code)
Expand Down
7 changes: 7 additions & 0 deletions monkeytype/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import inspect
import types

try:
from django.utils.functional import cached_property # type: ignore
except ImportError:
cached_property = None

from typing import (
Any,
Callable,
Expand Down Expand Up @@ -46,6 +51,8 @@ def get_func_in_module(module: str, qualname: str) -> Callable:
else:
raise InvalidTypeError(
f"Property {module}.{qualname} is missing getter")
elif cached_property and isinstance(func, cached_property):
func = func.func
elif not isinstance(func, (types.FunctionType, types.BuiltinFunctionType)):
raise InvalidTypeError(
f"{module}.{qualname} is of type '{type(func)}', not function.")
Expand Down
14 changes: 14 additions & 0 deletions tests/test_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ def test_property(self):
])
assert stub.render() == expected

def test_cached_property(self):
stub = FunctionStub('test',
inspect.signature(Dummy.a_cached_property.func), FunctionKind.DJANGO_CACHED_PROPERTY)
expected = "\n".join([
'@cached_property',
'def test%s: ...' % (render_signature(stub.signature),),
])
assert stub.render() == expected

def test_simple(self):
for kind in [FunctionKind.MODULE, FunctionKind.INSTANCE]:
stub = FunctionStub('test', inspect.signature(simple_add), kind)
Expand Down Expand Up @@ -477,6 +486,7 @@ class TestFunctionKind:
(Dummy.a_class_method.__func__, FunctionKind.CLASS),
(Dummy.an_instance_method, FunctionKind.INSTANCE),
(Dummy.a_property.fget, FunctionKind.PROPERTY),
(Dummy.a_cached_property.func, FunctionKind.DJANGO_CACHED_PROPERTY),
(a_module_func, FunctionKind.MODULE),
],
)
Expand All @@ -492,6 +502,7 @@ class TestFunctionDefinition:
(Dummy.a_class_method.__func__, True),
(Dummy.an_instance_method, True),
(Dummy.a_property.fget, True),
(Dummy.a_cached_property.func, True),
(a_module_func, False),
],
)
Expand All @@ -514,6 +525,9 @@ def test_has_self(self, func, expected):
(Dummy.a_property.fget, FunctionDefinition(
'tests.util', 'Dummy.a_property', FunctionKind.PROPERTY,
Signature.from_callable(Dummy.a_property.fget))),
(Dummy.a_cached_property.func, FunctionDefinition(
'tests.util', 'Dummy.a_cached_property', FunctionKind.DJANGO_CACHED_PROPERTY,
Signature.from_callable(Dummy.a_cached_property.func))),
(a_module_func, FunctionDefinition(
'tests.test_stubs', 'a_module_func', FunctionKind.MODULE,
Signature.from_callable(a_module_func))),
Expand Down
6 changes: 6 additions & 0 deletions tests/test_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)

import pytest
from django.utils.functional import cached_property

from monkeytype.tracing import (
CallTrace,
Expand Down Expand Up @@ -69,6 +70,10 @@ def an_instance_method(self) -> Optional[FrameType]:
def a_property(self) -> Optional[FrameType]:
return inspect.currentframe()

@cached_property
def a_cached_property(self) -> Optional[FrameType]:
return inspect.currentframe()


def a_module_function() -> Optional[FrameType]:
return inspect.currentframe()
Expand All @@ -83,6 +88,7 @@ class TestGetFunc:
(GetFuncHelper().an_instance_method(), GetFuncHelper.an_instance_method),
(a_module_function(), a_module_function),
(GetFuncHelper().a_property, GetFuncHelper.a_property.fget),
(GetFuncHelper().a_cached_property, GetFuncHelper.a_cached_property.func),
],
)
def test_get_func(self, frame, expected_func):
Expand Down
7 changes: 7 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def test_get_settable_property(self):
with pytest.raises(InvalidTypeError):
get_func_in_module(func.__module__, func.__qualname__)

def test_get_cached_property(self):
"""We should be able to look up properties that are decorated
with django.utils.functional.cached_property"""
func = Dummy.a_cached_property.func
obj = get_func_in_module(func.__module__, func.__qualname__)
assert obj == func

def test_get_non_function(self):
"""Raise an error if lookup returns something that isn't a function"""
with pytest.raises(InvalidTypeError):
Expand Down
5 changes: 5 additions & 0 deletions tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
List,
Optional,
)
from django.utils.functional import cached_property


class Dummy:
Expand All @@ -37,6 +38,10 @@ def a_settable_property(self) -> Optional[FrameType]:
def a_settable_property(self, unused) -> Optional[FrameType]:
return inspect.currentframe()

@cached_property
def a_cached_property(self) -> Optional[FrameType]:
return inspect.currentframe()


class Outer:
class Inner:
Expand Down

0 comments on commit 2a35a0c

Please sign in to comment.