-
-
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
ignore property errors when parsing fixure factories #2235
ignore property errors when parsing fixure factories #2235
Conversation
I wonder if this should go to the |
_pytest/fixtures.py
Outdated
@@ -1068,6 +1068,9 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): | |||
self._holderobjseen.add(holderobj) | |||
autousenames = [] | |||
for name in dir(holderobj): | |||
# Skip properties before the following getattr executes them. | |||
if isinstance(getattr(type(holderobj), name, None), property): |
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.
hmm, maybe this should use safe_getattr
from _pytest.compat
? Probably a getattr on a type will never raise something, but you never know with metaclasses and stuff I guess 😆
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.
It is technically possible to raise any exception from __getattr__
implemented on metaclass. Eg:
>>> getattr(fields.Fields.a, 'a', None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python2.7/dist-packages/fields/__init__.py", line 301, in __getattr__
raise TypeError("Field %r is already specified as required." % name)
TypeError: Field 'a' is already specified as required.
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.
Perhaps a better way to look at the object is to iterate on holderobj.__dict__
(getattr will involve the descriptor protocol, even if it's a class).
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.
the real problem is, that avoiding the descriptor protocol correctly is tricky
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.
Hehe. By far this is my „favourite” python quirk:
>>> class Desc(object):
... def __get__(self, inst, owner):
... print 'Surprise!', inst, owner
...
>>> class Foo(object):
... d = Desc()
...
>>> Foo.d
Surprise! None <class '__main__.Foo'>
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.
I will switch from getattr(type(holderobj), name, None)
to type(holderobj).__dict__.get(name)
, which is hopefully safer. Here I assume that type(X)
has a __dict__
for all X
. I think this is true - even if I try something like this:
>>> class FooMeta(type):
... __slots__ = ()
...
>>> class Foo(metaclass=FooMeta):
... pass
it still has a __dict__
:
>>> type(Foo())
<class '__main__.Foo'>
>>> type(Foo()).__dict__
mappingproxy({...stuff...})
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.
(Indeed the third argument to type(...)
must be a dict, None
or other types are not accepted)
I agree there is some slight breakage potential here. If you want me to change the target branch, let me know (the maintainers can also edit the PR). |
Let's wait what others think - no strong feelings either way, but history shows it's usually better to err on the side of caution 😉 |
0138d0b
to
34718c6
Compare
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.
btw, with just the switch to safe_getattr i believe its fine to go in to a patch release
_pytest/fixtures.py
Outdated
@@ -1068,6 +1068,9 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): | |||
self._holderobjseen.add(holderobj) | |||
autousenames = [] | |||
for name in dir(holderobj): | |||
# Skip properties before the following getattr executes them. | |||
if isinstance(type(holderobj).__dict__.get(name), property): |
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 really should use safe_getattr
instead
else this protects only against one specific case of error while staying open for dozens of others
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.
What sort of cases are you thinking of? As I understand it, this line cannot throw, since:
-
type(holderobj)
always has a__dict__
for allholderobj
, and that__dict__
is indeed adict
(as I mentioned in a comment above). -
Accessing the
__dict__
and checkingisinstance
can't trigger any Python voodoo.
On the other hand, using safe_getattr
will still trigger voodoo, though it will continue if it throws.
That said, having properties on the class type is really getting farfetched, so using safe_getattr
(or just getattr
) is fine by me as well. Just let me know if you still want me to change it.
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.
@bluetech basically any implementation of the descriptor protocol that does something at the class level can trigger
on the other hand i also think its fine to trigger those objects, since well, python mantra is "consenting adults" - we dont need to protect people from their own traps too much
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.
Just an idea: what if instead of explicitly skipping properties we just make sure we only accept function objects? After all fixtures can only be created using functions.
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.
@nicoddemus well, thats more strict in a good way ^^ i like the idea good thought :)
KeyboardInterrupt is a subclass of BaseException, but not of Exception. Hence if we remove this except, KeyboardInterrupts will still be raised so the behavior stays the same.
Descriptors (e.g. properties) such as in the added test case are triggered during collection, executing arbitrary code which can raise. Previously, such exceptions were propagated and failed the collection. Now these exceptions are caught and the corresponding attributes are silently ignored. A better solution would be to completely skip access to all custom descriptors, such that the offending code doesn't even trigger. However I think this requires manually going through the instance and all of its MRO for each and every attribute checking if it might be a proper fixture before accessing it. So I took the easy route here. In other words, putting something like this in your test class is still a bad idea...: @Property def innocent(self): os.system('rm -rf /') Fixes pytest-dev#2234.
34718c6
to
3a0a0c2
Compare
Thanks for the comment. I've updated it again to just replace the original This patch fixes my immediate problem; hopefully someone will fix the problem properly, but doing so is a bit over my head as a passerby (haven't even had a chance to use pytest fixtures yet). I also added another tiny change I noticed, you can drop that commit if you want. |
yup, the current code is fine to merge - additional enhancements can be done as part of a new pr |
Agreed, thanks again @bluetech! |
Previously, properties such as in the added test case were triggered
during collection, possibly executing unintended code. Let's skip them
instead.
We should probably skip all custom descriptors, however I am not sure
how to do this cleanly, so for now just skip properties.
Fixes #2234.