Skip to content

Commit 476d4df

Browse files
Merge pull request #3010 from cryvate/fix-issue-2985
Improve handling of pyargs
2 parents 5244990 + 1e29553 commit 476d4df

File tree

4 files changed

+114
-3
lines changed

4 files changed

+114
-3
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
""" core implementation of testing process: init, session, runtest loop. """
22
from __future__ import absolute_import, division, print_function
33

4+
import contextlib
45
import functools
56
import os
7+
import pkgutil
68
import six
79
import sys
810

@@ -206,6 +208,46 @@ def pytest_ignore_collect(path, config):
206208
return False
207209

208210

211+
@contextlib.contextmanager
212+
def _patched_find_module():
213+
"""Patch bug in pkgutil.ImpImporter.find_module
214+
215+
When using pkgutil.find_loader on python<3.4 it removes symlinks
216+
from the path due to a call to os.path.realpath. This is not consistent
217+
with actually doing the import (in these versions, pkgutil and __import__
218+
did not share the same underlying code). This can break conftest
219+
discovery for pytest where symlinks are involved.
220+
221+
The only supported python<3.4 by pytest is python 2.7.
222+
"""
223+
if six.PY2: # python 3.4+ uses importlib instead
224+
def find_module_patched(self, fullname, path=None):
225+
# Note: we ignore 'path' argument since it is only used via meta_path
226+
subname = fullname.split(".")[-1]
227+
if subname != fullname and self.path is None:
228+
return None
229+
if self.path is None:
230+
path = None
231+
else:
232+
# original: path = [os.path.realpath(self.path)]
233+
path = [self.path]
234+
try:
235+
file, filename, etc = pkgutil.imp.find_module(subname,
236+
path)
237+
except ImportError:
238+
return None
239+
return pkgutil.ImpLoader(fullname, file, filename, etc)
240+
241+
old_find_module = pkgutil.ImpImporter.find_module
242+
pkgutil.ImpImporter.find_module = find_module_patched
243+
try:
244+
yield
245+
finally:
246+
pkgutil.ImpImporter.find_module = old_find_module
247+
else:
248+
yield
249+
250+
209251
class FSHookProxy:
210252
def __init__(self, fspath, pm, remove_mods):
211253
self.fspath = fspath
@@ -728,17 +770,19 @@ def _tryconvertpyarg(self, x):
728770
"""Convert a dotted module name to path.
729771
730772
"""
731-
import pkgutil
773+
732774
try:
733-
loader = pkgutil.find_loader(x)
775+
with _patched_find_module():
776+
loader = pkgutil.find_loader(x)
734777
except ImportError:
735778
return x
736779
if loader is None:
737780
return x
738781
# This method is sometimes invoked when AssertionRewritingHook, which
739782
# does not define a get_filename method, is already in place:
740783
try:
741-
path = loader.get_filename(x)
784+
with _patched_find_module():
785+
path = loader.get_filename(x)
742786
except AttributeError:
743787
# Retrieve path from AssertionRewritingHook:
744788
path = loader.modules[x][0].co_filename

changelog/2985.bugfix

Lines changed: 1 addition & 0 deletions
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

Lines changed: 65 additions & 0 deletions
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,69 @@ def join_pythonpath(*dirs):
645647
"*1 passed*"
646648
])
647649

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

0 commit comments

Comments
 (0)