Skip to content

Commit d792a56

Browse files
committed
feat: Add analysis attribute on objects and aliases, telling whether they were loaded through static or dynamic analysis, or created manually
This change also documents load events better, and shows usage of load events rather than analysis events in docs example where possible.
1 parent 2a8d824 commit d792a56

6 files changed

Lines changed: 115 additions & 76 deletions

File tree

docs/guide/users/extending.md

Lines changed: 43 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ Hopefully this flowchart gives you a pretty good idea of what happens when Griff
188188

189189
### Events and hooks
190190

191-
There are two kinds of events in Griffe: **load events** and **analysis events**. Load events are scoped to the Griffe loader (triggered once a package is fully loaded). Analysis events are scoped to the visitor and inspector agents (triggered during static and dynamic analysis).
191+
There are two kinds of events in Griffe: [**load events**](#load-events) and [**analysis events**](#analysis-events). Load events are scoped to the Griffe loader (triggered once a package is fully loaded). Analysis events are scoped to the visitor and inspector agents (triggered during static and dynamic analysis).
192+
193+
**Hooks** are methods that are called when a particular event is triggered. To target a specific event, the hook must be named after it. See [Extensions and hooks](#extensions-and-hooks).
192194

193195
#### Load events
194196

@@ -234,26 +236,23 @@ There are also specific **analysis events** for each object kind:
234236
- [`on_type_alias_instance`][griffe.Extension.on_type_alias_instance]
235237
- [`on_alias_instance`][griffe.Extension.on_alias_instance]
236238

237-
---
238-
239-
**Hooks** are methods that are called when a particular event is triggered. To target a specific event, the hook must be named after it.
239+
#### Extensions and hooks
240240

241241
**Extensions** are classes that inherit from [Griffe's Extension base class][griffe.Extension] and define some hooks as methods:
242242

243243
```python
244-
import ast
245244
import griffe
246245
247246
248247
class MyExtension(griffe.Extension):
249-
def on_instance(
248+
def on_object(
250249
self,
251-
node: ast.AST | griffe.ObjectNode,
250+
*,
252251
obj: griffe.Object,
253-
agent: griffe.Visitor | griffe.Inspector,
252+
loader: griffe.GriffeLoader,
254253
**kwargs,
255254
) -> None:
256-
"""Do something with `node` and/or `obj`."""
255+
"""Do something with `obj`."""
257256
```
258257

259258
Hooks are always defined as methods of a class inheriting from [Extension][griffe.Extension], never as standalone functions. IDEs should autocomplete the signature when you start typing `def` followed by a hook name.
@@ -271,79 +270,42 @@ class MyExtension(Extension):
271270
self.state_thingy = "initial stuff"
272271
self.list_of_things = []
273272

274-
def on_instance(
273+
def on_object(
275274
self,
276-
node: ast.AST | griffe.ObjectNode,
275+
*,
277276
obj: griffe.Object,
278-
agent: griffe.Visitor | griffe.Inspector,
277+
loader: griffe.GriffeLoader,
279278
**kwargs,
280279
) -> None:
281-
"""Do something with `node` and/or `obj`."""
280+
"""Do something with `obj`."""
282281
```
283282

284283
### Static/dynamic support
285284

286-
Extensions can support both static and dynamic analysis of modules. If a module is scanned statically, your extension hooks will receive AST nodes (from the [ast][] module of the standard library). If the module is scanned dynamically, your extension hooks will receive [object nodes][griffe.ObjectNode]. Similarly, your hooks will receive a reference to the analysis agent that calls them, either a [Visitor][griffe.Visitor] or an [Inspector][griffe.Inspector].
285+
Extensions can support both static and dynamic analysis of modules.
286+
287+
Objects have an `analysis` attribute whose value will be `"static"` if they were loaded using static analysis, or `"dynamic"` if they were loaded using dynamic analysis. If the value is `None`, it means the object was created manually (for example by another extension).
287288

288-
To support static analysis, dynamic analysis, or both, you can therefore check the type of the received node or agent:
289+
To support static analysis, dynamic analysis, or both in your load events, you can therefore check the value of the `analysis` attribute:
289290

290291
```python
291-
import ast
292292
import griffe
293293

294294

295295
class MyExtension(griffe.Extension):
296-
def on_instance(
297-
self,
298-
node: ast.AST | griffe.ObjectNode,
299-
obj: griffe.Object,
300-
agent: griffe.Visitor | griffe.Inspector,
301-
**kwargs,
302-
) -> None:
303-
"""Do something with `node` and/or `obj`."""
304-
if isinstance(node, ast.AST):
296+
def on_object(self, *, obj: griffe.Object, **kwargs) -> None:
297+
"""Do something with `obj`."""
298+
if obj.analysis == "static":
305299
... # Apply logic for static analysis.
306-
else:
300+
elif obj.analysis == "dynamic":
307301
... # Apply logic for dynamic analysis.
308-
```
309-
310-
```python
311-
import ast
312-
import griffe
313-
314-
315-
class MyExtension(Extension):
316-
def on_instance(
317-
self,
318-
node: ast.AST | griffe.ObjectNode,
319-
obj: griffe.Object,
320-
agent: griffe.Visitor | griffe.Inspector,
321-
**kwargs,
322-
) -> None:
323-
"""Do something with `node` and/or `obj`."""
324-
if isinstance(agent, griffe.Visitor):
325-
... # Apply logic for static analysis.
326302
else:
327-
... # Apply logic for dynamic analysis.
328-
```
329-
330-
The preferred method is to check the type of the received node rather than the agent.
331-
332-
Since hooks also receive instantiated modules, classes, functions, attributes and type aliases, most of the time you will not need to use the `node` argument other than for checking its type and deciding what to do based on the result. And since we always add `**kwargs` to the hooks' signatures, you can drop any parameter you don't use from the signature:
333-
334-
```python
335-
import griffe
336-
337-
338-
class MyExtension(Extension):
339-
def on_instance(self, obj: griffe.Object, **kwargs) -> None:
340-
"""Do something with `obj`."""
341-
...
303+
... # Apply logic for manually built objects.
342304
```
343305

344306
### Visiting trees
345307

346-
Extensions provide basic functionality to help you visit trees:
308+
Extensions provide basic functionality to help you visit trees during analysis of the code:
347309

348310
- [`visit`][griffe.Extension.visit]: call `self.visit(node)` to start visiting an abstract syntax tree.
349311
- [`generic_visit`][griffe.Extension.generic_visit]: call `self.generic_visit(node)` to visit each subnode of a given node.
@@ -395,7 +357,16 @@ import griffe
395357

396358

397359
class MyExtension(griffe.Extension):
398-
def on_node(self, node: ast.AST | griffe.ObjectNode, agent: griffe.Visitor | griffe.Inspector, **kwargs) -> None:
360+
# Example from within a load event:
361+
def on_package(self, *, pkg: griffe.Module, loader: griffe.GriffeLoader, **kwargs) -> None:
362+
# New object created for whatever reason.
363+
function = griffe.Function(...)
364+
365+
# Trigger other extensions.
366+
loader.extensions.call("on_function", func=function, loader=loader)
367+
368+
# Example from within an analysis event:
369+
def on_node(self, *, node: ast.AST | griffe.ObjectNode, agent: griffe.Visitor | griffe.Inspector, **kwargs) -> None:
399370
# New object created for whatever reason.
400371
function = griffe.Function(...)
401372

@@ -414,7 +385,7 @@ self_namespace = "my_extension"
414385

415386

416387
class MyExtension(griffe.Extension):
417-
def on_instance(self, obj: griffe.Object, **kwargs) -> None:
388+
def on_object(self, obj: griffe.Object, **kwargs) -> None:
418389
obj.extra[self_namespace]["some_key"] = "some_value"
419390
```
420391

@@ -428,8 +399,8 @@ mkdocstrings_namespace = "mkdocstrings"
428399

429400

430401
class MyExtension(griffe.Extension):
431-
def on_class_instance(self, cls: griffe.Class, **kwargs) -> None:
432-
obj.extra[mkdocstrings_namespace]["template"] = "my_custom_template"
402+
def on_class(self, cls: griffe.Class, **kwargs) -> None:
403+
cls.extra[mkdocstrings_namespace]["template"] = "my_custom_template"
433404
```
434405

435406
[Read more about mkdocstrings handler extensions.](https://mkdocstrings.github.io/usage/handlers/#handler-extensions)
@@ -448,7 +419,7 @@ class MyExtension(griffe.Extension):
448419
self.option1 = option1
449420
self.option2 = option2
450421

451-
def on_attribute_instance(self, attr: griffe.Attribute, **kwargs) -> None:
422+
def on_attribute(self, attr: griffe.Attribute, **kwargs) -> None:
452423
if self.option2:
453424
... # Do something.
454425
```
@@ -464,8 +435,8 @@ logger = griffe.get_logger(__name__)
464435

465436

466437
class MyExtension(griffe.Extension):
467-
def on_module_members(self, mod: griffe.Module, **kwargs) -> None:
468-
logger.info("Doing some work on module %s and its members", mod.path)
438+
def on_module(self, mod: griffe.Module, **kwargs) -> None:
439+
logger.info("Doing some work on module %s", mod.path)
469440
```
470441

471442
### Full example
@@ -495,14 +466,13 @@ class DynamicDocstrings(griffe.Extension):
495466
def __init__(self, object_paths: list[str] | None = None) -> None:
496467
self.object_paths = object_paths
497468

498-
def on_instance(
469+
def on_object(
499470
self,
500-
node: ast.AST | griffe.ObjectNode,
501471
obj: griffe.Object,
502-
agent: griffe.Visitor | griffe.Inspector,
472+
loader: griffe.GriffeLoader,
503473
**kwargs,
504474
) -> None:
505-
if isinstance(node, griffe.ObjectNode):
475+
if obj.analysis == "dynamic":
506476
return # Skip runtime objects, their docstrings are already right.
507477

508478
if self.object_paths and obj.path not in self.object_paths:
@@ -527,8 +497,8 @@ class DynamicDocstrings(griffe.Extension):
527497
obj.docstring = griffe.Docstring(
528498
docstring,
529499
parent=obj,
530-
docstring_parser=agent.docstring_parser,
531-
docstring_options=agent.docstring_options,
500+
docstring_parser=loader.docstring_parser,
501+
docstring_options=loader.docstring_options,
532502
)
533503
```
534504

docs/schema.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,21 @@
6161
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.deprecated",
6262
"type": [
6363
"null",
64-
"boolean"
64+
"boolean",
65+
"string"
66+
]
67+
},
68+
"analysis": {
69+
"title": "The type of analysis used to load this alias.",
70+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.analysis",
71+
"type": [
72+
"null",
73+
"string"
74+
],
75+
"enum": [
76+
null,
77+
"static",
78+
"dynamic"
6579
]
6680
},
6781
"is_public": {
@@ -204,6 +218,19 @@
204218
"boolean"
205219
]
206220
},
221+
"analysis": {
222+
"title": "The type of analysis used to load this object.",
223+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Object.analysis",
224+
"type": [
225+
"null",
226+
"string"
227+
],
228+
"enum": [
229+
null,
230+
"static",
231+
"dynamic"
232+
]
233+
},
207234
"labels": {
208235
"title": "The labels of the object.",
209236
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.labels",

src/griffe/_internal/agents/inspector.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def generic_inspect(self, node: ObjectNode) -> None:
291291
self.current.set_member(child.name, inspector.current.module)
292292
# Otherwise, alias the object.
293293
else:
294-
alias = Alias(child.name, target_path)
294+
alias = Alias(child.name, target_path, analysis="dynamic")
295295
self.current.set_member(child.name, alias)
296296
self.extensions.call("on_alias_instance", alias=alias, node=node, agent=self)
297297
else:
@@ -312,6 +312,7 @@ def inspect_module(self, node: ObjectNode) -> None:
312312
docstring=self._get_docstring(node),
313313
lines_collection=self.lines_collection,
314314
modules_collection=self.modules_collection,
315+
analysis="dynamic",
315316
)
316317
self.extensions.call("on_instance", node=node, obj=module, agent=self)
317318
self.extensions.call("on_module_instance", node=node, mod=module, agent=self)
@@ -342,6 +343,7 @@ def inspect_class(self, node: ObjectNode) -> None:
342343
type_parameters=TypeParameters(*_convert_type_parameters(node.obj, parent=self.current, member=node.name)),
343344
lineno=lineno,
344345
endlineno=endlineno,
346+
analysis="dynamic",
345347
)
346348
self.current.set_member(node.name, class_)
347349
self.current = class_
@@ -483,6 +485,7 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
483485
docstring=self._get_docstring(node),
484486
lineno=lineno,
485487
endlineno=endlineno,
488+
analysis="dynamic",
486489
)
487490
else:
488491
obj = Function(
@@ -495,6 +498,7 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
495498
docstring=self._get_docstring(node),
496499
lineno=lineno,
497500
endlineno=endlineno,
501+
analysis="dynamic",
498502
)
499503
obj.labels |= labels
500504
self.current.set_member(node.name, obj)
@@ -523,6 +527,7 @@ def inspect_type_alias(self, node: ObjectNode) -> None:
523527
type_parameters=TypeParameters(*_convert_type_parameters(node.obj, parent=self.current, member=node.name)),
524528
docstring=self._get_docstring(node),
525529
parent=self.current,
530+
analysis="dynamic",
526531
)
527532
self.current.set_member(node.name, type_alias)
528533
self.extensions.call("on_instance", node=node, obj=type_alias, agent=self)
@@ -574,6 +579,7 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Expr | None = Non
574579
value=value,
575580
annotation=annotation,
576581
docstring=docstring,
582+
analysis="dynamic",
577583
)
578584
attribute.labels |= labels
579585
parent.set_member(node.name, attribute)

