Skip to content

Commit 083084f

Browse files
Merge pull request #2842 from ceridwen/features
Use funcsigs and inspect.signature to do function argument analysis
2 parents 5c71151 + f7387e4 commit 083084f

File tree

6 files changed

+63
-51
lines changed

6 files changed

+63
-51
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Brianna Laugher
3030
Bruno Oliveira
3131
Cal Leeming
3232
Carl Friedrich Bolz
33+
Ceridwen
3334
Charles Cloud
3435
Charnjit SiNGH (CCSJ)
3536
Chris Lamb

_pytest/compat.py

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
python version compatibility code
33
"""
44
from __future__ import absolute_import, division, print_function
5-
import sys
5+
6+
import codecs
7+
import functools
68
import inspect
79
import re
8-
import functools
9-
import codecs
10+
import sys
1011

1112
import py
1213

@@ -25,19 +26,23 @@
2526
_PY2 = not _PY3
2627

2728

29+
if _PY3:
30+
from inspect import signature, Parameter as Parameter
31+
else:
32+
from funcsigs import signature, Parameter as Parameter
33+
34+
2835
NoneType = type(None)
2936
NOTSET = object()
3037

3138
PY35 = sys.version_info[:2] >= (3, 5)
3239
PY36 = sys.version_info[:2] >= (3, 6)
3340
MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError'
3441

35-
if hasattr(inspect, 'signature'):
36-
def _format_args(func):
37-
return str(inspect.signature(func))
38-
else:
39-
def _format_args(func):
40-
return inspect.formatargspec(*inspect.getargspec(func))
42+
43+
def _format_args(func):
44+
return str(signature(func))
45+
4146

4247
isfunction = inspect.isfunction
4348
isclass = inspect.isclass
@@ -63,7 +68,6 @@ def iscoroutinefunction(func):
6368

6469

6570
def getlocation(function, curdir):
66-
import inspect
6771
fn = py.path.local(inspect.getfile(function))
6872
lineno = py.builtin._getcode(function).co_firstlineno
6973
if fn.relto(curdir):
@@ -83,40 +87,45 @@ def num_mock_patch_args(function):
8387
return len(patchings)
8488

8589

86-
def getfuncargnames(function, startindex=None, cls=None):
87-
"""
88-
@RonnyPfannschmidt: This function should be refactored when we revisit fixtures. The
89-
fixture mechanism should ask the node for the fixture names, and not try to obtain
90-
directly from the function object well after collection has occurred.
90+
def getfuncargnames(function, is_method=False, cls=None):
91+
"""Returns the names of a function's mandatory arguments.
92+
93+
This should return the names of all function arguments that:
94+
* Aren't bound to an instance or type as in instance or class methods.
95+
* Don't have default values.
96+
* Aren't bound with functools.partial.
97+
* Aren't replaced with mocks.
98+
99+
The is_method and cls arguments indicate that the function should
100+
be treated as a bound method even though it's not unless, only in
101+
the case of cls, the function is a static method.
102+
103+
@RonnyPfannschmidt: This function should be refactored when we
104+
revisit fixtures. The fixture mechanism should ask the node for
105+
the fixture names, and not try to obtain directly from the
106+
function object well after collection has occurred.
107+
91108
"""
92-
if startindex is None and cls is not None:
93-
is_staticmethod = isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
94-
startindex = 0 if is_staticmethod else 1
95-
# XXX merge with main.py's varnames
96-
# assert not isclass(function)
97-
realfunction = function
98-
while hasattr(realfunction, "__wrapped__"):
99-
realfunction = realfunction.__wrapped__
100-
if startindex is None:
101-
startindex = inspect.ismethod(function) and 1 or 0
102-
if realfunction != function:
103-
startindex += num_mock_patch_args(function)
104-
function = realfunction
105-
if isinstance(function, functools.partial):
106-
argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0]
107-
partial = function
108-
argnames = argnames[len(partial.args):]
109-
if partial.keywords:
110-
for kw in partial.keywords:
111-
argnames.remove(kw)
112-
else:
113-
argnames = inspect.getargs(_pytest._code.getrawcode(function))[0]
114-
defaults = getattr(function, 'func_defaults',
115-
getattr(function, '__defaults__', None)) or ()
116-
numdefaults = len(defaults)
117-
if numdefaults:
118-
return tuple(argnames[startindex:-numdefaults])
119-
return tuple(argnames[startindex:])
109+
# The parameters attribute of a Signature object contains an
110+
# ordered mapping of parameter names to Parameter instances. This
111+
# creates a tuple of the names of the parameters that don't have
112+
# defaults.
113+
arg_names = tuple(
114+
p.name for p in signature(function).parameters.values()
115+
if (p.kind is Parameter.POSITIONAL_OR_KEYWORD
116+
or p.kind is Parameter.KEYWORD_ONLY) and
117+
p.default is Parameter.empty)
118+
# If this function should be treated as a bound method even though
119+
# it's passed as an unbound method or function, remove the first
120+
# parameter name.
121+
if (is_method or
122+
(cls and not isinstance(cls.__dict__.get(function.__name__, None),
123+
staticmethod))):
124+
arg_names = arg_names[1:]
125+
# Remove any names that will be replaced with mocks.
126+
if hasattr(function, "__wrapped__"):
127+
arg_names = arg_names[num_mock_patch_args(function):]
128+
return arg_names
120129

