diff --git a/changelog/4980.feature.rst b/changelog/4980.feature.rst new file mode 100644 index 00000000000..40f1de9c13f --- /dev/null +++ b/changelog/4980.feature.rst @@ -0,0 +1 @@ +Namespace packages are handled better with ``monkeypatch.syspath_prepend`` and ``testdir.syspathinsert`` (via ``pkg_resources.fixup_namespace_packages``). diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 46d9718da7f..f6c13466433 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -262,10 +262,15 @@ def delenv(self, name, raising=True): def syspath_prepend(self, path): """ Prepend ``path`` to ``sys.path`` list of import locations. """ + from pkg_resources import fixup_namespace_packages + if self._savesyspath is None: self._savesyspath = sys.path[:] sys.path.insert(0, str(path)) + # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 + fixup_namespace_packages(str(path)) + def chdir(self, path): """ Change the current working directory to the specified path. Path can be a string or a py.path.local object. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index ffcd2982ade..f8a79ebc93b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -593,11 +593,16 @@ def syspathinsert(self, path=None): This is undone automatically when this object dies at the end of each test. - """ + from pkg_resources import fixup_namespace_packages + if path is None: path = self.tmpdir - sys.path.insert(0, str(path)) + + dirname = str(path) + sys.path.insert(0, dirname) + fixup_namespace_packages(dirname) + # a call to syspathinsert() usually means that the caller wants to # import some dynamically created files, thus with python3 we # invalidate its import caches @@ -606,12 +611,10 @@ def syspathinsert(self, path=None): def _possibly_invalidate_import_caches(self): # invalidate caches if we can (py33 and above) try: - import importlib + from importlib import invalidate_caches except ImportError: - pass - else: - if hasattr(importlib, "invalidate_caches"): - importlib.invalidate_caches() + return + invalidate_caches() def mkdir(self, name): """Create a new (sub)directory.""" diff --git a/testing/python/collect.py b/testing/python/collect.py index bc7462674e3..df6070b8d1d 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -34,8 +34,6 @@ def test_import_duplicate(self, testdir): ) def test_import_prepend_append(self, testdir, monkeypatch): - syspath = list(sys.path) - monkeypatch.setattr(sys, "path", syspath) root1 = testdir.mkdir("root1") root2 = testdir.mkdir("root2") root1.ensure("x456.py") diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index bdfbf823c59..a92ffcf7514 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1335,7 +1335,7 @@ def test_cwd_changed(self, testdir, monkeypatch): # Setup conditions for py's fspath trying to import pathlib on py34 # always (previously triggered via xdist only). # Ref: https://github.com/pytest-dev/py/pull/207 - monkeypatch.setattr(sys, "path", [""] + sys.path) + monkeypatch.syspath_prepend("") monkeypatch.delitem(sys.modules, "pathlib", raising=False) testdir.makepyfile( diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 0a953d3f1bf..d43fb6babdf 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -437,3 +437,28 @@ def test_context(): m.setattr(functools, "partial", 3) assert not inspect.isclass(functools.partial) assert inspect.isclass(functools.partial) + + +def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch): + for dirname in "hello", "world": + d = testdir.mkdir(dirname) + ns = d.mkdir("ns_pkg") + ns.join("__init__.py").write( + "__import__('pkg_resources').declare_namespace(__name__)" + ) + lib = ns.mkdir(dirname) + lib.join("__init__.py").write("def check(): return %r" % dirname) + + monkeypatch.syspath_prepend("hello") + import ns_pkg.hello + + assert ns_pkg.hello.check() == "hello" + + with pytest.raises(ImportError): + import ns_pkg.world + + # Prepending should call fixup_namespace_packages. + monkeypatch.syspath_prepend("world") + import ns_pkg.world + + assert ns_pkg.world.check() == "world" diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 6b44f3e0cc7..10b54c1125d 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -4,7 +4,6 @@ from __future__ import print_function import os -import re import sys import types @@ -165,10 +164,10 @@ def test_traceback(): with pytest.raises(ImportError) as excinfo: pytestpm.import_plugin("qwe") - expected_message = '.*Error importing plugin "qwe": Not possible to import: .' - expected_traceback = ".*in test_traceback" - assert re.match(expected_message, str(excinfo.value)) - assert re.match(expected_traceback, str(excinfo.traceback[-1])) + assert str(excinfo.value).endswith( + 'Error importing plugin "qwe": Not possible to import: ☺' + ) + assert "in test_traceback" in str(excinfo.traceback[-1]) class TestPytestPluginManager(object):