Skip to content

Commit

Permalink
feat: Rework extension system
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pawamoy committed Jul 12, 2023
1 parent 6d9d6b0 commit dea4c83
Show file tree
Hide file tree
Showing 10 changed files with 1,004 additions and 100 deletions.
617 changes: 547 additions & 70 deletions docs/extensions.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ markdown_extensions:
- pymdownx.magiclink
- pymdownx.snippets:
check_paths: true
- pymdownx.superfences
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.tabbed:
alternate_style: true
slugify: !!python/object/apply:pymdownx.slugs.slugify
Expand Down
22 changes: 21 additions & 1 deletion src/griffe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,28 @@

from __future__ import annotations

from griffe.agents.nodes import ObjectNode
from griffe.dataclasses import Attribute, Class, Docstring, Function, Module, Object
from griffe.diff import find_breaking_changes
from griffe.extensions import Extension, load_extensions
from griffe.git import load_git
from griffe.importer import dynamic_import
from griffe.loader import load
from griffe.logger import get_logger

__all__: list[str] = ["find_breaking_changes", "load", "load_git"]
__all__: list[str] = [
"Attribute",
"Class",
"Docstring",
"dynamic_import",
"Extension",
"Function",
"find_breaking_changes",
"get_logger",
"load",
"load_extensions",
"load_git",
"Module",
"Object",
"ObjectNode",
]
28 changes: 27 additions & 1 deletion src/griffe/agents/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,22 +215,31 @@ def inspect_module(self, node: ObjectNode) -> None:
Parameters:
node: The node to inspect.
"""
self.current = Module(
self.extensions.call("on_node", node=node)
self.extensions.call("on_module_node", node=node)
self.current = module = Module(
name=self.module_name,
filepath=self.filepath,
parent=self.parent,
docstring=self._get_docstring(node),
lines_collection=self.lines_collection,
modules_collection=self.modules_collection,
)
self.extensions.call("on_instance", node=node, obj=module)
self.extensions.call("on_module_instance", node=node, mod=module)
self.generic_inspect(node)
self.extensions.call("on_members", node=node, obj=module)
self.extensions.call("on_module_members", node=node, mod=module)

def inspect_class(self, node: ObjectNode) -> None:
"""Inspect a class.
Parameters:
node: The node to inspect.
"""
self.extensions.call("on_node", node=node)
self.extensions.call("on_class_node", node=node)

bases = []
for base in node.obj.__bases__:
if base is object:
Expand All @@ -244,7 +253,11 @@ def inspect_class(self, node: ObjectNode) -> None:
)
self.current.set_member(node.name, class_)
self.current = class_
self.extensions.call("on_instance", node=node, obj=class_)
self.extensions.call("on_class_instance", node=node, cls=class_)
self.generic_inspect(node)
self.extensions.call("on_members", node=node, obj=class_)
self.extensions.call("on_class_members", node=node, cls=class_)
self.current = self.current.parent # type: ignore[assignment]

def inspect_staticmethod(self, node: ObjectNode) -> None:
Expand Down Expand Up @@ -335,6 +348,9 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
node: The node to inspect.
labels: Labels to add to the data object.
"""
self.extensions.call("on_node", node=node)
self.extensions.call("on_function_node", node=node)

try:
signature = getsignature(node.obj)
except Exception: # noqa: BLE001
Expand Down Expand Up @@ -371,6 +387,11 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None:
)
obj.labels |= labels
self.current.set_member(node.name, obj)
self.extensions.call("on_instance", node=node, obj=obj)
if obj.is_attribute:
self.extensions.call("on_attribute_instance", node=node, attr=obj)
else:
self.extensions.call("on_function_instance", node=node, func=obj)

def inspect_attribute(self, node: ObjectNode) -> None:
"""Inspect an attribute.
Expand All @@ -387,6 +408,9 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Name | Expression
node: The node to inspect.
annotation: A potentiel annotation.
"""
self.extensions.call("on_node", node=node)
self.extensions.call("on_attribute_node", node=node)

# TODO: to improve
parent = self.current
labels: set[str] = set()
Expand Down Expand Up @@ -421,6 +445,8 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Name | Expression

if node.name == "__all__":
parent.exports = set(node.obj)
self.extensions.call("on_instance", node=node, obj=attribute)
self.extensions.call("on_attribute_instance", node=node, attr=attribute)


