mark: reuse compiled expression for all items in -k/-m
The previous commit made this possible, so utilize it. Since legacy.py becomes pretty bare, I inlined it into __init__.py. I'm not sure it's really "legacy" anyway! Using a simple 50000 items benchmark with `--collect-only -k nomatch`: Before (two commits ago): ======================== 50000 deselected in 10.31s ===================== 19129345 function calls (18275596 primitive calls) in 10.634 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.001 0.001 2.270 2.270 __init__.py:149(pytest_collection_modifyitems) 1 0.036 0.036 2.270 2.270 __init__.py:104(deselect_by_keyword) 50000 0.055 0.000 2.226 0.000 legacy.py:87(matchkeyword) After: ======================== 50000 deselected in 9.37s ========================= 18029363 function calls (17175972 primitive calls) in 9.701 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 1.394 1.394 __init__.py:239(pytest_collection_modifyitems) 1 0.057 0.057 1.393 1.393 __init__.py:162(deselect_by_keyword) The matching itself can be optimized more but that's a different story.
This commit is contained in:
parent
622c4ce02e
commit
c714f05ad7
|
@ -1,9 +1,12 @@
|
|||
""" generic mechanism for marking and selecting python functions. """
|
||||
import warnings
|
||||
from typing import AbstractSet
|
||||
from typing import Optional
|
||||
|
||||
from .legacy import matchkeyword
|
||||
from .legacy import matchmark
|
||||
import attr
|
||||
|
||||
from .expression import Expression
|
||||
from .expression import ParseError
|
||||
from .structures import EMPTY_PARAMETERSET_OPTION
|
||||
from .structures import get_empty_parameterset_mark
|
||||
from .structures import Mark
|
||||
|
@ -11,6 +14,7 @@ from .structures import MARK_GEN
|
|||
from .structures import MarkDecorator
|
||||
from .structures import MarkGenerator
|
||||
from .structures import ParameterSet
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import UsageError
|
||||
|
@ -18,6 +22,9 @@ from _pytest.deprecated import MINUS_K_COLON
|
|||
from _pytest.deprecated import MINUS_K_DASH
|
||||
from _pytest.store import StoreKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.nodes import Item
|
||||
|
||||
__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"]
|
||||
|
||||
|
||||
|
@ -104,6 +111,57 @@ def pytest_cmdline_main(config):
|
|||
return 0
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class KeywordMatcher:
|
||||
"""A matcher for keywords.
|
||||
|
||||
Given a list of names, matches any substring of one of these names. The
|
||||
string inclusion check is case-insensitive.
|
||||
|
||||
Will match on the name of colitem, including the names of its parents.
|
||||
Only matches names of items which are either a :class:`Class` or a
|
||||
:class:`Function`.
|
||||
|
||||
Additionally, matches on names in the 'extra_keyword_matches' set of
|
||||
any item, as well as names directly assigned to test functions.
|
||||
"""
|
||||
|
||||
_names = attr.ib(type=AbstractSet[str])
|
||||
|
||||
@classmethod
|
||||
def from_item(cls, item: "Item") -> "KeywordMatcher":
|
||||
mapped_names = set()
|
||||
|
||||
# Add the names of the current item and any parent items
|
||||
import pytest
|
||||
|
||||
for item in item.listchain():
|
||||
if not isinstance(item, pytest.Instance):
|
||||
mapped_names.add(item.name)
|
||||
|
||||
# Add the names added as extra keywords to current or parent items
|
||||
mapped_names.update(item.listextrakeywords())
|
||||
|
||||
# Add the names attached to the current function through direct assignment
|
||||
function_obj = getattr(item, "function", None)
|
||||
if function_obj:
|
||||
mapped_names.update(function_obj.__dict__)
|
||||
|
||||
# add the markers to the keywords as we no longer handle them correctly
|
||||
mapped_names.update(mark.name for mark in item.iter_markers())
|
||||
|
||||
return cls(mapped_names)
|
||||
|
||||
def __call__(self, subname: str) -> bool:
|
||||
subname = subname.lower()
|
||||
names = (name.lower() for name in self._names)
|
||||
|
||||
for name in names:
|
||||
if subname in name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def deselect_by_keyword(items, config):
|
||||
keywordexpr = config.option.keyword.lstrip()
|
||||
if not keywordexpr:
|
||||
|
@ -120,10 +178,17 @@ def deselect_by_keyword(items, config):
|
|||
selectuntil = True
|
||||
keywordexpr = keywordexpr[:-1]
|
||||
|
||||
try:
|
||||
expression = Expression.compile(keywordexpr)
|
||||
except ParseError as e:
|
||||
raise UsageError(
|
||||
"Wrong expression passed to '-k': {}: {}".format(keywordexpr, e)
|
||||
) from None
|
||||
|
||||
remaining = []
|
||||
deselected = []
|
||||
for colitem in items:
|
||||
if keywordexpr and not matchkeyword(colitem, keywordexpr):
|
||||
if keywordexpr and not expression.evaluate(KeywordMatcher.from_item(colitem)):
|
||||
deselected.append(colitem)
|
||||
else:
|
||||
if selectuntil:
|
||||
|
@ -135,15 +200,40 @@ def deselect_by_keyword(items, config):
|
|||
items[:] = remaining
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class MarkMatcher:
|
||||
"""A matcher for markers which are present.
|
||||
|
||||
Tries to match on any marker names, attached to the given colitem.
|
||||
"""
|
||||
|
||||
own_mark_names = attr.ib()
|
||||
|
||||
@classmethod
|
||||
def from_item(cls, item) -> "MarkMatcher":
|
||||
mark_names = {mark.name for mark in item.iter_markers()}
|
||||
return cls(mark_names)
|
||||
|
||||
def __call__(self, name: str) -> bool:
|
||||
return name in self.own_mark_names
|
||||
|
||||
|
||||
def deselect_by_mark(items, config):
|
||||
matchexpr = config.option.markexpr
|
||||
if not matchexpr:
|
||||
return
|
||||
|
||||
try:
|
||||
expression = Expression.compile(matchexpr)
|
||||
except ParseError as e:
|
||||
raise UsageError(
|
||||
"Wrong expression passed to '-m': {}: {}".format(matchexpr, e)
|
||||
) from None
|
||||
|
||||
remaining = []
|
||||
deselected = []
|
||||
for item in items:
|
||||
if matchmark(item, matchexpr):
|
||||
if expression.evaluate(MarkMatcher.from_item(item)):
|
||||
remaining.append(item)
|
||||
else:
|
||||
deselected.append(item)
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
"""
|
||||
this is a place where we put datastructures used by legacy apis
|
||||
we hope to remove
|
||||
"""
|
||||
from typing import Set
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.mark.expression import Expression
|
||||
from _pytest.mark.expression import ParseError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.nodes import Item
|
||||
|
||||
|
||||
@attr.s
|
||||
class MarkMatcher:
|
||||
"""A matcher for markers which are present."""
|
||||
|
||||
own_mark_names = attr.ib()
|
||||
|
||||
@classmethod
|
||||
def from_item(cls, item) -> "MarkMatcher":
|
||||
mark_names = {mark.name for mark in item.iter_markers()}
|
||||
return cls(mark_names)
|
||||
|
||||
def __call__(self, name: str) -> bool:
|
||||
return name in self.own_mark_names
|
||||
|
||||
|
||||
@attr.s
|
||||
class KeywordMatcher:
|
||||
"""A matcher for keywords.
|
||||
|
||||
Given a list of names, matches any substring of one of these names. The
|
||||
string inclusion check is case-insensitive.
|
||||
"""
|
||||
|
||||
_names = attr.ib(type=Set[str])
|
||||
|
||||
@classmethod
|
||||
def from_item(cls, item: "Item") -> "KeywordMatcher":
|
||||
mapped_names = set()
|
||||
|
||||
# Add the names of the current item and any parent items
|
||||
import pytest
|
||||
|
||||
for item in item.listchain():
|
||||
if not isinstance(item, pytest.Instance):
|
||||
mapped_names.add(item.name)
|
||||
|
||||
# Add the names added as extra keywords to current or parent items
|
||||
mapped_names.update(item.listextrakeywords())
|
||||
|
||||
# Add the names attached to the current function through direct assignment
|
||||
function_obj = getattr(item, "function", None)
|
||||
if function_obj:
|
||||
mapped_names.update(function_obj.__dict__)
|
||||
|
||||
# add the markers to the keywords as we no longer handle them correctly
|
||||
mapped_names.update(mark.name for mark in item.iter_markers())
|
||||
|
||||
return cls(mapped_names)
|
||||
|
||||
def __call__(self, subname: str) -> bool:
|
||||
subname = subname.lower()
|
||||
names = (name.lower() for name in self._names)
|
||||
|
||||
for name in names:
|
||||
if subname in name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def matchmark(colitem, markexpr: str) -> bool:
|
||||
"""Tries to match on any marker names, attached to the given colitem."""
|
||||
try:
|
||||
expression = Expression.compile(markexpr)
|
||||
except ParseError as e:
|
||||
raise UsageError(
|
||||
"Wrong expression passed to '-m': {}: {}".format(markexpr, e)
|
||||
) from None
|
||||
return expression.evaluate(MarkMatcher.from_item(colitem))
|
||||
|
||||
|
||||
def matchkeyword(colitem, keywordexpr: str) -> bool:
|
||||
"""Tries to match given keyword expression to given collector item.
|
||||
|
||||
Will match on the name of colitem, including the names of its parents.
|
||||
Only matches names of items which are either a :class:`Class` or a
|
||||
:class:`Function`.
|
||||
Additionally, matches on names in the 'extra_keyword_matches' set of
|
||||
any item, as well as names directly assigned to test functions.
|
||||
"""
|
||||
try:
|
||||
expression = Expression.compile(keywordexpr)
|
||||
except ParseError as e:
|
||||
raise UsageError(
|
||||
"Wrong expression passed to '-k': {}: {}".format(keywordexpr, e)
|
||||
) from None
|
||||
return expression.evaluate(KeywordMatcher.from_item(colitem))
|
|
@ -443,7 +443,7 @@ def test_testdir_subprocess_via_runpytest_arg(testdir) -> None:
|
|||
|
||||
|
||||
def test_unicode_args(testdir) -> None:
|
||||
result = testdir.runpytest("-k", "💩")
|
||||
result = testdir.runpytest("-k", "אבג")
|
||||
assert result.ret == ExitCode.NO_TESTS_COLLECTED
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue