Skip to content

Commit

Permalink
Turn keywords into a property
Browse files Browse the repository at this point in the history
This improves collection time when keywords are not used, and helps by
factoring out the code in general.

With `PYTEST_REORDER_TESTS=0` (since the re-ordering triggers getting keywords
to add marks to it...):

Before:

    raw times: 1.58 sec, 1.56 sec, 1.57 sec, 1.57 sec, 1.57 sec
    1 loop, best of 5: 1.56 sec per loop

After:

    raw times: 1.49 sec, 1.48 sec, 1.48 sec, 1.5 sec, 1.47 sec
    1 loop, best of 5: 1.47 sec per loop
  • Loading branch information
blueyed committed Apr 9, 2020
1 parent bdd4eac commit fc985ce
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 28 deletions.
12 changes: 9 additions & 3 deletions src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class Node(metaclass=NodeMeta):
""" base class for Collector and Item the test collection tree.
Collector subclasses have children, Items are terminal nodes."""

_keywords = None

def __init__(
self,
name: str,
Expand Down Expand Up @@ -126,9 +128,6 @@ def __init__(
#: filesystem path where this node was collected from (can be None)
self.fspath = fspath or getattr(parent, "fspath", None) # type: py.path.local

#: keywords/markers collected from all scopes
self.keywords = NodeKeywords(self)

#: The (manually added) marks belonging to this node (start, end).
self._own_markers = ([], []) # type: Tuple[List[Mark], List[Mark]]

Expand Down Expand Up @@ -178,6 +177,13 @@ def own_markers(self) -> List[Mark]:
"""The marker objects belonging to this node."""
return self._own_markers[0] + self._own_markers[1]

@property
def keywords(self) -> NodeKeywords:
"""keywords/markers collected from all scopes."""
if self._keywords is None:
self._keywords = NodeKeywords(self)
return self._keywords

def __repr__(self):
return "<{} nodeid={!r}>".format(
self.__class__.__name__, getattr(self, "nodeid", None)
Expand Down
63 changes: 39 additions & 24 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from _pytest.mark.structures import get_unpacked_marks
from _pytest.mark.structures import Mark
from _pytest.mark.structures import normalize_mark_list
from _pytest.nodes import NodeKeywords
from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.pathlib import parts
Expand Down Expand Up @@ -273,6 +274,8 @@ def obj(self):
@obj.setter
def obj(self, value):
self._obj = value
self._obj_markers = None
self._keywords = None

def _getobj(self):
"""Gets the underlying Python object. May be overwritten by subclasses."""
Expand Down Expand Up @@ -1443,29 +1446,11 @@ def __init__(
if callobj is not NOTSET:
self.obj = callobj

self.keywords.update(self.obj.__dict__)
self._callspec = callspec
if callspec:
self.callspec = callspec
# this is total hostile and a mess
# keywords are broken by design by now
# this will be redeemed later
for mark in callspec.marks:
# feel free to cry, this was broken for years before
# and keywords cant fix it per design
self.keywords[mark.name] = mark
if keywords:
self.keywords.update(keywords)

# todo: this is a hell of a hack
# https://github.com/pytest-dev/pytest/issues/4569

self.keywords.update(
{
mark.name: True
for mark in self.iter_markers()
if mark.name not in self.keywords
}
)
# XXX: only set for existing hasattr checks..!
self.callspec = self._callspec
self._keywords_arg = keywords

if fixtureinfo is None:
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
Expand Down Expand Up @@ -1501,10 +1486,40 @@ def function(self):
def own_markers(self) -> List[Mark]:
if self._obj_markers is None:
self._obj_markers = get_unpacked_marks(self.obj)
if hasattr(self, "callspec"):
self._obj_markers += normalize_mark_list(self.callspec.marks)
if self._callspec:
self._obj_markers += normalize_mark_list(self._callspec.marks)
return self._own_markers[0] + self._obj_markers + self._own_markers[1]

@property
def keywords(self) -> NodeKeywords:
if self._keywords is not None:
return self._keywords

keywords = super().keywords
keywords.update(self.obj.__dict__)
if self._callspec:
# this is total hostile and a mess
# keywords are broken by design by now
# this will be redeemed later
for mark in self._callspec.marks:
# feel free to cry, this was broken for years before
# and keywords cant fix it per design
self.keywords[mark.name] = mark
if self._keywords_arg:
keywords.update(self._keywords_arg)

# todo: this is a hell of a hack
# https://github.com/pytest-dev/pytest/issues/4569
keywords.update(
{
mark.name: True
for mark in self.iter_markers()
if mark.name not in self.keywords
}
)
self._keywords = keywords
return self._keywords

def _getobj(self):
name = self.name
i = name.find("[") # parametrization
Expand Down
55 changes: 54 additions & 1 deletion testing/test_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
from unittest import mock

import pytest
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.mark import EMPTY_PARAMETERSET_OPTION
from _pytest.mark import MarkGenerator as Mark
from _pytest.mark.structures import NodeKeywords
from _pytest.nodes import Collector
from _pytest.nodes import Node
from _pytest.python import Function


class TestMark:
Expand Down Expand Up @@ -1012,7 +1015,7 @@ def test_3():
assert reprec.countoutcomes() == [3, 0, 0]


def test_addmarker_order():
def test_addmarker_order(pytestconfig: Config, monkeypatch) -> None:
session = mock.Mock()
session.own_markers = []
session.parent = None
Expand All @@ -1025,6 +1028,56 @@ def test_addmarker_order():
extracted = [x.name for x in node.iter_markers()]
assert extracted == ["baz", "foo", "bar"]

# Check marks/keywords with Function.
session.name = "session"
session.keywords = NodeKeywords(session)

# Register markers for `--strict-markers`.
added_markers = pytestconfig._inicache["markers"] + [
"funcmark",
"prepended",
"funcmark2",
]
monkeypatch.setitem(pytestconfig._inicache, "markers", added_markers)

@pytest.mark.funcmark
def f1():
pass

func = Function.from_parent(node, name="func", callobj=f1)
expected_marks = ["funcmark", "baz", "foo", "bar"]
assert [x.name for x in func.iter_markers()] == expected_marks
func.add_marker("prepended", append=False)
assert [x.name for x in func.iter_markers()] == ["prepended"] + expected_marks
assert set(func.keywords) == {
"Test",
"bar",
"baz",
"foo",
"func",
"funcmark",
"prepended",
"pytestmark",
"session",
}

# Changing the "obj" updates marks and keywords (lazily).
@pytest.mark.funcmark2
def f2():
pass

func.obj = f2
assert [x.name for x in func.iter_markers()] == [
"prepended",
"funcmark2",
"baz",
"foo",
"bar",
]
keywords = set(func.keywords)
assert "funcmark2" in keywords
assert "funcmark" not in keywords


@pytest.mark.filterwarnings("ignore")
def test_markers_from_parametrize(testdir):
Expand Down

0 comments on commit fc985ce

Please sign in to comment.