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