From 84ff8649b635ade7a77ff5e2d4ab4bf08954ab61 Mon Sep 17 00:00:00 2001 From: David Berlioz Date: Wed, 2 Sep 2026 09:17:59 +0200 Subject: [PATCH 1/2] pycore: don't stop source folder detection at the first package `_find_source_folders` returned as soon as a folder had a package among its children, leaving every sibling folder unscanned. On a project whose root holds a package, detection therefore stopped at the root and no other source folder was ever found. That contradicts what the existing tests describe. Both `test_multi_source_folders` and `test_multi_source_folders2` expect two roots side by side -- they only pass because in their layouts no package sits directly at the root, so the early return never fires. The consequence is silent. Imports resolving through the unscanned folders simply do not resolve, so rename skips those occurrences instead of reporting them: a repository-wide rename comes back having quietly renamed a subset. Apply the same rule at every level instead of stopping at the first match, and skip package subfolders when recursing -- descending into a package would make `package.sub` importable as plain `sub`. Measured on a project with two packages at the root and further code under `src/`: 1 source folder detected before, 15 after, and an occurrence that resolved 0 of its 2 references now resolves both. This costs time where it previously skipped work. The same rename went from 3.5s to 43.8s, and touches 19 files rather than 8 -- the added time is spent analysing code that was invisible before, not overhead. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V5GqJKaXZzFzmFEYgo4pPR --- rope/base/pycore.py | 23 ++++++++++++++--------- ropetest/projecttest.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/rope/base/pycore.py b/rope/base/pycore.py index 4d9cd5ccf..55b5d2e71 100644 --- a/rope/base/pycore.py +++ b/rope/base/pycore.py @@ -146,16 +146,21 @@ def _is_package(self, folder): ) def _find_source_folders(self, folder): - for resource in folder.get_folders(): - if self._is_package(resource): - return [folder] result = [] - for resource in folder.get_files(): - if resource.name.endswith(".py"): - result.append(folder) - break - for resource in folder.get_folders(): - result.extend(self._find_source_folders(resource)) + subfolders = folder.get_folders() + if any(self._is_package(resource) for resource in subfolders): + result.append(folder) + else: + for resource in folder.get_files(): + if resource.name.endswith(".py"): + result.append(folder) + break + for resource in subfolders: + # Inside a package the subfolders are subpackages, not roots + # of their own -- descending would make `pkg.sub` importable + # as plain `sub`. + if not self._is_package(resource): + result.extend(self._find_source_folders(resource)) return result def run_module(self, resource, args=None, stdin=None, stdout=None): diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index c076669f9..0dc1c3734 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -618,6 +618,26 @@ def test_multi_source_folders2(self): self.assertEqual(2, len(source_folders)) self.assertTrue(self.project.root in source_folders and src in source_folders) + def test_multi_source_folders_with_a_package_at_the_root(self): + # A package sitting directly at the root used to end the search + # there, leaving every sibling folder unscanned -- so `src` was + # never a source folder and nothing under it could be resolved. + testutils.create_package(self.project, "rootpkg") + src = self.project.root.create_folder("src") + testutils.create_package(self.project, "package", src) + source_folders = self.project.get_source_folders() + self.assertEqual(2, len(source_folders)) + self.assertTrue(self.project.root in source_folders and src in source_folders) + + def test_a_subpackage_is_not_a_source_folder(self): + # Descending into a package would make `package.sub` importable + # as plain `sub`. + src = self.project.root.create_folder("src") + package = testutils.create_package(self.project, "package", src) + testutils.create_package(self.project, "sub", package) + source_folders = self.project.get_source_folders() + self.assertEqual([src], source_folders) + def test_folder_is_pathlike(self): resource = self.project.root.create_folder("src") self.assertIsInstance(resource, Folder) From 91f6f7ab56ae105bb8d5732d2a060a71b8bb1cc4 Mon Sep 17 00:00:00 2001 From: David Berlioz Date: Wed, 2 Sep 2026 09:29:14 +0200 Subject: [PATCH 2/2] Add #869 to the changelog Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V5GqJKaXZzFzmFEYgo4pPR --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 010425d5d..75114fa96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - #853 Implement patchedast handlers TypeVar - #847 Avoid printing autoimport syntax errors (@yangfan-yf-yf) - #623, #819, #863 Support MatchOr, MatchSequence, MatchStar (@jheld, @lieryan) +- #869 Fix source folder detection stopping at the first package - #870 Add default implementation for is_dir() (@lieryan) # Release 1.14.0