Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that a module within a namespace package can be found by --pyargs #1568

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Ronny Pfannschmidt
Ross Lawley
Ryan Wooden
Samuele Pedroni
Stefano Taschini
Tom Viner
Trevor Bekolay
Wouter van Ackooy
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

*

* Ensure that a module within a namespace package can be found when it
is specified on the command line together with the``--pyargs``
option.

* Fix win32 path issue when puttinging custom config file with absolute path
in ``pytest.main("-c your_absolute_path")``.

Expand Down
58 changes: 40 additions & 18 deletions _pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,37 @@ def _recurse(self, path):
ihook.pytest_collect_directory(path=path, parent=self)
return True

def _locate_module(self, modulename, searchpath):
"""Find the locations of a module or package in the filesytem.

In case of a presumptive namespace package return all of its possible
locations.

"""
try:
fd, pathname, type_ = imp.find_module(modulename, searchpath)
except ImportError:
return []
else:
if fd is not None:
fd.close()
if type_[2] != imp.PKG_DIRECTORY:
return [pathname]
else:
init_file = os.path.join(pathname, '__init__.py')
if os.path.getsize(init_file) == 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please put a comment explaining why do you check for the file size at this point?

return [pathname]
return [pathname] + self._locate_module(
modulename,
searchpath[searchpath.index(os.path.dirname(pathname)) + 1:])

def _tryconvertpyarg(self, x):
mod = None
"""Convert a dotted module name to a list of file-system locations.

Always return a list. If the module cannot be found, the list contains
just the given argument.

"""
path = [os.path.abspath('.')] + sys.path
for name in x.split('.'):
# ignore anything that's not a proper name here
Expand All @@ -658,27 +687,20 @@ def _tryconvertpyarg(self, x):
# 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()

if type_[2] != imp.PKG_DIRECTORY:
path = [os.path.dirname(mod)]
else:
path = [mod]
return mod
return [x]
path = self._locate_module(name, path)
if len(path) == 0:
return [x]
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:
paths = self._tryconvertpyarg(parts[0])
if len(paths) != 1:
raise pytest.UsageError("Cannot uniquely resolve package directory: " + arg)
parts[0] = paths[0]
relpath = parts[0].replace("/", os.sep)
path = self.config.invocation_dir.join(relpath, abs=True)
if not path.check():
Expand Down
63 changes: 62 additions & 1 deletion testing/acceptance_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import sys

import _pytest._code
Expand Down Expand Up @@ -557,6 +558,67 @@ def join_pythonpath(what):
"*not*found*test_hello*",
])

def test_cmdline_python_namespace_package(self, testdir, monkeypatch):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nitpick: add a docstring which a comment pointing to the issue:

def test_cmdline_python_namespace_package(self, testdir, monkeypatch):
    """
    test --pyargs option with namespace packages (#1567)
    """

monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: would you mind using a keyword argument here (raising=False)? I find keyword arguments for boolean parameters greatly improve readability. 😁


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 ':'.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", "ns_pkg.hello", "world/ns_pkg")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*4 passed*"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to run pytest with -v and check for the actual test names, to ensure we are not missing anything or introduce faulty behavior in the future:

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",
])

(the same comment apply to the next checks done here)

])

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

# namespace package
result = testdir.runpytest("--pyargs", "ns_pkg")
assert result.ret
result.stderr.fnmatch_lines([
"ERROR:*Cannot*uniquely*resolve*package*directory:*ns_pkg",
])

def test_cmdline_python_package_not_exists(self, testdir):
result = testdir.runpytest("--pyargs", "tpkgwhatv")
assert result.ret
Expand Down Expand Up @@ -697,4 +759,3 @@ def test_setup_function(self, testdir):
* setup *test_1*
* call *test_1*
""")