Merge pull request #2842 from ceridwen/features

Use funcsigs and inspect.signature to do function argument analysis
This commit is contained in:
Ronny Pfannschmidt 2017-10-20 07:49:08 +02:00 committed by GitHub
commit 083084fcbc
6 changed files with 63 additions and 51 deletions

View File

@ -30,6 +30,7 @@ Brianna Laugher
Bruno Oliveira Bruno Oliveira
Cal Leeming Cal Leeming
Carl Friedrich Bolz Carl Friedrich Bolz
Ceridwen
Charles Cloud Charles Cloud
Charnjit SiNGH (CCSJ) Charnjit SiNGH (CCSJ)
Chris Lamb Chris Lamb

View File

@ -2,11 +2,12 @@
python version compatibility code python version compatibility code
""" """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys
import codecs
import functools
import inspect import inspect
import re import re
import functools import sys
import codecs
import py import py
@ -25,6 +26,12 @@ _PY3 = sys.version_info > (3, 0)
_PY2 = not _PY3 _PY2 = not _PY3
if _PY3:
from inspect import signature, Parameter as Parameter
else:
from funcsigs import signature, Parameter as Parameter
NoneType = type(None) NoneType = type(None)
NOTSET = object() NOTSET = object()
@ -32,12 +39,10 @@ PY35 = sys.version_info[:2] >= (3, 5)
PY36 = sys.version_info[:2] >= (3, 6) PY36 = sys.version_info[:2] >= (3, 6)
MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError' MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError'
if hasattr(inspect, 'signature'):
def _format_args(func): def _format_args(func):
return str(inspect.signature(func)) return str(signature(func))
else:
def _format_args(func):
return inspect.formatargspec(*inspect.getargspec(func))
isfunction = inspect.isfunction isfunction = inspect.isfunction
isclass = inspect.isclass isclass = inspect.isclass
@ -63,7 +68,6 @@ def iscoroutinefunction(func):
def getlocation(function, curdir): def getlocation(function, curdir):
import inspect
fn = py.path.local(inspect.getfile(function)) fn = py.path.local(inspect.getfile(function))
lineno = py.builtin._getcode(function).co_firstlineno lineno = py.builtin._getcode(function).co_firstlineno
if fn.relto(curdir): if fn.relto(curdir):
@ -83,40 +87,45 @@ def num_mock_patch_args(function):
return len(patchings) return len(patchings)
def getfuncargnames(function, startindex=None, cls=None): def getfuncargnames(function, is_method=False, cls=None):
"""Returns the names of a function's mandatory arguments.
This should return the names of all function arguments that:
* Aren't bound to an instance or type as in instance or class methods.
* Don't have default values.
* Aren't bound with functools.partial.
* Aren't replaced with mocks.
The is_method and cls arguments indicate that the function should
be treated as a bound method even though it's not unless, only in
the case of cls, the function is a static method.
@RonnyPfannschmidt: This function should be refactored when we
revisit fixtures. The fixture mechanism should ask the node for
the fixture names, and not try to obtain directly from the
function object well after collection has occurred.
""" """
@RonnyPfannschmidt: This function should be refactored when we revisit fixtures. The # The parameters attribute of a Signature object contains an
fixture mechanism should ask the node for the fixture names, and not try to obtain # ordered mapping of parameter names to Parameter instances. This
directly from the function object well after collection has occurred. # creates a tuple of the names of the parameters that don't have
""" # defaults.
if startindex is None and cls is not None: arg_names = tuple(
is_staticmethod = isinstance(cls.__dict__.get(function.__name__, None), staticmethod) p.name for p in signature(function).parameters.values()
startindex = 0 if is_staticmethod else 1 if (p.kind is Parameter.POSITIONAL_OR_KEYWORD
# XXX merge with main.py's varnames or p.kind is Parameter.KEYWORD_ONLY) and
# assert not isclass(function) p.default is Parameter.empty)
realfunction = function # If this function should be treated as a bound method even though
while hasattr(realfunction, "__wrapped__"): # it's passed as an unbound method or function, remove the first
realfunction = realfunction.__wrapped__ # parameter name.
if startindex is None: if (is_method or
startindex = inspect.ismethod(function) and 1 or 0 (cls and not isinstance(cls.__dict__.get(function.__name__, None),
if realfunction != function: staticmethod))):
startindex += num_mock_patch_args(function) arg_names = arg_names[1:]
function = realfunction # Remove any names that will be replaced with mocks.
if isinstance(function, functools.partial): if hasattr(function, "__wrapped__"):
argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0] arg_names = arg_names[num_mock_patch_args(function):]
partial = function return arg_names
argnames = argnames[len(partial.args):]
if partial.keywords:
for kw in partial.keywords:
argnames.remove(kw)
else:
argnames = inspect.getargs(_pytest._code.getrawcode(function))[0]
defaults = getattr(function, 'func_defaults',
getattr(function, '__defaults__', None)) or ()
numdefaults = len(defaults)
if numdefaults:
return tuple(argnames[startindex:-numdefaults])
return tuple(argnames[startindex:])
if _PY3: if _PY3:

View File

@ -728,8 +728,7 @@ class FixtureDef:
where=baseid where=baseid
) )
self.params = params self.params = params
startindex = unittest and 1 or None self.argnames = getfuncargnames(func, is_method=unittest)
self.argnames = getfuncargnames(func, startindex=startindex)
self.unittest = unittest self.unittest = unittest
self.ids = ids self.ids = ids
self._finalizer = [] self._finalizer = []

