Skip to content

Commit d81ee9a

Browse files
authored
Merge pull request #1597 from taschini/pyargs-fix
Ensure that a module within a namespace package can be found by --pyargs
2 parents 7f8e315 + 1218392 commit d81ee9a

File tree

4 files changed

+101
-41
lines changed

4 files changed

+101
-41
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Russel Winder
8888
Ryan Wooden
8989
Samuele Pedroni
9090
Simon Gomizelj
91+
Stefano Taschini
9192
Thomas Grainger
9293
Tom Viner
9394
Trevor Bekolay

CHANGELOG.rst

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,22 @@
99
* Fix internal error issue when ``method`` argument is missing for
1010
``teardown_method()``. Fixes (`#1605`_).
1111

12-
*
13-
1412
* Fix exception visualization in case the current working directory (CWD) gets
1513
deleted during testing. Fixes (`#1235`). Thanks `@bukzor` for reporting. PR by
1614
`@marscher`. Thanks `@nicoddemus` for his help.
1715

18-
.. _#1580: https://github.com/pytest-dev/pytest/issues/1580
16+
* Ensure that a module within a namespace package can be found when it
17+
is specified on the command line together with the ``--pyargs``
18+
option. Thanks to `@taschini`_ for the PR (`#1597`_).
19+
20+
*
21+
22+
.. _#1580: https://github.com/pytest-dev/pytest/pull/1580
1923
.. _#1605: https://github.com/pytest-dev/pytest/issues/1605
24+
.. _#1597: https://github.com/pytest-dev/pytest/pull/1597
2025

2126
.. _@graingert: https://github.com/graingert
27+
.. _@taschini: https://github.com/taschini
2228

2329

2430
2.9.2

_pytest/main.py

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
""" core implementation of testing process: init, session, runtest loop. """
2-
import imp
32
import os
4-
import re
53
import sys
64

75
import _pytest
@@ -25,8 +23,6 @@
2523
EXIT_USAGEERROR = 4
2624
EXIT_NOTESTSCOLLECTED = 5
2725

28-
name_re = re.compile("^[a-zA-Z_]\w*$")
29-
3026
def pytest_addoption(parser):
3127
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
3228
type="args", default=['.*', 'CVS', '_darcs', '{arch}', '*.egg'])
@@ -649,36 +645,32 @@ def _recurse(self, path):
649645
return True
650646

651647
def _tryconvertpyarg(self, x):
652-
mod = None
653-
path = [os.path.abspath('.')] + sys.path
654-
for name in x.split('.'):
655-
# ignore anything that's not a proper name here
656-
# else something like --pyargs will mess up '.'
657-
# since imp.find_module will actually sometimes work for it
658-
# but it's supposed to be considered a filesystem path
659-
# not a package
660-
if name_re.match(name) is None:
661-
return x
662-
try:
663-
fd, mod, type_ = imp.find_module(name, path)
664-
except ImportError:
665-
return x
666-
else:
667-
if fd is not None:
668-
fd.close()
648+
"""Convert a dotted module name to path.
669649
670-
if type_[2] != imp.PKG_DIRECTORY:
671-
path = [os.path.dirname(mod)]
672-
else:
673-
path = [mod]
674-
return mod
650+
"""
651+
import pkgutil
652+
try:
653+
loader = pkgutil.find_loader(x)
654+
except ImportError:
655+
return x
656+
if loader is None:
657+
return x
658+
# This method is sometimes invoked when AssertionRewritingHook, which
659+
# does not define a get_filename method, is already in place:
660+
try:
661+
path = loader.get_filename()
662+
except AttributeError:
663+
# Retrieve path from AssertionRewritingHook:
664+
path = loader.modules[x][0].co_filename
665+
if loader.is_package(x):
666+
path = os.path.dirname(path)
667+
return path
675668

676669
def _parsearg(self, arg):
677670
""" return (fspath, names) tuple after checking the file exists. """
678-
arg = str(arg)
679-
if self.config.option.pyargs:
680-
arg = self._tryconvertpyarg(arg)
681671
parts = str(arg).split("::")
672+
if self.config.option.pyargs:
673+
parts[0] = self._tryconvertpyarg(parts[0])
682674
relpath = parts[0].replace("/", os.sep)
683675
path = self.config.invocation_dir.join(relpath, abs=True)
684676
if not path.check():

