griffe icon indicating copy to clipboard operation
griffe copied to clipboard

feature: I'd like to be able to use `Docstring` without requiring `parent` but get no warning

Open samuelcolvin opened this issue 1 year ago • 7 comments

Thanks again for griffe - it's doing exactly what I need.

I have the following code:

import inspect
from typing import Literal, Callable, Any

from griffe.dataclasses import Docstring
from griffe.enumerations import DocstringSectionKind

def parameter_descriptions(
    func: Callable[..., Any], *, style: Literal['google', 'numpy', 'sphinx'] = 'google'
) -> dict[str, str]:
    docstring = Docstring(func.__doc__, lineno=1, parser=style)
    try:
        parameters = next(p for p in docstring.parse() if p.kind == DocstringSectionKind.parameters)
    except StopIteration:
        return {}
    else:
        return {p.name: p.description for p in parameters.value}

It works perfectly, but emits a warning (or similar, depending on the style)

<module>:1: No matching parameter for 'a'
<module>:1: No matching parameter for 'b'

To avoid that warning I can pass parent=inspect.signature(func) to Docstring, but that's unfortunate since inspect.signature doesn't actually return griffe's Object type - future changes to griffe could break my code.

Please could you provide either:

  • a way to silence those warnings through options
  • a way to safely convert a inspect.Signature to a griffe Object

?

samuelcolvin avatar Jun 14 '24 05:06 samuelcolvin

Hi @samuelcolvin, thanks for the kind words and the feature request :slightly_smiling_face:

Have you tried the warn_unknown_params=False option? There's a quick mention here: https://mkdocstrings.github.io/griffe/docstrings/#google-options. It exists for the three parsers, google, numpy and sphinx.

a way to silence those warnings through options

I have a draft PR that will bring configuration of all the log messages throughout the code base. It still needs some work, and I think it will end up in post 1.0 (which I'm working on too), and Insiders :thinking: So not viable for this issue here.

In any case we can rework warn_unknown_params first if it's not enough.

a way to safely convert a inspect.Signature to a griffe Object

Good idea for a utility function, yes. We can do that too :slightly_smiling_face:

future changes to griffe could break my code.

Even though Griffe is still in v0, we do use deprecation periods for every breaking change, and they are generally documented in the changelog. As mentioned above, 1.0 is coming soon though, and will remove all the legacy code, so you might want to add an upper bound to Griffe :smile:

pawamoy avatar Jun 14 '24 06:06 pawamoy

Ye I tried warn_unknown_params, it doesn't work in this case.

samuelcolvin avatar Jun 14 '24 07:06 samuelcolvin

Thanks. OK so there's an additional place in the Sphinx parser that emits such warnings without checking the value of warn_unknown_params. We should fix that.

But even then, the parsers will still emit warnings like "No type or annotation for parameter 'a'" (when the parameter types are not added to the docstring). So we would need to add an option to ignore these ones too.

Since there's work in progress for logs configuration, which will supersede such parser options, I'll first go with the signature-to-Griffe-function utility.

pawamoy avatar Jun 14 '24 07:06 pawamoy

I'm not sure where your func comes from, but an alternative solution is to actually inspect its whole parent module. It's a bit heavy though, you'd have to cache the griffe_module below to avoid running introspection on the same module over and over.

import inspect
from pathlib import Path
from typing import Literal, Callable, Any

from griffe.agents.inspector import Inspector, ObjectNode
from griffe.enumerations import DocstringSectionKind
from griffe.extensions import load_extensions

def parameter_descriptions(
    func: Callable[..., Any], *, style: Literal['google', 'numpy', 'sphinx'] = 'google'
) -> dict[str, str]:
    module = inspect.getmodule(func)
    module_name = module.__name__.rsplit(".", 1)[-1]
    node = ObjectNode(module, module_name)
    inspector = Inspector(module_name, filepath=Path(module.__file__), extensions=load_extensions())
    inspector.inspect_module(node)
    griffe_module = inspector.current
    docstring = griffe_module[func.__qualname__].docstring
    try:
        parameters = next(p for p in docstring.parse(style) if p.kind == DocstringSectionKind.parameters)
    except StopIteration:
        return {}
    else:
        return {p.name: p.description for p in parameters.value}

pawamoy avatar Jun 14 '24 08:06 pawamoy

I have implemented this utility:

def convert_function(func: Callable, parent: Module | Class | None = None) -> Function:
    """Convert a runtime function to a Griffe function.

    Parameters:
        func: The function to convert.
        parent: The parent of the function.

    Returns:
        The Griffe function.
    """
    parent = parent or Module("__griffe_helpers__")
    sig = signature(func)
    parameters = [convert_parameter(parameter, parent=parent) for parameter in sig.parameters.values()]
    return_annotation = sig.return_annotation
    returns = None if return_annotation is empty else convert_object_to_annotation(return_annotation, parent=parent)
    return Function(func.__name__, parameters=Parameters(*parameters), returns=returns)

As you can see it has to fake a parent module. That's why I thought of the alternative solution mentioned above. I'm still pondering what's best in terms of API :thinking: Maybe it's just easier to add an option to the docstring parsers to ignore warnings in the end...

pawamoy avatar Jun 14 '24 09:06 pawamoy

Another alternative while I'm making up my mind: temporarily disabling logging.

import logging
from contextlib import contextmanager

@contextmanager
def disable_logging():
    old_level = logging.root.getEffectiveLevel()
    logging.root.setLevel(logging.CRITICAL + 1)
    yield
    logging.root.setLevel(old_level)


...

with disable_logging():
    parameters = next(p for p in docstring.parse() if p.kind == DocstringSectionKind.parameters)

WDYT?

Do you want to disable all warnings emitted during docstring parsing or only the "unknown parameter" and "no type or annotation" ones?

pawamoy avatar Jun 14 '24 09:06 pawamoy

Oh that makes sense, I thought warning, was from the warnings module. I'll do that.

samuelcolvin avatar Jun 14 '24 20:06 samuelcolvin

In the end, the recommended way will be to disable logging when parsing the docstring, as demonstrated above, or with a future configuration system for log messages. Closing, feel free to comment further :slightly_smiling_face:

pawamoy avatar Aug 11 '24 15:08 pawamoy

Now that many projects are imitating Pydantic AI and using Griffe to parse docstrings, while disabling the logger to prevent warnings, and since the "logs configuration" PR hasn't progressed, I'd like to come back to this and add a warnings: bool = True parameter on the parse functions to make this easier 🙂

pawamoy avatar Mar 12 '25 10:03 pawamoy

Available in v1.7.0, example: https://mkdocstrings.github.io/griffe/reference/api/docstrings/parsers/#griffe.parse_google(warnings).

pawamoy avatar Mar 27 '25 15:03 pawamoy