From 82475ed5f5f8f56215b9807a44f8569c7340614a Mon Sep 17 00:00:00 2001 From: "p.kamayev" Date: Wed, 27 Apr 2016 20:51:11 +0300 Subject: [PATCH 1/4] fixed check for simple function in fields to support type annotated functions[F --- rest_framework/fields.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5dcd546c0..643aa762f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -55,8 +55,10 @@ def is_simple_callable(obj): if not (function or method): return False - - args, _, _, defaults = inspect.getargspec(obj) + if six.PY2: + args, _, _, defaults = inspect.getargspec(obj) + else: + args, _, _, defaults, _, _, _ = inspect.getfullargspec(obj) len_args = len(args) if function else len(args) - 1 len_defaults = len(defaults) if defaults else 0 return len_args <= len_defaults From aadbc0c83c728fe99d1a01b50a10c55c0a5ef018 Mon Sep 17 00:00:00 2001 From: "p.kamayev" Date: Thu, 28 Apr 2016 08:02:28 +0300 Subject: [PATCH 2/4] moved to compat and covered by tests --- rest_framework/compat.py | 34 ++++++++++++++++++++ rest_framework/fields.py | 20 +----------- rest_framework/relations.py | 3 +- tests/compat/__init__.py | 0 tests/compat/test_compat_py35.py | 17 ++++++++++ tests/test_compat.py | 53 ++++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 tests/compat/__init__.py create mode 100644 tests/compat/test_compat_py35.py create mode 100644 tests/test_compat.py diff --git a/rest_framework/compat.py b/rest_framework/compat.py index e435618a2..759cd1fbc 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -6,6 +6,8 @@ versions of Django/Python, and compatibility wrappers around optional packages. # flake8: noqa from __future__ import unicode_literals +import inspect + import django from django.conf import settings from django.db import connection, transaction @@ -136,11 +138,43 @@ if six.PY3: SHORT_SEPARATORS = (',', ':') LONG_SEPARATORS = (', ', ': ') INDENT_SEPARATORS = (',', ': ') + + def is_simple_callable(obj): + function = inspect.isfunction(obj) + method = inspect.ismethod(obj) + + if not (function or method): + return False + # when we drop support of python3.2, we should replace getfullargspec with singnature + # signature = inspect.signature(obj) + # defaults = [p for p in signature.parameters.values() if p.default is not inspect.Parameter.empty] + # return len(signature.parameters) <= len(defaults) + function = inspect.isfunction(obj) + args, _, _, defaults, _, kwonly, kwdefaults = inspect.getfullargspec(obj) + len_args = (len(args) if function else len(args) - 1) + len(kwonly or ()) + len(kwdefaults or ()) + len_defaults = (len(defaults) if defaults else 0) + len(kwdefaults or ()) + return len_args <= len_defaults + else: SHORT_SEPARATORS = (b',', b':') LONG_SEPARATORS = (b', ', b': ') INDENT_SEPARATORS = (b',', b': ') + def is_simple_callable(obj): + """ + True if the object is a callable that takes no arguments. + """ + function = inspect.isfunction(obj) + method = inspect.ismethod(obj) + + if not (function or method): + return False + + args, _, _, defaults = inspect.getargspec(obj) + len_args = len(args) if function else len(args) - 1 + len_defaults = len(defaults) if defaults else 0 + return len_args <= len_defaults + try: # DecimalValidator is unavailable in Django < 1.9 from django.core.validators import DecimalValidator diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 643aa762f..0e79cf918 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -30,7 +30,7 @@ from django.utils.ipv6 import clean_ipv6_address from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 -from rest_framework.compat import unicode_repr, unicode_to_repr +from rest_framework.compat import unicode_repr, unicode_to_repr, is_simple_callable from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings from rest_framework.utils import html, humanize_datetime, representation @@ -46,24 +46,6 @@ class empty: pass -def is_simple_callable(obj): - """ - True if the object is a callable that takes no arguments. - """ - function = inspect.isfunction(obj) - method = inspect.ismethod(obj) - - if not (function or method): - return False - if six.PY2: - args, _, _, defaults = inspect.getargspec(obj) - else: - args, _, _, defaults, _, _, _ = inspect.getfullargspec(obj) - len_args = len(args) if function else len(args) - 1 - len_defaults = len(defaults) if defaults else 0 - return len_args <= len_defaults - - def get_attribute(instance, attrs): """ Similar to Python's built in `getattr(instance, attr)`, diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 572b69170..554a8c943 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -14,8 +14,9 @@ from django.utils.encoding import smart_text from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ +from rest_framework.compat import is_simple_callable from rest_framework.fields import ( - Field, empty, get_attribute, is_simple_callable, iter_options + Field, empty, get_attribute, iter_options ) from rest_framework.reverse import reverse from rest_framework.utils import html diff --git a/tests/compat/__init__.py b/tests/compat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/compat/test_compat_py35.py b/tests/compat/test_compat_py35.py new file mode 100644 index 000000000..19b849b0c --- /dev/null +++ b/tests/compat/test_compat_py35.py @@ -0,0 +1,17 @@ +class FunctionSimplicityCheckPy35Mixin: + def get_good_cases(self): + def annotated_simple() -> int: + return 0 + + def annotated_defaults(x: int = 0) -> int: + return 0 + + def kwonly_defaults(*, x=0): + pass + return super().get_good_cases() + (annotated_simple, annotated_defaults, kwonly_defaults) + + def get_bad_cases(self): + def kwonly(*, x): + pass + + return super().get_bad_cases() + (kwonly,) diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 000000000..b0c40ce34 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,53 @@ +from __future__ import unicode_literals + +import pytest +import sys + +from rest_framework.compat import is_simple_callable + + +class TestFunctionSimplicityCheck: + def get_good_cases(self): + def simple(): + pass + + def simple_with_default(x=0): + pass + + class SimpleMethods(object): + def simple(self): + pass + + def simple_with_default(self, x=0): + pass + + return simple, simple_with_default, SimpleMethods().simple, SimpleMethods().simple_with_default + + def get_bad_cases(self): + def positional(x): + pass + + def many_positional_and_defaults(x, y, z=0): + pass + + nofunc = 0 + + class Callable: + pass + + return positional, many_positional_and_defaults, nofunc, Callable + + def test_good_cases(self): + for case in self.get_good_cases(): + assert is_simple_callable(case) + + def test_bad_cases(self): + for case in self.get_bad_cases(): + assert not is_simple_callable(case) + + +if sys.version_info >= (3, 5): + from tests.compat.test_compat_py35 import FunctionSimplicityCheckPy35Mixin + + class TestFunctionSimplicityCheckPy35(FunctionSimplicityCheckPy35Mixin, TestFunctionSimplicityCheck): + pass From edc7680b76103818874e580c8c7a342d7530905a Mon Sep 17 00:00:00 2001 From: "p.kamayev" Date: Thu, 28 Apr 2016 10:45:38 +0300 Subject: [PATCH 3/4] small refactoring --- rest_framework/compat.py | 42 ++++++++----------- .../{test_compat_py35.py => compat_py35.py} | 0 tests/test_compat.py | 2 +- 3 files changed, 18 insertions(+), 26 deletions(-) rename tests/compat/{test_compat_py35.py => compat_py35.py} (100%) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 759cd1fbc..2a6af4c5c 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -7,6 +7,7 @@ versions of Django/Python, and compatibility wrappers around optional packages. from __future__ import unicode_literals import inspect +import sys import django from django.conf import settings @@ -139,37 +140,28 @@ if six.PY3: LONG_SEPARATORS = (', ', ': ') INDENT_SEPARATORS = (',', ': ') - def is_simple_callable(obj): - function = inspect.isfunction(obj) - method = inspect.ismethod(obj) - - if not (function or method): - return False - # when we drop support of python3.2, we should replace getfullargspec with singnature - # signature = inspect.signature(obj) - # defaults = [p for p in signature.parameters.values() if p.default is not inspect.Parameter.empty] - # return len(signature.parameters) <= len(defaults) - function = inspect.isfunction(obj) - args, _, _, defaults, _, kwonly, kwdefaults = inspect.getfullargspec(obj) - len_args = (len(args) if function else len(args) - 1) + len(kwonly or ()) + len(kwdefaults or ()) - len_defaults = (len(defaults) if defaults else 0) + len(kwdefaults or ()) - return len_args <= len_defaults - else: SHORT_SEPARATORS = (b',', b':') LONG_SEPARATORS = (b', ', b': ') INDENT_SEPARATORS = (b',', b': ') - def is_simple_callable(obj): - """ - True if the object is a callable that takes no arguments. - """ + +def is_simple_callable(obj): + """ + True if the object is a callable that takes no arguments. + """ + + function = inspect.isfunction(obj) + method = inspect.ismethod(obj) + + if not (function or method): + return False + if sys.version_info >= (3, 3): + signature = inspect.signature(obj) + defaults = [p for p in signature.parameters.values() if p.default is not inspect.Parameter.empty] + return len(signature.parameters) <= len(defaults) + else: function = inspect.isfunction(obj) - method = inspect.ismethod(obj) - - if not (function or method): - return False - args, _, _, defaults = inspect.getargspec(obj) len_args = len(args) if function else len(args) - 1 len_defaults = len(defaults) if defaults else 0 diff --git a/tests/compat/test_compat_py35.py b/tests/compat/compat_py35.py similarity index 100% rename from tests/compat/test_compat_py35.py rename to tests/compat/compat_py35.py diff --git a/tests/test_compat.py b/tests/test_compat.py index b0c40ce34..a93ccd398 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -47,7 +47,7 @@ class TestFunctionSimplicityCheck: if sys.version_info >= (3, 5): - from tests.compat.test_compat_py35 import FunctionSimplicityCheckPy35Mixin + from tests.compat.compat_py35 import FunctionSimplicityCheckPy35Mixin class TestFunctionSimplicityCheckPy35(FunctionSimplicityCheckPy35Mixin, TestFunctionSimplicityCheck): pass From 3cbb515787ff8d83996627b05d94be359707f7da Mon Sep 17 00:00:00 2001 From: "p.kamayev" Date: Thu, 28 Apr 2016 11:20:34 +0300 Subject: [PATCH 4/4] linting --- .isort.cfg | 2 +- rest_framework/fields.py | 4 +++- rest_framework/relations.py | 4 +--- tests/compat/compat_py35.py | 4 ++++ tests/test_compat.py | 1 - 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 4d4a6a509..6a749cf25 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,5 +1,5 @@ [settings] -skip=.tox +skip=.tox,tests/compat/compat_py35.py atomic=true multi_line_output=5 known_standard_library=types diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0e79cf918..e7535bfe3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -30,7 +30,9 @@ from django.utils.ipv6 import clean_ipv6_address from django.utils.translation import ugettext_lazy as _ from rest_framework import ISO_8601 -from rest_framework.compat import unicode_repr, unicode_to_repr, is_simple_callable +from rest_framework.compat import ( + is_simple_callable, unicode_repr, unicode_to_repr +) from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings from rest_framework.utils import html, humanize_datetime, representation diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 554a8c943..5d8a41ad8 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -15,9 +15,7 @@ from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import is_simple_callable -from rest_framework.fields import ( - Field, empty, get_attribute, iter_options -) +from rest_framework.fields import Field, empty, get_attribute, iter_options from rest_framework.reverse import reverse from rest_framework.utils import html diff --git a/tests/compat/compat_py35.py b/tests/compat/compat_py35.py index 19b849b0c..8265f2658 100644 --- a/tests/compat/compat_py35.py +++ b/tests/compat/compat_py35.py @@ -1,3 +1,7 @@ +# for now, linting is done by python2.7, so for that file it should be disabled. +# flake8: noqa + + class FunctionSimplicityCheckPy35Mixin: def get_good_cases(self): def annotated_simple() -> int: diff --git a/tests/test_compat.py b/tests/test_compat.py index a93ccd398..974b9ba25 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import pytest import sys from rest_framework.compat import is_simple_callable