Skip to content

Commit 92149ba

Browse files
committed
ci: Increase minimum bounds of pysource-codegen and pysource-minimize
1 parent 48e18ea commit 92149ba

6 files changed

Lines changed: 360 additions & 51 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ ci = [
9898
"duty>=1.6",
9999
"griffe-inherited-docstrings>=1.1",
100100
"jsonschema>=4.17",
101-
"pysource-codegen>=0.4",
102-
"pysource-minimize>=0.5",
101+
"pysource-codegen>=0.7",
102+
"pysource-minimize>=0.10",
103103
"pytest>=8.2",
104104
"pytest-cov>=5.0",
105105
"pytest-randomly>=3.15",

src/griffe/_internal/extensions/unpack_typeddict.py

Lines changed: 151 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
# TODO: Support `typing.ReadOnly`.
2+
# TODO: Support `extra_items=type`.
3+
# TODO: Support `closed=True/False`.
4+
15
from __future__ import annotations
26

3-
from typing import TYPE_CHECKING, Any
7+
import ast
8+
from typing import TYPE_CHECKING, Any, TypedDict
49

5-
from griffe._internal.docstrings.models import DocstringParameter, DocstringSectionParameters
10+
from griffe._internal.docstrings.models import (
11+
DocstringParameter,
12+
DocstringSectionOtherParameters,
13+
DocstringSectionParameters,
14+
)
615
from griffe._internal.enumerations import DocstringSectionKind, ParameterKind
716
from griffe._internal.expressions import Expr, ExprSubscript
817
from griffe._internal.extensions.base import Extension
@@ -12,47 +21,152 @@
1221
from collections.abc import Iterable
1322

1423

15-
def _update_docstring(func: Function, parameters: Iterable[Parameter], kwparam: Parameter | None = None) -> None:
24+
class _TypedDictAttr(TypedDict):
25+
name: str
26+
annotation: str | Expr | None
27+
docstring: Docstring | None
28+
required: bool
29+
30+
31+
def _get_or_set_attrs(cls: Class) -> list[_TypedDictAttr]:
32+
if (attrs := cls.extra.get("unpack_typeddict", {}).get("_attributes")) is not None:
33+
return attrs
34+
35+
attrs = []
36+
default_required = True
37+
for arg, value in cls.keywords.items():
38+
if arg == "total":
39+
try:
40+
total = ast.literal_eval(str(value))
41+
except (ValueError, SyntaxError):
42+
break
43+
if total is True:
44+
default_required = True
45+
elif total is False:
46+
default_required = False
47+
break
48+
for attr in cls.attributes.values():
49+
annotation = attr.annotation
50+
required = default_required
51+
if isinstance(annotation, ExprSubscript):
52+
if annotation.canonical_path in {
53+
"typing.Required",
54+
"typing_extensions.Required",
55+
}:
56+
annotation = annotation.slice.elements[0] # type: ignore[union-attr]
57+
required = True
58+
elif annotation.canonical_path in {
59+
"typing.NotRequired",
60+
"typing_extensions.NotRequired",
61+
}:
62+
annotation = annotation.slice.elements[0] # type: ignore[union-attr]
63+
required = False
64+
attrs.append(
65+
_TypedDictAttr(
66+
name=attr.name,
67+
annotation=annotation,
68+
docstring=attr.docstring,
69+
required=required,
70+
),
71+
)
72+
73+
cls.extra["unpack_typeddict"]["_attributes"] = attrs
74+
return attrs
75+
76+
77+
def _update_docstring(func: Function, attributes: Iterable[_TypedDictAttr], kwparam: Parameter | None = None) -> None:
1678
if not func.docstring:
1779
func.docstring = Docstring("", parent=func)
80+
81+
required = []
82+
optional = []
83+
for attribute in attributes:
84+
if attribute["required"]:
85+
required.append(attribute)
86+
else:
87+
optional.append(attribute)
88+
89+
params_section = None
90+
other_params_section = None
1891
sections = func.docstring.parsed
92+
93+
# Find existing "Parameters" section.
1994
section_gen = (section for section in sections if section.kind is DocstringSectionKind.parameters)
20-
if kwparam and (params_section := next(section_gen, None)):
21-
# Remove the `**kwargs` entry.
95+
params_section = next(section_gen, None)
96+
97+
# Pop original variadic keyword parameter from section.
98+
varkw_param = None
99+
if kwparam and params_section is not None:
22100
param_gen = (i for i, arg in enumerate(params_section.value) if arg.name.lstrip("*") == kwparam.name)
23101
if (kwarg_pos := next(param_gen, None)) is not None:
24-
params_section.value.pop(kwarg_pos)
25-
else:
26-
# Create a parameters section if none exists.
27-
params_section = DocstringSectionParameters([])
28-
func.docstring.parsed.append(params_section)
29-
# Add entries for all parameters.
30-
for param in parameters:
31-
if param.name != "self":
102+
varkw_param = params_section.value.pop(kwarg_pos)
103+
104+
# If we have required parameters, add them to the "Parameters" section.
105+
if required:
106+
# Create a "Parameters" section if none exists.
107+
if params_section is None:
108+
params_section = DocstringSectionParameters([])
109+
func.docstring.parsed.append(params_section)
110+
111+
# Add required parameters to the section.
112+
for attr in required:
32113
params_section.value.append(
33114
DocstringParameter(
34-
name=param.name,
35-
description=param.docstring.value if param.docstring else "",
36-
annotation=param.annotation,
37-
value=param.default,
115+
name=attr["name"],
116+
description=attr["docstring"].value if attr["docstring"] else "",
117+
annotation=attr["annotation"],
38118
),
39119
)
40120

121+
# Add back the original variadic keyword parameter if it was present,
122+
# and if some parameters are optional.
123+
if optional and varkw_param is not None:
124+
params_section.value.append(
125+
DocstringParameter(
126+
name=varkw_param.name,
127+
description=varkw_param.description,
128+
annotation=varkw_param.annotation,
129+
),
130+
)
41131

42-
def _params_from_attrs(attrs: Iterable[Any]) -> Parameters:
43-
return Parameters(
44-
Parameter(name="self", kind=ParameterKind.positional_or_keyword),
45-
*(
46-
Parameter(
47-
name=attr.name,
48-
annotation=attr.annotation,
49-
kind=ParameterKind.keyword_only,
50-
default=attr.value,
51-
docstring=attr.docstring,
132+
# If we have optional parameters, add them to the "Other parameters" section.
133+
if optional:
134+
# Create an "Other parameters" section if none exists.
135+
section_gen = (section for section in sections if section.kind is DocstringSectionKind.other_parameters)
136+
if (other_params_section := next(section_gen, None)) is None:
137+
other_params_section = DocstringSectionOtherParameters([])
138+
func.docstring.parsed.append(other_params_section)
139+
140+
# Add optional parameters to the section.
141+
for attr in optional:
142+
other_params_section.value.append(
143+
DocstringParameter(
144+
name=attr["name"],
145+
description=attr["docstring"].value if attr["docstring"] else "",
146+
annotation=attr["annotation"],
147+
),
52148
)
53-
for attr in attrs
54-
),
55-
)
149+
150+
151+
def _params_from_attrs(attrs: Iterable[_TypedDictAttr]) -> Parameters:
152+
parameters = Parameters(Parameter(name="self", kind=ParameterKind.positional_or_keyword))
153+
found_optional = False
154+
for attr in attrs:
155+
if attr["required"]:
156+
parameters.add(
157+
Parameter(
158+
name=attr["name"],
159+
annotation=attr["annotation"],
160+
kind=ParameterKind.keyword_only,
161+
docstring=attr["docstring"],
162+
),
163+
)
164+
else:
165+
found_optional = True
166+
if found_optional:
167+
name = "_kwargs" if "kwargs" in parameters else "kwargs"
168+
parameters.add(Parameter(name=name, kind=ParameterKind.var_keyword))
169+
return parameters
56170

57171

58172
class UnpackTypedDictExtension(Extension):
@@ -67,19 +181,19 @@ def on_class(self, *, cls: Class, **kwargs: Any) -> None: # noqa: ARG002
67181
else:
68182
return
69183

70-
attributes = cls.attributes.values()
184+
attributes = _get_or_set_attrs(cls)
71185

72186
if "__init__" not in cls.members:
73187
# Build the `__init__` method and add it to the class.
74188
parameters = _params_from_attrs(attributes)
75189
init = Function(name="__init__", parameters=parameters, returns="None")
76190
cls.set_member("__init__", init)
77191
# Update the `__init__` docstring.
78-
_update_docstring(init, parameters)
192+
_update_docstring(init, attributes)
79193

80194
# Remove attributes from the class, as they are now in the `__init__` method.
81195
for attr in attributes:
82-
cls.del_member(attr.name)
196+
cls.del_member(attr["name"])
83197

84198
def on_function(self, *, func: Function, **kwargs: Any) -> None: # noqa: ARG002
85199
"""Expand `**kwargs: Unpack[TypedDict]` in function signatures."""
@@ -102,17 +216,19 @@ def on_function(self, *, func: Function, **kwargs: Any) -> None: # noqa: ARG002
102216
else:
103217
return
104218

219+
attributes = _get_or_set_attrs(typed_dict)
220+
105221
if "__init__" in typed_dict.members:
106222
# The `__init__` was already generated: use its parameters.
107223
parameters = typed_dict["__init__"].parameters
108224
else:
109225
# Fallback to building parameters from attributes.
110-
parameters = _params_from_attrs(typed_dict.attributes.values())
226+
parameters = _params_from_attrs(attributes)
111227

112228
# Update any parameter section in the docstring.
113229
# We do this before updating the signature so that
114230
# parsing the docstring doesn't emit warnings.
115-
_update_docstring(func, parameters, parameter)
231+
_update_docstring(func, attributes, parameter)
116232

117233
# Update the function parameters.
118234
del func.parameters[parameter.name]

src/griffe/_internal/finder.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ def append_search_path(self, path: Path) -> None:
125125
Parameters:
126126
path: The path to append.
127127
"""
128-
path = path.resolve()
128+
self._append_search_path(path.resolve())
129+
130+
def _append_search_path(self, path: Path) -> None:
129131
if path not in self.search_paths:
130132
self.search_paths.append(path)
131133

@@ -379,10 +381,6 @@ def _contents(self, path: Path) -> list[Path]:
379381
self._paths_contents[path] = []
380382
return self._paths_contents[path]
381383

382-
def _append_search_path(self, path: Path) -> None:
383-
if path not in self.search_paths:
384-
self.search_paths.append(path)
385-
386384
def _extend_from_pth_files(self) -> None:
387385
for path in self.search_paths:
388386
for item in self._contents(path):

src/griffe/_internal/mixins.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ def from_json(cls: type[_ObjType], json_string: str, **kwargs: Any) -> _ObjType:
249249
raise TypeError(f"provided JSON object is not of type {cls}")
250250
return obj
251251

252+
def as_index(self, **kwargs: Any) -> dict[str, dict[str, Any]]:
253+
"""Return an index of this object/alias and its members.
254+
255+
Parameters:
256+
**kwargs: Additional serialization options.
257+
258+
Returns:
259+
A dictionary mapping paths to dictionaries.
260+
"""
261+
index = {self.path: self.as_dict(full=True, flat=True, **kwargs)} # type: ignore[attr-defined]
262+
try:
263+
for member in self.members.values(): # type: ignore[attr-defined]
264+
index.update(member.as_index(**kwargs))
265+
except AliasResolutionError:
266+
pass
267+
return index
268+
252269

253270
class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, SerializationMixin):
254271
"""Mixin class to share methods that appear both in objects and aliases, unchanged."""

0 commit comments

Comments
 (0)