From 2e74e8f81146ea97fa723a687d8ee5f8c53d37c0 Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Mon, 18 Dec 2023 15:47:12 -0800 Subject: [PATCH 01/24] Fix for issue 137 --- sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 0966bc7f..9911e6fd 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -396,7 +396,7 @@ def _get_reference_sort_func(self) -> Callable: """ all_validators = self.pydantic.inspect.validators.names - all_fields = self.pydantic.inspect.fields.names + all_fields = self._get_valid_fields() idx_validators = self._get_idx_mappings(all_validators) idx_fields = self._get_idx_mappings(all_fields) From ea97fe912391dbb40ea6232342b23f3bb77d1e62 Mon Sep 17 00:00:00 2001 From: mansenfranzen Date: Thu, 28 Dec 2023 14:23:30 +0100 Subject: [PATCH 02/24] Add reproducible test case. --- tests/roots/test-base/target/configuration.py | 9 ++++ tests/test_configuration_model.py | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/roots/test-base/target/configuration.py b/tests/roots/test-base/target/configuration.py index c4741038..1c405d03 100644 --- a/tests/roots/test-base/target/configuration.py +++ b/tests/roots/test-base/target/configuration.py @@ -83,6 +83,15 @@ def validate_b(cls, v): @field_validator("field_a") def validate_a(cls, v): return v + +class ModelSummaryListOrderInherited(ModelSummaryListOrder): + """ModelSummaryListOrderInherited.""" + + field_c: int = 1 + + @field_validator("field_c") + def validate_c(cls, v): + return v class ModelHideParamList(BaseModel): diff --git a/tests/test_configuration_model.py b/tests/test_configuration_model.py index 0f128667..28ea0f03 100644 --- a/tests/test_configuration_model.py +++ b/tests/test_configuration_model.py @@ -607,6 +607,56 @@ def test_autodoc_pydantic_model_summary_list_order_bysource(autodocument): assert result == actual +def test_autodoc_pydantic_model_summary_list_order_bysource_inherited(autodocument): + kwargs = dict(object_path='target.configuration.ModelSummaryListOrderInherited', + **KWARGS) + enable = {"autodoc_pydantic_model_show_validator_summary": True, + "autodoc_pydantic_model_show_field_summary": True} + + result = [ + '', + '.. py:pydantic_model:: ModelSummaryListOrderInherited', + ' :module: target.configuration', + '', + ' ModelSummaryListOrderInherited.', + '', + ' :Fields:', + ' - :py:obj:`field_b (int) `', + ' - :py:obj:`field_a (int) `', + ' - :py:obj:`field_c (int) `', + '', + ' :Validators:', + ' - :py:obj:`validate_b ` » :py:obj:`field_b `', + ' - :py:obj:`validate_a ` » :py:obj:`field_a `', + ' - :py:obj:`validate_c ` » :py:obj:`field_c `', + + '' + ] + + # explict global + actual = autodocument( + options_app={"autodoc_pydantic_model_summary_list_order": "bysource", + **enable}, + **kwargs) + assert result == actual + + # explict local + actual = autodocument( + options_app=enable, + options_doc={"model-summary-list-order": "bysource"}, + **kwargs) + assert result == actual + + # explicit local overwrite global + actual = autodocument( + options_app={ + "autodoc_pydantic_model_summary_list_order": "alphabetical", + **enable}, + options_doc={"model-summary-list-order": "bysource"}, + **kwargs) + assert result == actual + + def test_autodoc_pydantic_model_hide_paramlist_false(autodocument): kwargs = dict(object_path='target.configuration.ModelHideParamList', **KWARGS) From 32cc4b321637d23f87a42606715bcf885b0dad1f Mon Sep 17 00:00:00 2001 From: j-carson Date: Sun, 31 Dec 2023 15:17:26 -0800 Subject: [PATCH 03/24] Work on edge cases --- .../directives/autodocumenters.py | 113 ++++++++++++++++-- tests/test_configuration_model.py | 67 ++++++++++- 2 files changed, 161 insertions(+), 19 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 65c2f80c..e5b09613 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -18,7 +18,13 @@ from sphinx.util.docstrings import prepare_docstring from sphinx.util.inspect import object_description -from sphinx.util.typing import get_type_hints, stringify +from sphinx.util.typing import get_type_hints + +try: + from sphinx.util.typing import stringify_annotation +except ImportError: + # fall back to older name for older versions of Sphinx + from sphinx.util.typing import stringify as stringify_annotation from sphinxcontrib.autodoc_pydantic.directives.options.enums import ( OptionsJsonErrorStrategy, @@ -133,10 +139,12 @@ def get_filtered_member_names(self) -> Set[str]: prefiltered to exclude inherited members. """ - object_members = self._documenter.get_object_members(True)[1] return {x.__name__ for x in object_members} + def get_base_class_names(self) -> List[str]: + return [x.__name__ for x in self.model.__mro__] + def resolve_inherited_validator_reference(self, ref: str) -> str: """Provide correct validator reference in case validator is inherited and explicitly shown in docs via directive option @@ -148,7 +156,6 @@ def resolve_inherited_validator_reference(self, ref: str) -> str: This logic is implemented here. """ - ref_parts = ref.split(".") class_name = ref_parts[-2] @@ -157,7 +164,7 @@ def resolve_inherited_validator_reference(self, ref: str) -> str: return ref validator_name = ref_parts[-1] - base_class_names = (x.__name__ for x in self.model.__mro__) + base_class_names = self.get_base_class_names() is_base_class = class_name in base_class_names is_inherited_enabled = "inherited-members" in self._documenter.options @@ -232,8 +239,23 @@ def document_members(self, *args, **kwargs): if self.pydantic.options.is_true("hide-reused-validator", True): self.hide_reused_validators() + if "inherited-members" in self.pydantic._documenter.options: + self.hide_inherited_members() + super().document_members(*args, **kwargs) + def hide_inherited_members(self): + """If inherited-members is set, make sure that these are excluded from + the class documenter, too""" + + exclude_members = self.options["exclude-members"] + squash_set = self.pydantic._documenter.options['inherited-members'] + for cl in self.pydantic.model.__mro__: + if cl.__name__ in squash_set: + for item in dir(cl): + exclude_members.add(item) + + def hide_config_member(self): """Add `Config` to `exclude_members` option. @@ -397,7 +419,7 @@ def _get_reference_sort_func(self) -> Callable: """ all_validators = self.pydantic.inspect.validators.names - all_fields = self._get_valid_fields() + all_fields = self.pydantic.inspect.fields.names idx_validators = self._get_idx_mappings(all_validators) idx_fields = self._get_idx_mappings(all_fields) @@ -415,7 +437,10 @@ def _get_validator_summary_references(self) -> List[ValidatorFieldMap]: """ - references = self.pydantic.inspect.references.mappings + base_class_validators = self._get_base_model_validators() + inherited_validators = self._get_inherited_validators() + references = base_class_validators + inherited_validators + sort_func = self._get_reference_sort_func() sorted_references = sorted(references, key=sort_func) @@ -448,6 +473,7 @@ def add_validators_summary(self): if not self.pydantic.inspect.validators: return + sorted_references = self._get_validator_summary_references() source_name = self.get_sourcename() @@ -458,15 +484,62 @@ def add_validators_summary(self): self.add_line("", source_name) + def _get_base_model_validators(self) -> List[str]: + """Return the validators on the model being documented""" + + result = [] + + base_model_fields = set(self._get_base_model_fields()) + base_object = self.object_name + references = self.pydantic.inspect.references.mappings + + # The validator is considered part of the base_object if + # the field that is being validated is on the object being + # documented, if the method that is doing the validating + # is on that object (even if that method is validating + # an inherited field) + for ref in references: + if ref.field_name in base_model_fields: + result.append(ref) + else: + validator_class = ref.validator_ref.split(".")[-2] + if validator_class == base_object: + result.append(ref) + return result + + + def _get_inherited_validators(self) -> List[str]: + """Return the validators on inherited fields to be documented, if any""" + + is_inherited_enabled = "inherited-members" in self.pydantic._documenter.options + if not is_inherited_enabled: + return [] + + squash_set = self.pydantic._documenter.options['inherited-members'] + references = self.pydantic.inspect.references.mappings + base_object = self.object_name + already_documented = self._get_base_model_validators() + + result = [] + for ref in references: + if ref not in already_documented: + validator_class = ref.validator_ref.split(".")[-2] + if (validator_class != base_object) and (validator_class not in squash_set): + result.append(ref) + return result + + def add_field_summary(self): """Adds summary section describing all fields. """ - if not self.pydantic.inspect.fields: return - valid_fields = self._get_valid_fields() + base_class_fields = self._get_base_model_fields() + inherited_fields = self._get_inherited_fields() + valid_fields = base_class_fields + inherited_fields + sorted_fields = self._sort_summary_list(valid_fields) source_name = self.get_sourcename() @@ -477,7 +550,7 @@ def add_field_summary(self): self.add_line("", source_name) - def _get_valid_fields(self) -> List[str]: + def _get_base_model_fields(self) -> List[str]: """Returns all field names that are valid members of pydantic model. """ @@ -486,6 +559,18 @@ def _get_valid_fields(self) -> List[str]: valid_members = self.pydantic.get_filtered_member_names() return [field for field in fields if field in valid_members] + def _get_inherited_fields(self) -> List[str]: + """Return the inherited fields if inheritance is enabled""" + + is_inherited_enabled = "inherited-members" in self.pydantic._documenter.options + if not is_inherited_enabled: + return [] + + fields = self.pydantic.inspect.fields.names + base_class_fields = self.pydantic.get_filtered_member_names() + return [field for field in fields if field not in base_class_fields] + + def _sort_summary_list(self, names: Iterable[str]) -> List[str]: """Sort member names according to given sort order `OptionsSummaryListOrder`. @@ -501,8 +586,10 @@ def sort_func(name: str): return name elif sort_order == OptionsSummaryListOrder.BYSOURCE: def sort_func(name: str): - name_with_class = f"{self.object_name}.{name}" - return self.analyzer.tagorder.get(name_with_class) + for base in self.pydantic.get_base_class_names(): + name_with_class = f"{base}.{name}" + if name_with_class in self.analyzer.tagorder: + return self.analyzer.tagorder.get(name_with_class) else: raise ValueError( f"Invalid value `{sort_order}` provided for " @@ -530,7 +617,7 @@ def _stringify_type(self, field_name: str) -> str: type_aliases = self.config.autodoc_type_aliases annotations = get_type_hints(self.object, None, type_aliases) - return stringify(annotations.get(field_name, "")) + return stringify_annotation(annotations.get(field_name, "")) @staticmethod def _convert_json_schema_to_rest(schema: Dict) -> List[str]: @@ -908,4 +995,4 @@ def add_field_list(self): line = self._build_field_list_rest_line(reference) self.add_line(line, source_name) - self.add_line("", source_name) + self.add_line("", source_name) \ No newline at end of file diff --git a/tests/test_configuration_model.py b/tests/test_configuration_model.py index 28ea0f03..6cba73e9 100644 --- a/tests/test_configuration_model.py +++ b/tests/test_configuration_model.py @@ -607,12 +607,17 @@ def test_autodoc_pydantic_model_summary_list_order_bysource(autodocument): assert result == actual -def test_autodoc_pydantic_model_summary_list_order_bysource_inherited(autodocument): +def test_autodoc_pydantic_model_summary_list_order_bysource_inherited_shown(autodocument): + """Test that you can order the output when in an inherited model, showing inherited fields/validators + """ kwargs = dict(object_path='target.configuration.ModelSummaryListOrderInherited', **KWARGS) enable = {"autodoc_pydantic_model_show_validator_summary": True, "autodoc_pydantic_model_show_field_summary": True} + local_bysource = {"model-summary-list-order": "bysource"} + show_inherited = {"inherited-members": "BaseModel"} + result = [ '', '.. py:pydantic_model:: ModelSummaryListOrderInherited', @@ -626,9 +631,9 @@ def test_autodoc_pydantic_model_summary_list_order_bysource_inherited(autodocume ' - :py:obj:`field_c (int) `', '', ' :Validators:', - ' - :py:obj:`validate_b ` » :py:obj:`field_b `', - ' - :py:obj:`validate_a ` » :py:obj:`field_a `', - ' - :py:obj:`validate_c ` » :py:obj:`field_c `', + ' - :py:obj:`validate_b ` » :py:obj:`field_b `', + ' - :py:obj:`validate_a ` » :py:obj:`field_a `', + ' - :py:obj:`validate_c ` » :py:obj:`field_c `', '' ] @@ -637,13 +642,14 @@ def test_autodoc_pydantic_model_summary_list_order_bysource_inherited(autodocume actual = autodocument( options_app={"autodoc_pydantic_model_summary_list_order": "bysource", **enable}, + options_doc=show_inherited, **kwargs) assert result == actual # explict local actual = autodocument( options_app=enable, - options_doc={"model-summary-list-order": "bysource"}, + options_doc=dict(**local_bysource, **show_inherited), **kwargs) assert result == actual @@ -652,7 +658,56 @@ def test_autodoc_pydantic_model_summary_list_order_bysource_inherited(autodocume options_app={ "autodoc_pydantic_model_summary_list_order": "alphabetical", **enable}, - options_doc={"model-summary-list-order": "bysource"}, + options_doc=dict(**local_bysource, **show_inherited), + **kwargs) + assert result == actual + + + +def test_autodoc_pydantic_model_summary_list_order_bysource_inherited_not_shown(autodocument): + kwargs = dict(object_path='target.configuration.ModelSummaryListOrderInherited', + **KWARGS) + enable = {"autodoc_pydantic_model_show_validator_summary": True, + "autodoc_pydantic_model_show_field_summary": True} + + local_bysource = {"model-summary-list-order": "bysource"} + + result = [ + '', + '.. py:pydantic_model:: ModelSummaryListOrderInherited', + ' :module: target.configuration', + '', + ' ModelSummaryListOrderInherited.', + '', + ' :Fields:', + ' - :py:obj:`field_c (int) `', + '', + ' :Validators:', + ' - :py:obj:`validate_c ` » :py:obj:`field_c `', + + '' + ] + + # explict global + actual = autodocument( + options_app={"autodoc_pydantic_model_summary_list_order": "bysource", + **enable}, + **kwargs) + assert result == actual + + # explict local + actual = autodocument( + options_app=enable, + options_doc=local_bysource, + **kwargs) + assert result == actual + + # explicit local overwrite global + actual = autodocument( + options_app={ + "autodoc_pydantic_model_summary_list_order": "alphabetical", + **enable}, + options_doc=local_bysource, **kwargs) assert result == actual From b13c17487defddb5382de8dccd05c918a9ee2f49 Mon Sep 17 00:00:00 2001 From: j-carson Date: Sun, 31 Dec 2023 15:40:18 -0800 Subject: [PATCH 04/24] lint --- .../directives/autodocumenters.py | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index e5b09613..15f3d638 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -241,20 +241,19 @@ def document_members(self, *args, **kwargs): if "inherited-members" in self.pydantic._documenter.options: self.hide_inherited_members() - + super().document_members(*args, **kwargs) def hide_inherited_members(self): """If inherited-members is set, make sure that these are excluded from the class documenter, too""" - + exclude_members = self.options["exclude-members"] - squash_set = self.pydantic._documenter.options['inherited-members'] + squash_set = self.pydantic._documenter.options['inherited-members'] for cl in self.pydantic.model.__mro__: if cl.__name__ in squash_set: for item in dir(cl): exclude_members.add(item) - def hide_config_member(self): """Add `Config` to `exclude_members` option. @@ -440,7 +439,7 @@ def _get_validator_summary_references(self) -> List[ValidatorFieldMap]: base_class_validators = self._get_base_model_validators() inherited_validators = self._get_inherited_validators() references = base_class_validators + inherited_validators - + sort_func = self._get_reference_sort_func() sorted_references = sorted(references, key=sort_func) @@ -488,14 +487,14 @@ def _get_base_model_validators(self) -> List[str]: """Return the validators on the model being documented""" result = [] - + base_model_fields = set(self._get_base_model_fields()) base_object = self.object_name references = self.pydantic.inspect.references.mappings # The validator is considered part of the base_object if # the field that is being validated is on the object being - # documented, if the method that is doing the validating + # documented, if the method that is doing the validating # is on that object (even if that method is validating # an inherited field) for ref in references: @@ -506,28 +505,30 @@ def _get_base_model_validators(self) -> List[str]: if validator_class == base_object: result.append(ref) return result - def _get_inherited_validators(self) -> List[str]: - """Return the validators on inherited fields to be documented, if any""" - - is_inherited_enabled = "inherited-members" in self.pydantic._documenter.options + """Return the validators on inherited fields to be documented, + if any""" + + is_inherited_enabled = ( + "inherited-members" in self.pydantic._documenter.options + ) if not is_inherited_enabled: return [] - - squash_set = self.pydantic._documenter.options['inherited-members'] + + squash_set = self.pydantic._documenter.options['inherited-members'] references = self.pydantic.inspect.references.mappings base_object = self.object_name already_documented = self._get_base_model_validators() - + result = [] for ref in references: if ref not in already_documented: validator_class = ref.validator_ref.split(".")[-2] - if (validator_class != base_object) and (validator_class not in squash_set): + if ((validator_class != base_object) and ( + validator_class not in squash_set)): result.append(ref) return result - def add_field_summary(self): """Adds summary section describing all fields. @@ -536,10 +537,10 @@ def add_field_summary(self): if not self.pydantic.inspect.fields: return - base_class_fields = self._get_base_model_fields() + base_class_fields = self._get_base_model_fields() inherited_fields = self._get_inherited_fields() valid_fields = base_class_fields + inherited_fields - + sorted_fields = self._sort_summary_list(valid_fields) source_name = self.get_sourcename() @@ -562,14 +563,15 @@ def _get_base_model_fields(self) -> List[str]: def _get_inherited_fields(self) -> List[str]: """Return the inherited fields if inheritance is enabled""" - is_inherited_enabled = "inherited-members" in self.pydantic._documenter.options + is_inherited_enabled = ( + "inherited-members" in self.pydantic._documenter.options + ) if not is_inherited_enabled: return [] - + fields = self.pydantic.inspect.fields.names base_class_fields = self.pydantic.get_filtered_member_names() return [field for field in fields if field not in base_class_fields] - def _sort_summary_list(self, names: Iterable[str]) -> List[str]: """Sort member names according to given sort order @@ -995,4 +997,4 @@ def add_field_list(self): line = self._build_field_list_rest_line(reference) self.add_line(line, source_name) - self.add_line("", source_name) \ No newline at end of file + self.add_line("", source_name) From 6d51607180a256ad72e161700e6ed57f342dff51 Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Fri, 19 Jan 2024 11:51:35 -0800 Subject: [PATCH 05/24] Fix some edgecases in sorting, change test case answer where inherited validator in expected result but inherit members not set in config --- .../directives/autodocumenters.py | 21 ++++++++++++------- tests/test_edgecases.py | 1 - 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 15f3d638..0a9fe16c 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -40,7 +40,7 @@ ) from sphinxcontrib.autodoc_pydantic.directives.templates import to_collapsable from sphinxcontrib.autodoc_pydantic.inspection import ModelInspector, \ - ValidatorFieldMap + ValidatorFieldMap, ASTERISK_FIELD_NAME from sphinxcontrib.autodoc_pydantic.directives.options.composites import ( AutoDocOptions ) @@ -408,7 +408,7 @@ def _get_idx_mappings(self, members: Iterable[str]) -> Dict[str, int]: sorted_members = self._sort_summary_list(members) return {name: idx for idx, name in enumerate(sorted_members)} - def _get_reference_sort_func(self) -> Callable: + def _get_reference_sort_func(self, references: list[ValidatorFieldMap]) -> Callable: """Helper function to create sorting function for instances of `ValidatorFieldMap` which first sorts by validator name and second by field name while respecting `OptionsSummaryListOrder`. @@ -417,11 +417,12 @@ def _get_reference_sort_func(self) -> Callable: """ - all_validators = self.pydantic.inspect.validators.names - all_fields = self.pydantic.inspect.fields.names + all_fields = [ref.field_name for ref in references] + all_validators = [ref.validator_name for ref in references] + idx_validators = self._get_idx_mappings(all_validators) idx_fields = self._get_idx_mappings(all_fields) - + def sort_func(reference: ValidatorFieldMap): return ( idx_validators.get(reference.validator_name, -1), @@ -440,7 +441,7 @@ def _get_validator_summary_references(self) -> List[ValidatorFieldMap]: inherited_validators = self._get_inherited_validators() references = base_class_validators + inherited_validators - sort_func = self._get_reference_sort_func() + sort_func = self._get_reference_sort_func(references) sorted_references = sorted(references, key=sort_func) return sorted_references @@ -578,7 +579,6 @@ def _sort_summary_list(self, names: Iterable[str]) -> List[str]: `OptionsSummaryListOrder`. """ - sort_order = self.pydantic.options.get_value(name="summary-list-order", prefix=True, force_availability=True) @@ -588,16 +588,21 @@ def sort_func(name: str): return name elif sort_order == OptionsSummaryListOrder.BYSOURCE: def sort_func(name: str): + if name in self.analyzer.tagorder: + return self.analyzer.tagorder.get(name) for base in self.pydantic.get_base_class_names(): name_with_class = f"{base}.{name}" if name_with_class in self.analyzer.tagorder: return self.analyzer.tagorder.get(name_with_class) + # a pseudo-field name used by root validators + if name == ASTERISK_FIELD_NAME: + return -1 else: raise ValueError( f"Invalid value `{sort_order}` provided for " f"`summary_list_order`. Valid options are: " f"{OptionsSummaryListOrder.values()}") - + return sorted(names, key=sort_func) def _get_field_summary_line(self, field_name: str) -> str: diff --git a/tests/test_edgecases.py b/tests/test_edgecases.py index 00cc3226..59960d36 100644 --- a/tests/test_edgecases.py +++ b/tests/test_edgecases.py @@ -384,7 +384,6 @@ def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherit ' ModelShowValidatorsSummaryInherited.', '', ' :Validators:', - ' - :py:obj:`check ` » :py:obj:`field `', ' - :py:obj:`check_inherited ` » :py:obj:`field `', '' ] From 571f4fca5887f4b3a55115513c3105640f92109d Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Fri, 19 Jan 2024 11:58:31 -0800 Subject: [PATCH 06/24] use backward compatible List type hint --- sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 0a9fe16c..31216942 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -408,7 +408,7 @@ def _get_idx_mappings(self, members: Iterable[str]) -> Dict[str, int]: sorted_members = self._sort_summary_list(members) return {name: idx for idx, name in enumerate(sorted_members)} - def _get_reference_sort_func(self, references: list[ValidatorFieldMap]) -> Callable: + def _get_reference_sort_func(self, references: List[ValidatorFieldMap]) -> Callable: """Helper function to create sorting function for instances of `ValidatorFieldMap` which first sorts by validator name and second by field name while respecting `OptionsSummaryListOrder`. From e85cf2c8c46c962bd06eaf8b58c4cfaa72ecdacd Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Fri, 19 Jan 2024 12:01:46 -0800 Subject: [PATCH 07/24] lint --- .../autodoc_pydantic/directives/autodocumenters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 31216942..a4cb9d17 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -408,7 +408,7 @@ def _get_idx_mappings(self, members: Iterable[str]) -> Dict[str, int]: sorted_members = self._sort_summary_list(members) return {name: idx for idx, name in enumerate(sorted_members)} - def _get_reference_sort_func(self, references: List[ValidatorFieldMap]) -> Callable: + def _get_reference_sort_func(self, references: List[ValidatorFieldMap]) -> Callable: # noqa: E501 """Helper function to create sorting function for instances of `ValidatorFieldMap` which first sorts by validator name and second by field name while respecting `OptionsSummaryListOrder`. @@ -419,10 +419,10 @@ def _get_reference_sort_func(self, references: List[ValidatorFieldMap]) -> Calla all_fields = [ref.field_name for ref in references] all_validators = [ref.validator_name for ref in references] - + idx_validators = self._get_idx_mappings(all_validators) idx_fields = self._get_idx_mappings(all_fields) - + def sort_func(reference: ValidatorFieldMap): return ( idx_validators.get(reference.validator_name, -1), @@ -602,7 +602,7 @@ def sort_func(name: str): f"Invalid value `{sort_order}` provided for " f"`summary_list_order`. Valid options are: " f"{OptionsSummaryListOrder.values()}") - + return sorted(names, key=sort_func) def _get_field_summary_line(self, field_name: str) -> str: From f3b8e4f7a62625e9320cfa381ed501808635b4a8 Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Fri, 19 Jan 2024 13:05:40 -0800 Subject: [PATCH 08/24] fix applehelp build issue --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 5380bf58..e3fc224a 100644 --- a/tox.ini +++ b/tox.ini @@ -22,8 +22,8 @@ deps = pydantic24: pydantic~=2.4.0 pydantic25: pydantic~=2.5.0 pydanticlatest: pydantic - sphinx40: sphinx~=4.0.0 - sphinx45: sphinx~=4.5.0 + sphinx40: sphinx~=4.0.0;sphinxcontrib-applehelp=1.0.4 + sphinx45: sphinx~=4.5.0;sphinxcontrib-applehelp=1.0.4 sphinx53: sphinx~=5.3.0 sphinx62: sphinx~=6.2.0 sphinx70: sphinx~=7.0.0 From 2348152fef0962a782c5e0fd6c02a67e95df0423 Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Fri, 19 Jan 2024 13:09:13 -0800 Subject: [PATCH 09/24] try applehelp again --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index e3fc224a..92dcd84d 100644 --- a/tox.ini +++ b/tox.ini @@ -22,8 +22,8 @@ deps = pydantic24: pydantic~=2.4.0 pydantic25: pydantic~=2.5.0 pydanticlatest: pydantic - sphinx40: sphinx~=4.0.0;sphinxcontrib-applehelp=1.0.4 - sphinx45: sphinx~=4.5.0;sphinxcontrib-applehelp=1.0.4 + sphinx40: sphinx~=4.0.0;sphinxcontrib-applehelp==1.0.4 + sphinx45: sphinx~=4.5.0;sphinxcontrib-applehelp==1.0.4 sphinx53: sphinx~=5.3.0 sphinx62: sphinx~=6.2.0 sphinx70: sphinx~=7.0.0 From 94f4ad850cfc74e36d91842e4914bb820193739b Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Fri, 19 Jan 2024 13:13:58 -0800 Subject: [PATCH 10/24] reverting applehelp stuff --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 92dcd84d..5380bf58 100644 --- a/tox.ini +++ b/tox.ini @@ -22,8 +22,8 @@ deps = pydantic24: pydantic~=2.4.0 pydantic25: pydantic~=2.5.0 pydanticlatest: pydantic - sphinx40: sphinx~=4.0.0;sphinxcontrib-applehelp==1.0.4 - sphinx45: sphinx~=4.5.0;sphinxcontrib-applehelp==1.0.4 + sphinx40: sphinx~=4.0.0 + sphinx45: sphinx~=4.5.0 sphinx53: sphinx~=5.3.0 sphinx62: sphinx~=6.2.0 sphinx70: sphinx~=7.0.0 From 23a1a4de16c47bdfee6d34dfe1b3c075e6d3f1b8 Mon Sep 17 00:00:00 2001 From: Janet Carson Date: Fri, 19 Jan 2024 15:22:44 -0800 Subject: [PATCH 11/24] another attempt at sphinx4 ci failure --- tox.ini | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5380bf58..aa33384f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ isolated_build = True [testenv] extras = test - erdantic commands = {envpython} -c "from sphinxcontrib.autodoc_pydantic.utility import show_versions; show_versions()" coverage run --source "sphinxcontrib/autodoc_pydantic" -m pytest -vv @@ -22,8 +21,23 @@ deps = pydantic24: pydantic~=2.4.0 pydantic25: pydantic~=2.5.0 pydanticlatest: pydantic + + ; pins for sphinx 4.X compatibility from + ; https://github.com/sphinx-doc/sphinx/issues/11890 sphinx40: sphinx~=4.0.0 + sphinx40: sphinxcontrib-applehelp==1.0.4 + sphinx40: sphinxcontrib-devhelp==1.0.2 + sphinx40: sphinxcontrib-htmlhelp==2.0.1 + sphinx40: sphinxcontrib-qthelp==1.0.3 + sphinx40: sphinxcontrib-serializinghtml==1.1.5 + sphinx45: sphinx~=4.5.0 + sphinx45: sphinxcontrib-applehelp==1.0.4 + sphinx45: sphinxcontrib-devhelp==1.0.2 + sphinx45: sphinxcontrib-htmlhelp==2.0.1 + sphinx45: sphinxcontrib-qthelp==1.0.3 + sphinx45: sphinxcontrib-serializinghtml==1.1.5 + sphinx53: sphinx~=5.3.0 sphinx62: sphinx~=6.2.0 sphinx70: sphinx~=7.0.0 From dd69791527ed5d526a6c71738247a9373ab0bc1b Mon Sep 17 00:00:00 2001 From: j-carson Date: Sun, 21 Jan 2024 18:31:14 -0800 Subject: [PATCH 12/24] tolerate inherited validator showing up if its python 3.9 or older --- tests/test_edgecases.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/test_edgecases.py b/tests/test_edgecases.py index 59960d36..572f6688 100644 --- a/tests/test_edgecases.py +++ b/tests/test_edgecases.py @@ -2,6 +2,7 @@ """ import copy +import sys import pytest import sphinx.errors @@ -11,6 +12,21 @@ from tests.compatibility import rst_alias_class_directive, \ TYPEHINTS_PREFIX, TYPING_MODULE_PREFIX_V1, module_doc_string_tab +@pytest.fixture +def pre_python310(): + """Python 3.10 and later lazily creates an __annotations__ object for + classes which do not have any type annotations. This prevents a corner-case + issue in python 3.9 and earlier. There is a workaround for the issue in + the python docs, but this workaround not currently implemented inside the + source code of sphinx itself, and it likely won't be implemented in the + older versions in the tox file. If a test is sensitive to this issue, this + fixture can be used to have a second acceptable answer + + https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older + """ + return sys.version_info < (3, 10) + + def test_not_json_compliant(autodocument): actual = autodocument( @@ -368,7 +384,8 @@ def test_autodoc_pydantic_model_show_validator_summary_inherited_with_inherited( def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherited( - autodocument): + autodocument, + pre_python310): """Ensure that references to inherited validators point to parent class when `inherited-members` is not given. @@ -388,13 +405,30 @@ def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherit '' ] + pre_python310_result = [ + '', + '.. py:pydantic_model:: ModelShowValidatorsSummaryInherited', + ' :module: target.configuration', + '', + ' ModelShowValidatorsSummaryInherited.', + '', + ' :Validators:', + ' - :py:obj:`check ` » :py:obj:`field `', + ' - :py:obj:`check_inherited ` » :py:obj:`field `', + '' + ] + actual = autodocument( documenter='pydantic_model', object_path='target.configuration.ModelShowValidatorsSummaryInherited', options_app={"autodoc_pydantic_model_show_validator_summary": True, "autodoc_pydantic_model_members": True}, deactivate_all=True) - assert result == actual + + if pre_python310: + assert (result == actual) or (pre_python310_result == actual) + else: + assert result == actual def test_autodoc_pydantic_field_list_validators_inherited_with_inherited( From f55e3207da9d228e0a3675ff2f6c91b214b0b7dd Mon Sep 17 00:00:00 2001 From: j-carson Date: Sun, 21 Jan 2024 19:03:08 -0800 Subject: [PATCH 13/24] accidentally removed erdantic from base testenv --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index aa33384f..0246e49d 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ isolated_build = True [testenv] extras = test + erdantic commands = {envpython} -c "from sphinxcontrib.autodoc_pydantic.utility import show_versions; show_versions()" coverage run --source "sphinxcontrib/autodoc_pydantic" -m pytest -vv From 65143842ccdf0868b39b1ff5dd24460c2c55c668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Tue, 12 Mar 2024 10:20:35 +0100 Subject: [PATCH 14/24] Introduce `options.exists` for better consistency. --- .../directives/autodocumenters.py | 2 +- .../directives/options/composites.py | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 6e9fc4e6..84398b7b 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -240,7 +240,7 @@ def document_members(self, *args, **kwargs): if self.pydantic.options.is_true("hide-reused-validator", True): self.hide_reused_validators() - if "inherited-members" in self.pydantic._documenter.options: + if self.pydantic.options.exists("inherited-members"): self.hide_inherited_members() super().document_members(*args, **kwargs) diff --git a/sphinxcontrib/autodoc_pydantic/directives/options/composites.py b/sphinxcontrib/autodoc_pydantic/directives/options/composites.py index 89244735..6018d017 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/options/composites.py +++ b/sphinxcontrib/autodoc_pydantic/directives/options/composites.py @@ -112,9 +112,11 @@ def get_value(self, name: str, return self.parent.options[name] elif force_availability or self.is_available(name): return self.get_app_cfg_by_name(name) + else: + return NONE def is_false(self, name: str, prefix: bool = False) -> bool: - """Get option value for given `name`. First, looks for explicit + """Check if option with `name` is False. First, looks for explicit directive option values (e.g. :member-order:) which have highest priority. Second, if no directive option is given, get the default option value provided via the app environment configuration. @@ -133,7 +135,7 @@ def is_false(self, name: str, prefix: bool = False) -> bool: return self.get_value(name=name, prefix=prefix) is False def is_true(self, name: str, prefix: bool = False) -> bool: - """Get option value for given `name`. First, looks for explicit + """Check if option with `name` is True. First, looks for explicit directive option values (e.g. :member-order:) which have highest priority. Second, if no directive option is given, get the default option value provided via the app environment configuration. @@ -150,6 +152,25 @@ def is_true(self, name: str, prefix: bool = False) -> bool: """ return self.get_value(name=name, prefix=prefix) is True + + def exists(self, name: str, prefix: bool = False) -> bool: + """Check if option with `name` is set. First, looks for explicit + directive option values (e.g. :member-order:) which have highest + priority. Second, if no directive option is given, get the default + option value provided via the app environment configuration. + + Enforces result to be either True or False. + + Parameters + ---------- + name: str + Name of the option. + prefix: bool + If True, add `pyautodoc_prefix` to name. + + """ + + return self.get_value(name=name, prefix=prefix) is not NONE def set_default_option(self, name: str): """Set default option value for given `name` from app environment From 8e9857dbe25b414829ecd1ce913ad4aeadecca5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Tue, 12 Mar 2024 10:22:17 +0100 Subject: [PATCH 15/24] Do not hide model config in any case. --- sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 84398b7b..5e438840 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -218,7 +218,6 @@ def __init__(self, *args: Any) -> None: super().__init__(*args) exclude_members = self.options.setdefault("exclude-members", set()) exclude_members.add("model_fields") - exclude_members.add("model_config") exclude_members.add("model_computed_fields") self.pydantic = PydanticAutoDoc(self, is_child=False) @@ -261,9 +260,7 @@ def hide_config_member(self): """ - exclude_members = self.options["exclude-members"] - exclude_members.add("Config") # deprecated since pydantic v2 - exclude_members.add("model_config") + self.options["exclude-members"].add("model_config") def hide_validator_members(self): """Add validator names to `exclude_members`. From c333d6a6514d73f70a59e20a8244bdf0f8923cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Tue, 12 Mar 2024 10:31:03 +0100 Subject: [PATCH 16/24] Remove obsolete `hide-config-member` --- .../autodoc_pydantic/directives/autodocumenters.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 5e438840..dd47cf71 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -218,6 +218,7 @@ def __init__(self, *args: Any) -> None: super().__init__(*args) exclude_members = self.options.setdefault("exclude-members", set()) exclude_members.add("model_fields") + exclude_members.add("model_config") exclude_members.add("model_computed_fields") self.pydantic = PydanticAutoDoc(self, is_child=False) @@ -230,9 +231,6 @@ def document_members(self, *args, **kwargs): if self.options.get("undoc-members") is False: self.options.pop("undoc-members") - if self.pydantic.options.is_false("show-config-member", True): - self.hide_config_member() - if self.pydantic.options.is_false("show-validator-members", True): self.hide_validator_members() @@ -255,13 +253,6 @@ def hide_inherited_members(self): for item in dir(cl): exclude_members.add(item) - def hide_config_member(self): - """Add `Config` to `exclude_members` option. - - """ - - self.options["exclude-members"].add("model_config") - def hide_validator_members(self): """Add validator names to `exclude_members`. From 13026a0664972090c2519316e4d57af462359e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 10:10:05 +0100 Subject: [PATCH 17/24] Add explicit tests for inheritance w/o overwrite. --- .../edgecase_inherited_members/disabled.py | 25 ++ .../disabled_with_overwrite.py | 40 +++ .../disabled_with_overwrite_module.py | 40 +++ .../enabled_without_base_model.py | 26 ++ tests/test_edgecases.py | 310 +++++++++++++++++- 5 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 tests/roots/test-base/target/edgecase_inherited_members/disabled.py create mode 100644 tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite.py create mode 100644 tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite_module.py create mode 100644 tests/roots/test-base/target/edgecase_inherited_members/enabled_without_base_model.py diff --git a/tests/roots/test-base/target/edgecase_inherited_members/disabled.py b/tests/roots/test-base/target/edgecase_inherited_members/disabled.py new file mode 100644 index 00000000..8437f260 --- /dev/null +++ b/tests/roots/test-base/target/edgecase_inherited_members/disabled.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, field_validator + + +class Parent(BaseModel): + """Base""" + + field_on_parent: str + """field_on_parent""" + + @field_validator("field_on_parent") + def validate_field_on_parent(cls, v): + """Validate field_on_parent""" + return v + + +class Child(Parent): + """Child""" + + field_on_child: str + """field_on_child""" + + @field_validator("field_on_child") + def validate_field_on_child(cls, v): + """Validate field_on_child""" + return v diff --git a/tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite.py b/tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite.py new file mode 100644 index 00000000..cbabc6e4 --- /dev/null +++ b/tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel, field_validator + + +class Parent(BaseModel): + """Base""" + + field_on_parent: str + """field_on_parent""" + + @field_validator("field_on_parent") + def validate_field_on_parent(cls, v): + """Validate field_on_parent""" + return v + + +class Child(Parent): + """Child""" + + field_on_child: str + """field_on_child""" + + @field_validator("field_on_child") + def validate_field_on_child(cls, v): + """Validate field_on_child""" + return v + + +class ChildWithOverwrite(Parent): + """ChildWithOverwrite""" + + field_on_parent: str + """overwritten field_on_parent""" + + field_on_child: str + """field_on_child""" + + @field_validator("field_on_child") + def validate_field_on_child(cls, v): + """Validate field_on_child""" + return v diff --git a/tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite_module.py b/tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite_module.py new file mode 100644 index 00000000..cbabc6e4 --- /dev/null +++ b/tests/roots/test-base/target/edgecase_inherited_members/disabled_with_overwrite_module.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel, field_validator + + +class Parent(BaseModel): + """Base""" + + field_on_parent: str + """field_on_parent""" + + @field_validator("field_on_parent") + def validate_field_on_parent(cls, v): + """Validate field_on_parent""" + return v + + +class Child(Parent): + """Child""" + + field_on_child: str + """field_on_child""" + + @field_validator("field_on_child") + def validate_field_on_child(cls, v): + """Validate field_on_child""" + return v + + +class ChildWithOverwrite(Parent): + """ChildWithOverwrite""" + + field_on_parent: str + """overwritten field_on_parent""" + + field_on_child: str + """field_on_child""" + + @field_validator("field_on_child") + def validate_field_on_child(cls, v): + """Validate field_on_child""" + return v diff --git a/tests/roots/test-base/target/edgecase_inherited_members/enabled_without_base_model.py b/tests/roots/test-base/target/edgecase_inherited_members/enabled_without_base_model.py new file mode 100644 index 00000000..b9be80c3 --- /dev/null +++ b/tests/roots/test-base/target/edgecase_inherited_members/enabled_without_base_model.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel, field_validator + + +class Parent(BaseModel): + """Base""" + + field_on_parent: str + """field_on_parent""" + + @field_validator("field_on_parent") + def validate_field_on_parent(cls, v): + """Validate field_on_parent""" + return v + + +class Child(Parent): + """Child""" + + field_on_child: str + """field_on_child""" + + @field_validator("field_on_child") + def validate_field_on_child(cls, v): + """Validate field_on_child""" + return v + \ No newline at end of file diff --git a/tests/test_edgecases.py b/tests/test_edgecases.py index fbe3f1a9..47646f3a 100644 --- a/tests/test_edgecases.py +++ b/tests/test_edgecases.py @@ -1,6 +1,5 @@ -"""This module contains tests for edgecases. +"""This module contains tests for edgecases.""" -""" import copy import sys @@ -745,3 +744,310 @@ def test_autodoc_pydantic_model_hide_reused_validator_true_identical_names( "undoc-members": None}, **kwargs) assert result == actual + + +def test_autodoc_pydantic_model_inherited_members_enabled_without_base_model( + autodocument, +): + """Ensure that inheritance from parent class is correct for fields and + validators considering both members and summary sections. + + """ + + kwargs = dict( + object_path="target.edgecase_inherited_members.enabled_without_base_model.Child", + documenter=PydanticModelDocumenter.objtype, + deactivate_all=True, + ) + + result = [ + "", + ".. py:pydantic_model:: Child", + " :module: target.edgecase_inherited_members.enabled_without_base_model", + "", + " Child", + "", + " :Fields:", + " - :py:obj:`field_on_child (str) `", + " - :py:obj:`field_on_parent (str) `", + "", + " :Validators:", + " - :py:obj:`validate_field_on_child ` » :py:obj:`field_on_child `", + " - :py:obj:`validate_field_on_parent ` » :py:obj:`field_on_parent `", + "", + "", + " .. py:pydantic_field:: Child.field_on_child", + " :module: target.edgecase_inherited_members.enabled_without_base_model", + " :type: str", + "", + " field_on_child", + "", + "", + " .. py:pydantic_field:: Child.field_on_parent", + " :module: target.edgecase_inherited_members.enabled_without_base_model", + " :type: str", + "", + " field_on_parent", + "", + "", + " .. py:pydantic_validator:: Child.validate_field_on_child", + " :module: target.edgecase_inherited_members.enabled_without_base_model", + " :classmethod:", + "", + " Validate field_on_child", + "", + "", + " .. py:pydantic_validator:: Child.validate_field_on_parent", + " :module: target.edgecase_inherited_members.enabled_without_base_model", + " :classmethod:", + "", + " Validate field_on_parent", + "", + ] + + actual = autodocument( + options_app={ + "autodoc_pydantic_model_show_validator_members": True, + "autodoc_pydantic_model_show_validator_summary": True, + "autodoc_pydantic_model_show_field_summary": True, + }, + options_doc={ + "members": None, + "inherited-members": "BaseModel", + }, + **kwargs, + ) + assert result == actual + + +def test_autodoc_pydantic_model_inherited_members_disabled(autodocument): + """Ensure that inheritance from parent class is correct for fields and + validators considering both members and summary sections. + + """ + + result = [ + "", + ".. py:pydantic_model:: Child", + " :module: target.edgecase_inherited_members.disabled", + "", + " Child", + "", + " :Fields:", + " - :py:obj:`field_on_child (str) `", + "", + " :Validators:", + " - :py:obj:`validate_field_on_child ` » :py:obj:`field_on_child `", + "", + "", + " .. py:pydantic_field:: Child.field_on_child", + " :module: target.edgecase_inherited_members.disabled", + " :type: str", + "", + " field_on_child", + "", + "", + " .. py:pydantic_validator:: Child.validate_field_on_child", + " :module: target.edgecase_inherited_members.disabled", + " :classmethod:", + "", + " Validate field_on_child", + "", + ] + + actual = autodocument( + options_app={ + "autodoc_pydantic_model_show_validator_members": True, + "autodoc_pydantic_model_show_validator_summary": True, + "autodoc_pydantic_model_show_field_summary": True, + "autodoc_pydantic_model_members": True, + }, + deactivate_all=True, + object_path="target.edgecase_inherited_members.disabled.Child", + documenter=PydanticModelDocumenter.objtype, + ) + + assert result == actual + + +def test_autodoc_pydantic_model_inherited_members_disabled_with_overwrite(autodocument): + """Ensure reference to parent validator is correctly set in + child field. Moreover, ensure that overwritten field is displayed and + not excluded. + + """ + + result = [ + "", + ".. py:pydantic_model:: ChildWithOverwrite", + " :module: target.edgecase_inherited_members.disabled_with_overwrite", + "", + " ChildWithOverwrite", + "", + " :Fields:", + " - :py:obj:`field_on_child (str) `", + " - :py:obj:`field_on_parent (str) `", + "", + " :Validators:", + " - :py:obj:`validate_field_on_child ` » :py:obj:`field_on_child `", + " - :py:obj:`validate_field_on_parent ` » :py:obj:`field_on_parent `", + "", + "", + " .. py:pydantic_field:: ChildWithOverwrite.field_on_child", + " :module: target.edgecase_inherited_members.disabled_with_overwrite", + " :type: str", + "", + " field_on_child", + "", + "", + " .. py:pydantic_field:: ChildWithOverwrite.field_on_parent", + " :module: target.edgecase_inherited_members.disabled_with_overwrite", + " :type: str", + "", + " overwritten field_on_parent", + "", + "", + " .. py:pydantic_validator:: ChildWithOverwrite.validate_field_on_child", + " :module: target.edgecase_inherited_members.disabled_with_overwrite", + " :classmethod:", + "", + " Validate field_on_child", + "", + ] + + actual = autodocument( + options_app={ + "autodoc_pydantic_model_show_validator_members": True, + "autodoc_pydantic_model_show_validator_summary": True, + "autodoc_pydantic_model_show_field_summary": True, + "autodoc_pydantic_model_members": True, + }, + deactivate_all=True, + object_path="target.edgecase_inherited_members.disabled_with_overwrite.ChildWithOverwrite", + documenter=PydanticModelDocumenter.objtype, + ) + + assert result == actual + + +def test_autodoc_module_inherited_members_disabled_with_overwrite(autodocument): + """Ensure that inheritance from parent class is correct for fields and + validators considering both members and summary sections, given that + a child class overwrites parent field. + + Additionally, ensure reference to parent validator is correctly set in + childs fields. Moreover, ensure that overwritten field is displayed. + + """ + + kwargs = dict( + object_path="target.edgecase_inherited_members.disabled_with_overwrite_module", + documenter="module", + deactivate_all=True, + ) + + result = [ + "", + ".. py:module:: target.edgecase_inherited_members.disabled_with_overwrite_module", + "", + "", + ".. py:pydantic_model:: Child", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + "", + " Child", + "", + " :Fields:", + " - :py:obj:`field_on_child (str) `", + "", + " :Validators:", + " - :py:obj:`validate_field_on_child ` » :py:obj:`field_on_child `", + "", + "", + " .. py:pydantic_field:: Child.field_on_child", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + " :type: str", + "", + " field_on_child", + "", + "", + " .. py:pydantic_validator:: Child.validate_field_on_child", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + " :classmethod:", + "", + " Validate field_on_child", + "", + "", + ".. py:pydantic_model:: ChildWithOverwrite", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + "", + " ChildWithOverwrite", + "", + " :Fields:", + " - :py:obj:`field_on_child (str) `", + " - :py:obj:`field_on_parent (str) `", + "", + " :Validators:", + " - :py:obj:`validate_field_on_child ` » :py:obj:`field_on_child `", + " - :py:obj:`validate_field_on_parent ` » :py:obj:`field_on_parent `", + "", + "", + " .. py:pydantic_field:: ChildWithOverwrite.field_on_child", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + " :type: str", + "", + " field_on_child", + "", + "", + " .. py:pydantic_field:: ChildWithOverwrite.field_on_parent", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + " :type: str", + "", + " overwritten field_on_parent", + "", + "", + " .. py:pydantic_validator:: ChildWithOverwrite.validate_field_on_child", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + " :classmethod:", + "", + " Validate field_on_child", + "", + "", + ".. py:pydantic_model:: Parent", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + "", + " Base", + "", + " :Fields:", + " - :py:obj:`field_on_parent (str) `", + "", + " :Validators:", + " - :py:obj:`validate_field_on_parent ` » :py:obj:`field_on_parent `", + "", + "", + " .. py:pydantic_field:: Parent.field_on_parent", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + " :type: str", + "", + " field_on_parent", + "", + "", + " .. py:pydantic_validator:: Parent.validate_field_on_parent", + " :module: target.edgecase_inherited_members.disabled_with_overwrite_module", + " :classmethod:", + "", + " Validate field_on_parent", + "", + ] + + actual = autodocument( + options_app={ + "autodoc_pydantic_model_show_validator_members": True, + "autodoc_pydantic_model_show_validator_summary": True, + "autodoc_pydantic_model_show_field_summary": True, + }, + options_doc={ + "members": None, + }, + **kwargs, + ) + assert result == actual From 50275f08d277528fa7114ec0c690ff035d991fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 10:10:40 +0100 Subject: [PATCH 18/24] Minor simplification and readability improvements. --- .../directives/autodocumenters.py | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index dd47cf71..6b4d3403 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -500,24 +500,26 @@ def _get_inherited_validators(self) -> List[str]: """Return the validators on inherited fields to be documented, if any""" - is_inherited_enabled = ( - "inherited-members" in self.pydantic._documenter.options - ) - if not is_inherited_enabled: + if not self.pydantic.options.exists("inherited-members"): return [] - squash_set = self.pydantic._documenter.options['inherited-members'] + squash_set = self.options['inherited-members'] references = self.pydantic.inspect.references.mappings base_object = self.object_name already_documented = self._get_base_model_validators() result = [] for ref in references: - if ref not in already_documented: - validator_class = ref.validator_ref.split(".")[-2] - if ((validator_class != base_object) and ( - validator_class not in squash_set)): - result.append(ref) + if ref in already_documented: + continue + + validator_class = ref.validator_ref.split(".")[-2] + foreign_validator = validator_class != base_object + not_ignored = validator_class not in squash_set + + if foreign_validator and not_ignored: + result.append(ref) + return result def add_field_summary(self): @@ -545,7 +547,7 @@ def _get_base_model_fields(self) -> List[str]: """Returns all field names that are valid members of pydantic model. """ - + fields = self.pydantic.inspect.fields.names valid_members = self.pydantic.get_filtered_member_names() return [field for field in fields if field in valid_members] @@ -553,10 +555,7 @@ def _get_base_model_fields(self) -> List[str]: def _get_inherited_fields(self) -> List[str]: """Return the inherited fields if inheritance is enabled""" - is_inherited_enabled = ( - "inherited-members" in self.pydantic._documenter.options - ) - if not is_inherited_enabled: + if not self.pydantic.options.exists("inherited-members"): return [] fields = self.pydantic.inspect.fields.names From 631d3129a8e9d9a4c5c602c93db4051826d9fc8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 12:29:44 +0100 Subject: [PATCH 19/24] Add test with field on child class. Streamline compat. --- tests/compatibility.py | 16 +++++ tests/roots/test-base/target/configuration.py | 9 +++ tests/test_edgecases.py | 67 ++++++++++++------- 3 files changed, 68 insertions(+), 24 deletions(-) diff --git a/tests/compatibility.py b/tests/compatibility.py index 29f25a4d..2a2197a1 100644 --- a/tests/compatibility.py +++ b/tests/compatibility.py @@ -3,6 +3,7 @@ """ import importlib +import sys from typing import Tuple, List import re @@ -169,8 +170,23 @@ def get_optional_type_expected(field_type: str): return field_type # 'Optional[int]' +def pre_python310(): + """Python 3.10 and later lazily creates an __annotations__ object for + classes which do not have any type annotations. This prevents a corner-case + issue in python 3.9 and earlier. There is a workaround for the issue in + the python docs, but this workaround not currently implemented inside the + source code of sphinx itself, and it likely won't be implemented in the + older versions in the tox file. If a test is sensitive to this issue, this + fixture can be used to have a second acceptable answer + + https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older + """ + return sys.version_info < (3, 10) + + 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_V1 + get_optional_type_expected( 'Optional[int]') +PYTHON_LT_310 = pre_python310() \ No newline at end of file diff --git a/tests/roots/test-base/target/configuration.py b/tests/roots/test-base/target/configuration.py index c65369aa..6ed1552d 100644 --- a/tests/roots/test-base/target/configuration.py +++ b/tests/roots/test-base/target/configuration.py @@ -44,6 +44,15 @@ class ModelShowValidatorsSummaryInherited(ModelShowValidatorsSummary): def check_inherited(cls, v) -> str: return v +class ModelShowValidatorsSummaryInheritedWithField(ModelShowValidatorsSummary): + """ModelShowValidatorsSummaryInheritedWithField.""" + + field2: int = 2 + + @field_validator("field") + def check_inherited(cls, v) -> str: + return v + class ModelShowValidatorsSummaryMultipleFields(BaseModel): """ModelShowValidatorsSummaryMultipleFields.""" diff --git a/tests/test_edgecases.py b/tests/test_edgecases.py index 47646f3a..c2588b21 100644 --- a/tests/test_edgecases.py +++ b/tests/test_edgecases.py @@ -9,22 +9,8 @@ from sphinxcontrib.autodoc_pydantic import PydanticModelDocumenter from tests.compatibility import rst_alias_class_directive, \ - TYPEHINTS_PREFIX, TYPING_MODULE_PREFIX_V2, module_doc_string_tab - -@pytest.fixture -def pre_python310(): - """Python 3.10 and later lazily creates an __annotations__ object for - classes which do not have any type annotations. This prevents a corner-case - issue in python 3.9 and earlier. There is a workaround for the issue in - the python docs, but this workaround not currently implemented inside the - source code of sphinx itself, and it likely won't be implemented in the - older versions in the tox file. If a test is sensitive to this issue, this - fixture can be used to have a second acceptable answer - - https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older - """ - return sys.version_info < (3, 10) - + TYPEHINTS_PREFIX, TYPING_MODULE_PREFIX_V2, module_doc_string_tab, \ + PYTHON_LT_310 def test_not_json_compliant(autodocument): @@ -382,11 +368,12 @@ def test_autodoc_pydantic_model_show_validator_summary_inherited_with_inherited( assert result == actual -def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherited( - autodocument, - pre_python310): - """Ensure that references to inherited validators point to parent class - when `inherited-members` is not given. +def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherited_no_field( + autodocument + ): + """Special edge case where inherited class without fields shows parent + fields/validator even though `inherited-members` is not activated. + This only occurs for python < 3.10. Relates to #122. @@ -404,7 +391,7 @@ def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherit '' ] - pre_python310_result = [ + result_python_lt_310 = [ '', '.. py:pydantic_model:: ModelShowValidatorsSummaryInherited', ' :module: target.configuration', @@ -424,12 +411,44 @@ def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherit "autodoc_pydantic_model_members": True}, deactivate_all=True) - if pre_python310: - assert (result == actual) or (pre_python310_result == actual) + if PYTHON_LT_310: + assert result_python_lt_310 == actual else: assert result == actual +def test_autodoc_pydantic_model_show_validator_summary_inherited_without_inherited_with_field( + autodocument + ): + """Ensure that references to inherited validators point to parent class + when `inherited-members` is not given. + + Relates to #122. + + """ + + result = [ + '', + '.. py:pydantic_model:: ModelShowValidatorsSummaryInheritedWithField', + ' :module: target.configuration', + '', + ' ModelShowValidatorsSummaryInheritedWithField.', + '', + ' :Validators:', + ' - :py:obj:`check_inherited ` » :py:obj:`field `', + '' + ] + + actual = autodocument( + documenter='pydantic_model', + object_path='target.configuration.ModelShowValidatorsSummaryInheritedWithField', + options_app={"autodoc_pydantic_model_show_validator_summary": True, + "autodoc_pydantic_model_members": True}, + deactivate_all=True) + + assert result == actual + + def test_autodoc_pydantic_field_list_validators_inherited_with_inherited( autodocument): """Ensure that references to inherited validators point to child class From e90e350348ea77429ff75b81f85101077b7da973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 12:30:11 +0100 Subject: [PATCH 20/24] Improve naming `get_non_inherited_members` --- .../autodoc_pydantic/directives/autodocumenters.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py index 6b4d3403..f9582562 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py +++ b/sphinxcontrib/autodoc_pydantic/directives/autodocumenters.py @@ -134,7 +134,7 @@ def get_field_name_or_alias(self, field_name: str): else: return field_name - def get_filtered_member_names(self) -> Set[str]: + def get_non_inherited_members(self) -> Set[str]: """Return all member names of autodocumented object which are prefiltered to exclude inherited members. @@ -167,10 +167,10 @@ def resolve_inherited_validator_reference(self, ref: str) -> str: base_class_names = self.get_base_class_names() is_base_class = class_name in base_class_names - is_inherited_enabled = "inherited-members" in self._documenter.options + is_inherited = self.options.exists("inherited-members") is_member = validator_name in self.inspect.validators.names - if is_member and is_base_class and is_inherited_enabled: + if is_member and is_base_class and is_inherited: ref_parts[-2] = self.model.__name__ return ".".join(ref_parts) else: @@ -547,9 +547,9 @@ def _get_base_model_fields(self) -> List[str]: """Returns all field names that are valid members of pydantic model. """ - + fields = self.pydantic.inspect.fields.names - valid_members = self.pydantic.get_filtered_member_names() + valid_members = self.pydantic.get_non_inherited_members() return [field for field in fields if field in valid_members] def _get_inherited_fields(self) -> List[str]: @@ -559,7 +559,7 @@ def _get_inherited_fields(self) -> List[str]: return [] fields = self.pydantic.inspect.fields.names - base_class_fields = self.pydantic.get_filtered_member_names() + base_class_fields = self.pydantic.get_non_inherited_members() return [field for field in fields if field not in base_class_fields] def _sort_summary_list(self, names: Iterable[str]) -> List[str]: From 98996911976d8cdb5f29ed4ce97d633ed795192a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 12:30:23 +0100 Subject: [PATCH 21/24] Flake 8 --- sphinxcontrib/autodoc_pydantic/directives/options/composites.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinxcontrib/autodoc_pydantic/directives/options/composites.py b/sphinxcontrib/autodoc_pydantic/directives/options/composites.py index 6018d017..58eb5f6e 100644 --- a/sphinxcontrib/autodoc_pydantic/directives/options/composites.py +++ b/sphinxcontrib/autodoc_pydantic/directives/options/composites.py @@ -152,7 +152,7 @@ def is_true(self, name: str, prefix: bool = False) -> bool: """ return self.get_value(name=name, prefix=prefix) is True - + def exists(self, name: str, prefix: bool = False) -> bool: """Check if option with `name` is set. First, looks for explicit directive option values (e.g. :member-order:) which have highest From b13eab84d9791f82b502fdc1d0a1077b55ae1562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 12:31:35 +0100 Subject: [PATCH 22/24] Remove obsolete config options. --- sphinxcontrib/autodoc_pydantic/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sphinxcontrib/autodoc_pydantic/__init__.py b/sphinxcontrib/autodoc_pydantic/__init__.py index 03092dc5..b0b7dc2d 100644 --- a/sphinxcontrib/autodoc_pydantic/__init__.py +++ b/sphinxcontrib/autodoc_pydantic/__init__.py @@ -77,9 +77,6 @@ def add_configuration_values(app: Sphinx): json_strategy = OptionsJsonErrorStrategy.WARN summary_list_order = OptionsSummaryListOrder.ALPHABETICAL - add(f'{stem}config_signature_prefix', "model", True, str) - add(f'{stem}config_members', True, True, bool) - add(f'{stem}settings_show_json', True, True, bool) add(f'{stem}settings_show_json_error_strategy', json_strategy, True, str) add(f'{stem}settings_show_config_summary', True, True, bool) From 9f4b1248db656f0255914fc4aa0f44cfbafdf942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 12:44:39 +0100 Subject: [PATCH 23/24] Update changelog. --- changelog.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/changelog.rst b/changelog.rst index 53c8ca6c..dc1cd8c7 100644 --- a/changelog.rst +++ b/changelog.rst @@ -17,15 +17,16 @@ Features Testing ~~~~~~~ -- Add pydantic 2.2/2.3/2.4/2.5/2.6 to test matrix. -- Add sphinx 7.1/7.2 to test matrix. -- Add python 3.12 to test matrix. +- Add pydantic 2.2/2.3/2.4/2.5/2.6 and sphinx 7.1/7.2 and python 3.12 + to test matrix. - Remove python 3.7 from test matrix. - Remove obsolete `skip ci` condition from github actions. - Update ``conftest`` to use ``pathlib`` instead of older Sphinx ``sphinx.testing.path`` module that is being deprecated for forward-compatibility with newer Sphinx versions. - Fix duplicated teset name ``test_non_field_attributes``. +- Add tests to cover inheritance behavior given ``inherited-members`` for + field and validator members and summaries. Bugfix ~~~~~~ @@ -37,12 +38,17 @@ Bugfix exception in some environments. This should be a namespace package per `PEP 420 `__ without ``__init_.py`` to match with other extensions. +- Removing deprecation warning ``sphinx.util.typing.stringify``. +- Fix bug a bug while sorting members `#137 `__. Internal ~~~~~~~~ - Fix deprecation warning for tuple interface of ``ObjectMember`` in ``directives/autodocumenters.py``. +- Remove obsolete configuration options which have been removed in v2. +- Introduce ``pydantic.options.exists`` to check for existence of sphinx + options. Documentation ~~~~~~~~~~~~~ @@ -73,6 +79,11 @@ Contributors - Thanks to `tony `__ for fixing a typo in the erdantic docs `#200 `__. +- Thanks to `j-carson `__ for providing a PR + fixing + - a bug while sorting members `#137 `__. + - broken CI pipeline with Sphinx 4.* + - removing deprecation warning `#178 `__. v2.0.1 - 2023-08-01 ------------------- From 05bf6612654cef27c03999dc6ccdd1d8d04911d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20W=C3=B6llert?= Date: Thu, 14 Mar 2024 13:09:04 +0100 Subject: [PATCH 24/24] Fix incorrect rst syntax. --- changelog.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/changelog.rst b/changelog.rst index dc1cd8c7..eee24e8b 100644 --- a/changelog.rst +++ b/changelog.rst @@ -80,10 +80,10 @@ Contributors erdantic docs `#200 `__. - Thanks to `j-carson `__ for providing a PR - fixing - - a bug while sorting members `#137 `__. - - broken CI pipeline with Sphinx 4.* - - removing deprecation warning `#178 `__. + that: + - fixes a bug while sorting members `#137 `__. + - fixes broken CI pipeline with Sphinx 4.* + - removing deprecation warning `#178 `__. v2.0.1 - 2023-08-01 -------------------