Skip to content

Commit dea4c83

Browse files
committed
feat: Rework extension system
Before, extensions were sub-visitors or sub-inspectors. It meant that to support both static and dynamic analysis, extension writers were forced to write two extensions, one of each type. Before, extensions ran wholy at particular points in time. It meant that it was not possible to run some logic just after a class was instantiated (and before its members were loaded), while running some other logic after everything (instance + members loaded if any). Some logic could also never be ran, because of the way the top visitor/inspector does not enter into every node. Now, extensions are generic and can handle both static and dynamic analysis. Now, extensions use hooks on events and are not limited by the visit/inspection of the top agent. Visitor and inspector extensions are deprecated.
1 parent 6d9d6b0 commit dea4c83

10 files changed

Lines changed: 1004 additions & 100 deletions

File tree

docs/extensions.md

Lines changed: 547 additions & 70 deletions
Large diffs are not rendered by default.

mkdocs.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ markdown_extensions:
8888
- pymdownx.magiclink
8989
- pymdownx.snippets:
9090
check_paths: true
91-
- pymdownx.superfences
91+
- pymdownx.superfences:
92+
custom_fences:
93+
- name: mermaid
94+
class: mermaid
95+
format: !!python/name:pymdownx.superfences.fence_code_format
9296
- pymdownx.tabbed:
9397
alternate_style: true
9498
slugify: !!python/object/apply:pymdownx.slugs.slugify

src/griffe/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,28 @@
77

88
from __future__ import annotations
99

10+
from griffe.agents.nodes import ObjectNode
11+
from griffe.dataclasses import Attribute, Class, Docstring, Function, Module, Object
1012
from griffe.diff import find_breaking_changes
13+
from griffe.extensions import Extension, load_extensions
1114
from griffe.git import load_git
15+
from griffe.importer import dynamic_import
1216
from griffe.loader import load
17+
from griffe.logger import get_logger
1318

14-
__all__: list[str] = ["find_breaking_changes", "load", "load_git"]
19+
__all__: list[str] = [
20+
"Attribute",
21+
"Class",
22+
"Docstring",
23+
"dynamic_import",
24+
"Extension",
25+
"Function",
26+
"find_breaking_changes",
27+
"get_logger",
28+
"load",
29+
"load_extensions",
30+
"load_git",
31+
"Module",
32+
"Object",
33+
"ObjectNode",
34+
]

src/griffe/agents/inspector.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,22 +215,31 @@ def inspect_module(self, node: ObjectNode) -> None:
215215
Parameters:
216216
node: The node to inspect.
217217
"""
218-
self.current = Module(
218+
self.extensions.call("on_node", node=node)
219+
self.extensions.call("on_module_node", node=node)
220+
self.current = module = Module(
219221
name=self.module_name,
220222
filepath=self.filepath,
221223
parent=self.parent,
222224
docstring=self._get_docstring(node),
223225
lines_collection=self.lines_collection,
224226
modules_collection=self.modules_collection,
225227
)
228+
self.extensions.call("on_instance", node=node, obj=module)
229+
self.extensions.call("on_module_instance", node=node, mod=module)
226230
self.generic_inspect(node)
231+
self.extensions.call("on_members", node=node, obj=module)
232+
self.extensions.call("on_module_members", node=node, mod=module)
227233

228234
def inspect_class(self, node: ObjectNode) -> None:
229235
"""Inspect a class.
230236
231237
Parameters:
232238
node: The node to inspect.
233239
"""
240+
self.extensions.call("on_node", node=node)
241+
self.extensions.call("on_class_node", node=node)
242+
234243
bases = []
235244
for base in node.obj.__bases__:
236245
if base is object:
@@ -244,7 +253,11 @@ def inspect_class(self, node: ObjectNode) -> None:
244253
)
245254
self.current.set_member(node.name, class_)
246255
self.current = class_
256+
self.extensions.call("on_instance", node=node, obj=class_)
257+
self.extensions.call("on_class_instance", node=node, cls=class_)
247258
self.generic_inspect(node)
259+
self.extensions.call("on_members", node=node, obj=class_)
260+
self.extensions.call("on_class_members", node=node, cls=class_)
248261
self.current = self.current.parent # type: ignore[assignment]
249262

250263
def inspect_staticmethod(self, node: ObjectNode) -> None:
@@ -335,6 +348,9 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
335348
node: The node to inspect.
336349
labels: Labels to add to the data object.
337350
"""
351+
self.extensions.call("on_node", node=node)
352+
self.extensions.call("on_function_node", node=node)
353+
338354
try:
339355
signature = getsignature(node.obj)
340356
except Exception: # noqa: BLE001
@@ -371,6 +387,11 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
371387
)
372388
obj.labels |= labels
373389
self.current.set_member(node.name, obj)
390+
self.extensions.call("on_instance", node=node, obj=obj)
391+
if obj.is_attribute:
392+
self.extensions.call("on_attribute_instance", node=node, attr=obj)
393+
else:
394+
self.extensions.call("on_function_instance", node=node, func=obj)
374395

