From e71e54169c11928954d23caea5a0b6567f421631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 17 Oct 2024 19:09:04 +0200 Subject: [PATCH] docs: Add how-to selectively inspect objects --- docs/guide/users.md | 24 +++ .../guide/users/how-to/selectively-inspect.md | 198 ++++++++++++++++++ docs/guide/users/loading.md | 3 +- mkdocs.yml | 1 + 4 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 docs/guide/users/how-to/selectively-inspect.md diff --git a/docs/guide/users.md b/docs/guide/users.md index 167acc9d..106dfeb0 100644 --- a/docs/guide/users.md +++ b/docs/guide/users.md @@ -81,3 +81,27 @@ These topics explore the user side: how to write code to better integrate with G [:octicons-arrow-right-24: See our docstring recommendations](users/recommendations/docstrings.md) + +## How-to + +These how-tos will show you how to achieve specific things with Griffe. + +
+ +- :octicons-ai-model-24:{ .lg .middle } **Parse docstrings** + + --- + + Griffe can be used as a docstring-parsing library. + + [:octicons-arrow-right-24: See how to parse docstrings](users/how-to/parse-docstrings.md) + +- :material-select:{ .lg .middle } **Selectively inspect objects** + + --- + + Sometimes static analysis is not enough, so you might want to use dynamic analysis (inspection) on certain objects. + + [:octicons-arrow-right-24: See how selectively inspect objects](users/how-to/selectively-inspect.md) + +
diff --git a/docs/guide/users/how-to/selectively-inspect.md b/docs/guide/users/how-to/selectively-inspect.md new file mode 100644 index 00000000..c3c2272a --- /dev/null +++ b/docs/guide/users/how-to/selectively-inspect.md @@ -0,0 +1,198 @@ +# Inspecting specific objects + +Griffe by default parses and visits your code (static analysis) instead of importing it and inspecting objects in memory (dynamic analysis). There are various reasons why static analysis is generally a better approach, but sometimes it is insufficient to handle particularly dynamic objects. When this happpens and Griffe cannot handle specific objects, you have a few solutions: + +1. enable dynamic analysis for the whole package +2. write a Griffe extension that dynamically handles just the problematic objects +3. write a Griffe extension that statically handles the objects + +This document will help you achieve point 2. + + + +Enabling dynamic analysis for whole packages is [not recommended][forcing-dynamic-analysis-not-recommended], but it can be useful to do it once and check the results, to see if our dynamic analysis agent is able to handle your code natively. Whether it is or not is not very important, you will be able to move onto creating an extension that will selectively inspect the relevant objects in any case. It could just be a bit more difficult in the latter case, and if you have trouble writing the extension we invite you to create a [Q&A discussion](https://github.com/mkdocstrings/griffe/discussions/categories/q-a) to get guidance. + +--- + +Start by creating an extensions module (a simple Python file) somewhere in your repository, if you don't already have one. Within it, create an extension class: + +```python +import griffe + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" +``` + +Make it accept configuration options by declaring an `__init__` method: + +```python hl_lines="7-8" +import griffe + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" + + def __init__(self, objects: list[str]) -> None: + self.objects = objects +``` + +Here we choose to store a list of strings, where each string is an object path, like `module.Class.method`. Feel free to store different values to help you filter objects according to your needs. For example, maybe you want to inspect all functions with a given label, in that case you could accept a single string which is the label name. Or you may want to inspect all functions decorated with a specific decorator, etc. + +With this `__init__` method, users (or simply yourself) will be able to configure the extension by passing a list of object paths. You could also hard-code everything in the extension if you don't want or need to configure it. + +Now that our extension accepts options, we implement its core functionality. We assume that the static analysis agent is able to see the objects we are interested in, and will actually create instances that represent them (Griffe objects). Therefore we hook onto the `on_instance` event, which runs each time a Griffe object is created. + +```python hl_lines="10-11" +import griffe + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" + + def __init__(self, objects: list[str]) -> None: + self.objects = objects + + def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: + ... +``` + +Check out the [available hooks][griffe.Extension] to see if there more appropriate hooks for your needs. + +Lets now use our configuration option to decide whether to do something or skip: + +```python hl_lines="11-12" +import griffe + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" + + def __init__(self, objects: list[str]) -> None: + self.objects = objects + + def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: + if obj.path not in self.objects: + return +``` + +Now we know that only the objects we're interested in will be handled, so lets handle them. + +```python hl_lines="3 16-20" +import griffe + +logger = griffe.get_logger("griffe_inspect_specific_objects") # (1)! + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" + + def __init__(self, objects: list[str]) -> None: + self.objects = objects + + def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: + if obj.path not in self.objects: + return + + try: + runtime_obj = griffe.dynamic_import(obj.path) + except ImportError as error: + logger.warning(f"Could not import {obj.path}: {error}") # (2)! + return +``` + +1. We integrate with Griffe's logging (which also ensures integration with MkDocs' logging) by creating a logger. The name should look like a package name, with underscores. +2. We decide to log the exception as a warning (causing MkDocs builds to fail in `--strict` mode), but you could also log an error, or a debug message. + +Now that we have a reference to our runtime object, we can use it to alter the Griffe object. + +For example, we could use the runtime object's `__doc__` attribute, which could have been declared dynamically, to fix the Griffe object docstring: + +```python hl_lines="22-25" +import griffe + +logger = griffe.get_logger("griffe_inspect_specific_objects") + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" + + def __init__(self, objects: list[str]) -> None: + self.objects = objects + + def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: + if obj.path not in self.objects: + return + + try: + runtime_obj = griffe.dynamic_import(obj.path) + except ImportError as error: + logger.warning(f"Could not import {obj.path}: {error}") + return + + if obj.docstring: + obj.docstring.value = runtime_obj.__doc__ + else: + obj.docstring = griffe.Docstring(runtime_obj.__doc__) +``` + +Or we could alter the Griffe object parameters in case of functions, which could have been modified by a signature-changing decorator: + +```python hl_lines="1 23-27" +import inspect +import griffe + +logger = griffe.get_logger("griffe_inspect_specific_objects") + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" + + def __init__(self, objects: list[str]) -> None: + self.objects = objects + + def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: + if obj.path not in self.objects: + return + + try: + runtime_obj = griffe.dynamic_import(obj.path) + except ImportError as error: + logger.warning(f"Could not import {obj.path}: {error}") + return + + # Update default values modified by decorator. + signature = inspect.signature(runtime_obj) + for param in signature.parameters: + if param.name in obj.parameters: + obj.parameters[param.name].default = repr(param.default) +``` + +We could also entirely replace the Griffe object obtained from static analysis by the same one obtained from dynamic analysis: + + +```python hl_lines="14-25" +import griffe + + +class InspectSpecificObjects(griffe.Extension): + """An extension to inspect just a few specific objects.""" + + def __init__(self, objects: list[str]) -> None: + self.objects = objects + + def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: + if obj.path not in self.objects: + return + + inspected_module = griffe.inspect(obj.module.path, filepath=obj.filepath) + obj.parent.set_member(obj.name, inspected_module[obj.name]) # (1)! +``` + +1. This assumes the object we're interested in is declared at the module level. diff --git a/docs/guide/users/loading.md b/docs/guide/users/loading.md index d82d965c..8e5e3397 100644 --- a/docs/guide/users/loading.md +++ b/docs/guide/users/loading.md @@ -103,6 +103,7 @@ import griffe my_package = griffe.load("my_package", force_inspection=True) ``` +[](){#forcing-dynamic-analysis-not-recommended} Forcing inspection can be useful when your code is highly dynamic, and static analysis has trouble keeping up. **However we don't recommend forcing inspection**, for a few reasons: @@ -112,7 +113,7 @@ Forcing inspection can be useful when your code is highly dynamic, and static an - dynamic analysis will potentially consume more resources (CPU, RAM) since it executes code - dynamic analysis will sometimes give you less precise or incomplete information - it's possible to write Griffe extensions that will *statically handle* the highly dynamic parts of your code (like custom decorators) that Griffe doesn't understand by default -- if really needed, it's possible to handle only a subset of objects with dynamic analysis, while the rest is loaded with static analysis, again thanks to Griffe extensions +- if really needed, it's possible to [handle only a subset of objects with dynamic analysis](how-to/selectively-inspect.md), while the rest is loaded with static analysis, again thanks to Griffe extensions The [Extending](extending.md) topic will explain how to write and use extensions for Griffe. diff --git a/mkdocs.yml b/mkdocs.yml index c69a03c4..741be123 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ nav: - Docstrings: guide/users/recommendations/docstrings.md - How-to: - Parse docstrings: guide/users/how-to/parse-docstrings.md + - Selectively inspect objects: guide/users/how-to/selectively-inspect.md - Contributor guide: - guide/contributors.md - Environment setup: guide/contributors/setup.md