From e2e6e317118d6d15ccdf6f6708a48c08d85cbc25 Mon Sep 17 00:00:00 2001 From: taschini Date: Wed, 8 Jun 2016 15:17:55 +0200 Subject: [PATCH 1/3] Ensure that a module within a namespace package can be found by --pyargs. --- _pytest/main.py | 47 +++++++++------------ testing/acceptance_test.py | 83 +++++++++++++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 35 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index 4a6c0877557..40df1323855 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -1,7 +1,5 @@ """ core implementation of testing process: init, session, runtest loop. """ -import imp import os -import re import sys import _pytest @@ -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']) @@ -658,36 +654,29 @@ 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 + try: + path = loader.get_filename() + except: + 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(): diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 4e964503756..c66a1740f11 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sys import _pytest._code @@ -514,11 +515,20 @@ def test_pyargs_importerror(self, testdir, monkeypatch): result = testdir.runpytest("--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*" - ]) + + # Depending on whether the process running the test is the + # same as the process parsing the command-line arguments, the + # type of failure can be different: + if result.stderr.str() == '': + # Different processes + result.stdout.fnmatch_lines([ + "collected*0*items*/*1*errors" + ]) + else: + # Same process + result.stderr.fnmatch_lines([ + "ERROR:*file*or*package*not*found:*tpkg.test_hello" + ]) def test_cmdline_python_package(self, testdir, monkeypatch): monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False) @@ -557,6 +567,68 @@ def join_pythonpath(what): "*not*found*test_hello*", ]) + 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 ':'.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): result = testdir.runpytest("--pyargs", "tpkgwhatv") assert result.ret @@ -697,4 +769,3 @@ def test_setup_function(self, testdir): * setup *test_1* * call *test_1* """) - From 4d9e293b4d32fe20b7c5a5ef37ca190481bbfaf3 Mon Sep 17 00:00:00 2001 From: taschini Date: Thu, 9 Jun 2016 08:22:11 +0200 Subject: [PATCH 2/3] Incorporated feedback (#1597). Fixed problem caused in a test on Windows by file left open by PyPy and not immediately garbage collected. --- _pytest/main.py | 5 ++++- testing/acceptance_test.py | 28 +++++++++------------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index 40df1323855..a4bb7205c1b 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -664,9 +664,12 @@ def _tryconvertpyarg(self, x): 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: + except AttributeError: + # Retrieve path from AssertionRewritingHook: path = loader.modules[x][0].co_filename if loader.is_package(x): path = os.path.dirname(path) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c66a1740f11..1b14d12a5f6 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import os import sys import _pytest._code @@ -513,22 +514,12 @@ 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 - # Depending on whether the process running the test is the - # same as the process parsing the command-line arguments, the - # type of failure can be different: - if result.stderr.str() == '': - # Different processes - result.stdout.fnmatch_lines([ - "collected*0*items*/*1*errors" - ]) - else: - # Same process - result.stderr.fnmatch_lines([ - "ERROR:*file*or*package*not*found:*tpkg.test_hello" - ]) + result.stdout.fnmatch_lines([ + "collected*0*items*/*1*errors" + ]) def test_cmdline_python_package(self, testdir, monkeypatch): monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False) @@ -549,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)) @@ -560,11 +551,10 @@ 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): @@ -605,7 +595,7 @@ def join_pythonpath(*dirs): cur = py.std.os.environ.get('PYTHONPATH') if cur: dirs += (cur,) - return ':'.join(str(p) for p in dirs) + 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) From 1218392413791345440c3e1d8d0bff0fb880442c Mon Sep 17 00:00:00 2001 From: taschini Date: Tue, 14 Jun 2016 07:16:20 +0200 Subject: [PATCH 3/3] Added taschini to AUTHORS and #1597 to CHANGELOG.rst. --- AUTHORS | 1 + CHANGELOG.rst | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 28cce969d63..9962325d23d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -87,6 +87,7 @@ Russel Winder Ryan Wooden Samuele Pedroni Simon Gomizelj +Stefano Taschini Thomas Grainger Tom Viner Trevor Bekolay diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1a8dc943331..2ab8834b731 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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