Use a PurePath instance to do matching against patterns in assertion rewrite

This way we don't need to have real file system path, which prevents the
original #3973 bug.
This commit is contained in:
Bruno Oliveira 2018-09-17 20:01:01 -03:00
parent 1df6d28080
commit 37d2469266
4 changed files with 121 additions and 20 deletions

View File

@ -16,7 +16,8 @@ import atomicwrites
import py
from _pytest.assertion import util
from _pytest.compat import PurePath, spec_from_file_location
from _pytest.paths import fnmatch_ex
# pytest caches rewritten pycs in __pycache__.
if hasattr(imp, "get_tag"):
@ -45,14 +46,6 @@ else:
return ast.Call(a, b, c, None, None)
if sys.version_info >= (3, 4):
from importlib.util import spec_from_file_location
else:
def spec_from_file_location(*_, **__):
return None
class AssertionRewritingHook(object):
"""PEP302 Import hook which rewrites asserts."""
@ -198,18 +191,14 @@ class AssertionRewritingHook(object):
return False
# For matching the name it must be as if it was a filename.
parts[-1] = parts[-1] + ".py"
try:
fn_pypath = py.path.local(os.path.sep.join(parts))
except EnvironmentError:
return False
path = PurePath(os.path.sep.join(parts) + ".py")
for pat in self.fnpats:
# if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
# on the name alone because we need to match against the full path
if os.path.dirname(pat):
return False
if fn_pypath.fnmatch(pat):
if fnmatch_ex(pat, path):
return False
if self._is_marked_for_rewrite(name, state):

View File

@ -23,7 +23,7 @@ except ImportError: # pragma: no cover
# Only available in Python 3.4+ or as a backport
enum = None
__all__ = ["Path"]
__all__ = ["Path", "PurePath"]
_PY3 = sys.version_info > (3, 0)
_PY2 = not _PY3
@ -42,9 +42,9 @@ PY36 = sys.version_info[:2] >= (3, 6)
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
if PY36:
from pathlib import Path
from pathlib import Path, PurePath
else:
from pathlib2 import Path
from pathlib2 import Path, PurePath
if _PY3:
@ -56,6 +56,14 @@ else:
from collections import Mapping, Sequence # noqa
if sys.version_info >= (3, 4):
from importlib.util import spec_from_file_location
else:
def spec_from_file_location(*_, **__):
return None
def _format_args(func):
return str(signature(func))

View File

@ -1,5 +1,11 @@
from .compat import Path
from os.path import expanduser, expandvars, isabs
from os.path import expanduser, expandvars, isabs, sep
from posixpath import sep as posix_sep
import fnmatch
import sys
import six
from .compat import Path, PurePath
def resolve_from_str(input, root):
@ -11,3 +17,32 @@ def resolve_from_str(input, root):
return Path(input)
else:
return root.joinpath(input)
def fnmatch_ex(pattern, path):
"""FNMatcher port from py.path.common which works with PurePath() instances.
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
for each part of the path, while this algorithm uses the whole path instead.
For example:
"tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
PurePath.match().
This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
this logic.
"""
path = PurePath(path)
iswin32 = sys.platform.startswith("win")
if iswin32 and sep not in pattern and posix_sep in pattern:
# Running on Windows, the pattern has no Windows path separators,
# and the pattern has one or more Posix path separators. Replace
# the Posix path separators with the Windows path separator.
pattern = pattern.replace(posix_sep, sep)
if sep not in pattern:
name = path.name
else:
name = six.text_type(path)
return fnmatch.fnmatch(name, pattern)

69
testing/test_paths.py Normal file
View File

@ -0,0 +1,69 @@
import sys
import py
import pytest
from _pytest.paths import fnmatch_ex
class TestPort:
"""Test that our port of py.common.FNMatcher (fnmatch_ex) produces the same results as the
original py.path.local.fnmatch method.
"""
@pytest.fixture(params=["pathlib", "py.path"])
def match(self, request):
if request.param == "py.path":
def match_(pattern, path):
return py.path.local(path).fnmatch(pattern)
else:
assert request.param == "pathlib"
def match_(pattern, path):
return fnmatch_ex(pattern, path)
return match_
if sys.platform == "win32":
drv1 = "c:"
drv2 = "d:"
else:
drv1 = "/c"
drv2 = "/d"
@pytest.mark.parametrize(
"pattern, path",
[
("*.py", "foo.py"),
("*.py", "bar/foo.py"),
("test_*.py", "foo/test_foo.py"),
("tests/*.py", "tests/foo.py"),
(drv1 + "/*.py", drv1 + "/foo.py"),
(drv1 + "/foo/*.py", drv1 + "/foo/foo.py"),
("tests/**/test*.py", "tests/foo/test_foo.py"),
("tests/**/doc/test*.py", "tests/foo/bar/doc/test_foo.py"),
("tests/**/doc/**/test*.py", "tests/foo/doc/bar/test_foo.py"),
],
)
def test_matching(self, match, pattern, path):
assert match(pattern, path)
@pytest.mark.parametrize(
"pattern, path",
[
("*.py", "foo.pyc"),
("*.py", "foo/foo.pyc"),
("tests/*.py", "foo/foo.py"),
(drv1 + "/*.py", drv2 + "/foo.py"),
(drv1 + "/foo/*.py", drv2 + "/foo/foo.py"),
("tests/**/test*.py", "tests/foo.py"),
("tests/**/test*.py", "foo/test_foo.py"),
("tests/**/doc/test*.py", "tests/foo/bar/doc/foo.py"),
("tests/**/doc/test*.py", "tests/foo/bar/test_foo.py"),
],
)
def test_not_matching(self, match, pattern, path):
assert not match(pattern, path)