From 809924990e41ae6daf68384210a6f0f9ace1a283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Fri, 10 Oct 2025 14:03:51 +0100 Subject: [PATCH 01/14] gh-139899: Introduce MetaPathFinder.discover and PathEntryFinder.discover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Doc/library/importlib.rst | 20 +++++++++++ Lib/importlib/_bootstrap_external.py | 36 +++++++++++++++++++ Lib/importlib/abc.py | 17 +++++++++ ...-10-10-14-08-58.gh-issue-139899.09leRY.rst | 2 ++ 4 files changed, 75 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index 3f0a54ac535cd6..fcfd8fb2152c13 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -275,6 +275,16 @@ ABC hierarchy:: .. versionchanged:: 3.4 Returns ``None`` when called instead of :data:`NotImplemented`. + .. method:: discover(parent=None) + + An optional method which searches for possible specs with given *parent* + module spec. If *parent* is *None*, :meth:`PathEntryFinder.discover` will + search for top-level modules. + + Returns an iterable of possible specs. + + .. versionadded:: next + .. class:: PathEntryFinder @@ -307,6 +317,16 @@ ABC hierarchy:: :meth:`importlib.machinery.PathFinder.invalidate_caches` when invalidating the caches of all cached finders. + .. method:: discover(parent=None) + + An optional method which searches for possible specs with given *parent* + module spec. If *parent* is *None*, :meth:`PathEntryFinder.discover` will + search for top-level modules. + + Returns an iterable of possible specs. + + .. versionadded:: next + .. class:: Loader diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 2f9307cba4f086..21d60d3393e1ca 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1323,6 +1323,21 @@ def find_spec(cls, fullname, path=None, target=None): else: return spec + @classmethod + def discover(cls, parent=None): + if parent is None: + path = sys.path + else: + path = parent.submodule_search_locations + + for entry in path: + if not isinstance(entry, str): + continue + if (finder := cls._path_importer_cache(entry)) is None: + continue + if discover := getattr(finder, 'discover', None): + yield from discover(parent) + @staticmethod def find_distributions(*args, **kwargs): """ @@ -1472,6 +1487,27 @@ def path_hook_for_FileFinder(path): return path_hook_for_FileFinder + def _find_children(self): + for entry in _os.scandir(self.path): + if entry.name == _PYCACHE: + continue + # packages + if entry.is_dir() and '.' not in entry.name: + yield entry.name + # files + if entry.is_file(): + yield from [ + entry.name.removesuffix(suffix) + for suffix, _ in self._loaders + if entry.name.endswith(suffix) + ] + + def discover(self, parent=None): + module_prefix = f'{parent.name}.' if parent else '' + for child_name in self._find_children(): + if spec := self.find_spec(module_prefix + child_name): + yield spec + def __repr__(self): return f'FileFinder({self.path!r})' diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py index 5c13432b5bda8c..8d336e737f5dab 100644 --- a/Lib/importlib/abc.py +++ b/Lib/importlib/abc.py @@ -45,6 +45,15 @@ def invalidate_caches(self): This method is used by importlib.invalidate_caches(). """ + def discover(self, parent=None): + """An optional method which searches for possible specs with given *parent*. + If *parent* is *None*, MetaPathFinder.discover will search for top-level modules. + + Returns an iterable of possible specs. + """ + return () + + _register(MetaPathFinder, machinery.BuiltinImporter, machinery.FrozenImporter, machinery.PathFinder, machinery.WindowsRegistryFinder) @@ -58,6 +67,14 @@ def invalidate_caches(self): This method is used by PathFinder.invalidate_caches(). """ + def discover(self, parent=None): + """An optional method which searches for possible specs with given *parent*. + If *parent* is *None*, PathEntryFinder.discover will search for top-level modules. + + Returns an iterable of possible specs. + """ + return () + _register(PathEntryFinder, machinery.FileFinder) diff --git a/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst b/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst new file mode 100644 index 00000000000000..5851566609188f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst @@ -0,0 +1,2 @@ +Introduced :meth:`MetaPathFinder.discover` and +:meth:`PathEntryFinder.discover`. From 6816705de0c2c6cd7289f405c5e11cbf13e4930e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 10 Dec 2025 12:54:09 +0000 Subject: [PATCH 02/14] Fix doc reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Doc/library/importlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index fcfd8fb2152c13..5cc5f3e7c3e848 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -278,7 +278,7 @@ ABC hierarchy:: .. method:: discover(parent=None) An optional method which searches for possible specs with given *parent* - module spec. If *parent* is *None*, :meth:`PathEntryFinder.discover` will + module spec. If *parent* is *None*, :meth:`MetaPathFinder.discover` will search for top-level modules. Returns an iterable of possible specs. From 31d1a8f5510e0f7a53016c7120ea2e1bda46e60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 10 Dec 2025 12:55:50 +0000 Subject: [PATCH 03/14] Remove specific doc references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Doc/library/importlib.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index 5cc5f3e7c3e848..53322beaceb501 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -278,8 +278,8 @@ ABC hierarchy:: .. method:: discover(parent=None) An optional method which searches for possible specs with given *parent* - module spec. If *parent* is *None*, :meth:`MetaPathFinder.discover` will - search for top-level modules. + module spec. If *parent* is *None*, ``discover`` will search for top-level + modules. Returns an iterable of possible specs. @@ -320,8 +320,8 @@ ABC hierarchy:: .. method:: discover(parent=None) An optional method which searches for possible specs with given *parent* - module spec. If *parent* is *None*, :meth:`PathEntryFinder.discover` will - search for top-level modules. + module spec. If *parent* is *None*, ``discover`` will search for top-level + modules. Returns an iterable of possible specs. From 051cd1e5cf99af1ca42610306351955b7601e0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 10 Dec 2025 13:05:29 +0000 Subject: [PATCH 04/14] Fix docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/importlib/abc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py index 8d336e737f5dab..e2d7b74af49bcc 100644 --- a/Lib/importlib/abc.py +++ b/Lib/importlib/abc.py @@ -46,8 +46,9 @@ def invalidate_caches(self): """ def discover(self, parent=None): - """An optional method which searches for possible specs with given *parent*. - If *parent* is *None*, MetaPathFinder.discover will search for top-level modules. + """An optional method which searches for possible specs with given *parent* + module spec. If *parent* is *None*, MetaPathFinder.discover will search + for top-level modules. Returns an iterable of possible specs. """ @@ -68,8 +69,9 @@ def invalidate_caches(self): """ def discover(self, parent=None): - """An optional method which searches for possible specs with given *parent*. - If *parent* is *None*, PathEntryFinder.discover will search for top-level modules. + """An optional method which searches for possible specs with given + *parent* module spec. If *parent* is *None*, PathEntryFinder.discover + will search for top-level modules. Returns an iterable of possible specs. """ From c343d328b90068df086b3482c498130fff305b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 10 Dec 2025 13:08:55 +0000 Subject: [PATCH 05/14] Revert "Remove specific doc references" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 31d1a8f5510e0f7a53016c7120ea2e1bda46e60c. Signed-off-by: Filipe Laíns --- Doc/library/importlib.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index 53322beaceb501..5cc5f3e7c3e848 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -278,8 +278,8 @@ ABC hierarchy:: .. method:: discover(parent=None) An optional method which searches for possible specs with given *parent* - module spec. If *parent* is *None*, ``discover`` will search for top-level - modules. + module spec. If *parent* is *None*, :meth:`MetaPathFinder.discover` will + search for top-level modules. Returns an iterable of possible specs. @@ -320,8 +320,8 @@ ABC hierarchy:: .. method:: discover(parent=None) An optional method which searches for possible specs with given *parent* - module spec. If *parent* is *None*, ``discover`` will search for top-level - modules. + module spec. If *parent* is *None*, :meth:`PathEntryFinder.discover` will + search for top-level modules. Returns an iterable of possible specs. From a324d96647735f19b97656b8f757f14ca78e1e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 10 Dec 2025 13:10:01 +0000 Subject: [PATCH 06/14] Fix news references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- .../Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst b/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst index 5851566609188f..74635e25b9b748 100644 --- a/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst +++ b/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst @@ -1,2 +1,2 @@ -Introduced :meth:`MetaPathFinder.discover` and -:meth:`PathEntryFinder.discover`. +Introduced :meth:`importlib.abc.MetaPathFinder.discover` +and :meth:`importlib.abc.PathEntryFinder.discover`. From dc6faa23360b749a8b146a4e95ca90851d3eeaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 20 Jan 2026 00:35:21 +0000 Subject: [PATCH 07/14] Add docs warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Doc/library/importlib.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index 5cc5f3e7c3e848..d4388980f13226 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -283,6 +283,16 @@ ABC hierarchy:: Returns an iterable of possible specs. + .. warning:: + This method can potentially yield a very large number of objects, and + it may carry out IO operations when computing these values. + + Because of this, it will generaly be desirable to compute the result + values on-the-fly, as they are needed. As such, the returned object is + only guaranteed to be an :class:`iterable `, + instead of a :class:`list` or other + :class:`collection ` type. + .. versionadded:: next @@ -325,6 +335,16 @@ ABC hierarchy:: Returns an iterable of possible specs. + .. warning:: + This method can potentially yield a very large number of objects, and + it may carry out IO operations when computing these values. + + Because of this, it will generaly be desirable to compute the result + values on-the-fly, as they are needed. As such, the returned object is + only guaranteed to be an :class:`iterable `, + instead of a :class:`list` or other + :class:`collection ` type. + .. versionadded:: next From 3be757f44533809df1d770fc41a0728f42438711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 20 Jan 2026 00:56:30 +0000 Subject: [PATCH 08/14] Raise ValueError on invalid parent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/importlib/_bootstrap_external.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 21d60d3393e1ca..7cf021fabdc2ee 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1327,6 +1327,8 @@ def find_spec(cls, fullname, path=None, target=None): def discover(cls, parent=None): if parent is None: path = sys.path + elif parent.submodule_search_locations is None: + raise ValueError(f'{parent} is not a package module') else: path = parent.submodule_search_locations From 0da477fabc65446298794185232f2804bac69c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 20 Jan 2026 00:56:51 +0000 Subject: [PATCH 09/14] Dedupe __path__ in PathFinder.discover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/importlib/_bootstrap_external.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 7cf021fabdc2ee..43995915dd3b29 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1332,7 +1332,7 @@ def discover(cls, parent=None): else: path = parent.submodule_search_locations - for entry in path: + for entry in set(path): if not isinstance(entry, str): continue if (finder := cls._path_importer_cache(entry)) is None: From 282bef7f8e95d0f4d0d02793c504d4b1758fb709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 20 Jan 2026 01:28:15 +0000 Subject: [PATCH 10/14] Use context manager and add error handling to os.scandir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/importlib/_bootstrap_external.py | 33 +++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 43995915dd3b29..ddef7bdd0232e1 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1490,19 +1490,26 @@ def path_hook_for_FileFinder(path): return path_hook_for_FileFinder def _find_children(self): - for entry in _os.scandir(self.path): - if entry.name == _PYCACHE: - continue - # packages - if entry.is_dir() and '.' not in entry.name: - yield entry.name - # files - if entry.is_file(): - yield from [ - entry.name.removesuffix(suffix) - for suffix, _ in self._loaders - if entry.name.endswith(suffix) - ] + with _os.scandir(self.path) as scan_iterator: + while True: + try: + entry = next(scan_iterator) + if entry.name == _PYCACHE: + continue + # packages + if entry.is_dir() and '.' not in entry.name: + yield entry.name + # files + if entry.is_file(): + yield from [ + entry.name.removesuffix(suffix) + for suffix, _ in self._loaders + if entry.name.endswith(suffix) + ] + except OSError: + pass # ignore exceptions from next(scan_iterator) and os.DirEntry + except StopIteration: + break def discover(self, parent=None): module_prefix = f'{parent.name}.' if parent else '' From 469dc2a58daebc96436f414b4ea2a221e39150a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 20 Jan 2026 01:37:56 +0000 Subject: [PATCH 11/14] Raise ValueError on invalid parent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Doc/library/importlib.rst | 4 ++++ Lib/importlib/_bootstrap_external.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index d4388980f13226..b8b8ca7989aed2 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -283,6 +283,8 @@ ABC hierarchy:: Returns an iterable of possible specs. + Raises :exc:`ValueError` if *parent* is not a package module. + .. warning:: This method can potentially yield a very large number of objects, and it may carry out IO operations when computing these values. @@ -335,6 +337,8 @@ ABC hierarchy:: Returns an iterable of possible specs. + Raises :exc:`ValueError` if *parent* is not a package module. + .. warning:: This method can potentially yield a very large number of objects, and it may carry out IO operations when computing these values. diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index ddef7bdd0232e1..7cce940a7fd060 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1512,6 +1512,9 @@ def _find_children(self): break def discover(self, parent=None): + if parent and parent.submodule_search_locations is None: + raise ValueError(f'{parent} is not a package module') + module_prefix = f'{parent.name}.' if parent else '' for child_name in self._find_children(): if spec := self.find_spec(module_prefix + child_name): From c09a12ecf9ca5d80953293051c24a0e9fac2f0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 20 Jan 2026 01:39:14 +0000 Subject: [PATCH 12/14] Dedupe when package exists with multiple suffixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/importlib/_bootstrap_external.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 7cce940a7fd060..bd0da7f9866ad1 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1501,11 +1501,11 @@ def _find_children(self): yield entry.name # files if entry.is_file(): - yield from [ + yield from { entry.name.removesuffix(suffix) for suffix, _ in self._loaders if entry.name.endswith(suffix) - ] + } except OSError: pass # ignore exceptions from next(scan_iterator) and os.DirEntry except StopIteration: From 41cd071cfb1dc33d548e6b41b0f8876d20854ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 20 Jan 2026 01:40:51 +0000 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Alyssa Coghlan --- .../Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst b/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst index 74635e25b9b748..fe5e7d17ab6c8c 100644 --- a/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst +++ b/Misc/NEWS.d/next/Library/2025-10-10-14-08-58.gh-issue-139899.09leRY.rst @@ -1,2 +1,3 @@ Introduced :meth:`importlib.abc.MetaPathFinder.discover` -and :meth:`importlib.abc.PathEntryFinder.discover`. +and :meth:`importlib.abc.PathEntryFinder.discover` to allow module and submodule +name discovery without assuming the use of traditional filesystem based imports. From 5488e93c319837330699e10b94f62f2ec38823ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 22 Jan 2026 17:30:22 +0000 Subject: [PATCH 14/14] Add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- Lib/test/test_importlib/test_discover.py | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 Lib/test/test_importlib/test_discover.py diff --git a/Lib/test/test_importlib/test_discover.py b/Lib/test/test_importlib/test_discover.py new file mode 100644 index 00000000000000..8c5fa65a564c6d --- /dev/null +++ b/Lib/test/test_importlib/test_discover.py @@ -0,0 +1,121 @@ +from unittest.mock import Mock + +from test.test_importlib import util + +importlib = util.import_importlib('importlib') +machinery = util.import_importlib('importlib.machinery') + + +class DiscoverableFinder: + def __init__(self, discover=[]): + self._discovered_values = discover + + def find_spec(self, fullname, path=None, target=None): + raise NotImplemented + + def discover(self, parent=None): + yield from self._discovered_values + + +class TestPathFinder: + """PathFinder implements MetaPathFinder, which uses the PathEntryFinder(s) + registered in sys.path_hooks (and sys.path_importer_cache) to search + sys.path or the parent's __path__. + + PathFinder.discover() should redirect to the .discover() method of the + PathEntryFinder for each path entry. + """ + + def test_search_path_hooks_top_level(self): + modules = [ + self.machinery.ModuleSpec(name='example1', loader=None), + self.machinery.ModuleSpec(name='example2', loader=None), + self.machinery.ModuleSpec(name='example3', loader=None), + ] + + with util.import_state( + path_importer_cache={ + 'discoverable': DiscoverableFinder(discover=modules), + }, + path=['discoverable'], + ): + discovered = list(self.machinery.PathFinder.discover()) + + self.assertEqual(discovered, modules) + + + def test_search_path_hooks_parent(self): + parent = self.machinery.ModuleSpec(name='example', loader=None, is_package=True) + parent.submodule_search_locations.append('discoverable') + + children = [ + self.machinery.ModuleSpec(name='example.child1', loader=None), + self.machinery.ModuleSpec(name='example.child2', loader=None), + self.machinery.ModuleSpec(name='example.child3', loader=None), + ] + + with util.import_state( + path_importer_cache={ + 'discoverable': DiscoverableFinder(discover=children) + }, + path=[], + ): + discovered = list(self.machinery.PathFinder.discover(parent)) + + self.assertEqual(discovered, children) + + def test_invalid_parent(self): + parent = self.machinery.ModuleSpec(name='example', loader=None) + with self.assertRaises(ValueError): + list(self.machinery.PathFinder.discover(parent)) + + +( + Frozen_TestPathFinder, + Source_TestPathFinder, +) = util.test_both(TestPathFinder, importlib=importlib, machinery=machinery) + + +class TestFileFinder: + """FileFinder implements PathEntryFinder and provides the base finder + implementation to search the file system. + """ + + def get_finder(self, path): + loader_details = [ + (self.machinery.SourceFileLoader, self.machinery.SOURCE_SUFFIXES), + (self.machinery.SourcelessFileLoader, self.machinery.BYTECODE_SUFFIXES), + ] + return self.machinery.FileFinder(path, *loader_details) + + def test_discover_top_level(self): + modules = {'example1', 'example2', 'example3'} + with util.create_modules(*modules) as mapping: + finder = self.get_finder(mapping['.root']) + discovered = list(finder.discover()) + self.assertEqual({spec.name for spec in discovered}, modules) + + def test_discover_parent(self): + modules = { + 'example.child1', + 'example.child2', + 'example.child3', + } + with util.create_modules(*modules) as mapping: + example = self.get_finder(mapping['.root']).find_spec('example') + finder = self.get_finder(example.submodule_search_locations[0]) + discovered = list(finder.discover(example)) + self.assertEqual({spec.name for spec in discovered}, modules) + + def test_invalid_parent(self): + with util.create_modules('example') as mapping: + finder = self.get_finder(mapping['.root']) + example = finder.find_spec('example') + with self.assertRaises(ValueError): + list(finder.discover(example)) + + +( + Frozen_TestFileFinder, + Source_TestFileFinder, +) = util.test_both(TestFileFinder, importlib=importlib, machinery=machinery)