Merge pull request #9493 from bluetech/conftesting
Some conftest changes
This commit is contained in:
		
						commit
						f1aa7a25de
					
				|  | @ -0,0 +1,10 @@ | ||||||
|  | Symbolic link components are no longer resolved in conftest paths. | ||||||
|  | This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice. | ||||||
|  | For example, given | ||||||
|  | 
 | ||||||
|  |     tests/real/conftest.py | ||||||
|  |     tests/real/test_it.py | ||||||
|  |     tests/link -> tests/real | ||||||
|  | 
 | ||||||
|  | running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``. | ||||||
|  | This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details). | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| """Command line options, ini-file and conftest.py processing.""" | """Command line options, ini-file and conftest.py processing.""" | ||||||
| import argparse | import argparse | ||||||
| import collections.abc | import collections.abc | ||||||
| import contextlib |  | ||||||
| import copy | import copy | ||||||
| import enum | import enum | ||||||
| import inspect | import inspect | ||||||
|  | @ -345,14 +344,19 @@ class PytestPluginManager(PluginManager): | ||||||
|         import _pytest.assertion |         import _pytest.assertion | ||||||
| 
 | 
 | ||||||
|         super().__init__("pytest") |         super().__init__("pytest") | ||||||
|         # The objects are module objects, only used generically. |  | ||||||
|         self._conftest_plugins: Set[types.ModuleType] = set() |  | ||||||
| 
 | 
 | ||||||
|         # State related to local conftest plugins. |         # -- State related to local conftest plugins. | ||||||
|  |         # All loaded conftest modules. | ||||||
|  |         self._conftest_plugins: Set[types.ModuleType] = set() | ||||||
|  |         # All conftest modules applicable for a directory. | ||||||
|  |         # This includes the directory's own conftest modules as well | ||||||
|  |         # as those of its parent directories. | ||||||
|         self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {} |         self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {} | ||||||
|         self._conftestpath2mod: Dict[Path, types.ModuleType] = {} |         # Cutoff directory above which conftests are no longer discovered. | ||||||
|         self._confcutdir: Optional[Path] = None |         self._confcutdir: Optional[Path] = None | ||||||
|  |         # If set, conftest loading is skipped. | ||||||
|         self._noconftest = False |         self._noconftest = False | ||||||
|  | 
 | ||||||
|         self._duplicatepaths: Set[Path] = set() |         self._duplicatepaths: Set[Path] = set() | ||||||
| 
 | 
 | ||||||
|         # plugins that were explicitly skipped with pytest.skip |         # plugins that were explicitly skipped with pytest.skip | ||||||
|  | @ -514,6 +518,19 @@ class PytestPluginManager(PluginManager): | ||||||
|         if not foundanchor: |         if not foundanchor: | ||||||
|             self._try_load_conftest(current, namespace.importmode, rootpath) |             self._try_load_conftest(current, namespace.importmode, rootpath) | ||||||
| 
 | 
 | ||||||
