Skip to content

Commit 2fcf763

Browse files
authored
Merge pull request #7671 from bluetech/ignored-names
RFC: python: skip work pytest_pycollect_makeitem work on certain names
2 parents 8730a7b + 98891a5 commit 2fcf763

File tree

3 files changed

+57
-0
lines changed

3 files changed

+57
-0
lines changed

changelog/7671.trivial.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
When collecting tests, pytest finds test classes and functions by examining the
2+
attributes of python objects (modules, classes and instances). To speed up this
3+
process, pytest now ignores builtin attributes (like ``__class__``,
4+
``__delattr__`` and ``__new__``) without consulting the ``python_classes`` and
5+
``python_functions`` configuration options and without passing them to plugins
6+
using the ``pytest_pycollect_makeitem`` hook.

src/_pytest/python.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import itertools
66
import os
77
import sys
8+
import types
89
import typing
910
import warnings
1011
from collections import Counter
@@ -343,6 +344,26 @@ def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]:
343344
return fspath, lineno, modpath
344345

345346

347+
# As an optimization, these builtin attribute names are pre-ignored when
348+
# iterating over an object during collection -- the pytest_pycollect_makeitem
349+
# hook is not called for them.
350+
# fmt: off
351+
class _EmptyClass: pass # noqa: E701
352+
IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
353+
frozenset(),
354+
# Module.
355+
dir(types.ModuleType("empty_module")),
356+
# Some extra module attributes the above doesn't catch.
357+
{"__builtins__", "__file__", "__cached__"},
358+
# Class.
359+
dir(_EmptyClass),
360+
# Instance.
361+
dir(_EmptyClass()),
362+
)
363+
del _EmptyClass
364+
# fmt: on
365+
366+
346367
class PyCollector(PyobjMixin, nodes.Collector):
347368
def funcnamefilter(self, name: str) -> bool:
348369
return self._matches_prefix_or_glob_option("python_functions", name)
@@ -404,6 +425,8 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
404425
# Note: seems like the dict can change during iteration -
405426
# be careful not to remove the list() without consideration.
406427
for name, obj in list(dic.items()):
428+
if name in IGNORED_ATTRIBUTES:
429+
continue
407430
if name in seen:
408431
continue
409432
seen.add(name)

testing/python/collect.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,34 @@ def test_something():
885885
result = testdir.runpytest_subprocess()
886886
result.stdout.fnmatch_lines(["*1 passed*"])
887887

888+
def test_early_ignored_attributes(self, testdir: Testdir) -> None:
889+
"""Builtin attributes should be ignored early on, even if
890+
configuration would otherwise allow them.
891+
892+
This tests a performance optimization, not correctness, really,
893+
although it tests PytestCollectionWarning is not raised, while
894+
it would have been raised otherwise.
895+
"""
896+
testdir.makeini(
897+
"""
898+
[pytest]
899+
python_classes=*
900+
python_functions=*
901+
"""
902+
)
903+
testdir.makepyfile(
904+
"""
905+
class TestEmpty:
906+
pass
907+
test_empty = TestEmpty()
908+
def test_real():
909+
pass
910+
"""
911+
)
912+
items, rec = testdir.inline_genitems()
913+
assert rec.ret == 0
914+
assert len(items) == 1
915+
888916

889917
def test_setup_only_available_in_subdir(testdir):
890918
sub1 = testdir.mkpydir("sub1")

0 commit comments

Comments
 (0)