Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug: Crash when using non-literal values as Pydantic field attributes #16

Closed
nardi opened this issue Dec 2, 2024 · 1 comment
Closed
Assignees
Labels
bug Something isn't working

Comments

@nardi
Copy link

nardi commented Dec 2, 2024

Description of the bug

If using a non-literal value as the description argument to a Pydantic Field, griffe will crash when trying to parse it using ast.literal_eval.

To Reproduce

import griffe

static_model = """
import pydantic

class TestModel(pydantic.BaseModel):
    abc: str = pydantic.Field(description="xyz")
"""

with griffe.temporary_visited_package(
    "package",
    modules={"__init__.py": static_model},
    extensions=griffe.load_extensions("griffe_pydantic"),
) as package:
    assert package["TestModel"]["abc"].docstring.value == "xyz"

dynamic_model = """
import pydantic

desc = "xyz"

class TestModel(pydantic.BaseModel):
    abc: str = pydantic.Field(description=desc)
"""

with griffe.temporary_visited_package(
    "package",
    modules={"__init__.py": dynamic_model},
    extensions=griffe.load_extensions("griffe_pydantic"),
) as package:
    assert package["TestModel"]["abc"].docstring.value == "xyz"

Full traceback

Full traceback
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[23], line 26
     15     assert package["TestModel"]["abc"].docstring.value == "xyz"
     17 dynamic_model = """
     18 import pydantic
     19 
   (...)
     23     abc: str = pydantic.Field(description=desc)
     24 """
---> 26 with griffe.temporary_visited_package(
     27     "package",
     28     modules={"__init__.py": dynamic_model},
     29     extensions=griffe.load_extensions("griffe_pydantic"),
     30 ) as package:
     31     assert package["TestModel"]["abc"].docstring.value == "xyz"

File [...]\Lib\contextlib.py:137, in _GeneratorContextManager.__enter__(self)
    135 del self.args, self.kwds, self.func
    136 try:
--> 137     return next(self.gen)
    138 except StopIteration:
    139     raise RuntimeError("generator didn't yield") from None

File [...]\Lib\site-packages\_griffe\tests.py:167, in temporary_visited_package(package, modules, init, inits, extensions, docstring_parser, docstring_options, lines_collection, modules_collection, allow_inspection, store_source, resolve_aliases, resolve_external, resolve_implicit)
    137 """Create and visit a temporary package.
    138 
    139 Parameters:
   (...)
    164     A module.
    165 """
    166 with temporary_pypackage(package, modules, init=init, inits=inits) as tmp_package:
--> 167     yield load(  # type: ignore[misc]
    168         tmp_package.name,
    169         search_paths=[tmp_package.tmpdir],
    170         extensions=extensions,
    171         docstring_parser=docstring_parser,
    172         docstring_options=docstring_options,
    173         lines_collection=lines_collection,
    174         modules_collection=modules_collection,
    175         allow_inspection=allow_inspection,
    176         store_source=store_source,
    177         resolve_aliases=resolve_aliases,
    178         resolve_external=resolve_external,
    179         resolve_implicit=resolve_implicit,
    180         force_inspection=False,
    181     )

File [...]\Lib\site-packages\_griffe\loader.py:811, in load(objspec, submodules, try_relative_path, extensions, search_paths, docstring_parser, docstring_options, lines_collection, modules_collection, allow_inspection, force_inspection, store_source, find_stubs_package, resolve_aliases, resolve_external, resolve_implicit)
    737 """Load and return a Griffe object.
    738 
    739 In Griffe's context, loading means:
   (...)
    798     A Griffe object.
    799 """
    800 loader = GriffeLoader(
    801     extensions=extensions,
    802     search_paths=search_paths,
   (...)
    809     store_source=store_source,
    810 )
--> 811 result = loader.load(
    812     objspec,
    813     submodules=submodules,
    814     try_relative_path=try_relative_path,
    815     find_stubs_package=find_stubs_package,
    816 )
    817 if resolve_aliases:
    818     loader.resolve_aliases(implicit=resolve_implicit, external=resolve_external)

File [...]\Lib\site-packages\_griffe\loader.py:186, in GriffeLoader.load(self, objspec, submodules, try_relative_path, find_stubs_package)
    183     logger.exception("Could not load package %s", package)
    184     raise
--> 186 return self._post_load(top_module, obj_path)

File [...]\Lib\site-packages\_griffe\loader.py:212, in GriffeLoader._post_load(self, module, obj_path)
    210 # Package is loaded, we now retrieve the initially requested object and return it.
    211 obj = self.modules_collection.get_member(obj_path)
--> 212 self.extensions.call("on_package_loaded", pkg=module, loader=self)
    213 return obj

File [...]\Lib\site-packages\_griffe\extensions\base.py:313, in Extensions.call(self, event, **kwargs)
    306 """Call the extension hook for the given event.
    307 
    308 Parameters:
    309     event: The triggered event.
    310     **kwargs: Arguments passed to the hook.
    311 """
    312 for extension in self._extensions:
--> 313     getattr(extension, event)(**kwargs)

File [...]\Lib\site-packages\griffe_pydantic\extension.py:42, in PydanticExtension.on_package_loaded(self, pkg, **kwargs)
     40 def on_package_loaded(self, *, pkg: Module, **kwargs: Any) -> None:  # noqa: ARG002
     41     """Detect models once the whole package is loaded."""
---> 42     static.process_module(pkg, processed=self.processed, schema=self.schema)

File [...]\Lib\site-packages\griffe_pydantic\static.py:167, in process_module(mod, processed, schema)
    164 for cls in mod.classes.values():
    165     # Don't process aliases, real classes will be processed at some point anyway.
    166     if not cls.is_alias:
--> 167         process_class(cls, processed=processed, schema=schema)
    169 for submodule in mod.modules.values():
    170     process_module(submodule, processed=processed, schema=schema)

File [...]\Lib\site-packages\griffe_pydantic\static.py:146, in process_class(cls, processed, schema)
    144 for member in cls.all_members.values():
    145     if isinstance(member, Attribute):
--> 146         process_attribute(member, cls, processed=processed)
    147     elif isinstance(member, Function):
    148         process_function(member, cls, processed=processed)

File [...]\Lib\site-packages\griffe_pydantic\static.py:100, in process_attribute(attr, cls, processed)
     98 # Populate docstring from the field's `description` argument.
     99 if not attr.docstring and (docstring := kwargs.get("description", None)):
--> 100     attr.docstring = Docstring(ast.literal_eval(docstring), parent=attr)

File [...]\Lib\ast.py:110, in literal_eval(node_or_string)
    108                 return left - right
    109     return _convert_signed_num(node)
--> 110 return _convert(node_or_string)

File [...]\Lib\ast.py:109, in literal_eval.<locals>._convert(node)
    107         else:
    108             return left - right
--> 109 return _convert_signed_num(node)

File [...]\Lib\ast.py:83, in literal_eval.<locals>._convert_signed_num(node)
     81     else:
     82         return - operand
---> 83 return _convert_num(node)

File [...]\Lib\ast.py:74, in literal_eval.<locals>._convert_num(node)
     72 def _convert_num(node):
     73     if not isinstance(node, Constant) or type(node.value) not in (int, float, complex):
---> 74         _raise_malformed_node(node)
     75     return node.value

File [...]\Lib\ast.py:71, in literal_eval.<locals>._raise_malformed_node(node)
     69 if lno := getattr(node, 'lineno', None):
     70     msg += f' on line {lno}'
---> 71 raise ValueError(msg + f': {node!r}')

ValueError: malformed node or string: ExprName(name='desc', parent=Class('TestModel', 6, 7))

Expected behavior

Either an error message that explains the dynamic description is not supported before halting, a warning and an empty docstring, or some kind of dynamic resolution of the description attribute.

Environment information

  • System: Windows-10-10.0.26100-SP0
  • Python: cpython 3.11.6 ([...]\python.exe)
  • Environment variables:
  • Installed packages:
    • griffe-pydantic v1.1.0

Additional context

Sibling to feature request here.

@pawamoy
Copy link
Member

pawamoy commented Dec 2, 2024

Thanks @nardi!

We won't support dynamic values statically (at least for now), but the extension should definitely not crash anyway 👍

@pawamoy pawamoy added bug Something isn't working and removed unconfirmed This bug was not reproduced yet labels Dec 2, 2024
@pawamoy pawamoy closed this as completed Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants