From 6cc4b98c9c61556cc1845b40416f624c9197e110 Mon Sep 17 00:00:00 2001 From: William Tanksley Jr Date: Mon, 22 Nov 2021 22:37:16 -0800 Subject: [PATCH] fix: glob characters in local paths (#552) * Prototype fix for local globbing problem with glob characters in pathnames. * refactor: support Python 2, add test * tests: skip glob test on Windows Co-authored-by: Henry Schreiner --- noxfile.py | 2 +- plumbum/lib.py | 15 +++++++++++++++ plumbum/path/local.py | 6 ++++-- tests/conftest.py | 9 ++++----- tests/test_local.py | 14 ++++++++++++++ 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/noxfile.py b/noxfile.py index 2b8582a44..9e286286f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -23,7 +23,7 @@ def tests(session): Run the unit and regular tests. """ session.install("-e", ".[dev]") - session.run("pytest", "--cov", *session.posargs) + session.run("pytest", *session.posargs) @nox.session(reuse_venv=True) diff --git a/plumbum/lib.py b/plumbum/lib.py index 959a2ecb0..95e11f905 100644 --- a/plumbum/lib.py +++ b/plumbum/lib.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import inspect import os +import re import sys from contextlib import contextmanager @@ -102,6 +103,20 @@ def get_method_function(m): else: from StringIO import StringIO +if sys.version_info >= (3,): + from glob import escape as glob_escape +else: + _magic_check = re.compile(u"([*?[])") + _magic_check_bytes = re.compile(b"([*?[])") + + def glob_escape(pathname): + drive, pathname = os.path.splitdrive(pathname) + if isinstance(pathname, str): + pathname = _magic_check_bytes.sub(r"[\1]", pathname) + else: + pathname = _magic_check.sub(u"[\\1]", pathname) + return drive + pathname + @contextmanager def captured_stdout(stdin=""): diff --git a/plumbum/path/local.py b/plumbum/path/local.py index a4af06917..a01d6c4c8 100644 --- a/plumbum/path/local.py +++ b/plumbum/path/local.py @@ -7,7 +7,7 @@ import sys from contextlib import contextmanager -from plumbum.lib import IS_WIN32, _setdoc, six +from plumbum.lib import IS_WIN32, _setdoc, glob_escape, six from plumbum.path.base import FSUser, Path from plumbum.path.remote import RemotePath @@ -170,7 +170,9 @@ def with_suffix(self, suffix, depth=1): @_setdoc(Path) def glob(self, pattern): - fn = lambda pat: [LocalPath(m) for m in glob.glob(str(self / pat))] + fn = lambda pat: [ + LocalPath(m) for m in glob.glob(os.path.join(glob_escape(str(self)), pat)) + ] return self._glob(pattern, fn) @_setdoc(Path) diff --git a/tests/conftest.py b/tests/conftest.py index 140ff6700..0b7cd3e3f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,16 +71,15 @@ def pytest_addoption(parser): default=None, help="Optional test markers to run, multiple and/or comma separated okay", ) + parser.addini( + "optional_tests", "list of optional markers", type="linelist", default="" + ) def pytest_configure(config): # register all optional tests declared in ini file as markers # https://docs.pytest.org/en/latest/writing_plugins.html#registering-custom-markers - ot_ini = config.inicfg.get("optional_tests") # None if NA - if ot_ini: - ot_ini = ot_ini.split("\n") - else: - ot_ini = [] + ot_ini = config.inicfg.get("optional_tests").splitlines() for ot in ot_ini: # ot should be a line like "optmarker: this is an opt marker", as with markers section config.addinivalue_line("markers", ot) diff --git a/tests/test_local.py b/tests/test_local.py index a2f51a858..5a74e8b74 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -1073,3 +1073,17 @@ def test_runfile_rich(self): os.chmod(name, st.st_mode | stat.S_IEXEC) assert "yes" in local[local.cwd / name]() + + +@pytest.mark.skipif( + IS_WIN32, reason="Windows does not support these weird paths, so unambiguous there" +) +def test_local_glob_path(tmpdir): + p = tmpdir.mkdir("a*b?c") + p2 = tmpdir.mkdir("aanythingbxc") + p2.join("something.txt").write("content") + p.join("hello.txt").write("content") + p.join("other.txt").write("content") + + pp = LocalPath(str(p)) + assert len(pp // "*.txt") == 2