Skip to content

Commit 7ccc72d

Browse files
committed
Fix issue 2985.
1 parent a9dd37f commit 7ccc72d

File tree

4 files changed

+124
-17
lines changed

4 files changed

+124
-17
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Grig Gheorghiu
7474
Grigorii Eremeev (budulianin)
7575
Guido Wesdorp
7676
Harald Armin Massa
77+
Henk-Jaap Wagenaar
7778
Hugo van Kemenade
7879
Hui Wang (coldnight)
7980
Ian Bicking

_pytest/main.py

+58-17
Original file line numberDiff line numberDiff line change
@@ -728,23 +728,64 @@ def _tryconvertpyarg(self, x):
728728
"""Convert a dotted module name to path.
729729
730730
"""
731-
import pkgutil
732-
try:
733-
loader = pkgutil.find_loader(x)
734-
except ImportError:
735-
return x
736-
if loader is None:
737-
return x
738-
# This method is sometimes invoked when AssertionRewritingHook, which
739-
# does not define a get_filename method, is already in place:
740-
try:
741-
path = loader.get_filename(x)
742-
except AttributeError:
743-
# Retrieve path from AssertionRewritingHook:
744-
path = loader.modules[x][0].co_filename
745-
if loader.is_package(x):
746-
path = os.path.dirname(path)
747-
return path
731+
if six.PY2:
732+
# this is deprecated in python 3.4 and above
733+
import pkgutil
734+
735+
def find_module_patched(self, fullname, path=None):
736+
subname = fullname.split(".")[-1]
737+
if subname != fullname and self.path is None:
738+
return None
739+
if self.path is None:
740+
path = None
741+
else:
742+
path = [self.path]
743+
try:
744+
file, filename, etc = pkgutil.imp.find_module(subname,
745+
path)
746+
except ImportError:
747+
return None
748+
return pkgutil.ImpLoader(fullname, file, filename, etc)
749+
750+
pkgutil.ImpImporter.find_module = find_module_patched
751+
752+
try:
753+
loader = pkgutil.find_loader(x)
754+
except ImportError:
755+
return x
756+
if loader is None:
757+
return x
758+
# This method is sometimes invoked when AssertionRewritingHook, which
759+
# does not define a get_filename method, is already in place:
760+
try:
761+
path = loader.get_filename(x)
762+
except AttributeError:
763+
# Retrieve path from AssertionRewritingHook:
764+
path = loader.modules[x][0].co_filename
765+
if loader.is_package(x):
766+
path = os.path.dirname(path)
767+
return path
768+
else:
769+
# this is not available (even as backport) for python 2.7
770+
import importlib.util
771+
772+
try:
773+
spec = importlib.util.find_spec(x)
774+
except (
775+
ImportError, # import failed
776+
AttributeError, # parent not package
777+
ValueError # (invalid) relative import
778+
):
779+
return x
780+
781+
if spec is None:
782+
return x
783+
elif spec.submodule_search_locations: # if package
784+
return os.path.dirname(spec.origin)
785+
elif spec.has_location: # origin is a valid location
786+
return spec.origin
787+
else: # the import might have involved AssertionRewritingHook
788+
return spec.loader.modules[x][0].co_filename
748789

749790
def _parsearg(self, arg):
750791
""" return (fspath, names) tuple after checking the file exists. """

changelog/2985.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix conversion of pyargs to filename to not convert symlinks and not use deprecated features on Python 3.

testing/acceptance_test.py

+64
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os
44
import sys
55

6+
import six
7+
68
import _pytest._code
79
import py
810
import pytest
@@ -645,6 +647,68 @@ def join_pythonpath(*dirs):
645647
"*1 passed*"
646648
])
647649

650+
def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
651+
"""
652+
test --pyargs option with packages with path containing symlink can
653+
have conftest.py in their package (#2985)
654+
"""
655+
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', raising=False)
656+
657+
search_path = ["lib", os.path.join("local", "lib")]
658+
659+
dirname = "lib"
660+
d = testdir.mkdir(dirname)
661+
foo = d.mkdir("foo")
662+
foo.ensure("__init__.py")
663+
lib = foo.mkdir('bar')
664+
lib.ensure("__init__.py")
665+
lib.join("test_bar.py"). \
666+
write("def test_bar(): pass\n"
667+
"def test_other(a_fixture):pass")
668+
lib.join("conftest.py"). \
669+
write("import pytest\n"
670+
"@pytest.fixture\n"
671+
"def a_fixture():pass")
672+
673+
d_local = testdir.mkdir("local")
674+
symlink_location = os.path.join(str(d_local), "lib")
675+
if six.PY2:
676+
os.symlink(str(d), symlink_location)
677+
else:
678+
os.symlink(str(d), symlink_location, target_is_directory=True)
679+
680+
# The structure of the test directory is now:
681+
# .
682+
# ├── local
683+
# │ └── lib -> ../world
684+
# └── lib
685+
# └── foo
686+
# ├── __init__.py
687+
# └── bar
688+
# ├── __init__.py
689+
# ├── conftest.py
690+
# └── test_world.py
691+
692+
def join_pythonpath(*dirs):
693+
cur = py.std.os.environ.get('PYTHONPATH')
694+
if cur:
695+
dirs += (cur,)
696+
return os.pathsep.join(str(p) for p in dirs)
697+
698+
monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path))
699+
for p in search_path:
700+
monkeypatch.syspath_prepend(p)
701+
print(sys.path)
702+
# module picked up in symlinked directory:
703+
result = testdir.runpytest("--pyargs", "-v", "foo.bar")
704+
testdir.chdir()
705+
assert result.ret == 0
706+
result.stdout.fnmatch_lines([
707+
"*lib/foo/bar/test_bar.py::test_bar*PASSED*",
708+
"*lib/foo/bar/test_bar.py::test_other*PASSED*",
709+
"*2 passed*"
710+
])
711+
648712
def test_cmdline_python_package_not_exists(self, testdir):
649713
result = testdir.runpytest("--pyargs", "tpkgwhatv")
650714
assert result.ret

0 commit comments

Comments
 (0)