121130

122131
if _PY3:

_pytest/fixtures.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -728,8 +728,7 @@ def __init__(self, fixturemanager, baseid, argname, func, scope, params,
728728
where=baseid
729729
)
730730
self.params = params
731-
startindex = unittest and 1 or None
732-
self.argnames = getfuncargnames(func, startindex=startindex)
731+
self.argnames = getfuncargnames(func, is_method=unittest)
733732
self.unittest = unittest
734733
self.ids = ids
735734
self._finalizer = []

changelog/2267.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Replace the old introspection code in compat.py that determines the
2+
available arguments of fixtures with inspect.signature on Python 3 and
3+
funcsigs.signature on Python 2. This should respect __signature__
4+
declarations on functions.

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,19 @@ def has_environment_marker_support():
4444

4545
def main():
4646
install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools']
47+
extras_require = {}
4748
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
4849
# used by tox.ini to test with pluggy master
4950
if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ:
5051
install_requires.append('pluggy>=0.4.0,<0.5')
51-
extras_require = {}
5252
if has_environment_marker_support():
53+
extras_require[':python_version<"3.0"'] = ['funcsigs']
5354
extras_require[':sys_platform=="win32"'] = ['colorama']
5455
else:
5556
if sys.platform == 'win32':
5657
install_requires.append('colorama')
58+
if sys.version_info < (3, 0):
59+
install_requires.append('funcsigs')
5760

5861
setup(
5962
name='pytest',

testing/python/fixture.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import _pytest._code
44
import pytest
5-
import sys
65
from _pytest.pytester import get_public_names
76
from _pytest.fixtures import FixtureLookupError
87
from _pytest import fixtures
@@ -34,9 +33,6 @@ def static(arg1, arg2):
3433
pass
3534

3635
assert fixtures.getfuncargnames(A().f) == ('arg1',)
37-
if sys.version_info < (3, 0):
38-
assert fixtures.getfuncargnames(A.f) == ('arg1',)
39-
4036
assert fixtures.getfuncargnames(A.static, cls=A) == ('arg1', 'arg2')
4137

4238

@@ -2826,7 +2822,7 @@ def test_show_fixtures_indented_in_class(self, testdir):
28262822
import pytest
28272823
class TestClass:
28282824
@pytest.fixture
2829-
def fixture1():
2825+
def fixture1(self):
28302826
"""line1
28312827
line2
28322828
indented line

0 commit comments

Comments
 (0)