Skip to content

Commit

Permalink
Merge pull request #1597 from taschini/pyargs-fix
Browse files Browse the repository at this point in the history
Ensure that a module within a namespace package can be found by --pyargs
  • Loading branch information
nicoddemus authored Jun 19, 2016
2 parents 7f8e315 + 1218392 commit d81ee9a
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 41 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Russel Winder
Ryan Wooden
Samuele Pedroni
Simon Gomizelj
Stefano Taschini
Thomas Grainger
Tom Viner
Trevor Bekolay
Expand Down
12 changes: 9 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@
* Fix internal error issue when ``method`` argument is missing for
``teardown_method()``. Fixes (`#1605`_).

*

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

.. _#1580: https://github.com/pytest-dev/pytest/issues/1580
* Ensure that a module within a namespace package can be found when it
is specified on the command line together with the ``--pyargs``
option. Thanks to `@taschini`_ for the PR (`#1597`_).

*

.. _#1580: https://github.com/pytest-dev/pytest/pull/1580
.. _#1605: https://github.com/pytest-dev/pytest/issues/1605
.. _#1597: https://github.com/pytest-dev/pytest/pull/1597

.. _@graingert: https://github.com/graingert
.. _@taschini: https://github.com/taschini


2.9.2
Expand Down
50 changes: 21 additions & 29 deletions _pytest/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
""" core implementation of testing process: init, session, runtest loop. """
import imp
import os
import re
import sys

import _pytest
Expand All @@ -25,8 +23,6 @@
EXIT_USAGEERROR = 4
EXIT_NOTESTSCOLLECTED = 5

name_re = re.compile("^[a-zA-Z_]\w*$")

def pytest_addoption(parser):
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
type="args", default=['.*', 'CVS', '_darcs', '{arch}', '*.egg'])
Expand Down Expand Up @@ -649,36 +645,32 @@ def _recurse(self, path):
return True

def _tryconvertpyarg(self, x):
mod = None
path = [os.path.abspath('.')] + sys.path
for name in x.split('.'):
# ignore anything that's not a proper name here
# else something like --pyargs will mess up '.'
# since imp.find_module will actually sometimes work for it
# but it's supposed to be considered a filesystem path
# not a package
if name_re.match(name) is None:
return x
try:
fd, mod, type_ = imp.find_module(name, path)
except ImportError:
return x
else:
if fd is not None:
fd.close()
"""Convert a dotted module name to path.
if type_[2] != imp.PKG_DIRECTORY:
path = [os.path.dirname(mod)]
else:
path = [mod]
return mod
"""
import pkgutil
try:
loader = pkgutil.find_loader(x)
except ImportError:
return x
if loader is None:
return x
# This method is sometimes invoked when AssertionRewritingHook, which
# does not define a get_filename method, is already in place:
try:
path = loader.get_filename()
except AttributeError:
# Retrieve path from AssertionRewritingHook:
path = loader.modules[x][0].co_filename
if loader.is_package(x):
path = os.path.dirname(path)
return path

def _parsearg(self, arg):
""" return (fspath, names) tuple after checking the file exists. """
arg = str(arg)
if self.config.option.pyargs:
arg = self._tryconvertpyarg(arg)
parts = str(arg).split("::")
if self.config.option.pyargs:
parts[0] = self._tryconvertpyarg(parts[0])
relpath = parts[0].replace("/", os.sep)
path = self.config.invocation_dir.join(relpath, abs=True)
if not path.check():
Expand Down
79 changes: 70 additions & 9 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
import os
import sys

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

result = testdir.runpytest("--pyargs", "tpkg.test_hello")
result = testdir.runpytest_subprocess("--pyargs", "tpkg.test_hello")
assert result.ret != 0
# FIXME: It would be more natural to match NOT
# "ERROR*file*or*package*not*found*".

result.stdout.fnmatch_lines([
"*collected 0 items*"
"collected*0*items*/*1*errors"
])

def test_cmdline_python_package(self, testdir, monkeypatch):
Expand All @@ -539,7 +540,7 @@ def test_cmdline_python_package(self, testdir, monkeypatch):
def join_pythonpath(what):
cur = py.std.os.environ.get('PYTHONPATH')
if cur:
return str(what) + ':' + cur
return str(what) + os.pathsep + cur
return what
empty_package = testdir.mkpydir("empty_package")
monkeypatch.setenv('PYTHONPATH', join_pythonpath(empty_package))
Expand All @@ -550,11 +551,72 @@ def join_pythonpath(what):
])

monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir))
path.join('test_hello.py').remove()
result = testdir.runpytest("--pyargs", "tpkg.test_hello")
result = testdir.runpytest("--pyargs", "tpkg.test_missing")
assert result.ret != 0
result.stderr.fnmatch_lines([
"*not*found*test_hello*",
"*not*found*test_missing*",
])

def test_cmdline_python_namespace_package(self, testdir, monkeypatch):
"""
test --pyargs option with namespace packages (#1567)
"""
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', raising=False)

search_path = []
for dirname in "hello", "world":
d = testdir.mkdir(dirname)
search_path.append(d)
ns = d.mkdir("ns_pkg")
ns.join("__init__.py").write(
"__import__('pkg_resources').declare_namespace(__name__)")
lib = ns.mkdir(dirname)
lib.ensure("__init__.py")
lib.join("test_{0}.py".format(dirname)). \
write("def test_{0}(): pass\n"
"def test_other():pass".format(dirname))

# The structure of the test directory is now:
# .
# ├── hello
# │   └── ns_pkg
# │   ├── __init__.py
# │   └── hello
# │   ├── __init__.py
# │   └── test_hello.py
# └── world
# └── ns_pkg
# ├── __init__.py
# └── world
# ├── __init__.py
# └── test_world.py

def join_pythonpath(*dirs):
cur = py.std.os.environ.get('PYTHONPATH')
if cur:
dirs += (cur,)
return os.pathsep.join(str(p) for p in dirs)
monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path))
for p in search_path:
monkeypatch.syspath_prepend(p)

# mixed module and filenames:
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "world/ns_pkg")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*test_hello.py::test_hello*PASSED",
"*test_hello.py::test_other*PASSED",
"*test_world.py::test_world*PASSED",
"*test_world.py::test_other*PASSED",
"*4 passed*"
])

# specify tests within a module
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*test_world.py::test_other*PASSED",
"*1 passed*"
])

def test_cmdline_python_package_not_exists(self, testdir):
Expand Down Expand Up @@ -697,4 +759,3 @@ def test_setup_function(self, testdir):
* setup *test_1*
* call *test_1*
""")

0 comments on commit d81ee9a

Please sign in to comment.