4
changelog/2267.feature Normal file
View File

@ -0,0 +1,4 @@
Replace the old introspection code in compat.py that determines the
available arguments of fixtures with inspect.signature on Python 3 and
funcsigs.signature on Python 2. This should respect __signature__
declarations on functions.

View File

@ -44,16 +44,19 @@ def has_environment_marker_support():
def main(): def main():
install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools'] install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools']
extras_require = {}
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
# used by tox.ini to test with pluggy master # used by tox.ini to test with pluggy master
if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ: if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ:
install_requires.append('pluggy>=0.4.0,<0.5') install_requires.append('pluggy>=0.4.0,<0.5')
extras_require = {}
if has_environment_marker_support(): if has_environment_marker_support():
extras_require[':python_version<"3.0"'] = ['funcsigs']
extras_require[':sys_platform=="win32"'] = ['colorama'] extras_require[':sys_platform=="win32"'] = ['colorama']
else: else:
if sys.platform == 'win32': if sys.platform == 'win32':
install_requires.append('colorama') install_requires.append('colorama')
if sys.version_info < (3, 0):
install_requires.append('funcsigs')
setup( setup(
name='pytest', name='pytest',

View File

@ -2,7 +2,6 @@ from textwrap import dedent
import _pytest._code import _pytest._code
import pytest import pytest
import sys
from _pytest.pytester import get_public_names from _pytest.pytester import get_public_names
from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupError
from _pytest import fixtures from _pytest import fixtures
@ -34,9 +33,6 @@ def test_getfuncargnames():
pass pass
assert fixtures.getfuncargnames(A().f) == ('arg1',) assert fixtures.getfuncargnames(A().f) == ('arg1',)
if sys.version_info < (3, 0):
assert fixtures.getfuncargnames(A.f) == ('arg1',)
assert fixtures.getfuncargnames(A.static, cls=A) == ('arg1', 'arg2') assert fixtures.getfuncargnames(A.static, cls=A) == ('arg1', 'arg2')
@ -2826,7 +2822,7 @@ class TestShowFixtures(object):
import pytest import pytest
class TestClass: class TestClass:
@pytest.fixture @pytest.fixture
def fixture1(): def fixture1(self):
"""line1 """line1
line2 line2
indented line indented line