| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f1cf762 commit 157f271
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -275,6 +275,28 @@ ABC hierarchy:: | |||
| 275 | 275 | .. versionchanged:: 3.4 | |
| 276 | 276 | Returns ``None`` when called instead of :data:`NotImplemented`. | |
| 277 | 277 | ||
| 278 | + .. method:: discover(parent=None) | ||
| 279 | + | ||
| 280 | + An optional method which searches for possible specs with given *parent* | ||
| 281 | + module spec. If *parent* is *None*, :meth:`MetaPathFinder.discover` will | ||
| 282 | + search for top-level modules. | ||
| 283 | + | ||
| 284 | + Returns an iterable of possible specs. | ||
| 285 | + | ||
| 286 | + Raises :exc:`ValueError` if *parent* is not a package module. | ||
| 287 | + | ||
| 288 | + .. warning:: | ||
| 289 | + This method can potentially yield a very large number of objects, and | ||
| 290 | + it may carry out IO operations when computing these values. | ||
| 291 | + | ||
| 292 | + Because of this, it will generaly be desirable to compute the result | ||
| 293 | + values on-the-fly, as they are needed. As such, the returned object is | ||
| 294 | + only guaranteed to be an :class:`iterable <collections.abc.Iterable>`, | ||
| 295 | + instead of a :class:`list` or other | ||
| 296 | + :class:`collection <collections.abc.Collection>` type. | ||
| 297 | + | ||
| 298 | + .. versionadded:: next | ||
| 299 | + | ||
| 278 | 300 | ||
| 279 | 301 | .. class:: PathEntryFinder | |
| 280 | 302 | ||
@@ -307,6 +329,28 @@ ABC hierarchy:: | |||
| 307 | 329 | :meth:`importlib.machinery.PathFinder.invalidate_caches` | |
| 308 | 330 | when invalidating the caches of all cached finders. | |
| 309 | 331 | ||
| 332 | + .. method:: discover(parent=None) | ||
| 333 | + | ||
| 334 | + An optional method which searches for possible specs with given *parent* | ||
| 335 | + module spec. If *parent* is *None*, :meth:`PathEntryFinder.discover` will | ||
| 336 | + search for top-level modules. | ||
| 337 | + | ||
| 338 | + Returns an iterable of possible specs. | ||
| 339 | + | ||
| 340 | + Raises :exc:`ValueError` if *parent* is not a package module. | ||
| 341 | + | ||
| 342 | + .. warning:: | ||
| 343 | + This method can potentially yield a very large number of objects, and | ||
| 344 | + it may carry out IO operations when computing these values. | ||
| 345 | + | ||
| 346 | + Because of this, it will generaly be desirable to compute the result | ||
| 347 | + values on-the-fly, as they are needed. As such, the returned object is | ||
| 348 | + only guaranteed to be an :class:`iterable <collections.abc.Iterable>`, | ||
| 349 | + instead of a :class:`list` or other | ||
| 350 | + :class:`collection <collections.abc.Collection>` type. | ||
| 351 | + | ||
| 352 | + .. versionadded:: next | ||
| 353 | + | ||
| 310 | 354 | ||
| 311 | 355 | .. class:: Loader | |
| 312 | 356 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1283,6 +1283,23 @@ def find_spec(cls, fullname, path=None, target=None): | |||
| 1283 | 1283 | else: | |
| 1284 | 1284 | return spec | |
| 1285 | 1285 | ||
| 1286 | + @classmethod | ||
| 1287 | + def discover(cls, parent=None): | ||
| 1288 | + if parent is None: | ||
| 1289 | + path = sys.path | ||
| 1290 | + elif parent.submodule_search_locations is None: | ||
| 1291 | + raise ValueError(f'{parent} is not a package module') | ||
| 1292 | + else: | ||
| 1293 | + path = parent.submodule_search_locations | ||
| 1294 | + | ||
| 1295 | + for entry in set(path): | ||
| 1296 | + if not isinstance(entry, str): | ||
| 1297 | + continue | ||
| 1298 | + if (finder := cls._path_importer_cache(entry)) is None: | ||
| 1299 | + continue | ||
| 1300 | + if discover := getattr(finder, 'discover', None): | ||
| 1301 | + yield from discover(parent) | ||
| 1302 | + | ||
| 1286 | 1303 | @staticmethod | |
| 1287 | 1304 | def find_distributions(*args, **kwargs): | |
| 1288 | 1305 | """ | |
@@ -1432,6 +1449,37 @@ def path_hook_for_FileFinder(path): | |||
| 1432 | 1449 | ||
| 1433 | 1450 | return path_hook_for_FileFinder | |
| 1434 | 1451 | ||
| 1452 | + def _find_children(self): | ||
| 1453 | + with _os.scandir(self.path) as scan_iterator: | ||
| 1454 | + while True: | ||
| 1455 | + try: | ||
| 1456 | + entry = next(scan_iterator) | ||
| 1457 | + if entry.name == _PYCACHE: | ||
| 1458 | + continue | ||
| 1459 | + # packages | ||
| 1460 | + if entry.is_dir() and '.' not in entry.name: | ||
| 1461 | + yield entry.name | ||
| 1462 | + # files | ||
| 1463 | + if entry.is_file(): | ||
| 1464 | + yield from { | ||
| 1465 | + entry.name.removesuffix(suffix) | ||
| 1466 | + for suffix, _ in self._loaders | ||
| 1467 | + if entry.name.endswith(suffix) | ||
| 1468 | + } | ||
| 1469 | + except OSError: | ||
| 1470 | + pass # ignore exceptions from next(scan_iterator) and os.DirEntry | ||
| 1471 | + except StopIteration: | ||
| 1472 | + break | ||
| 1473 | + | ||
| 1474 | + def discover(self, parent=None): | ||
| 1475 | + if parent and parent.submodule_search_locations is None: | ||
| 1476 | + raise ValueError(f'{parent} is not a package module') | ||
| 1477 | + | ||
| 1478 | + module_prefix = f'{parent.name}.' if parent else '' | ||
| 1479 | + for child_name in self._find_children(): | ||
| 1480 | + if spec := self.find_spec(module_prefix + child_name): | ||
| 1481 | + yield spec | ||
| 1482 | + | ||
| 1435 | 1483 | def __repr__(self): | |
| 1436 | 1484 | return f'FileFinder({self.path!r})' | |
| 1437 | 1485 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -45,6 +45,16 @@ def invalidate_caches(self): | |||
| 45 | 45 | This method is used by importlib.invalidate_caches(). | |
| 46 | 46 | """ | |
| 47 | 47 | ||
| 48 | + def discover(self, parent=None): | ||
| 49 | + """An optional method which searches for possible specs with given *parent* | ||
| 50 | + module spec. If *parent* is *None*, MetaPathFinder.discover will search | ||
| 51 | + for top-level modules. | ||
| 52 | + | ||
| 53 | + Returns an iterable of possible specs. | ||
| 54 | + """ | ||
| 55 | + return () | ||
| 56 | + | ||
| 57 | + | ||
| 48 | 58 | _register(MetaPathFinder, machinery.BuiltinImporter, machinery.FrozenImporter, | |
| 49 | 59 | machinery.PathFinder, machinery.WindowsRegistryFinder) | |
| 50 | 60 | ||
@@ -58,6 +68,15 @@ def invalidate_caches(self): | |||
| 58 | 68 | This method is used by PathFinder.invalidate_caches(). | |
| 59 | 69 | """ | |
| 60 | 70 | ||
| 71 | + def discover(self, parent=None): | ||
| 72 | + """An optional method which searches for possible specs with given | ||
| 73 | + *parent* module spec. If *parent* is *None*, PathEntryFinder.discover | ||
| 74 | + will search for top-level modules. | ||
| 75 | + | ||
| 76 | + Returns an iterable of possible specs. | ||
| 77 | + """ | ||
| 78 | + return () | ||
| 79 | + | ||
| 61 | 80 | _register(PathEntryFinder, machinery.FileFinder) | |
| 62 | 81 | ||
| 63 | 82 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,121 @@ | |||
| 1 | + from unittest.mock import Mock | ||
| 2 | + | ||
| 3 | + from test.test_importlib import util | ||
| 4 | + | ||
| 5 | + importlib = util.import_importlib('importlib') | ||
| 6 | + machinery = util.import_importlib('importlib.machinery') | ||
| 7 | + | ||
| 8 | + | ||
| 9 | + class DiscoverableFinder: | ||
| 10 | + def __init__(self, discover=[]): | ||
| 11 | + self._discovered_values = discover | ||
| 12 | + | ||
| 13 | + def find_spec(self, fullname, path=None, target=None): | ||
| 14 | + raise NotImplemented | ||
| 15 | + | ||
| 16 | + def discover(self, parent=None): | ||
| 17 | + yield from self._discovered_values | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + class TestPathFinder: | ||
| 21 | + """PathFinder implements MetaPathFinder, which uses the PathEntryFinder(s) | ||
| 22 | + registered in sys.path_hooks (and sys.path_importer_cache) to search | ||
| 23 | + sys.path or the parent's __path__. | ||
| 24 | + | ||
| 25 | + PathFinder.discover() should redirect to the .discover() method of the | ||
| 26 | + PathEntryFinder for each path entry. | ||
| 27 | + """ | ||
| 28 | + | ||
| 29 | + def test_search_path_hooks_top_level(self): | ||
| 30 | + modules = [ | ||
| 31 | + self.machinery.ModuleSpec(name='example1', loader=None), | ||
| 32 | + self.machinery.ModuleSpec(name='example2', loader=None), | ||
| 33 | + self.machinery.ModuleSpec(name='example3', loader=None), | ||
| 34 | + ] | ||
| 35 | + | ||
| 36 | + with util.import_state( | ||
| 37 | + path_importer_cache={ | ||
| 38 | + 'discoverable': DiscoverableFinder(discover=modules), | ||
| 39 | + }, | ||
| 40 | + path=['discoverable'], | ||
| 41 | + ): | ||
| 42 | + discovered = list(self.machinery.PathFinder.discover()) | ||
| 43 | + | ||
| 44 | + self.assertEqual(discovered, modules) | ||
| 45 | + | ||
| 46 | + | ||
| 47 | + def test_search_path_hooks_parent(self): | ||
| 48 | + parent = self.machinery.ModuleSpec(name='example', loader=None, is_package=True) | ||
| 49 | + parent.submodule_search_locations.append('discoverable') | ||
| 50 | + | ||
| 51 | + children = [ | ||
| 52 | + self.machinery.ModuleSpec(name='example.child1', loader=None), | ||
| 53 | + self.machinery.ModuleSpec(name='example.child2', loader=None), | ||
| 54 | + self.machinery.ModuleSpec(name='example.child3', loader=None), | ||
| 55 | + ] | ||
| 56 | + | ||
| 57 | + with util.import_state( | ||
| 58 | + path_importer_cache={ | ||
| 59 | + 'discoverable': DiscoverableFinder(discover=children) | ||
| 60 | + }, | ||
| 61 | + path=[], | ||
| 62 | + ): | ||
| 63 | + discovered = list(self.machinery.PathFinder.discover(parent)) | ||
| 64 | + | ||
| 65 | + self.assertEqual(discovered, children) | ||
| 66 | + | ||
| 67 | + def test_invalid_parent(self): | ||
| 68 | + parent = self.machinery.ModuleSpec(name='example', loader=None) | ||
| 69 | + with self.assertRaises(ValueError): | ||
| 70 | + list(self.machinery.PathFinder.discover(parent)) | ||
| 71 | + | ||
| 72 | + | ||
| 73 | + ( | ||
| 74 | + Frozen_TestPathFinder, | ||
| 75 | + Source_TestPathFinder, | ||
| 76 | + ) = util.test_both(TestPathFinder, importlib=importlib, machinery=machinery) | ||
| 77 | + | ||
| 78 | + | ||
| 79 | + class TestFileFinder: | ||
| 80 | + """FileFinder implements PathEntryFinder and provides the base finder | ||
| 81 | + implementation to search the file system. | ||
| 82 | + """ | ||
| 83 | + | ||
| 84 | + def get_finder(self, path): | ||
| 85 | + loader_details = [ | ||
| 86 | + (self.machinery.SourceFileLoader, self.machinery.SOURCE_SUFFIXES), | ||
| 87 | + (self.machinery.SourcelessFileLoader, self.machinery.BYTECODE_SUFFIXES), | ||
| 88 | + ] | ||
| 89 | + return self.machinery.FileFinder(path, *loader_details) | ||
| 90 | + | ||
| 91 | + def test_discover_top_level(self): | ||
| 92 | + modules = {'example1', 'example2', 'example3'} | ||
| 93 | + with util.create_modules(*modules) as mapping: | ||
| 94 | + finder = self.get_finder(mapping['.root']) | ||
| 95 | + discovered = list(finder.discover()) | ||
| 96 | + self.assertEqual({spec.name for spec in discovered}, modules) | ||
| 97 | + | ||
| 98 | + def test_discover_parent(self): | ||
| 99 | + modules = { | ||
| 100 | + 'example.child1', | ||
| 101 | + 'example.child2', | ||
| 102 | + 'example.child3', | ||
| 103 | + } | ||
| 104 | + with util.create_modules(*modules) as mapping: | ||
| 105 | + example = self.get_finder(mapping['.root']).find_spec('example') | ||
| 106 | + finder = self.get_finder(example.submodule_search_locations[0]) | ||
| 107 | + discovered = list(finder.discover(example)) | ||
| 108 | + self.assertEqual({spec.name for spec in discovered}, modules) | ||
| 109 | + | ||
| 110 | + def test_invalid_parent(self): | ||
| 111 | + with util.create_modules('example') as mapping: | ||
| 112 | + finder = self.get_finder(mapping['.root']) | ||
| 113 | + example = finder.find_spec('example') | ||
| 114 | + with self.assertRaises(ValueError): | ||
| 115 | + list(finder.discover(example)) | ||
| 116 | + | ||
| 117 | + | ||
| 118 | + ( | ||
| 119 | + Frozen_TestFileFinder, | ||
| 120 | + Source_TestFileFinder, | ||
| 121 | + ) = util.test_both(TestFileFinder, importlib=importlib, machinery=machinery) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,3 @@ | |||
| 1 | + Introduced :meth:`importlib.abc.MetaPathFinder.discover` | ||
| 2 | + and :meth:`importlib.abc.PathEntryFinder.discover` to allow module and submodule | ||
| 3 | + name discovery without assuming the use of traditional filesystem based imports. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments