From 8d5246259b9707f2039587792c53a42cb68fe42a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 1 Sep 2020 19:12:24 +0200 Subject: [PATCH 1/4] tests: revisit test_issue2369_collect_module_fileext (#420) --- testing/python/collect.py | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/testing/python/collect.py b/testing/python/collect.py index 705b36d4d44..eab4732e44d 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -864,37 +864,37 @@ def __world(): pass modcol.collect() assert values == ["_hello"] - def test_issue2369_collect_module_fileext(self, testdir): - """Ensure we can collect files with weird file extensions as Python + def test_issue2369_collect_module_fileext(self, testdir: "Testdir") -> None: + """Ensure we can collect files with custom file extensions as Python modules (#2369)""" - # We'll implement a little finder and loader to import files containing - # Python source code whose file extension is ".narf". testdir.makeconftest( """ - import sys, os, imp + from importlib.machinery import SourceFileLoader + from importlib.util import spec_from_file_location + import os + import sys + from _pytest.python import Module - class Loader(object): - def load_module(self, name): - return imp.load_source(name, name + ".narf") - class Finder(object): - def find_module(self, name, path=None): - if os.path.exists(name + ".narf"): - return Loader() + class Finder: + @classmethod + def find_spec(cls, fullname, path=None, target=None): + location = os.path.abspath(fullname + ".narf") + if os.path.exists(location): + return spec_from_file_location( + fullname, + location, + loader=SourceFileLoader(fullname, location), + ) + sys.meta_path.append(Finder()) def pytest_collect_file(path, parent): if path.ext == ".narf": return Module(path, parent)""" ) - testdir.makefile( - ".narf", - """\ - def test_something(): - assert 1 + 1 == 2""", - ) - # Use runpytest_subprocess, since we're futzing with sys.meta_path. - result = testdir.runpytest_subprocess() + testdir.makefile(".narf", "def test_pass(): pass") + result = testdir.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) From 449732e3d4e7c5035bf04acd03f3f789d3bc7cfe Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 1 Sep 2020 20:31:39 +0200 Subject: [PATCH 2/4] PytestDeprecationWarning for pytest.collect.X Adopts `_pytest.deprecated.PYTEST_COLLECT_MODULE` from f1d51ba1f. The main difference to pytest is that `import pytest.collect` did not work since ae234786e (3.0.7-161, 2017 - no issues reported for it), and this is behavior is kept here for backward compatibility. With ae234786e (3.0.7-161): > ModuleNotFoundError: No module named 'pytest.collect'; 'pytest' is not a package The error changed in e5bd7fb05 (5.3.1-12): > ModuleNotFoundError: No module named 'pytest.collect' --- changelog/6981.deprecation.rst | 1 + src/_pytest/compat.py | 47 +++++++++++++++++++++------------- src/_pytest/deprecated.py | 6 +++++ testing/deprecated_test.py | 24 +++++++++++++++++ testing/test_meta.py | 16 +++++++++--- 5 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 changelog/6981.deprecation.rst diff --git a/changelog/6981.deprecation.rst b/changelog/6981.deprecation.rst new file mode 100644 index 00000000000..f52675c501c --- /dev/null +++ b/changelog/6981.deprecation.rst @@ -0,0 +1 @@ +The forwarding attributes from `pytest.collect.X` (to `pytest.X`) are deprecated (`pytest.collect` was not a module since 3.0.7-161-gae234786e (in 2017) already). diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 53c78e8fb11..92fcfda72ce 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -7,6 +7,7 @@ import os import re import sys +import warnings from contextlib import contextmanager from inspect import Parameter from inspect import signature @@ -34,6 +35,7 @@ if TYPE_CHECKING: + from typing import List from types import ModuleType # noqa: F401 (used in type string) from typing import Type # noqa: F401 (used in type string) @@ -340,25 +342,34 @@ def safe_isclass(obj: object) -> bool: def _setup_collect_fakemodule() -> "ModuleType": """Setup pytest.collect fake module for backward compatibility.""" from types import ModuleType - import _pytest.nodes - - collect_fakemodule_attributes = ( - ("Collector", _pytest.nodes.Collector), - ("Module", _pytest.python.Module), - ("Function", _pytest.python.Function), - ("Instance", _pytest.python.Instance), - ("Session", _pytest.main.Session), - ("Item", _pytest.nodes.Item), - ("Class", _pytest.python.Class), - ("File", _pytest.nodes.File), - ("_fillfuncargs", _pytest.fixtures.fillfixtures), - ) - mod = ModuleType("pytest.collect") - mod.__all__ = [] # type: ignore # used for setns (obsolete?) - for attr_name, value in collect_fakemodule_attributes: - setattr(mod, attr_name, value) - return mod + import _pytest.nodes + from _pytest.deprecated import PYTEST_COLLECT_MODULE + + collect_fakemodule_attributes = { + "Collector": _pytest.nodes.Collector, + "Module": _pytest.python.Module, + "Function": _pytest.python.Function, + "Instance": _pytest.python.Instance, + "Session": _pytest.main.Session, + "Item": _pytest.nodes.Item, + "Class": _pytest.python.Class, + "File": _pytest.nodes.File, + "_fillfuncargs": _pytest.fixtures.fillfixtures, + } + + class FakeCollectModule(ModuleType): + def __init__(self) -> None: + super().__init__("pytest.collect") + self.__all__ = [] # type: List[str] # backward compatibility. + + def __getattr__(self, name: str) -> Any: + if name in collect_fakemodule_attributes: + warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) + return collect_fakemodule_attributes[name] + raise AttributeError(name) + + return FakeCollectModule() class CaptureIO(io.TextIOWrapper): diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 4e2c8cd9c2b..1d8a8883c5d 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -19,6 +19,12 @@ "pytest_faulthandler", } +PYTEST_COLLECT_MODULE = UnformattedWarning( + PytestDeprecationWarning, + "pytest.collect.{name} was moved to pytest.{name}\n" + "Please update to the new name.", +) + FUNCARGNAMES = PytestDeprecationWarning( "The `funcargnames` attribute was an alias for `fixturenames`, " "since pytest 2.3 - use the newer attribute instead." diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index d22c2e64ad5..8ee1d3d7855 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -5,6 +5,30 @@ from _pytest import nodes +@pytest.mark.parametrize( + "attribute", + ( + "Collector", + "Module", + "Function", + "Instance", + "Session", + "Item", + "Class", + "File", + "_fillfuncargs", + ), +) +def test_pytest_collect_module_deprecated(attribute: str) -> None: + msg = str(deprecated.PYTEST_COLLECT_MODULE.format(name=attribute)) + with pytest.warns(DeprecationWarning, match=msg) as wr: + getattr(pytest.collect, attribute) + assert len(wr) == 1 + assert wr[0].filename == __file__ + f_lineno = inspect.currentframe().f_lineno # type: ignore[union-attr] + assert wr[0].lineno == f_lineno - 3 + + @pytest.mark.filterwarnings("default") def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--help") diff --git a/testing/test_meta.py b/testing/test_meta.py index 267d66419e3..0630d18a21a 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -37,7 +37,10 @@ def test_no_warnings(module: str) -> None: # fmt: on -def test_pytest_collect_attribute(_sys_snapshot): +@pytest.mark.filterwarnings( + "ignore:pytest.collect.Item was moved to pytest.Item:pytest.PytestDeprecationWarning", +) +def test_pytest_collect_attribute(_sys_snapshot) -> None: from types import ModuleType del sys.modules["pytest"] @@ -45,17 +48,24 @@ def test_pytest_collect_attribute(_sys_snapshot): import pytest assert isinstance(pytest.collect, ModuleType) - assert pytest.collect.Item is pytest.Item + assert pytest.collect.Item is pytest.Item # type: ignore[attr-defined] with pytest.raises(ImportError): import pytest.collect + from pytest import collect + + with pytest.raises(AttributeError): + collect.doesnotexist # type: ignore[attr-defined] + + +def test_pytest___get_attr__(_sys_snapshot) -> None: if sys.version_info >= (3, 7): with pytest.raises(AttributeError, match=r"^doesnotexist$"): pytest.doesnotexist else: with pytest.raises(AttributeError, match=r"doesnotexist"): - pytest.doesnotexist + pytest.doesnotexist # type: ignore[attr-defined] def test_pytest_circular_import(testdir: Testdir, symlink_or_skip) -> None: From 31480d2b1a5ce3e735aa7339e48c1f9c3c39d0b4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 2 Sep 2020 02:49:27 +0200 Subject: [PATCH 3/4] ci: Travis: py35: run integration tests --- .github/workflows/main.yml | 2 +- .travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bce5490f94c..324d1179603 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: # LsofFdLeakChecker itself. # (https://github.com/blueyed/pytest/issues/195) tox_env: "py38-lsof-pexpect-coverage" - pytest_addopts: "-m 'uses_pexpect or integration' --run-integration-tests" + pytest_addopts: "-m 'uses_pexpect or integration'" script_prefix: "env -u COLUMNS" # Coverage for: diff --git a/.travis.yml b/.travis.yml index 3985d1ec134..02182266667 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ jobs: dist: trusty env: - TOXENV=py35-coverage - - PYTEST_ADDOPTS="-ra --durations=50 -m 'py35_specific or acceptance_tests'" + - PYTEST_ADDOPTS="-ra --durations=50 -m 'py35_specific or acceptance_tests or integration' --run-integration-tests" before_install: - python -m pip install -U pip==19.3.1 # Coverage for Python 3.9 From 1f86852c42426674cf23e2f9fb6f172dbe27eae7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 2 Sep 2020 03:28:15 +0200 Subject: [PATCH 4/4] tests: imply `--run-integration-tests` with `-m integration` --- .travis.yml | 2 +- testing/conftest.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 02182266667..664ecaf636e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ jobs: dist: trusty env: - TOXENV=py35-coverage - - PYTEST_ADDOPTS="-ra --durations=50 -m 'py35_specific or acceptance_tests or integration' --run-integration-tests" + - PYTEST_ADDOPTS="-ra --durations=50 -m 'py35_specific or acceptance_tests or integration'" before_install: - python -m pip install -U pip==19.3.1 # Coverage for Python 3.9 diff --git a/testing/conftest.py b/testing/conftest.py index a71f1e1fa3f..5d172734aee 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -4,6 +4,7 @@ from typing import List import pytest +from _pytest.mark.legacy import matchmark from _pytest.pytester import RunResult @@ -41,6 +42,11 @@ def pytest_runtest_setup(item): if any(item.nodeid == arg for arg in item.config.invocation_params.args): return + # Run the test if selected by mark explicitly. + markexpr = item.config.option.markexpr + if markexpr and matchmark(item, markexpr): + return + pytest.skip("Not running {} test (use {})".format(mark, option))