Skip to content

Commit

Permalink
Replace introspection in compat.getfuncargnames() with inspect/funcsi…
Browse files Browse the repository at this point in the history
…gs.signature
  • Loading branch information
ceridwen committed Oct 19, 2017
1 parent c750a5b commit 3da2806
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 50 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Brianna Laugher
Bruno Oliveira
Cal Leeming
Carl Friedrich Bolz
Ceridwen
Charles Cloud
Charnjit SiNGH (CCSJ)
Chris Lamb
Expand Down
95 changes: 52 additions & 43 deletions _pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -25,19 +26,23 @@
_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()

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))

def _format_args(func):
return str(signature(func))


isfunction = inspect.isfunction
isclass = inspect.isclass
Expand All @@ -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):
Expand All @@ -83,40 +87,45 @@ def num_mock_patch_args(function):
return len(patchings)


def getfuncargnames(function, startindex=None, cls=None):
"""
@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.
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.
"""
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:
Expand Down
3 changes: 1 addition & 2 deletions _pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,7 @@ def __init__(self, fixturemanager, baseid, argname, func, scope, params,
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 = []
Expand Down
4 changes: 4 additions & 0 deletions changelog/2267.feature
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 1 addition & 4 deletions testing/python/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ def static(arg1, arg2):
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')


Expand Down Expand Up @@ -2826,7 +2823,7 @@ def test_show_fixtures_indented_in_class(self, testdir):
import pytest
class TestClass:
@pytest.fixture
def fixture1():
def fixture1(self):
"""line1
line2
indented line
Expand Down

0 comments on commit 3da2806

Please sign in to comment.