testing/acceptance_test.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
import os
13
import sys
24

35
import _pytest._code
@@ -512,12 +514,11 @@ def test_pyargs_importerror(self, testdir, monkeypatch):
512514
path = testdir.mkpydir("tpkg")
513515
path.join("test_hello.py").write('raise ImportError')
514516

515-
result = testdir.runpytest("--pyargs", "tpkg.test_hello")
517+
result = testdir.runpytest_subprocess("--pyargs", "tpkg.test_hello")
516518
assert result.ret != 0
517-
# FIXME: It would be more natural to match NOT
518-
# "ERROR*file*or*package*not*found*".
519+
519520
result.stdout.fnmatch_lines([
520-
"*collected 0 items*"
521+
"collected*0*items*/*1*errors"
521522
])
522523

523524
def test_cmdline_python_package(self, testdir, monkeypatch):
@@ -539,7 +540,7 @@ def test_cmdline_python_package(self, testdir, monkeypatch):
539540
def join_pythonpath(what):
540541
cur = py.std.os.environ.get('PYTHONPATH')
541542
if cur:
542-
return str(what) + ':' + cur
543+
return str(what) + os.pathsep + cur
543544
return what
544545
empty_package = testdir.mkpydir("empty_package")
545546
monkeypatch.setenv('PYTHONPATH', join_pythonpath(empty_package))
@@ -550,11 +551,72 @@ def join_pythonpath(what):
550551
])
551552

552553
monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir))
553-
path.join('test_hello.py').remove()
554-
result = testdir.runpytest("--pyargs", "tpkg.test_hello")
554+
result = testdir.runpytest("--pyargs", "tpkg.test_missing")
555555
assert result.ret != 0
556556
result.stderr.fnmatch_lines([
557-
"*not*found*test_hello*",
557+
"*not*found*test_missing*",
558+
])
559+
560+
def test_cmdline_python_namespace_package(self, testdir, monkeypatch):
561+
"""
562+
test --pyargs option with namespace packages (#1567)
563+
"""
564+
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', raising=False)
565+
566+
search_path = []
567+
for dirname in "hello", "world":
568+
d = testdir.mkdir(dirname)
569+
search_path.append(d)
570+
ns = d.mkdir("ns_pkg")
571+
ns.join("__init__.py").write(
572+
"__import__('pkg_resources').declare_namespace(__name__)")
573+
lib = ns.mkdir(dirname)
574+
lib.ensure("__init__.py")
575+
lib.join("test_{0}.py".format(dirname)). \
576+
write("def test_{0}(): pass\n"
577+
"def test_other():pass".format(dirname))
578+
579+
# The structure of the test directory is now:
580+
# .
581+
# ├── hello
582+
# │   └── ns_pkg
583+
# │   ├── __init__.py
584+
# │   └── hello
585+
# │   ├── __init__.py
586+
# │   └── test_hello.py
587+
# └── world
588+
# └── ns_pkg
589+
# ├── __init__.py
590+
# └── world
591+
# ├── __init__.py
592+
# └── test_world.py
593+
594+
def join_pythonpath(*dirs):
595+
cur = py.std.os.environ.get('PYTHONPATH')
596+
if cur:
597+
dirs += (cur,)
598+
return os.pathsep.join(str(p) for p in dirs)
599+
monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path))
600+
for p in search_path:
601+
monkeypatch.syspath_prepend(p)
602+
603+
# mixed module and filenames:
604+
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "world/ns_pkg")
605+
assert result.ret == 0
606+
result.stdout.fnmatch_lines([
607+
"*test_hello.py::test_hello*PASSED",
608+
"*test_hello.py::test_other*PASSED",
609+
"*test_world.py::test_world*PASSED",
610+
"*test_world.py::test_other*PASSED",
611+
"*4 passed*"
612+
])
613+
614+
# specify tests within a module
615+
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other")
616+
assert result.ret == 0
617+
result.stdout.fnmatch_lines([
618+
"*test_world.py::test_other*PASSED",
619+
"*1 passed*"
558620
])
559621

560622
def test_cmdline_python_package_not_exists(self, testdir):
@@ -697,4 +759,3 @@ def test_setup_function(self, testdir):
697759
* setup *test_1*
698760
* call *test_1*
699761
""")
700-

0 commit comments

Comments
 (0)