375396
def inspect_attribute(self, node: ObjectNode) -> None:
376397
"""Inspect an attribute.
@@ -387,6 +408,9 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Name | Expression
387408
node: The node to inspect.
388409
annotation: A potentiel annotation.
389410
"""
411+
self.extensions.call("on_node", node=node)
412+
self.extensions.call("on_attribute_node", node=node)
413+
390414
# TODO: to improve
391415
parent = self.current
392416
labels: set[str] = set()
@@ -421,6 +445,8 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Name | Expression
421445

422446
if node.name == "__all__":
423447
parent.exports = set(node.obj)
448+
self.extensions.call("on_instance", node=node, obj=attribute)
449+
self.extensions.call("on_attribute_instance", node=node, attr=attribute)
424450

425451

426452
_kind_map = {

src/griffe/agents/nodes/_runtime.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,31 @@ class ObjectKind(enum.Enum):
2424
"""Enumeration for the different kinds of objects."""
2525

2626
MODULE: str = "module"
27+
"""Modules."""
2728
CLASS: str = "class"
29+
"""Classes."""
2830
STATICMETHOD: str = "staticmethod"
31+
"""Static methods."""
2932
CLASSMETHOD: str = "classmethod"
33+
"""Class methods."""
3034
METHOD_DESCRIPTOR: str = "method_descriptor"
35+
"""Method descriptors."""
3136
METHOD: str = "method"
37+
"""Methods."""
3238
BUILTIN_METHOD: str = "builtin_method"
39+
"""Built-in ethods."""
3340
COROUTINE: str = "coroutine"
41+
"""Coroutines"""
3442
FUNCTION: str = "function"
43+
"""Functions."""
3544
BUILTIN_FUNCTION: str = "builtin_function"
45+
"""Built-in functions."""
3646
CACHED_PROPERTY: str = "cached_property"
47+
"""Cached properties."""
3748
PROPERTY: str = "property"
49+
"""Properties."""
3850
ATTRIBUTE: str = "attribute"
51+
"""Attributes."""
3952

4053
def __str__(self) -> str:
4154
return self.value

src/griffe/agents/visitor.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,23 +211,31 @@ def visit_module(self, node: ast.Module) -> None:
211211
Parameters:
212212
node: The node to visit.
213213
"""
214-
module = Module(
214+
self.extensions.call("on_node", node=node)
215+
self.extensions.call("on_module_node", node=node)
216+
self.current = module = Module(
215217
name=self.module_name,
216218
filepath=self.filepath,
217219
parent=self.parent,
218220
docstring=self._get_docstring(node),
219221
lines_collection=self.lines_collection,
220222
modules_collection=self.modules_collection,
221223
)
222-
self.current = module
224+
self.extensions.call("on_instance", node=node, obj=module)
225+
self.extensions.call("on_module_instance", node=node, mod=module)
223226
self.generic_visit(node)
227+
self.extensions.call("on_members", node=node, obj=module)
228+
self.extensions.call("on_module_members", node=node, mod=module)
224229

225230
def visit_classdef(self, node: ast.ClassDef) -> None:
226231
"""Visit a class definition node.
227232
228233
Parameters:
229234
node: The node to visit.
230235
"""
236+
self.extensions.call("on_node", node=node)
237+
self.extensions.call("on_class_node", node=node)
238+
231239
# handle decorators
232240
decorators = []
233241
if node.decorator_list:
@@ -261,7 +269,11 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
261269
class_.labels |= self.decorators_to_labels(decorators)
262270
self.current.set_member(node.name, class_)
263271
self.current = class_
272+
self.extensions.call("on_instance", node=node, obj=class_)
273+
self.extensions.call("on_class_instance", node=node, cls=class_)
264274
self.generic_visit(node)
275+
self.extensions.call("on_members", node=node, obj=class_)
276+
self.extensions.call("on_class_members", node=node, cls=class_)
265277
self.current = self.current.parent # type: ignore[assignment]
266278

267279
def decorators_to_labels(self, decorators: list[Decorator]) -> set[str]:
@@ -313,6 +325,9 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
313325
node: The node to visit.
314326
labels: Labels to add to the data object.
315327
"""
328+
self.extensions.call("on_node", node=node)
329+
self.extensions.call("on_function_node", node=node)
330+
316331
labels = labels or set()
317332

318333
# handle decorators
@@ -348,6 +363,8 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
348363
)
349364
attribute.labels |= labels
350365
self.current.set_member(node.name, attribute)
366+
self.extensions.call("on_instance", node=node, obj=attribute)
367+
self.extensions.call("on_attribute_instance", node=node, attr=attribute)
351368
return
352369

