summaryrefslogtreecommitdiffstats
path: root/library/python/pytest/plugins/collection.py
diff options
context:
space:
mode:
authorantonyzhilin <[email protected]>2026-03-18 20:01:00 +0300
committerantonyzhilin <[email protected]>2026-03-18 20:39:57 +0300
commite156035788bddca3eb74b11bddc9afc4256d1f7b (patch)
tree864514f5bfa3dc41c754f7ad4ce15d903020084e /library/python/pytest/plugins/collection.py
parentb58f49094ff1acc06d494406856270d6c3b61761 (diff)
feat pytest: clean up CONFTEST_LOAD_POLICY_LOCAL implementation
* Add docs for `CONFTEST_LOAD_POLICY_LOCAL` and `CONFTEST_LOAD_POLICY_LEGACY_GLOBAL` * Use `ya_` prefixes for custom fields of `LoadedModule` to differentiate from fields used by pytest * Clean up `LoadedModule.__init__` parameters * Move resfs modules lookup from `main.py` to `collection.py` to gather module name lookup and loading in the same place * Turn `collection.py` into a plugin instead of using a class plugin. As a benefit, it is now displayed as `library.python.pytest.plugins.collection` in the plugin list (previously it was displayed as `<unnamed plugin>`) commit_hash:be61134f075fbc9b645cc98635ab33fe7bf8a979
Diffstat (limited to 'library/python/pytest/plugins/collection.py')
-rw-r--r--library/python/pytest/plugins/collection.py148
1 files changed, 90 insertions, 58 deletions
diff --git a/library/python/pytest/plugins/collection.py b/library/python/pytest/plugins/collection.py
index e00d9292921..b3b316ade78 100644
--- a/library/python/pytest/plugins/collection.py
+++ b/library/python/pytest/plugins/collection.py
@@ -8,15 +8,16 @@ import py
import pytest
import _pytest.doctest
import six
-from six import reraise
-
-if six.PY3:
- import pathlib
from library.python.pytest import module_utils
import library.python.testing.filter.filter as test_filter
import yatest.common
+import __res
+
+if six.PY3:
+ import pathlib
+
def _make_module_with_single_skipped_test(module_name, file_name, exc):
if exc.msg is not None:
@@ -37,20 +38,19 @@ def _make_module_with_single_skipped_test(module_name, file_name, exc):
class LoadedModule(pytest.Module):
- def __init__(self, parent, name, namespace=True, **kwargs):
- self.module_name = name
- if namespace:
- assert name.startswith('__tests__.')
- self.display_module_name = name[len('__tests__.') :]
+ def __init__(self, module_name, **kwargs):
+ self.ya_module_name = module_name
+ if module_name.startswith('__tests__.'):
+ self.ya_display_module_name = module_name[len('__tests__.') :]
else:
- self.display_module_name = name
- self.is_fake_module = False
+ self.ya_display_module_name = module_name
# Always passed to `super().__init__` explicitly.
kwargs.pop('nodeid', None)
- nodeid = module_utils.get_proper_module_path(self.module_name)
+ nodeid = module_utils.get_proper_module_path(self.ya_module_name)
+ self.ya_is_fake_module = False
if nodeid is None:
- self.is_fake_module = True
+ self.ya_is_fake_module = True
return
# Avoid specializing on `name`. It may change in the future if we add virtual collection nodes for directories.
@@ -62,10 +62,10 @@ class LoadedModule(pytest.Module):
if six.PY3:
kwargs['path'] = kwargs.get('path') or pathlib.Path(path)
- super().__init__(parent=parent, name=name, nodeid=nodeid, **kwargs)
+ super().__init__(name=name, nodeid=nodeid, **kwargs)
else:
kwargs['fspath'] = kwargs.get('fspath') or py.path.local(path)
- super(LoadedModule, self).__init__(parent=parent, nodeid=nodeid, **kwargs)
+ super(LoadedModule, self).__init__(nodeid=nodeid, **kwargs)
self.name = name
@classmethod
@@ -75,7 +75,7 @@ class LoadedModule(pytest.Module):
def _getobj(self):
# A simplified version of pytest `importtestmodule` that works with resfs.
try:
- __import__(self.module_name)
+ __import__(self.ya_module_name)
except pytest.skip.Exception as e:
if not e.allow_module_level:
raise RuntimeError(
@@ -83,14 +83,16 @@ class LoadedModule(pytest.Module):
)
# DEVTOOLSSUPPORT-79470: reraising pytest.skip.Exception from here seems to break reporting in ya plugin.
# Instead, pretend to be a module with a single skipped test.
- return _make_module_with_single_skipped_test(self.module_name, self.nodeid, e)
+ return _make_module_with_single_skipped_test(self.ya_module_name, self.nodeid, e)
except Exception as e:
- msg = 'Failed to load module "{}" and obtain list of tests due to an error'.format(self.display_module_name)
+ msg = 'Failed to load module "{}" and obtain list of tests due to an error'.format(
+ self.ya_display_module_name
+ )
logging.exception('%s: %s', msg, e)
etype, exc, tb = sys.exc_info()
- reraise(etype, type(exc)('{}\n{}'.format(exc, msg)), tb)
+ six.reraise(etype, type(exc)('{}\n{}'.format(exc, msg)), tb)
- return sys.modules[self.module_name]
+ return sys.modules[self.ya_module_name]
class DoctestModule(LoadedModule):
@@ -108,7 +110,7 @@ class DoctestModule(LoadedModule):
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags)
try:
- for test in finder.find(module, self.display_module_name):
+ for test in finder.find(module, self.ya_display_module_name):
if test.examples: # skip empty doctests
yield getattr(_pytest.doctest.DoctestItem, 'from_parent', _pytest.doctest.DoctestItem)(
name=test.name, parent=self, runner=runner, dtest=test
@@ -117,18 +119,18 @@ class DoctestModule(LoadedModule):
logging.exception('DoctestModule failed, probably you can add NO_DOCTESTS() macro to ya.make')
etype, exc, tb = sys.exc_info()
msg = 'DoctestModule failed, probably you can add NO_DOCTESTS() macro to ya.make'
- reraise(etype, type(exc)('{}\n{}'.format(exc, msg)), tb)
+ six.reraise(etype, type(exc)('{}\n{}'.format(exc, msg)), tb)
# NOTE: Since we are overriding collect method of pytest session, pytest hooks are not invoked during collection.
-# This function is only used in CollectionPlugin below, this is not an implementation of a pytest hook.
+# This is solely a helper function, not an implementation of a pytest hook.
def _pytest_ignore_collect(module, config, filenames_from_full_filters, accept_filename_predicate):
- if module.is_fake_module:
+ if module.ya_is_fake_module:
return True
# TODO refactor to use meaningful attributes like `path` instead?
# test_file_filter would need to be relative to repository root.
- legacy_name = module.display_module_name + '.py'
+ legacy_name = module.ya_display_module_name + '.py'
if config.option.mode == 'list':
return not accept_filename_predicate(legacy_name)
@@ -163,47 +165,77 @@ def _patch_set_initial_conftests(pluginmanager):
pluginmanager._set_initial_conftests = _set_initial_conftests_py2
-class CollectionPlugin(object):
- def __init__(self, test_modules, doctest_modules):
- self._test_modules = test_modules
- self._doctest_modules = doctest_modules
+def _find_resfs_test_and_doctest_modules():
+ prefix = '__tests__.'
- @pytest.hookimpl(tryfirst=True)
- def pytest_load_initial_conftests(self, early_config):
- _patch_set_initial_conftests(early_config.pluginmanager)
+ test_modules = [
+ # fmt: off
+ name
+ for name in sys.extra_modules
+ if name.startswith(prefix) and not name.endswith('.conftest')
+ # fmt: on
+ ]
- def pytest_sessionstart(self, session):
+ doctest_packages = __res.find("PY_DOCTEST_PACKAGES") or ""
+ if isinstance(doctest_packages, bytes):
+ doctest_packages = doctest_packages.decode('utf-8')
+ doctest_packages = doctest_packages.split()
- def collect(*args, **kwargs):
- config = session.config
+ def is_doctest_module(name):
+ for package in doctest_packages:
+ if name == package or name.startswith(str(package) + "."):
+ return True
+ return False
- # A custom function that is set in conftests.py.
- config._ya_register_non_initial_conftests()
+ doctest_modules = [
+ # fmt: off
+ name
+ for name in sys.extra_modules
+ if is_doctest_module(name)
+ # fmt: on
+ ]
- accept_filename_predicate = test_filter.make_py_file_filter(config.option.test_filter)
- full_test_names_file_path = config.option.test_list_path
- filenames_filter = None
+ return test_modules, doctest_modules
- if full_test_names_file_path and os.path.exists(full_test_names_file_path):
- with open(full_test_names_file_path, 'r') as afile:
- # in afile stored 2 dimensional array such that array[modulo_index] contains tests which should be run in this test suite
- full_names_filter = set(json.load(afile)[int(config.option.modulo_index)])
- filenames_filter = set(map(lambda x: x.split('::')[0], full_names_filter))
- for test_module in self._test_modules:
- module = LoadedModule.from_parent(name=test_module, parent=session)
- if not _pytest_ignore_collect(module, config, filenames_filter, accept_filename_predicate):
- yield module
[email protected](tryfirst=True)
+def pytest_load_initial_conftests(early_config):
+ _patch_set_initial_conftests(early_config.pluginmanager)
+
- if os.environ.get('YA_PYTEST_DISABLE_DOCTEST', 'no') == 'no':
- module = DoctestModule.from_parent(name=test_module, parent=session)
- if not _pytest_ignore_collect(module, config, filenames_filter, accept_filename_predicate):
- yield module
+def pytest_sessionstart(session):
+ resfs_test_modules, resfs_doctest_modules = _find_resfs_test_and_doctest_modules()
+
+ def collect(*args, **kwargs):
+ config = session.config
+
+ # A custom function that is set in conftests.py.
+ config._ya_register_non_initial_conftests()
+
+ accept_filename_predicate = test_filter.make_py_file_filter(config.option.test_filter)
+ full_test_names_file_path = config.option.test_list_path
+ filenames_filter = None
+
+ if full_test_names_file_path and os.path.exists(full_test_names_file_path):
+ with open(full_test_names_file_path, 'r') as afile:
+ # in afile stored 2 dimensional array such that array[modulo_index] contains tests which should be run in this test suite
+ full_names_filter = set(json.load(afile)[int(config.option.modulo_index)])
+ filenames_filter = set(map(lambda x: x.split('::')[0], full_names_filter))
+
+ for test_module in resfs_test_modules:
+ module = LoadedModule.from_parent(module_name=test_module, parent=session)
+ if not _pytest_ignore_collect(module, config, filenames_filter, accept_filename_predicate):
+ yield module
if os.environ.get('YA_PYTEST_DISABLE_DOCTEST', 'no') == 'no':
- for doctest_module in self._doctest_modules:
- module = DoctestModule.from_parent(name=doctest_module, parent=session, namespace=False)
- if not module.is_fake_module:
- yield module
+ module = DoctestModule.from_parent(module_name=test_module, parent=session)
+ if not _pytest_ignore_collect(module, config, filenames_filter, accept_filename_predicate):
+ yield module
+
+ if os.environ.get('YA_PYTEST_DISABLE_DOCTEST', 'no') == 'no':
+ for doctest_module in resfs_doctest_modules:
+ module = DoctestModule.from_parent(module_name=doctest_module, parent=session)
+ if not module.ya_is_fake_module:
+ yield module
- session.collect = collect
+ session.collect = collect