|  |     def _is_in_confcutdir(self, path: Path) -> bool: | ||||||
|  |         """Whether a path is within the confcutdir. | ||||||
|  | 
 | ||||||
|  |         When false, should not load conftest. | ||||||
|  |         """ | ||||||
|  |         if self._confcutdir is None: | ||||||
|  |             return True | ||||||
|  |         try: | ||||||
|  |             path.relative_to(self._confcutdir) | ||||||
|  |         except ValueError: | ||||||
|  |             return False | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|     def _try_load_conftest( |     def _try_load_conftest( | ||||||
|         self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path |         self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  | @ -526,7 +543,7 @@ class PytestPluginManager(PluginManager): | ||||||
| 
 | 
 | ||||||
|     def _getconftestmodules( |     def _getconftestmodules( | ||||||
|         self, path: Path, importmode: Union[str, ImportMode], rootpath: Path |         self, path: Path, importmode: Union[str, ImportMode], rootpath: Path | ||||||
|     ) -> List[types.ModuleType]: |     ) -> Sequence[types.ModuleType]: | ||||||
|         if self._noconftest: |         if self._noconftest: | ||||||
|             return [] |             return [] | ||||||
| 
 | 
 | ||||||
|  | @ -545,14 +562,12 @@ class PytestPluginManager(PluginManager): | ||||||
|         # and allow users to opt into looking into the rootdir parent |         # and allow users to opt into looking into the rootdir parent | ||||||
|         # directories instead of requiring to specify confcutdir. |         # directories instead of requiring to specify confcutdir. | ||||||
|         clist = [] |         clist = [] | ||||||
|         confcutdir_parents = self._confcutdir.parents if self._confcutdir else [] |  | ||||||
|         for parent in reversed((directory, *directory.parents)): |         for parent in reversed((directory, *directory.parents)): | ||||||
|             if parent in confcutdir_parents: |             if self._is_in_confcutdir(parent): | ||||||
|                 continue |                 conftestpath = parent / "conftest.py" | ||||||
|             conftestpath = parent / "conftest.py" |                 if conftestpath.is_file(): | ||||||
|             if conftestpath.is_file(): |                     mod = self._importconftest(conftestpath, importmode, rootpath) | ||||||
|                 mod = self._importconftest(conftestpath, importmode, rootpath) |                     clist.append(mod) | ||||||
|                 clist.append(mod) |  | ||||||
|         self._dirpath2confmods[directory] = clist |         self._dirpath2confmods[directory] = clist | ||||||
|         return clist |         return clist | ||||||
| 
 | 
 | ||||||
|  | @ -574,15 +589,9 @@ class PytestPluginManager(PluginManager): | ||||||
|     def _importconftest( |     def _importconftest( | ||||||
|         self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path |         self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path | ||||||
|     ) -> types.ModuleType: |     ) -> types.ModuleType: | ||||||
|         # Use a resolved Path object as key to avoid loading the same conftest |         existing = self.get_plugin(str(conftestpath)) | ||||||
|         # twice with build systems that create build directories containing |         if existing is not None: | ||||||
|         # symlinks to actual files. |             return cast(types.ModuleType, existing) | ||||||
|         # Using Path().resolve() is better than py.path.realpath because |  | ||||||
|         # it resolves to the correct path/drive in case-insensitive file systems (#5792) |  | ||||||
|         key = conftestpath.resolve() |  | ||||||
| 
 |  | ||||||
|         with contextlib.suppress(KeyError): |  | ||||||
|             return self._conftestpath2mod[key] |  | ||||||
| 
 | 
 | ||||||
|         pkgpath = resolve_package_path(conftestpath) |         pkgpath = resolve_package_path(conftestpath) | ||||||
|         if pkgpath is None: |         if pkgpath is None: | ||||||
|  | @ -598,11 +607,10 @@ class PytestPluginManager(PluginManager): | ||||||
|         self._check_non_top_pytest_plugins(mod, conftestpath) |         self._check_non_top_pytest_plugins(mod, conftestpath) | ||||||
| 
 | 
 | ||||||
|         self._conftest_plugins.add(mod) |         self._conftest_plugins.add(mod) | ||||||
|         self._conftestpath2mod[key] = mod |  | ||||||
|         dirpath = conftestpath.parent |         dirpath = conftestpath.parent | ||||||
|         if dirpath in self._dirpath2confmods: |         if dirpath in self._dirpath2confmods: | ||||||
|             for path, mods in self._dirpath2confmods.items(): |             for path, mods in self._dirpath2confmods.items(): | ||||||
|                 if path and dirpath in path.parents or path == dirpath: |                 if dirpath in path.parents or path == dirpath: | ||||||
|                     assert mod not in mods |                     assert mod not in mods | ||||||
|                     mods.append(mod) |                     mods.append(mod) | ||||||
|         self.trace(f"loading conftestmodule {mod!r}") |         self.trace(f"loading conftestmodule {mod!r}") | ||||||
|  |  | ||||||
|  | @ -689,9 +689,8 @@ class Session(nodes.FSCollector): | ||||||
|             # No point in finding packages when collecting doctests. |             # No point in finding packages when collecting doctests. | ||||||
|             if not self.config.getoption("doctestmodules", False): |             if not self.config.getoption("doctestmodules", False): | ||||||
|                 pm = self.config.pluginmanager |                 pm = self.config.pluginmanager | ||||||
|                 confcutdir = pm._confcutdir |  | ||||||
|                 for parent in (argpath, *argpath.parents): |                 for parent in (argpath, *argpath.parents): | ||||||
|                     if confcutdir and parent in confcutdir.parents: |                     if not pm._is_in_confcutdir(argpath): | ||||||
|                         break |                         break | ||||||
| 
 | 
 | ||||||
|                     if parent.is_dir(): |                     if parent.is_dir(): | ||||||
|  |  | ||||||
|  | @ -146,10 +146,9 @@ def test_issue151_load_all_conftests(pytester: Pytester) -> None: | ||||||
|         p = pytester.mkdir(name) |         p = pytester.mkdir(name) | ||||||
|         p.joinpath("conftest.py").touch() |         p.joinpath("conftest.py").touch() | ||||||
| 
 | 
 | ||||||
|     conftest = PytestPluginManager() |     pm = PytestPluginManager() | ||||||
|     conftest_setinitial(conftest, names) |     conftest_setinitial(pm, names) | ||||||
|     d = list(conftest._conftestpath2mod.values()) |     assert len(set(pm.get_plugins()) - {pm}) == len(names) | ||||||
|     assert len(d) == len(names) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_conftest_global_import(pytester: Pytester) -> None: | def test_conftest_global_import(pytester: Pytester) -> None: | ||||||
|  | @ -192,7 +191,7 @@ def test_conftestcutdir(pytester: Pytester) -> None: | ||||||
|         conf.parent, importmode="prepend", rootpath=pytester.path |         conf.parent, importmode="prepend", rootpath=pytester.path | ||||||
|     ) |     ) | ||||||
|     assert len(values) == 0 |     assert len(values) == 0 | ||||||
|     assert Path(conf) not in conftest._conftestpath2mod |     assert not conftest.has_plugin(str(conf)) | ||||||
|     # but we can still import a conftest directly |     # but we can still import a conftest directly | ||||||
|     conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path) |     conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path) | ||||||
|     values = conftest._getconftestmodules( |     values = conftest._getconftestmodules( | ||||||
|  | @ -226,15 +225,15 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None: | ||||||
|     sub = pytester.mkdir(name) |     sub = pytester.mkdir(name) | ||||||
|     subconftest = sub.joinpath("conftest.py") |     subconftest = sub.joinpath("conftest.py") | ||||||
|     subconftest.touch() |     subconftest.touch() | ||||||
|     conftest = PytestPluginManager() |     pm = PytestPluginManager() | ||||||
|     conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path) |     conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path) | ||||||
|     key = subconftest.resolve() |     key = subconftest.resolve() | ||||||
|     if name not in ("whatever", ".dotdir"): |     if name not in ("whatever", ".dotdir"): | ||||||
|         assert key in conftest._conftestpath2mod |         assert pm.has_plugin(str(key)) | ||||||
|         assert len(conftest._conftestpath2mod) == 1 |         assert len(set(pm.get_plugins()) - {pm}) == 1 | ||||||
|     else: |     else: | ||||||
|         assert key not in conftest._conftestpath2mod |         assert not pm.has_plugin(str(key)) | ||||||
|         assert len(conftest._conftestpath2mod) == 0 |         assert len(set(pm.get_plugins()) - {pm}) == 0 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_conftest_confcutdir(pytester: Pytester) -> None: | def test_conftest_confcutdir(pytester: Pytester) -> None: | ||||||
|  |  | ||||||
|  | @ -50,21 +50,24 @@ def test_setattr() -> None: | ||||||
| 
 | 
 | ||||||