353370
# handle parameters
@@ -456,6 +473,8 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
456473

457474
function.labels |= labels
458475

476+
self.extensions.call("on_instance", node=node, obj=function)
477+
self.extensions.call("on_function_instance", node=node, func=function)
459478
if self.current.kind is Kind.CLASS and function.name == "__init__":
460479
self.current = function # type: ignore[assignment] # temporary assign a function
461480
self.generic_visit(node)
@@ -541,6 +560,8 @@ def handle_attribute(
541560
node: The node to visit.
542561
annotation: A potential annotation.
543562
"""
563+
self.extensions.call("on_node", node=node)
564+
self.extensions.call("on_attribute_node", node=node)
544565
parent = self.current
545566
labels = set()
546567

@@ -625,6 +646,8 @@ def handle_attribute(
625646
if name == "__all__":
626647
with suppress(AttributeError):
627648
parent.exports = safe_get__all__(node, self.current) # type: ignore[arg-type]
649+
self.extensions.call("on_instance", node=node, obj=attribute)
650+
self.extensions.call("on_attribute_instance", node=node, attr=attribute)
628651

629652
def visit_assign(self, node: ast.Assign) -> None:
630653
"""Visit an assignment node.

src/griffe/cli.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from griffe.stats import _format_stats
3636

3737
if TYPE_CHECKING:
38-
from griffe.extensions import Extension, Extensions
38+
from griffe.extensions import Extensions, ExtensionType
3939

4040

4141
DEFAULT_LOG_LEVEL = os.getenv("GRIFFE_LOG_LEVEL", "INFO").upper()
@@ -98,6 +98,13 @@ def _load_packages(
9898
_level_choices = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
9999

100100

101+
def _extensions_type(value: str) -> Sequence[str | dict[str, Any]]:
102+
try:
103+
return json.loads(value)
104+
except json.JSONDecodeError:
105+
return value.split(",")
106+
107+
101108
def get_parser() -> argparse.ArgumentParser:
102109
"""Return the CLI argument parser.
103110
@@ -133,7 +140,7 @@ def add_common_options(subparser: argparse.ArgumentParser) -> None:
133140
"-e",
134141
"--extensions",
135142
default={},
136-
type=json.loads,
143+
type=_extensions_type,
137144
help="A list of extensions to use.",
138145
)
139146
loading_options.add_argument(
@@ -277,7 +284,7 @@ def dump(
277284
full: bool = False,
278285
docstring_parser: Parser | None = None,
279286
docstring_options: dict[str, Any] | None = None,
280-
extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None,
287+
extensions: Sequence[str | dict[str, Any] | ExtensionType | type[ExtensionType]] | None = None,
281288
resolve_aliases: bool = False,
282289
resolve_implicit: bool = False,
283290
resolve_external: bool = False,
@@ -356,7 +363,7 @@ def check(
356363
against_path: str | Path | None = None,
357364
*,
358365
base_ref: str | None = None,
359-
extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None,
366+
extensions: Sequence[str | dict[str, Any] | ExtensionType | type[ExtensionType]] | None = None,
360367
search_paths: Sequence[str | Path] | None = None,
361368
allow_inspection: bool = True,
362369
verbose: bool = False,

src/griffe/extensions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from griffe.extensions.base import (
44
Extension,
55
Extensions,
6+
ExtensionType,
67
InspectorExtension,
78
VisitorExtension,
89
When,
@@ -13,6 +14,7 @@
1314
__all__ = [
1415
"Extensions",
1516
"Extension",
17+
"ExtensionType",
1618
"InspectorExtension",
1719
"VisitorExtension",
1820
"When",

0 commit comments

Comments
 (0)