Spaces:
Running
Running
File size: 44,402 Bytes
d631808 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 |
# This module contains all the logic for loading API data from sources or compiled modules.
from __future__ import annotations
from contextlib import suppress
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, cast
from _griffe.agents.inspector import inspect
from _griffe.agents.visitor import visit
from _griffe.collections import LinesCollection, ModulesCollection
from _griffe.enumerations import Kind
from _griffe.exceptions import (
AliasResolutionError,
CyclicAliasError,
LoadingError,
UnimportableModuleError,
)
from _griffe.expressions import ExprName
from _griffe.extensions.base import Extensions, load_extensions
from _griffe.finder import ModuleFinder, NamespacePackage, Package
from _griffe.git import tmp_worktree
from _griffe.importer import dynamic_import
from _griffe.logger import logger
from _griffe.merger import merge_stubs
from _griffe.models import Alias, Module, Object
from _griffe.stats import Stats
if TYPE_CHECKING:
from collections.abc import Sequence
from _griffe.docstrings.parsers import DocstringStyle
from _griffe.enumerations import Parser
class GriffeLoader:
"""The Griffe loader, allowing to load data from modules."""
ignored_modules: ClassVar[set[str]] = {"debugpy", "_pydev"}
"""Special modules to ignore when loading.
For example, `debugpy` and `_pydev` are used when debugging with VSCode and should generally never be loaded.
"""
def __init__(
self,
*,
extensions: Extensions | None = None,
search_paths: Sequence[str | Path] | None = None,
docstring_parser: DocstringStyle | Parser | None = None,
docstring_options: dict[str, Any] | None = None,
lines_collection: LinesCollection | None = None,
modules_collection: ModulesCollection | None = None,
allow_inspection: bool = True,
force_inspection: bool = False,
store_source: bool = True,
) -> None:
"""Initialize the loader.
Parameters:
extensions: The extensions to use.
search_paths: The paths to search into.
docstring_parser: The docstring parser to use. By default, no parsing is done.
docstring_options: Additional docstring parsing options.
lines_collection: A collection of source code lines.
modules_collection: A collection of modules.
allow_inspection: Whether to allow inspecting modules when visiting them is not possible.
store_source: Whether to store code source in the lines collection.
"""
self.extensions: Extensions = extensions or load_extensions()
"""Loaded Griffe extensions."""
self.docstring_parser: DocstringStyle | Parser | None = docstring_parser
"""Selected docstring parser."""
self.docstring_options: dict[str, Any] = docstring_options or {}
"""Configured parsing options."""
self.lines_collection: LinesCollection = lines_collection or LinesCollection()
"""Collection of source code lines."""
self.modules_collection: ModulesCollection = modules_collection or ModulesCollection()
"""Collection of modules."""
self.allow_inspection: bool = allow_inspection
"""Whether to allow inspecting (importing) modules for which we can't find sources."""
self.force_inspection: bool = force_inspection
"""Whether to force inspecting (importing) modules, even when sources were found."""
self.store_source: bool = store_source
"""Whether to store source code in the lines collection."""
self.finder: ModuleFinder = ModuleFinder(search_paths)
"""The module source finder."""
self._time_stats: dict = {
"time_spent_visiting": 0,
"time_spent_inspecting": 0,
}
def load(
self,
objspec: str | Path | None = None,
/,
*,
submodules: bool = True,
try_relative_path: bool = True,
find_stubs_package: bool = False,
) -> Object | Alias:
"""Load an object as a Griffe object, given its Python or file path.
Note that this will load the whole object's package,
and return only the specified object.
The rest of the package can be accessed from the returned object
with regular methods and properties (`parent`, `members`, etc.).
Examples:
>>> loader.load("griffe.Module")
Alias("Module", "_griffe.models.Module")
Parameters:
objspec: The Python path of an object, or file path to a module.
submodules: Whether to recurse on the submodules.
This parameter only makes sense when loading a package (top-level module).
try_relative_path: Whether to try finding the module as a relative path.
find_stubs_package: Whether to search for stubs-only package.
If both the package and its stubs are found, they'll be merged together.
If only the stubs are found, they'll be used as the package itself.
Raises:
LoadingError: When loading a module failed for various reasons.
ModuleNotFoundError: When a module was not found and inspection is disallowed.
Returns:
A Griffe object.
"""
obj_path: str
package = None
top_module = None
# We always start by searching paths on the disk,
# even if inspection is forced.
logger.debug("Searching path(s) for %s", objspec)
try:
obj_path, package = self.finder.find_spec(
objspec, # type: ignore[arg-type]
try_relative_path=try_relative_path,
find_stubs_package=find_stubs_package,
)
except ModuleNotFoundError:
# If we couldn't find paths on disk and inspection is disabled,
# re-raise ModuleNotFoundError.
logger.debug("Could not find path for %s on disk", objspec)
if not (self.allow_inspection or self.force_inspection):
raise
# Otherwise we try to dynamically import the top-level module.
obj_path = str(objspec)
top_module_name = obj_path.split(".", 1)[0]
logger.debug("Trying to dynamically import %s", top_module_name)
top_module_object = dynamic_import(top_module_name, self.finder.search_paths)
try:
top_module_path = top_module_object.__path__
if not top_module_path:
raise ValueError(f"Module {top_module_name} has no paths set") # noqa: TRY301
except (AttributeError, ValueError):
# If the top-level module has no `__path__`, we inspect it as-is,
# and do not try to recurse into submodules (there shouldn't be any in builtin/compiled modules).
logger.debug("Module %s has no paths set (built-in module?). Inspecting it as-is.", top_module_name)
top_module = self._inspect_module(top_module_name)
self.modules_collection.set_member(top_module.path, top_module)
return self._post_load(top_module, obj_path)
# We found paths, and use them to build our intermediate Package or NamespacePackage struct.
logger.debug("Module %s has paths set: %s", top_module_name, top_module_path)
top_module_path = [Path(path) for path in top_module_path]
if len(top_module_path) > 1:
package = NamespacePackage(top_module_name, top_module_path)
else:
package = Package(top_module_name, top_module_path[0])
# We have an intermediate package, and an object path: we're ready to load.
logger.debug("Found %s: loading", objspec)
try:
top_module = self._load_package(package, submodules=submodules)
except LoadingError:
logger.exception("Could not load package %s", package)
raise
return self._post_load(top_module, obj_path)
def _post_load(self, module: Module, obj_path: str) -> Object | Alias:
# Pre-emptively expand exports (`__all__` values),
# as well as wildcard imports (without ever loading additional packages).
# This is a best-effort to return the most correct API data
# before firing the `on_package_loaded` event.
#
# Packages that wildcard imports from external, non-loaded packages
# will still have incomplete data, requiring subsequent calls to
# `load()` and/or `resolve_aliases()`.
self.expand_exports(module)
self.expand_wildcards(module, external=False)
# Package is loaded, we now retrieve the initially requested object and return it.
obj = self.modules_collection.get_member(obj_path)
self.extensions.call("on_package_loaded", pkg=module, loader=self)
return obj
def resolve_aliases(
self,
*,
implicit: bool = False,
external: bool | None = None,
max_iterations: int | None = None,
) -> tuple[set[str], int]:
"""Resolve aliases.
Parameters:
implicit: When false, only try to resolve an alias if it is explicitly exported.
external: When false, don't try to load unspecified modules to resolve aliases.
max_iterations: Maximum number of iterations on the loader modules collection.
Returns:
The unresolved aliases and the number of iterations done.
"""
if max_iterations is None:
max_iterations = float("inf") # type: ignore[assignment]
prev_unresolved: set[str] = set()
unresolved: set[str] = set("0") # Init to enter loop.
iteration = 0
collection = self.modules_collection.members
# Before resolving aliases, we try to expand wildcard imports again
# (this was already done in `_post_load()`),
# this time with the user-configured `external` setting,
# and with potentially more packages loaded in the collection,
# allowing to resolve more aliases.
for wildcards_module in list(collection.values()):
self.expand_wildcards(wildcards_module, external=external)
load_failures: set[str] = set()
while unresolved and unresolved != prev_unresolved and iteration < max_iterations: # type: ignore[operator]
prev_unresolved = unresolved - {"0"}
unresolved = set()
resolved: set[str] = set()
iteration += 1
for module_name in list(collection.keys()):
module = collection[module_name]
next_resolved, next_unresolved = self.resolve_module_aliases(
module,
implicit=implicit,
external=external,
load_failures=load_failures,
)
resolved |= next_resolved
unresolved |= next_unresolved
logger.debug(
"Iteration %s finished, %s aliases resolved, still %s to go",
iteration,
len(resolved),
len(unresolved),
)
return unresolved, iteration
def expand_exports(self, module: Module, seen: set | None = None) -> None:
"""Expand exports: try to recursively expand all module exports (`__all__` values).
See also: [`Module.exports`][griffe.Module.exports].
Parameters:
module: The module to recurse on.
seen: Used to avoid infinite recursion.
"""
seen = seen or set()
seen.add(module.path)
if module.exports is None:
return
expanded = []
for export in module.exports:
# It's a name: we resolve it, get the module it comes from,
# recurse into it, and add its exports to the current ones.
if isinstance(export, ExprName):
module_path = export.canonical_path.rsplit(".", 1)[0] # Remove trailing `.__all__`.
try:
next_module = self.modules_collection.get_member(module_path)
except KeyError:
logger.debug("Cannot expand '%s', try pre-loading corresponding package", export.canonical_path)
continue
if next_module.path not in seen:
self.expand_exports(next_module, seen)
try:
expanded += [export for export in next_module.exports if export not in expanded]
except TypeError:
logger.warning("Unsupported item in %s.__all__: %s (use strings only)", module.path, export)
# It's a string, simply add it to the current exports.
else:
expanded.append(export)
module.exports = expanded
# Make sure to expand exports in all modules.
for submodule in module.modules.values():
if not submodule.is_alias and submodule.path not in seen:
self.expand_exports(submodule, seen)
def expand_wildcards(
self,
obj: Object,
*,
external: bool | None = None,
seen: set | None = None,
) -> None:
"""Expand wildcards: try to recursively expand all found wildcards.
See also: [`Alias.wildcard`][griffe.Alias.wildcard].
Parameters:
obj: The object and its members to recurse on.
external: When true, try to load unspecified modules to expand wildcards.
seen: Used to avoid infinite recursion.
"""
expanded = []
to_remove = []
seen = seen or set()
seen.add(obj.path)
# First we expand wildcard imports and store the objects in a temporary `expanded` variable,
# while also keeping track of the members representing wildcard import, to remove them later.
for member in obj.members.values():
# Handle a wildcard.
if member.is_alias and member.wildcard: # type: ignore[union-attr]
package = member.wildcard.split(".", 1)[0] # type: ignore[union-attr]
not_loaded = obj.package.path != package and package not in self.modules_collection
# Try loading the (unknown) package containing the wildcard importe module (if allowed to).
if not_loaded:
if external is False or (external is None and package != f"_{obj.package.name}"):
continue
try:
self.load(package, try_relative_path=False)
except (ImportError, LoadingError) as error:
logger.debug("Could not expand wildcard import %s in %s: %s", member.name, obj.path, error)
continue
# Try getting the module from which every public object is imported.
try:
target = self.modules_collection.get_member(member.target_path) # type: ignore[union-attr]
except KeyError:
logger.debug(
"Could not expand wildcard import %s in %s: %s not found in modules collection",
member.name,
obj.path,
cast("Alias", member).target_path,
)
continue
# Recurse into this module, expanding wildcards there before collecting everything.
if target.path not in seen:
try:
self.expand_wildcards(target, external=external, seen=seen)
except (AliasResolutionError, CyclicAliasError) as error:
logger.debug("Could not expand wildcard import %s in %s: %s", member.name, obj.path, error)
continue
# Collect every imported object.
expanded.extend(self._expand_wildcard(member)) # type: ignore[arg-type]
to_remove.append(member.name)
# Recurse in unseen submodules.
elif not member.is_alias and member.is_module and member.path not in seen:
self.expand_wildcards(member, external=external, seen=seen) # type: ignore[arg-type]
# Then we remove the members representing wildcard imports.
for name in to_remove:
obj.del_member(name)
# Finally we process the collected objects.
for new_member, alias_lineno, alias_endlineno in expanded:
overwrite = False
already_present = new_member.name in obj.members
self_alias = (
new_member.is_alias and cast("Alias", new_member).target_path == f"{obj.path}.{new_member.name}"
)
# If a member with the same name is already present in the current object,
# we only overwrite it if the alias is imported lower in the module
# (meaning that the alias takes precedence at runtime).
if already_present:
old_member = obj.get_member(new_member.name)
old_lineno = old_member.alias_lineno if old_member.is_alias else old_member.lineno
overwrite = alias_lineno > (old_lineno or 0) # type: ignore[operator]
# 1. If the expanded member is an alias with a target path equal to its own path, we stop.
# This situation can arise because of Griffe's mishandling of (abusive) wildcard imports.
# We have yet to check how Python handles this itself, or if there's an algorithm
# that we could follow to untangle back-and-forth wildcard imports.
# 2. If the expanded member was already present and we decided not to overwrite it, we stop.
# 3. Otherwise we proceed further.
if not self_alias and (not already_present or overwrite):
alias = Alias(
new_member.name,
new_member,
lineno=alias_lineno,
endlineno=alias_endlineno,
parent=obj, # type: ignore[arg-type]
)
# Special case: we avoid overwriting a submodule with an alias pointing to it.
# Griffe suffers from this design flaw where an object cannot store both
# a submodule and a member of the same name, while this poses (almost) no issue in Python.
# We at least prevent this case where a submodule is overwritten by an imported version of itself.
if already_present:
prev_member = obj.get_member(new_member.name)
with suppress(AliasResolutionError, CyclicAliasError):
if prev_member.is_module:
if prev_member.is_alias:
prev_member = prev_member.final_target
if alias.final_target is prev_member:
# Alias named after the module it targets: skip to avoid cyclic aliases.
continue
# Everything went right (supposedly), we add the alias as a member of the current object.
obj.set_member(new_member.name, alias)
self.extensions.call("on_wildcard_expansion", alias=alias, loader=self)
def resolve_module_aliases(
self,
obj: Object | Alias,
*,
implicit: bool = False,
external: bool | None = None,
seen: set[str] | None = None,
load_failures: set[str] | None = None,
) -> tuple[set[str], set[str]]:
"""Follow aliases: try to recursively resolve all found aliases.
Parameters:
obj: The object and its members to recurse on.
implicit: When false, only try to resolve an alias if it is explicitly exported.
external: When false, don't try to load unspecified modules to resolve aliases.
seen: Used to avoid infinite recursion.
load_failures: Set of external packages we failed to load (to prevent retries).
Returns:
Both sets of resolved and unresolved aliases.
"""
resolved = set()
unresolved = set()
if load_failures is None:
load_failures = set()
seen = seen or set()
seen.add(obj.path)
for member in obj.members.values():
# Handle aliases.
if member.is_alias:
if member.wildcard or member.resolved: # type: ignore[union-attr]
continue
if not implicit and not member.is_exported:
continue
# Try resolving the alias. If it fails, check if it is because it comes
# from an external package, and decide if we should load that package
# to allow the alias to be resolved at the next iteration (maybe).
try:
member.resolve_target() # type: ignore[union-attr]
except AliasResolutionError as error:
target = error.alias.target_path
unresolved.add(member.path)
package = target.split(".", 1)[0]
load_module = (
(external is True or (external is None and package == f"_{obj.package.name}"))
and package not in load_failures
and obj.package.path != package
and package not in self.modules_collection
)
if load_module:
logger.debug("Failed to resolve alias %s -> %s", member.path, target)
try:
self.load(package, try_relative_path=False)
except (ImportError, LoadingError) as error:
logger.debug("Could not follow alias %s: %s", member.path, error)
load_failures.add(package)
except CyclicAliasError as error:
logger.debug(str(error))
else:
logger.debug("Alias %s was resolved to %s", member.path, member.final_target.path) # type: ignore[union-attr]
resolved.add(member.path)
# Recurse into unseen modules and classes.
elif member.kind in {Kind.MODULE, Kind.CLASS} and member.path not in seen:
sub_resolved, sub_unresolved = self.resolve_module_aliases(
member,
implicit=implicit,
external=external,
seen=seen,
load_failures=load_failures,
)
resolved |= sub_resolved
unresolved |= sub_unresolved
return resolved, unresolved
def stats(self) -> Stats:
"""Compute some statistics.
Returns:
Some statistics.
"""
stats = Stats(self)
stats.time_spent_visiting = self._time_stats["time_spent_visiting"]
stats.time_spent_inspecting = self._time_stats["time_spent_inspecting"]
return stats
def _load_package(self, package: Package | NamespacePackage, *, submodules: bool = True) -> Module:
top_module = self._load_module(package.name, package.path, submodules=submodules)
if isinstance(package, NamespacePackage):
return top_module
if package.stubs:
self.expand_wildcards(top_module)
# If stubs are in the package itself, they have been merged while loading modules,
# so only the top-level init module needs to be merged still.
# If stubs are in another package (a stubs-only package),
# then we need to load the entire stubs package to merge everything.
submodules = submodules and package.stubs.parent != package.path.parent
stubs = self._load_module(package.name, package.stubs, submodules=submodules)
return merge_stubs(top_module, stubs)
return top_module
def _load_module(
self,
module_name: str,
module_path: Path | list[Path],
*,
submodules: bool = True,
parent: Module | None = None,
) -> Module:
try:
return self._load_module_path(module_name, module_path, submodules=submodules, parent=parent)
except SyntaxError as error:
raise LoadingError(f"Syntax error: {error}") from error
except ImportError as error:
raise LoadingError(f"Import error: {error}") from error
except UnicodeDecodeError as error:
raise LoadingError(f"UnicodeDecodeError when loading {module_path}: {error}") from error
except OSError as error:
raise LoadingError(f"OSError when loading {module_path}: {error}") from error
def _load_module_path(
self,
module_name: str,
module_path: Path | list[Path],
*,
submodules: bool = True,
parent: Module | None = None,
) -> Module:
logger.debug("Loading path %s", module_path)
if isinstance(module_path, list):
module = self._create_module(module_name, module_path)
elif self.force_inspection:
module = self._inspect_module(module_name, module_path, parent)
elif module_path.suffix in {".py", ".pyi"}:
module = self._visit_module(module_name, module_path, parent)
elif self.allow_inspection:
module = self._inspect_module(module_name, module_path, parent)
else:
raise LoadingError("Cannot load compiled module without inspection")
if parent is None:
self.modules_collection.set_member(module.path, module)
if submodules:
self._load_submodules(module)
return module
def _load_submodules(self, module: Module) -> None:
for subparts, subpath in self.finder.submodules(module):
self._load_submodule(module, subparts, subpath)
def _load_submodule(self, module: Module, subparts: tuple[str, ...], subpath: Path) -> None:
for subpart in subparts:
if "." in subpart:
logger.debug("Skip %s, dots in filenames are not supported", subpath)
return
try:
parent_module = self._get_or_create_parent_module(module, subparts, subpath)
except UnimportableModuleError as error:
# NOTE: Why don't we load submodules when there's no init module in their folder?
# Usually when a folder with Python files does not have an __init__.py module,
# it's because the Python files are scripts that should never be imported.
# Django has manage.py somewhere for example, in a folder without init module.
# This script isn't part of the Python API, as it's meant to be called on the CLI exclusively
# (at least it was the case a few years ago when I was still using Django).
# The other case when there's no init module is when a package is a native namespace package (PEP 420).
# It does not make sense to have a native namespace package inside of a regular package (having init modules above),
# because the regular package above blocks the namespace feature from happening, so I consider it a user error.
# It's true that users could have a native namespace package inside of a pkg_resources-style namespace package,
# but I've never seen this happen.
# It's also true that Python can actually import the module under the (wrongly declared) native namespace package,
# so the Griffe debug log message is a bit misleading,
# but that's because in that case Python acts like the whole tree is a regular package.
# It works when the namespace package appears in only one search path (`sys.path`),
# but will fail if it appears in multiple search paths: Python will only find the first occurrence.
# It's better to not falsely support this, and to warn users.
logger.debug("%s. Missing __init__ module?", error)
return
submodule_name = subparts[-1]
try:
submodule = self._load_module(
submodule_name,
subpath,
submodules=False,
parent=parent_module,
)
except LoadingError as error:
logger.debug(str(error))
else:
if submodule_name in parent_module.members:
member = parent_module.members[submodule_name]
if member.is_alias or not member.is_module:
logger.debug(
"Submodule '%s' is shadowing the member at the same path. "
"We recommend renaming the member or the submodule (for example prefixing it with `_`), "
"see https://mkdocstrings.github.io/griffe/best_practices/#avoid-member-submodule-name-shadowing.",
submodule.path,
)
parent_module.set_member(submodule_name, submodule)
def _create_module(self, module_name: str, module_path: Path | list[Path]) -> Module:
return Module(
module_name,
filepath=module_path,
lines_collection=self.lines_collection,
modules_collection=self.modules_collection,
)
def _visit_module(self, module_name: str, module_path: Path, parent: Module | None = None) -> Module:
code = module_path.read_text(encoding="utf8")
if self.store_source:
self.lines_collection[module_path] = code.splitlines(keepends=False)
start = datetime.now(tz=timezone.utc)
module = visit(
module_name,
filepath=module_path,
code=code,
extensions=self.extensions,
parent=parent,
docstring_parser=self.docstring_parser,
docstring_options=self.docstring_options,
lines_collection=self.lines_collection,
modules_collection=self.modules_collection,
)
elapsed = datetime.now(tz=timezone.utc) - start
self._time_stats["time_spent_visiting"] += elapsed.microseconds
return module
def _inspect_module(self, module_name: str, filepath: Path | None = None, parent: Module | None = None) -> Module:
for prefix in self.ignored_modules:
if module_name.startswith(prefix):
raise ImportError(f"Ignored module '{module_name}'")
if self.store_source and filepath and filepath.suffix in {".py", ".pyi"}:
self.lines_collection[filepath] = filepath.read_text(encoding="utf8").splitlines(keepends=False)
start = datetime.now(tz=timezone.utc)
try:
module = inspect(
module_name,
filepath=filepath,
import_paths=self.finder.search_paths,
extensions=self.extensions,
parent=parent,
docstring_parser=self.docstring_parser,
docstring_options=self.docstring_options,
lines_collection=self.lines_collection,
modules_collection=self.modules_collection,
)
except SystemExit as error:
raise ImportError(f"Importing '{module_name}' raised a system exit") from error
except Exception as error:
raise ImportError(f"Importing '{module_name}' raised an exception") from error
elapsed = datetime.now(tz=timezone.utc) - start
self._time_stats["time_spent_inspecting"] += elapsed.microseconds
return module
def _get_or_create_parent_module(
self,
module: Module,
subparts: tuple[str, ...],
subpath: Path,
) -> Module:
parent_parts = subparts[:-1]
if not parent_parts:
return module
parent_module = module
parents = list(subpath.parents)
if subpath.stem == "__init__":
parents.pop(0)
for parent_offset, parent_part in enumerate(parent_parts, 2):
module_filepath = parents[len(subparts) - parent_offset]
try:
parent_module = parent_module.get_member(parent_part)
except KeyError as error:
if parent_module.is_namespace_package or parent_module.is_namespace_subpackage:
next_parent_module = self._create_module(parent_part, [module_filepath])
parent_module.set_member(parent_part, next_parent_module)
parent_module = next_parent_module
else:
raise UnimportableModuleError(f"Skip {subpath}, it is not importable") from error
else:
parent_namespace = parent_module.is_namespace_package or parent_module.is_namespace_subpackage
if parent_namespace and module_filepath not in parent_module.filepath: # type: ignore[operator]
parent_module.filepath.append(module_filepath) # type: ignore[union-attr]
return parent_module
def _expand_wildcard(self, wildcard_obj: Alias) -> list[tuple[Object | Alias, int | None, int | None]]:
module = self.modules_collection.get_member(wildcard_obj.wildcard) # type: ignore[arg-type]
return [
(imported_member, wildcard_obj.alias_lineno, wildcard_obj.alias_endlineno)
for imported_member in module.members.values()
if imported_member.is_wildcard_exposed
]
def load(
objspec: str | Path | None = None,
/,
*,
submodules: bool = True,
try_relative_path: bool = True,
extensions: Extensions | None = None,
search_paths: Sequence[str | Path] | None = None,
docstring_parser: DocstringStyle | Parser | None = None,
docstring_options: dict[str, Any] | None = None,
lines_collection: LinesCollection | None = None,
modules_collection: ModulesCollection | None = None,
allow_inspection: bool = True,
force_inspection: bool = False,
store_source: bool = True,
find_stubs_package: bool = False,
resolve_aliases: bool = False,
resolve_external: bool | None = None,
resolve_implicit: bool = False,
) -> Object | Alias:
"""Load and return a Griffe object.
In Griffe's context, loading means:
- searching for a package, and finding it on the file system or as a builtin module
(see the [`ModuleFinder`][griffe.ModuleFinder] class for more information)
- extracting information from each of its (sub)modules, by either parsing
the source code (see the [`visit`][griffe.visit] function)
or inspecting the module at runtime (see the [`inspect`][griffe.inspect] function)
The extracted information is stored in a collection of modules, which can be queried later.
Each collected module is a tree of objects, representing the structure of the module.
See the [`Module`][griffe.Module], [`Class`][griffe.Class],
[`Function`][griffe.Function], and [`Attribute`][griffe.Attribute] classes
for more information.
The main class used to load modules is [`GriffeLoader`][griffe.GriffeLoader].
Convenience functions like this one and [`load_git`][griffe.load_git] are also available.
Example:
```python
import griffe
module = griffe.load(...)
```
This is a shortcut for:
```python
from griffe import GriffeLoader
loader = GriffeLoader(...)
module = loader.load(...)
```
See the documentation for the loader: [`GriffeLoader`][griffe.GriffeLoader].
Parameters:
objspec: The Python path of an object, or file path to a module.
submodules: Whether to recurse on the submodules.
This parameter only makes sense when loading a package (top-level module).
try_relative_path: Whether to try finding the module as a relative path.
extensions: The extensions to use.
search_paths: The paths to search into.
docstring_parser: The docstring parser to use. By default, no parsing is done.
docstring_options: Additional docstring parsing options.
lines_collection: A collection of source code lines.
modules_collection: A collection of modules.
allow_inspection: Whether to allow inspecting modules when visiting them is not possible.
force_inspection: Whether to force using dynamic analysis when loading data.
store_source: Whether to store code source in the lines collection.
find_stubs_package: Whether to search for stubs-only package.
If both the package and its stubs are found, they'll be merged together.
If only the stubs are found, they'll be used as the package itself.
resolve_aliases: Whether to resolve aliases.
resolve_external: Whether to try to load unspecified modules to resolve aliases.
Default value (`None`) means to load external modules only if they are the private sibling
or the origin module (for example when `ast` imports from `_ast`).
resolve_implicit: When false, only try to resolve an alias if it is explicitly exported.
Returns:
A Griffe object.
"""
loader = GriffeLoader(
extensions=extensions,
search_paths=search_paths,
docstring_parser=docstring_parser,
docstring_options=docstring_options,
lines_collection=lines_collection,
modules_collection=modules_collection,
allow_inspection=allow_inspection,
force_inspection=force_inspection,
store_source=store_source,
)
result = loader.load(
objspec,
submodules=submodules,
try_relative_path=try_relative_path,
find_stubs_package=find_stubs_package,
)
if resolve_aliases:
loader.resolve_aliases(implicit=resolve_implicit, external=resolve_external)
return result
def load_git(
objspec: str | Path | None = None,
/,
*,
ref: str = "HEAD",
repo: str | Path = ".",
submodules: bool = True,
extensions: Extensions | None = None,
search_paths: Sequence[str | Path] | None = None,
docstring_parser: DocstringStyle | Parser | None = None,
docstring_options: dict[str, Any] | None = None,
lines_collection: LinesCollection | None = None,
modules_collection: ModulesCollection | None = None,
allow_inspection: bool = True,
force_inspection: bool = False,
find_stubs_package: bool = False,
resolve_aliases: bool = False,
resolve_external: bool | None = None,
resolve_implicit: bool = False,
) -> Object | Alias:
"""Load and return a module from a specific Git reference.
This function will create a temporary
[git worktree](https://git-scm.com/docs/git-worktree) at the requested reference
before loading `module` with [`griffe.load`][griffe.load].
This function requires that the `git` executable is installed.
Examples:
```python
from griffe import load_git
old_api = load_git("my_module", ref="v0.1.0", repo="path/to/repo")
```
Parameters:
objspec: The Python path of an object, or file path to a module.
ref: A Git reference such as a commit, tag or branch.
repo: Path to the repository (i.e. the directory *containing* the `.git` directory)
submodules: Whether to recurse on the submodules.
This parameter only makes sense when loading a package (top-level module).
extensions: The extensions to use.
search_paths: The paths to search into (relative to the repository root).
docstring_parser: The docstring parser to use. By default, no parsing is done.
docstring_options: Additional docstring parsing options.
lines_collection: A collection of source code lines.
modules_collection: A collection of modules.
allow_inspection: Whether to allow inspecting modules when visiting them is not possible.
force_inspection: Whether to force using dynamic analysis when loading data.
find_stubs_package: Whether to search for stubs-only package.
If both the package and its stubs are found, they'll be merged together.
If only the stubs are found, they'll be used as the package itself.
resolve_aliases: Whether to resolve aliases.
resolve_external: Whether to try to load unspecified modules to resolve aliases.
Default value (`None`) means to load external modules only if they are the private sibling
or the origin module (for example when `ast` imports from `_ast`).
resolve_implicit: When false, only try to resolve an alias if it is explicitly exported.
Returns:
A Griffe object.
"""
with tmp_worktree(repo, ref) as worktree:
search_paths = [worktree / path for path in search_paths or ["."]]
if isinstance(objspec, Path):
objspec = worktree / objspec
return load(
objspec,
submodules=submodules,
try_relative_path=False,
extensions=extensions,
search_paths=search_paths,
docstring_parser=docstring_parser,
docstring_options=docstring_options,
lines_collection=lines_collection,
modules_collection=modules_collection,
allow_inspection=allow_inspection,
force_inspection=force_inspection,
find_stubs_package=find_stubs_package,
resolve_aliases=resolve_aliases,
resolve_external=resolve_external,
resolve_implicit=resolve_implicit,
)
def load_pypi(
package: str, # noqa: ARG001
distribution: str, # noqa: ARG001
version_spec: str, # noqa: ARG001
*,
submodules: bool = True, # noqa: ARG001
extensions: Extensions | None = None, # noqa: ARG001
search_paths: Sequence[str | Path] | None = None, # noqa: ARG001
docstring_parser: DocstringStyle | Parser | None = None, # noqa: ARG001
docstring_options: dict[str, Any] | None = None, # noqa: ARG001
lines_collection: LinesCollection | None = None, # noqa: ARG001
modules_collection: ModulesCollection | None = None, # noqa: ARG001
allow_inspection: bool = True, # noqa: ARG001
force_inspection: bool = False, # noqa: ARG001
find_stubs_package: bool = False, # noqa: ARG001
) -> Object | Alias:
"""Load and return a module from a specific package version downloaded using pip.
[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } —
[:octicons-tag-24: Insiders 1.1.0](../../insiders/changelog.md#1.1.0).
Parameters:
package: The package import name.
distribution: The distribution name.
version_spec: The version specifier to use when installing with pip.
submodules: Whether to recurse on the submodules.
This parameter only makes sense when loading a package (top-level module).
extensions: The extensions to use.
search_paths: The paths to search into (relative to the repository root).
docstring_parser: The docstring parser to use. By default, no parsing is done.
docstring_options: Additional docstring parsing options.
lines_collection: A collection of source code lines.
modules_collection: A collection of modules.
allow_inspection: Whether to allow inspecting modules when visiting them is not possible.
force_inspection: Whether to force using dynamic analysis when loading data.
find_stubs_package: Whether to search for stubs-only package.
If both the package and its stubs are found, they'll be merged together.
If only the stubs are found, they'll be used as the package itself.
"""
raise ValueError("Not available in non-Insiders versions of Griffe")
|