| 617 | return ModuleNotFoundReason.NOT_FOUND, True |
| 618 | |
| 619 | def find_modules_recursive(self, module: str) -> list[BuildSource]: |
| 620 | module_path = self.find_module(module, fast_path=True) |
| 621 | if isinstance(module_path, ModuleNotFoundReason): |
| 622 | return [] |
| 623 | sources = [BuildSource(module_path, module, None)] |
| 624 | |
| 625 | package_path = None |
| 626 | if is_init_file(module_path): |
| 627 | package_path = os.path.dirname(module_path) |
| 628 | elif self.fscache.isdir(module_path): |
| 629 | package_path = module_path |
| 630 | if package_path is None: |
| 631 | return sources |
| 632 | |
| 633 | # This logic closely mirrors that in find_sources. One small but important difference is |
| 634 | # that we do not sort names with keyfunc. The recursive call to find_modules_recursive |
| 635 | # calls find_module, which will handle the preference between packages, pyi and py. |
| 636 | # Another difference is it doesn't handle nested search paths / package roots. |
| 637 | |
| 638 | seen: set[str] = set() |
| 639 | names = sorted(self.fscache.listdir(package_path)) |
| 640 | for name in names: |
| 641 | # Skip certain names altogether |
| 642 | if name in ("__pycache__", "site-packages", "node_modules") or name.startswith("."): |
| 643 | continue |
| 644 | subpath = os_path_join(package_path, name) |
| 645 | |
| 646 | if self.options and matches_exclude( |
| 647 | subpath, self.options.exclude, self.fscache, self.options.verbosity >= 2 |
| 648 | ): |
| 649 | continue |
| 650 | if ( |
| 651 | self.options |
| 652 | and self.options.exclude_gitignore |
| 653 | and matches_gitignore(subpath, self.fscache, self.options.verbosity >= 2) |
| 654 | ): |
| 655 | continue |
| 656 | |
| 657 | if self.fscache.isdir(subpath): |
| 658 | # Only recurse into packages |
| 659 | if (self.options and self.options.namespace_packages) or ( |
| 660 | self.fscache.isfile(os_path_join(subpath, "__init__.py")) |
| 661 | or self.fscache.isfile(os_path_join(subpath, "__init__.pyi")) |
| 662 | ): |
| 663 | seen.add(name) |
| 664 | sources.extend(self.find_modules_recursive(module + "." + name)) |
| 665 | else: |
| 666 | stem, suffix = os.path.splitext(name) |
| 667 | if stem == "__init__": |
| 668 | continue |
| 669 | if stem not in seen and "." not in stem and suffix in PYTHON_EXTENSIONS: |
| 670 | # (If we sorted names by keyfunc) we could probably just make the BuildSource |
| 671 | # ourselves, but this ensures compatibility with find_module / the cache |
| 672 | seen.add(stem) |
| 673 | sources.extend(self.find_modules_recursive(module + "." + stem)) |
| 674 | return sources |
| 675 | |
| 676 | |