_kind_map = {
Expand Down
13 changes: 13 additions & 0 deletions src/griffe/agents/nodes/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,31 @@ class ObjectKind(enum.Enum):
"""Enumeration for the different kinds of objects."""

MODULE: str = "module"
"""Modules."""
CLASS: str = "class"
"""Classes."""
STATICMETHOD: str = "staticmethod"
"""Static methods."""
CLASSMETHOD: str = "classmethod"
"""Class methods."""
METHOD_DESCRIPTOR: str = "method_descriptor"
"""Method descriptors."""
METHOD: str = "method"
"""Methods."""
BUILTIN_METHOD: str = "builtin_method"
"""Built-in ethods."""
COROUTINE: str = "coroutine"
"""Coroutines"""
FUNCTION: str = "function"
"""Functions."""
BUILTIN_FUNCTION: str = "builtin_function"
"""Built-in functions."""
CACHED_PROPERTY: str = "cached_property"
"""Cached properties."""
PROPERTY: str = "property"
"""Properties."""
ATTRIBUTE: str = "attribute"
"""Attributes."""

def __str__(self) -> str:
return self.value
Expand Down
27 changes: 25 additions & 2 deletions src/griffe/agents/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,23 +211,31 @@ def visit_module(self, node: ast.Module) -> None:
Parameters:
node: The node to visit.
"""
module = Module(
self.extensions.call("on_node", node=node)
self.extensions.call("on_module_node", node=node)
self.current = module = Module(
name=self.module_name,
filepath=self.filepath,
parent=self.parent,
docstring=self._get_docstring(node),
lines_collection=self.lines_collection,
modules_collection=self.modules_collection,
)
self.current = module
self.extensions.call("on_instance", node=node, obj=module)
self.extensions.call("on_module_instance", node=node, mod=module)
self.generic_visit(node)
self.extensions.call("on_members", node=node, obj=module)
self.extensions.call("on_module_members", node=node, mod=module)

def visit_classdef(self, node: ast.ClassDef) -> None:
"""Visit a class definition node.
Parameters:
node: The node to visit.
"""
self.extensions.call("on_node", node=node)
self.extensions.call("on_class_node", node=node)

# handle decorators
decorators = []
if node.decorator_list:
Expand Down Expand Up @@ -261,7 +269,11 @@ def visit_classdef(self, node: ast.ClassDef) -> None:
class_.labels |= self.decorators_to_labels(decorators)
self.current.set_member(node.name, class_)
self.current = class_
self.extensions.call("on_instance", node=node, obj=class_)
self.extensions.call("on_class_instance", node=node, cls=class_)
self.generic_visit(node)
self.extensions.call("on_members", node=node, obj=class_)
self.extensions.call("on_class_members", node=node, cls=class_)
self.current = self.current.parent # type: ignore[assignment]

def decorators_to_labels(self, decorators: list[Decorator]) -> set[str]:
Expand Down Expand Up @@ -313,6 +325,9 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
node: The node to visit.
labels: Labels to add to the data object.
"""
self.extensions.call("on_node", node=node)
self.extensions.call("on_function_node", node=node)

labels = labels or set()

# handle decorators
Expand Down Expand Up @@ -348,6 +363,8 @@ def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels:
)
attribute.labels |= labels
self.current.set_member(node.name, attribute)
self.extensions.call("on_instance", node=node, obj=attribute)
self.extensions.call("on_attribute_instance", node=node, attr=attribute)
return

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

function.labels |= labels

self.extensions.call("on_instance", node=node, obj=function)
self.extensions.call("on_function_instance", node=node, func=function)
if self.current.kind is Kind.CLASS and function.name == "__init__":
self.current = function # type: ignore[assignment] # temporary assign a function
self.generic_visit(node)
Expand Down Expand Up @@ -541,6 +560,8 @@ def handle_attribute(
node: The node to visit.
annotation: A potential annotation.
"""
self.extensions.call("on_node", node=node)
self.extensions.call("on_attribute_node", node=node)
parent = self.current
labels = set()

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

def visit_assign(self, node: ast.Assign) -> None:
"""Visit an assignment node.
Expand Down
15 changes: 11 additions & 4 deletions src/griffe/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from griffe.stats import _format_stats

if TYPE_CHECKING:
from griffe.extensions import Extension, Extensions
from griffe.extensions import Extensions, ExtensionType


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


def _extensions_type(value: str) -> Sequence[str | dict[str, Any]]:
try:
return json.loads(value)
except json.JSONDecodeError:
return value.split(",")


def get_parser() -> argparse.ArgumentParser:
"""Return the CLI argument parser.
Expand Down Expand Up @@ -133,7 +140,7 @@ def add_common_options(subparser: argparse.ArgumentParser) -> None:
"-e",
"--extensions",
default={},
type=json.loads,
type=_extensions_type,
help="A list of extensions to use.",
)
loading_options.add_argument(
Expand Down Expand Up @@ -277,7 +284,7 @@ def dump(
full: bool = False,
docstring_parser: Parser | None = None,
docstring_options: dict[str, Any] | None = None,
extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None,
extensions: Sequence[str | dict[str, Any] | ExtensionType | type[ExtensionType]] | None = None,
resolve_aliases: bool = False,
resolve_implicit: bool = False,
resolve_external: bool = False,
Expand Down Expand Up @@ -356,7 +363,7 @@ def check(
against_path: str | Path | None = None,
*,
base_ref: str | None = None,
extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None,
extensions: Sequence[str | dict[str, Any] | ExtensionType | type[ExtensionType]] | None = None,
search_paths: Sequence[str | Path] | None = None,
allow_inspection: bool = True,
verbose: bool = False,
Expand Down
2 changes: 2 additions & 0 deletions src/griffe/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from griffe.extensions.base import (
Extension,
Extensions,
ExtensionType,
InspectorExtension,
VisitorExtension,
When,
Expand All @@ -13,6 +14,7 @@
__all__ = [
"Extensions",
"Extension",
"ExtensionType",
"InspectorExtension",
"VisitorExtension",
"When",
Expand Down
Loading

0 comments on commit dea4c83

Please sign in to comment.