From 92de18f3bba696a67699d3cd7043be8559691902 Mon Sep 17 00:00:00 2001 From: David Foster Date: Thu, 8 Jan 2026 17:46:26 -0500 Subject: [PATCH 1/2] Fix modutils._is_subpath to handle base with trailing os.sep --- ChangeLog | 4 ++++ astroid/modutils.py | 6 +++++- tests/test_modutils.py | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 0e4312e579..01182a975d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -42,6 +42,10 @@ What's New in astroid 4.0.4? ============================ Release date: TBA +* Fix ability to detect .py modules inside PATH directories on Windows + described by a UNC path with a trailing backslash (`\`) + - Example: modutils.modpath_from_file(filename=r"\\Mac\Code\tests\test_resources.py", path=["\\mac\code\"]) == ['tests', 'test_resources'] + What's New in astroid 4.0.3? diff --git a/astroid/modutils.py b/astroid/modutils.py index 0868c60c0a..acca79cbea 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -237,7 +237,11 @@ def _is_subpath(path: str, base: str) -> bool: base = os.path.normcase(os.path.normpath(base)) if not path.startswith(base): return False - return (len(path) == len(base)) or (path[len(base)] == os.path.sep) + return ( + (len(path) == len(base)) or + (path[len(base)] == os.path.sep) or + (base.endswith(os.path.sep) and path[len(base) - 1] == os.path.sep) + ) def _get_relative_base_path(filename: str, path_to_check: str) -> list[str] | None: diff --git a/tests/test_modutils.py b/tests/test_modutils.py index 1d6959f11c..ad020a68e5 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -151,6 +151,28 @@ def test_get_module_part_only_dot(self) -> None: self.assertEqual(modutils.get_module_part(".", modutils.__file__), ".") + +class IsSubpathTest(unittest.TestCase): + """Tests for modutils._is_subpath, + which is used internally by modutils.modpath_from_file.""" + + @unittest.skipUnless(sys.platform == "win32", "Windows-specific path test") + def test_is_subpath_with_trailing_separator_unc_path(self) -> None: + self.assertTrue(modutils._is_subpath( + "\\\\Mac\\Code\\tests\\test_resources.py", + # UNC path with trailing separator + "\\\\mac\\code\\" + )) + + @unittest.skipUnless(sys.platform == "win32", "Windows-specific path test") + def test_is_subpath_without_trailing_separator_unc_path(self) -> None: + self.assertTrue(modutils._is_subpath( + "\\\\Mac\\Code\\tests\\test_resources.py", + # UNC path without trailing separator + "\\\\mac\\code" + )) + + class ModPathFromFileTest(unittest.TestCase): """Given an absolute file path return the python module's path as a list.""" From 2d471d3424a28e744f1cb1e192e6bbedf132e1dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:06:54 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- astroid/modutils.py | 6 +++--- tests/test_modutils.py | 25 ++++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/astroid/modutils.py b/astroid/modutils.py index acca79cbea..bb2c7c4dde 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -238,9 +238,9 @@ def _is_subpath(path: str, base: str) -> bool: if not path.startswith(base): return False return ( - (len(path) == len(base)) or - (path[len(base)] == os.path.sep) or - (base.endswith(os.path.sep) and path[len(base) - 1] == os.path.sep) + (len(path) == len(base)) + or (path[len(base)] == os.path.sep) + or (base.endswith(os.path.sep) and path[len(base) - 1] == os.path.sep) ) diff --git a/tests/test_modutils.py b/tests/test_modutils.py index ad020a68e5..f50c535902 100644 --- a/tests/test_modutils.py +++ b/tests/test_modutils.py @@ -151,26 +151,29 @@ def test_get_module_part_only_dot(self) -> None: self.assertEqual(modutils.get_module_part(".", modutils.__file__), ".") - class IsSubpathTest(unittest.TestCase): """Tests for modutils._is_subpath, which is used internally by modutils.modpath_from_file.""" @unittest.skipUnless(sys.platform == "win32", "Windows-specific path test") def test_is_subpath_with_trailing_separator_unc_path(self) -> None: - self.assertTrue(modutils._is_subpath( - "\\\\Mac\\Code\\tests\\test_resources.py", - # UNC path with trailing separator - "\\\\mac\\code\\" - )) + self.assertTrue( + modutils._is_subpath( + "\\\\Mac\\Code\\tests\\test_resources.py", + # UNC path with trailing separator + "\\\\mac\\code\\", + ) + ) @unittest.skipUnless(sys.platform == "win32", "Windows-specific path test") def test_is_subpath_without_trailing_separator_unc_path(self) -> None: - self.assertTrue(modutils._is_subpath( - "\\\\Mac\\Code\\tests\\test_resources.py", - # UNC path without trailing separator - "\\\\mac\\code" - )) + self.assertTrue( + modutils._is_subpath( + "\\\\Mac\\Code\\tests\\test_resources.py", + # UNC path without trailing separator + "\\\\mac\\code", + ) + ) class ModPathFromFileTest(unittest.TestCase):