-
-
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
Mark expose nontransfered marks via pytestmark property #2517
Mark expose nontransfered marks via pytestmark property #2517
Conversation
@petr-balogh please take a look if this addition can help your use-case |
@RonnyPfannschmidt Cool, seems that now I can access everything I need and affect collecting tests exactly how I need. When do you thing this change could be merged and released? Content of: conftest.py import subprocess
COLUMNS = int(subprocess.check_output(['stty', 'size']).split()[1])
def pytest_collection_modifyitems(session, config, items):
for item in items:
print str(item).center(COLUMNS, "#")
g = item.get_marker("glob")
pytestmark = getattr(item.function, 'pytestmark', None)
print ("Marker got from item.get_marker('glob') is %s" % g)
if pytestmark:
print (
"pytestmark from item.pytestmark of item: %s is: %s" %
(item, pytestmark)
)
if g is not None:
for info in g:
print ("Glob args: %s, kwargs: %s" % (info.args, info.kwargs)) Content of: test_file.py import pytest
glob = pytest.mark.glob
tier1 = pytest.mark.tier1
tier2 = pytest.mark.tier2
tier3 = pytest.mark.tier3
@tier3
@glob("class", tier=3)
class ParentClass(object):
__test__ = False
@tier2
@glob("class", tier=2)
class TestClass(ParentClass):
__test__ = True
@tier1
@glob("method", tier=1)
def test_demo1(self):
print "Info test 1"
def test_demo2(self):
print "Info test 2" Output of pytest: py.test --capture=no ./test_file.py
Thank you very much |
_pytest/python.py
Outdated
@@ -23,7 +23,7 @@ | |||
safe_str, getlocation, enum, | |||
) | |||
from _pytest.runner import fail | |||
|
|||
from _pytest.mark import transfer_markers |
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.
maybe just empty line after imports?
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.
Thanks a ton for your never ending quest for improving our marks code! 👍 Of course you know the other devs and I really appreciate it!
Overall the change is good but I left some comments that I would us to discuss before moving this forward.
_pytest/mark.py
Outdated
return func | ||
|
||
mark = Mark(self.name, args, kwargs) | ||
return self.__class__(self.mark.combined_with(mark)) | ||
|
||
|
||
def apply_mark(obj, mark): | ||
assert isinstance(mark, Mark), mark | ||
"""applies a marker to an object, |
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 docstring should be the first statement here (before the assert
)
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 had to review the PR, the code, the tests and use the debugger to finally get a picture of what's going on. 😅
OK IIUC apply_mark
only sets or updates the pytestmark
attribute of the given obj
, while apply_legacy_mark
actually adds/updates the MarkInfo
instance with the mark name in obj
.
Perhaps the docstring of both methods should describe exactly what's going on? Suggestions:
def apply_mark(obj, mark):
"""
Sets or updates the ``pytestmark`` attribute on ``obj`` with the given ``mark`` object.
In the future (see issue X), this is the only effect ``@pytest.mark`` will have, and the
mark transfering from classes and modules to test functions will be done on
``pytest.Item`` objects instead.
"""
def update_legacy_mark_info(obj, mark):
"""
Updates or creates a ``MarkInfo`` as an attribute of the given object.
This is how markers are transfered from classes and modules to test functions in the legacy way,
which is planned to be phased out.
"""
Also, we are now introducing a public way to get "pristine" (non-inherited) markers on functions by using a machanism which is already used elsewhere, the pytestmark
attribute. The pytestmark
attribute in classes and modules has a different semantic in that they will transfer their marks to child test items.
I suggest we take this opportunity to introduce a proper public API which hides this detail, like pytest.get_pristine_marks
:
def get_pristine_marks(obj):
"""
Returns pytest marks applied directly to the given object, without any inheritance from
parent classes decorated with ``@pytest.mark`` or parent modules with the ``pytestmark``
attribute.
"""
Then we don't have to "promise" that we will be supporting a pytestmark
attribute in function objects directly in the future. If this suggesiton sounds good, I also believe we should name the new pytestmark
in functions to _pytest_marks
to avoid users using it by accident.
On the other hand now that I think about it, pytestmark
in modules have to be implemented like this given that you can't really decorate a module, so perhaps in the end pytestmark
as the holder for pristine marks is the way to go anyway.
What do you think?
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.
pytestmark will stay, no need to change it
im against a get_pristine_marks function, - the marks api people use to get/interact with marks sould be an new object on item that we will provide
after all marks in and from parameterization are to be considered as well and those are a pain to get
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.
im against a get_pristine_marks function, - the marks api people use to get/interact with marks sould be an new object on item that we will provide
I was actually suggesting using get_pristine_marks
instead of pytestmark
. Certainly accessing the merged marks will done through Item
's API. But after I concluded later it probably won't make much sense given that for modules at least the pytestmark
mechanism to declare marks will have to stay, so we might as well use the same for classes and functions as well. So let's move this forward as it is. 😁
Side topic: usually when things decorate user code, it might be a better to use a public API to manipulate what's being decorated instead of making the decorated attribute public. For example the mock
package experienced user experience problems because you can add arbitrary attributes to Mock
objects and use the same objects to use functionality, for example Mock().assert_called_once()
. The problem with that is that it was common for users to mistype some name and it would silently do nothing (it would return a new Mock
instance) as opposed to a failing error. This could have been better addressed I think by providing that functionality through an API instead through the object directly, for example mock.assert_called_once(m)
(with mock
here being the module and m
the Mock
object), which would prevent clashes and mistakes.
That was the general feeling that I had when proposing to use an API to access the list of unmerged marks: it would allow us to use an internal ugly name, possibly change this in the future to store that information somewhere without breaking user expectations, and even do some more work instead of just returning an internal attribute of the object.
But as I said, let's move forward with pytestmark
then. 👍
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.
(sorry for all the internal rambling)
@@ -329,30 +336,40 @@ def __call__(self, *args, **kwargs): | |||
is_class = inspect.isclass(func) | |||
if len(args) == 1 and (istestfunc(func) or is_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.
Suggestion to improve readability here:
- move
istestfunc(func)
to ais_test_func
local variable - add a
assert is_test_func
as the first line of theelse
statement;
I find a good practice to always assert for a known condition in a else
statement when the condition is not really obvious. This is more apparent when checking multiple possible paths:
if condition1:
...
elif condition2:
...
else:
assert condition3
...
_pytest/mark.py
Outdated
def apply_mark(obj, mark): | ||
assert isinstance(mark, Mark), mark | ||
"""applies a marker to an object, | ||
makrer transfers only update legacy markinfo objects |
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.
typo: marker
_pytest/mark.py
Outdated
|
||
def transfer_markers(funcobj, cls, mod): | ||
""" | ||
transfer legacy markers to the function level marminfo objects |
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.
Typo: marminfo
-> MarkInfo
_pytest/mark.py
Outdated
def transfer_markers(funcobj, cls, mod): | ||
""" | ||
transfer legacy markers to the function level marminfo objects | ||
this one is a major fsckup for mark breakages |
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 think it would be nice to expand on this here; the details are scattered through multiple issues, here would be a good place to summarize them IMO
changelog/2516.feature
Outdated
@@ -0,0 +1 @@ | |||
store unmeshed marks on functions pytestmark attribute |
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 could be more descriptive to end users:
Now test function objects have a ``pytestmark`` attribute containing a list of marks applied directly to the test function, as opposed to marks inherited from parent classes or modules.
Also we should definitely document this properly somewhere on the docs and make it official part of the API.
_pytest/deprecated.py
Outdated
@@ -7,6 +7,11 @@ | |||
""" | |||
from __future__ import absolute_import, division, print_function | |||
|
|||
|
|||
class RemovedInPytest4_0Warning(DeprecationWarning): |
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 think just RemovedPytest4Warning
is enough, no need to add the _0
in my opinion.
_pytest/deprecated.py
Outdated
@@ -7,6 +7,11 @@ | |||
""" | |||
from __future__ import absolute_import, division, print_function | |||
|
|||
|
|||
class RemovedInPytest4_0Warning(DeprecationWarning): | |||
"warning class for features removed in pytest 4.0" |
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.
Minor nitpick, please use triple strings for docstrings. 😁
_pytest/deprecated.py
Outdated
@@ -22,3 +27,6 @@ | |||
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue" | |||
|
|||
RESULT_LOG = '--result-log is deprecated and scheduled for removal in pytest 4.0' | |||
|
|||
MARK_INFO_ATTRIBUTE = RemovedInPytest4_0Warning( | |||
"Markinfo attributes are deprecated, please iterate the mark Collection") |
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 should be documented on the docs, I think. Users who see this probably won't know what this means.
I also suggest to add a (see https://docs.pytest.org/...)
pointing to the docs so users can easily go to it to know how to update their code.
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.
there is no new code for this yet ^^
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 do you mean? Actually, now that I read it again, I'm not sure what you mean by "please iterate the mark Collection"... oh do you mean the marks in the collected item?
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.
markinfo objects are iterables, atm the iteration yields markinfo objects as wel
@hpk42 i wonder if we can make it return the new mark type instead
* enhance api for fetching marks off an object * rename functions for storing marks * enhance deprecation message for MarkInfo
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.
+1
May I ask you if I can expect this change in 3.1.3 release? Thanks |
@petr-balogh that will have to go into |
@petr-balogh this will be part of the 3.2 release |
will fix #2516