Merge pull request #2842 from ceridwen/features
Use funcsigs and inspect.signature to do function argument analysis
This commit is contained in:
commit
083084fcbc
1
AUTHORS
1
AUTHORS
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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
setup.py
5
setup.py
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue