Replace introspection in compat.getfuncargnames() with inspect/funcsigs.signature

This commit is contained in:
Ceridwen 2017-10-19 16:01:26 -07:00
parent c750a5beec
commit 3da28067f3
6 changed files with 63 additions and 50 deletions

View File

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

View File

@ -2,11 +2,12 @@
python version compatibility code
"""
from __future__ import absolute_import, division, print_function
import sys
import codecs
import functools
import inspect
import re
import functools
import codecs
import sys
import py
@ -25,6 +26,12 @@ _PY3 = sys.version_info > (3, 0)
_PY2 = not _PY3
if _PY3:
from inspect import signature, Parameter as Parameter
else:
from funcsigs import signature, Parameter as Parameter
NoneType = type(None)
NOTSET = object()
@ -32,12 +39,10 @@ PY35 = sys.version_info[:2] >= (3, 5)
PY36 = sys.version_info[:2] >= (3, 6)
MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError'
if hasattr(inspect, 'signature'):
def _format_args(func):
return str(inspect.signature(func))
else:
def _format_args(func):
return inspect.formatargspec(*inspect.getargspec(func))
return str(signature(func))
isfunction = inspect.isfunction
isclass = inspect.isclass
@ -63,7 +68,6 @@ def iscoroutinefunction(func):
def getlocation(function, curdir):
import inspect
fn = py.path.local(inspect.getfile(function))
lineno = py.builtin._getcode(function).co_firstlineno
if fn.relto(curdir):
@ -83,40 +87,45 @@ def num_mock_patch_args(function):
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
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.
"""
if startindex is None and cls is not None:
is_staticmethod = isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
startindex = 0 if is_staticmethod else 1
# XXX merge with main.py's varnames
# assert not isclass(function)
realfunction = function
while hasattr(realfunction, "__wrapped__"):
realfunction = realfunction.__wrapped__
if startindex is None:
startindex = inspect.ismethod(function) and 1 or 0
if realfunction != function:
startindex += num_mock_patch_args(function)
function = realfunction
if isinstance(function, functools.partial):
argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0]
partial = function
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:])
# The parameters attribute of a Signature object contains an
# ordered mapping of parameter names to Parameter instances. This
# creates a tuple of the names of the parameters that don't have
# defaults.
arg_names = tuple(
p.name for p in signature(function).parameters.values()
if (p.kind is Parameter.POSITIONAL_OR_KEYWORD
or p.kind is Parameter.KEYWORD_ONLY) and
p.default is Parameter.empty)
# If this function should be treated as a bound method even though
# it's passed as an unbound method or function, remove the first
# parameter name.
if (is_method or
(cls and not isinstance(cls.__dict__.get(function.__name__, None),
staticmethod))):
arg_names = arg_names[1:]
# Remove any names that will be replaced with mocks.
if hasattr(function, "__wrapped__"):
arg_names = arg_names[num_mock_patch_args(function):]
return arg_names
if _PY3:

View File

@ -728,8 +728,7 @@ class FixtureDef:
where=baseid
)
self.params = params
startindex = unittest and 1 or None
self.argnames = getfuncargnames(func, startindex=startindex)
self.argnames = getfuncargnames(func, is_method=unittest)
self.unittest = unittest
self.ids = ids
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():
install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools']
extras_require = {}
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
# used by tox.ini to test with pluggy master
if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ:
install_requires.append('pluggy>=0.4.0,<0.5')
extras_require = {}
if has_environment_marker_support():
extras_require[':python_version<"3.0"'] = ['funcsigs']
extras_require[':sys_platform=="win32"'] = ['colorama']
else:
if sys.platform == 'win32':
install_requires.append('colorama')
if sys.version_info < (3, 0):
install_requires.append('funcsigs')
setup(
name='pytest',

View File

@ -34,9 +34,6 @@ def test_getfuncargnames():
pass
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')
@ -2826,7 +2823,7 @@ class TestShowFixtures(object):
import pytest
class TestClass:
@pytest.fixture
def fixture1():
def fixture1(self):
"""line1
line2
indented line