src/griffe/_internal/agents/visitor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ def visit_module(self, node: ast.Module) -> None:
288288
docstring=self._get_docstring(node),
289289
lines_collection=self.lines_collection,
290290
modules_collection=self.modules_collection,
291+
analysis="static",
291292
)
292293
self.extensions.call("on_instance", node=node, obj=module, agent=self)
293294
self.extensions.call("on_module_instance", node=node, mod=module, agent=self)
@@ -331,6 +332,7 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
331332
type_parameters=TypeParameters(*self._get_type_parameters(node, scope=node.name)),
332333
bases=bases, # type: ignore[arg-type]
333334
runtime=not self.type_guarded,
335+
analysis="static",
334336
)
335337
class_.labels |= self.decorators_to_labels(decorators)
336338

@@ -427,6 +429,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
427429
endlineno=node.end_lineno,
428430
docstring=self._get_docstring(node),
429431
runtime=not self.type_guarded,
432+
analysis="static",
430433
)
431434
attribute.labels |= labels
432435
self.current.set_member(node.name, attribute)
@@ -460,6 +463,7 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
460463
docstring=self._get_docstring(node),
461464
runtime=not self.type_guarded,
462465
parent=self.current,
466+
analysis="static",
463467
)
464468

465469
property_function = self.get_base_property(decorators, function)
@@ -538,6 +542,7 @@ def visit_typealias(self, node: ast.TypeAlias) -> None:
538542
type_parameters=TypeParameters(*self._get_type_parameters(node, scope=name)),
539543
docstring=docstring,
540544
parent=self.current,
545+
analysis="static",
541546
)
542547
self.current.set_member(name, type_alias)
543548
self.extensions.call("on_instance", node=node, obj=type_alias, agent=self)
@@ -559,6 +564,7 @@ def visit_import(self, node: ast.Import) -> None:
559564
lineno=node.lineno,
560565
endlineno=node.end_lineno,
561566
runtime=not self.type_guarded,
567+
analysis="static",
562568
)
563569
self.current.set_member(alias_name, alias)
564570
self.extensions.call("on_alias_instance", alias=alias, node=node, agent=self)
@@ -594,6 +600,7 @@ def visit_importfrom(self, node: ast.ImportFrom) -> None:
594600
lineno=node.lineno,
595601
endlineno=node.end_lineno,
596602
runtime=not self.type_guarded,
603+
analysis="static",
597604
)
598605
self.current.set_member(alias_name, alias)
599606
self.extensions.call("on_alias_instance", alias=alias, node=node, agent=self)
@@ -691,6 +698,7 @@ def handle_attribute(
691698
endlineno=node.end_lineno,
692699
docstring=docstring,
693700
runtime=not self.type_guarded,
701+
analysis="static",
694702
)
695703
attribute.labels |= labels
696704
parent.set_member(name, attribute)

0 commit comments

Comments
 (0)