| class TestSetattrWithImportPath: | class TestSetattrWithImportPath: | ||||||
|     def test_string_expression(self, monkeypatch: MonkeyPatch) -> None: |     def test_string_expression(self, monkeypatch: MonkeyPatch) -> None: | ||||||
|         monkeypatch.setattr("os.path.abspath", lambda x: "hello2") |         with monkeypatch.context() as mp: | ||||||
|         assert os.path.abspath("123") == "hello2" |             mp.setattr("os.path.abspath", lambda x: "hello2") | ||||||
|  |             assert os.path.abspath("123") == "hello2" | ||||||
| 
 | 
 | ||||||
|     def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None: |     def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None: | ||||||
|         monkeypatch.setattr("_pytest.config.Config", 42) |         with monkeypatch.context() as mp: | ||||||
|         import _pytest |             mp.setattr("_pytest.config.Config", 42) | ||||||
|  |             import _pytest | ||||||
| 
 | 
 | ||||||
|         assert _pytest.config.Config == 42  # type: ignore |             assert _pytest.config.Config == 42  # type: ignore | ||||||
| 
 | 
 | ||||||
|     def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None: |     def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None: | ||||||
|         monkeypatch.setattr("_pytest.config.Config", 42) |         with monkeypatch.context() as mp: | ||||||
|         import _pytest |             mp.setattr("_pytest.config.Config", 42) | ||||||
|  |             import _pytest | ||||||
| 
 | 
 | ||||||
|         assert _pytest.config.Config == 42  # type: ignore |             assert _pytest.config.Config == 42  # type: ignore | ||||||
|         monkeypatch.delattr("_pytest.config.Config") |             mp.delattr("_pytest.config.Config") | ||||||
| 
 | 
 | ||||||
|     def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None: |     def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None: | ||||||
|         with pytest.raises(TypeError): |         with pytest.raises(TypeError): | ||||||
|  | @ -80,14 +83,16 @@ class TestSetattrWithImportPath: | ||||||
| 
 | 
 | ||||||
|     def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None: |     def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None: | ||||||
|         # https://github.com/pytest-dev/pytest/issues/746 |         # https://github.com/pytest-dev/pytest/issues/746 | ||||||
|         monkeypatch.setattr("os.path.qweqwe", 42, raising=False) |         with monkeypatch.context() as mp: | ||||||
|         assert os.path.qweqwe == 42  # type: ignore |             mp.setattr("os.path.qweqwe", 42, raising=False) | ||||||
|  |             assert os.path.qweqwe == 42  # type: ignore | ||||||
| 
 | 
 | ||||||
|     def test_delattr(self, monkeypatch: MonkeyPatch) -> None: |     def test_delattr(self, monkeypatch: MonkeyPatch) -> None: | ||||||
|         monkeypatch.delattr("os.path.abspath") |         with monkeypatch.context() as mp: | ||||||
|         assert not hasattr(os.path, "abspath") |             mp.delattr("os.path.abspath") | ||||||
|         monkeypatch.undo() |             assert not hasattr(os.path, "abspath") | ||||||
|         assert os.path.abspath |             mp.undo() | ||||||
|  |             assert os.path.abspath | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_delattr() -> None: | def test_delattr() -> None: | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue