Skip to content

Commit

Permalink
Merge pull request #170 from jerryjiahaha/main
Browse files Browse the repository at this point in the history
fix get_given_constraint_keys for pydantic 2.1
  • Loading branch information
mansenfranzen authored Aug 1, 2023
2 parents 6d40446 + afe28cb commit 001504a
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 192 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- python_version: "3.11"
pydantic_version: "20"
sphinx_version: "70"
- python_version: "3.11"
pydantic_version: "21"
sphinx_version: "70"

runs-on: ubuntu-22.04
steps:
Expand Down
41 changes: 40 additions & 1 deletion changelog.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
Changelog
=========

v2.0.0 - 2023-07-XX
v2.0.1 - 2023-08-01
-------------------

This is a bugfix release handling an exception while documenting pydantic
models using enums.

Internal
~~~~~~~~

- Simplify model field's constraint extraction removing dependency on private
methods.

Bugfix
~~~~~~

- Properly handle constraint extraction avoiding exceptions due to prior
reliance on unpredictable behavior of private attributes.
- Fix incorrect type annotation string results for non-builtin types in
``intercept_type_annotations_py_gt_39``.

Testing
~~~~~~~

- Add pydantic 2.1 to test matrix.
- Add tests for various kinds of constrained types.
- Add "no exception" tests for specific pydantic models, such as containing
enums.

Contributors
~~~~~~~~~~~~

- Thanks to `nagledb <https://github.com/nagledb>`__ for reporting a bug
related to enums
`#169 <https://github.com/mansenfranzen/autodoc_pydantic/issues/169>`__.
- Thanks to `jerryjiahaha <https://github.com/jerryjiahaha>`__ for providing
a pull request fixing the enums bug
`#170 <https://github.com/mansenfranzen/autodoc_pydantic/pull/170>`__.


v2.0.0 - 2023-07-24
-------------------

This is a major release supporting pydantic v2. In June 2023, pydantic v2 was
Expand Down
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
# -- Project information -----------------------------------------------------

project = 'autodoc_pydantic'
copyright = '2022, Franz Wöllert'
copyright = '2023, Franz Wöllert'
author = 'Franz Wöllert'

# The full version, including alpha/beta/rc tags
release = '2.0.0'
release = '2.0.1'

# -- General configuration ---------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ the appearance of standard sphinx autodoc with **autodoc_pydantic**.

.. note::

This documentation is based on ``autodoc_pydantic >= 2.0.0``. If you are
This documentation is based on ``autodoc_pydantic >= 2.0.1``. If you are
using pydantic v1 along with ``autodoc_pydantic < 2.0.0``, please find the
latest v1 documentation `here <https://autodoc-pydantic.readthedocs.io/en/main-1.x/>`_ .

Expand Down Expand Up @@ -86,7 +86,7 @@ and all `contributors <https://github.com/mansenfranzen/autodoc_pydantic/tree/re
.. |DownloadsBadge| image:: https://img.shields.io/pypi/dm/autodoc_pydantic?color=fe7d37&style=flat
.. _DownloadsBadge: https://pypistats.org/packages/autodoc-pydantic

.. |ContributersBadge| image:: https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat
.. |ContributersBadge| image:: https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat
.. _ContributersBadge: https://github.com/mansenfranzen/autodoc_pydantic/tree/refactor_inspection#acknowledgements

.. |CoverageBadge| image:: https://img.shields.io/codecov/c/gh/mansenfranzen/autodoc_pydantic?style=flat
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "autodoc_pydantic"
version = "2.0.0"
version = "2.0.1
description = "Seamlessly integrate pydantic models in your Sphinx documentation."
authors = ["mansenfranzen <franz.woellert@gmail.com>"]
packages = [{ include = "sphinxcontrib" }]
Expand Down
3 changes: 2 additions & 1 deletion sphinxcontrib/autodoc_pydantic/directives/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from sphinx.addnodes import pending_xref
from sphinx.environment import BuildEnvironment

REGEX_TYPE_ANNOT = re.compile(r"\s+:type:\s([a-zA-Z1-9]+)\[([a-zA-Z1-9]+)\]")
REGEX_TYPE_ANNOT = re.compile(
r"\s+:type:\s([a-zA-Z1-9\._\[]+\]?)\[([a-zA-Z1-9\._\[\]]+)\]")


class NullType:
Expand Down
58 changes: 6 additions & 52 deletions sphinxcontrib/autodoc_pydantic/inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,6 @@ def get_property_from_field_info(self,
field = self.get(field_name)
return getattr(field, property_name, None)

def get_constraint_items(self, field_name: str) -> Dict[str, str]:
"""Extract all possible constraints along with their default values
from a fields meta attribute.
"""

metadata = self.model.model_fields[field_name].metadata
available = [meta for meta in metadata if meta is not None]

return {key: getattr(meta, key)
for meta in available
for key, value in self._get_meta_items(meta).items()}

@staticmethod
def _get_meta_items(meta_class: Any) -> Dict[str, str]:
"""Helper method to extract constraint names and values from different
Expand All @@ -172,51 +159,18 @@ def _get_meta_items(meta_class: Any) -> Dict[str, str]:
except AttributeError:
return meta_class.__dict__

def get_given_constraint_keys(self, field_name: str) -> Set[str]:
"""Retrieves all schema attribute keys that have been set.
This information is relevant to distinguish given values that are
equivalent to their default values. Otherwise, there is no chance
to determine if a constraint was actually given by the user or set as
a default value.
Note: Accessing private attributes with many levels of nesting is far
from being desired but currently there is no proper solution around
this. Accessing the `properties` via the `model_schema_json` may fail
in cases where fields are not serializable.
"""

definition = self.model.__pydantic_core_schema__["definitions"][0]

# account for varying levels of nesting :-(
try:
field_schemas = definition["schema"]["fields"]
except KeyError:
field_schemas = definition["schema"]["schema"]["fields"]

# account for generics and other non-native fields without schema info
try:
schema = field_schemas[field_name]["schema"]
except KeyError:
return set()

# account for yet another level on model-field :-(
if "schema" in schema:
schema = schema["schema"]

return set(schema.keys())

def get_constraints(self, field_name: str) -> Dict[str, Any]:
"""Get constraints for given `field_name`.
"""

constraint_items = self.get_constraint_items(field_name).items()
given_constraint_keys = self.get_given_constraint_keys(field_name)
metadata = self.model.model_fields[field_name].metadata
available = [meta for meta in metadata if meta is not None]

return {key: value
for key, value in constraint_items
if key in given_constraint_keys}
return {key: getattr(meta, key)
for meta in available
for key, value in self._get_meta_items(meta).items()
if getattr(meta, key) is not None}

def is_required(self, field_name: str) -> bool:
"""Check if a given pydantic field is required/mandatory. Returns True,
Expand Down
28 changes: 26 additions & 2 deletions tests/compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,29 @@ def typing_module_prefix() -> str:

return ""

def typing_module_prefix_v1() -> str:
"""Provides compatibility abstraction to account for changed behaviour of
`autodoc_typehints_format` in sphinx 5.0 from fully qualified to short
which requires types of typing module to prefixed with `~typing.`.
"""

if (5,) <= sphinx.version_info < (6, 1):
return "~typing."

return ""

def typing_module_prefix_v2() -> str:
"""Provides compatibility abstraction to account for changed behaviour of
`autodoc_typehints_format` in sphinx 5.0 from fully qualified to short
which requires types of typing module to prefixed with `~typing.`.
"""

if (5,) <= sphinx.version_info:
return "~typing."

return ""

def typehints_prefix() -> str:
"""Provides compatibility abstraction to account for changed behaviour of
Expand Down Expand Up @@ -147,7 +170,8 @@ def get_optional_type_expected(field_type: str):
return field_type # 'Optional[int]'


TYPING_MODULE_PREFIX = typing_module_prefix()
TYPING_MODULE_PREFIX_V1 = typing_module_prefix_v1()
TYPING_MODULE_PREFIX_V2 = typing_module_prefix_v2()
TYPEHINTS_PREFIX = typehints_prefix()
OPTIONAL_INT = TYPING_MODULE_PREFIX + get_optional_type_expected(
OPTIONAL_INT = TYPING_MODULE_PREFIX_V1 + get_optional_type_expected(
'Optional[int]')
46 changes: 39 additions & 7 deletions tests/roots/test-base/target/configuration.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import datetime
from pathlib import Path
from typing import Optional
from uuid import UUID

import annotated_types
from pydantic.types import UuidVersion, PathType, condate, condecimal, conlist, \
conset

try:
from typing import Annotated
except ImportError:
from typing_extensions import Annotated

from pydantic import BaseModel, field_validator, Field, model_validator, \
ConfigDict, conint, constr, Strict
ConfigDict, conint, constr, Strict, AllowInfNan
from pydantic_settings import BaseSettings, SettingsConfigDict


Expand Down Expand Up @@ -395,14 +402,39 @@ class FieldShowConstraints(BaseModel):
class FieldShowConstraintsNativeConstraintTypes(BaseModel):
"""FieldShowConstraints."""

field_int: conint(ge=0, le=100, strict=True)
"""field_int"""
field_conint: conint(ge=0, le=100, strict=True)
"""field_conint"""

field_constr: constr(min_length=5, pattern="[a-z]+")
"""field_constr"""

field_condate: condate(strict=True,
gt=datetime.date(year=2023, month=8, day=1))
"""field_condate"""

field_condecimal: condecimal(max_digits=4, decimal_places=1)
"""field_condecimal"""

field_conset: conset(item_type=int, max_length=5, min_length=3)
"""field_conset"""

field_conlist: conlist(max_length=3, item_type=str)
"""field_conlist"""

field_strict_float: Annotated[float, Strict()]
"""field_strict_float"""

field_strict_bool: Annotated[bool, Strict()]
"""field_strict_bool"""

field_positive_int: Annotated[int, annotated_types.Gt(0)]
"""field_positive_int"""

field_str: constr(min_length=5, strict=True, pattern="[a-z]+")
"""field_str"""
uuid4: Annotated[UUID, UuidVersion(4)]
"""uuid4"""

field_annotated: Annotated[float, Strict()]
"""field_annotated"""
file_path: Annotated[Path, PathType('file')]
"""file_path"""


class FieldShowConstraintsIgnoreExtraKwargs(BaseModel):
Expand Down
19 changes: 19 additions & 0 deletions tests/roots/test-base/target/supported_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from enum import Enum, IntEnum

from pydantic import BaseModel


class FruitEnum(str, Enum):
pear = 'pear'
banana = 'banana'


class ToolEnum(IntEnum):
spanner = 1
wrench = 2


class CookingModel(BaseModel):
fruit: FruitEnum = FruitEnum.pear
tool: ToolEnum = ToolEnum.spanner

Loading

0 comments on commit 001504a

Please sign in to comment.