-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use funcsigs and inspect.signature to do function argument analysis #2842
Conversation
Hi @ceridwen, thanks for the detailed description.
I agree. Regarding I don't expect this change in behavior will break any test suites, save for those doing some very exotic things. Thanks again for looking into this problem @ceridwen! |
I've replaced the core functionality of getfuncargnames() with inspect.signature. There are two tests still failing:
There's a lot of commented-out debugging code still. I think startindex should probably be renamed since the way it's really used at the moment is to pass a boolean if and only if a test is collected from a unittest class. |
setup.py
Outdated
@@ -44,6 +44,8 @@ def has_environment_marker_support(): | |||
|
|||
def main(): | |||
install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools'] | |||
if sys.version_info < (3, 0): | |||
install_requires.append('funcsigs') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is broken for universal wheels
please use extras_requires
_pytest/compat.py
Outdated
@@ -21,6 +22,12 @@ | |||
enum = None | |||
|
|||
|
|||
try: | |||
from inspect import signature, Parameter as Parameter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please prefer to use explicit version checking instead of relying on ImportError
:
if _PY3:
from inspect import signature, Parameter as Parameter
else:
from funcsigs import signature as signature, Parameter as Parameter
I've seen before a broken installation throwing an ImportError
, which would go to the fallback statement and also fail, leaving me baffled for a awhile until finally figuring it out. 😅
About
import pytest
class TestClass:
@pytest.fixture
def fixture1():
"""line1
line2
indented line
""" I would guess the problem is that the fixture is missing |
You're correct, it's getting passed to signature() as a bound method, but it's invalid because it lacks the self argument. I added the self argument and deleted the other test, and all the tests now pass locally. |
Weird, for some reasons your PR now includes some commits that are already on |
I fixed that and squashed commits. |
Nevermind, the code certainly does look ready! 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work!
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice docs!
Yes, it's ready! |
well done, the failures are unrelated |
staticmethod))): | ||
arg_names = arg_names[1:] | ||
# Remove any names that will be replaced with mocks. | ||
if hasattr(function, "__wrapped__"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we might need to alter this bit when people start to use signature to fix up wrappers/decorators
# defaults. | ||
arg_names = tuple( | ||
p.name for p in signature(function).parameters.values() | ||
if (p.kind is Parameter.POSITIONAL_OR_KEYWORD |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we ought to obtain the constants via the instance in order to account for code that accidentially mixes different implementations of funcsigs
cc @nicoddemus #2828
This is not finished. I'm opening the PR now because I need to ask questions about implementation details.
The specific detail I'm looking at right now is that getfuncargnames's handling of functions with arguments with defaults or keyword-only arguments and arguments bound with functools.partial is distinctly... odd, and arguably buggy. Here's an example of functools.partial without any Python 3 features:
If I understand the use of getfuncargnames in fixtures.py correctly, it's to determine which arguments aren't bound and thus are available for substitution with the return values of fixtures. In this case, pytest would call
f0
with the fixtureb
but notc
, even ifc
is available. However, it would callg0
, the partially bound version of that same test function, withc
ifc
is available. If I instead create a function that givesb
a default value without using partial, getfuncargnames thinks that function should receive no fixtures.With keyword-only arguments in the mix, it gets weirder:
In the first case, even though
f
is the same function asf0
if you look only at mandatory arguments (it requires an argumentb
), the existence of the keyword-only argumentd
means that getfuncargnames believes that it should accept fixtures for bothb
andc
. Bindingb
with partial makes it so thatg
acceptsc
andd
fixtures, if pytest passes fixture values as keyword arguments rather than positional arguments (otherwise it will crash). Bindingb
andc
makes it so thath
will only be givend
fixture, even thoughf
doesn't accept ad
fixture at all.In the following example, adding to
f
ane
argument with a default makesb
,c
, andd
available for fixtures:getfuncargnames's effective API isn't documented anywhere, AFAICT. The unit tests for this function (in fixture.py) aren't very extensive, only covering methods, static methods, and positional-or-keyword arguments with defaults and not in all possible combinations. There are integration tests for mocking and partials in collect.py, integration.py, and metafunc.py. (Did I miss any?) These test those features in isolation, though, not in combination. Keyword-only arguments aren't tested anywhere, I'm sure because of pytest's age.
If it were up to me, I'd simplify this problem by treating only mandatory arguments (those without defaults) as available for fixtures, whether or not those mandatory arguments are keyword-or-positional or keyword-only. This would change the existing behavior. I believe this will make all the existing tests pass, though I haven't checked that yet. As I look into what happens with mock and other combinations of things, I may run into more of this, so stay tuned.