• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

domdfcoding / attr_utils / 5715121359

pending completion
5715121359

push

github

domdfcoding
Add FUNDING.yml

357 of 372 relevant lines covered (95.97%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.31
/attr_utils/annotations.py
1
#!/usr/bin/env python
2
#
3
#  annotations.py
4
"""
1✔
5
Add type annotations to the ``__init__`` of an attrs_ class.
6

7
Since :pull:`363 <python-attrs/attrs>` attrs has
8
populated the ``__init__.__annotations__`` based on the types of attributes.
9
However, annotations were deliberately omitted when converter functions were used.
10
This module attempts to generate the annotations for use in Sphinx documentation,
11
even when converter functions *are* used, based on the following assumptions:
12

13
* If the converter function is a Python ``type``, such as :class:`str`, :class:`int`,
14
  or :class:`list`, the type annotation will be that type.
15
  If the converter and the type annotation refer to the same type
16
  (e.g. :class:`list` and :class:`typing.List`) the type annotation will be used.
17

18
* If the converter function has an annotation for its first argument, that annotation is used.
19

20
* If the converter function is not annotated, the type of the attribute will be used.
21

22
The annotation can also be provided via the ``'annotation'`` key in the
23
`metadata dict <https://www.attrs.org/en/stable/examples.html#metadata>`_.
24
If you prefer you can instead provide this as a keyword argument to :func:`~.attrib`
25
which will construct the metadata dict and call :func:`attr.ib` for you.
26

27
.. _attrs: https://www.attrs.org/en/stable/
28

29
.. versionchanged:: 0.2.0
30

31
        Improved support for container types.
32

33

34
.. attention::
35

36
        Due to changes in the :mod:`typing` module :mod:`~attr_utils.annotations`
37
        is only officially supported on Python 3.7 and above.
38

39
Examples
40
---------------
41

42
**Library Usage:**
43

44
.. code-block:: python
45
        :linenos:
46

47
        def my_converter(arg: List[Dict[str, Any]]):
48
                return arg
49

50

51
        def untyped_converter(arg):
52
                return arg
53

54

55
        @attr.s
56
        class SomeClass:
57
                a_string: str = attr.ib(converter=str)
58
                custom_converter: Any = attr.ib(converter=my_converter)
59
                untyped: Tuple[str, int, float] = attr.ib(converter=untyped_converter)
60
                annotated: List[str] = attr.ib(
61
                        converter=list,
62
                        metadata={"annotation": Sequence[str]},
63
                )
64

65
        add_attrs_annotations(SomeClass)
66

67
        print(SomeClass.__init__.__annotations__)
68
        # {
69
        #        'return': None,
70
        #        'a_string': <class 'str'>,
71
        #        'custom_converter': typing.List[typing.Dict[str, typing.Any]],
72
        #        'untyped': typing.Tuple[str, int, float],
73
        #        }
74

75
**Sphinx documentation**:
76

77
.. literalinclude:: ../../attr_utils/annotations.py
78
        :tab-width: 4
79
        :pyobject: AttrsClass
80

81
The ``parse_occupations`` function looks like:
82

83
.. literalinclude:: ../../attr_utils/annotations.py
84
        :tab-width: 4
85
        :pyobject: parse_occupations
86

87
The Sphinx output looks like:
88

89
        .. autoclass:: attr_utils.annotations.AttrsClass
90
                :members:
91
                :no-special-members:
92

93

94
API Reference
95
---------------
96

97
"""  # noqa: RST399
98
#
99
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
100
#
101
#  Permission is hereby granted, free of charge, to any person obtaining a copy
102
#  of this software and associated documentation files (the "Software"), to deal
103
#  in the Software without restriction, including without limitation the rights
104
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
105
#  copies of the Software, and to permit persons to whom the Software is
106
#  furnished to do so, subject to the following conditions:
107
#
108
#  The above copyright notice and this permission notice shall be included in all
109
#  copies or substantial portions of the Software.
110
#
111
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
112
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
113
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
114
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
115
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
116
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
117
#  OR OTHER DEALINGS IN THE SOFTWARE.
118
#
119

120
# stdlib
121
import inspect
1✔
122
import sys
1✔
123
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Type, TypeVar, Union, cast
1✔
124

125
# 3rd party
126
import attr
1✔
127

128
# this package
129
import attr_utils
1✔
130

131
if sys.version_info > (3, 7):  # pragma: no cover (<py37)
1✔
132
        # 3rd party
133
        from typing_extensions import get_origin
1✔
134
else:  # pragma: no cover (py37+)
135
        # 3rd party
136
        from typing_inspect import get_origin  # type: ignore
137

138
if TYPE_CHECKING or attr_utils._docs:
1✔
139
        # 3rd party
140
        from sphinx.application import Sphinx
×
141
        from sphinx_toolbox.utils import SphinxExtMetadata
×
142

143
__all__ = ["attrib", "_A", "_C", "add_init_annotations", "attr_docstring_hook", "setup"]
1✔
144

145
_A = TypeVar("_A", bound=Any)
1✔
146
_C = TypeVar("_C", bound=Callable)
1✔
147

148

149
def add_init_annotations(obj: _C) -> _C:
1✔
150
        """
151
        Add type annotations to the ``__init__`` method of an attrs_ class.
152

153
        .. _attrs: https://www.attrs.org/en/stable/
154
        """
155

156
        if not attr.has(obj):  # type: ignore
1✔
157
                return obj
1✔
158

159
        if hasattr(obj, "__attrs_init__"):
1✔
160
                return obj
1✔
161

162
        annotations: Dict[str, Optional[Type]] = {"return": None}
1✔
163

164
        attrs = attr.fields(obj)
1✔
165

166
        for a in attrs:
1✔
167
                arg_name = a.name.lstrip('_')
1✔
168

169
                if a.init is True and a.type is not None:
1✔
170
                        if a.converter is None:
1✔
171
                                annotations[arg_name] = a.type
1✔
172
                        else:
173

174
                                if "annotation" in a.metadata:
1✔
175
                                        annotations[arg_name] = a.metadata["annotation"]
1✔
176

177
                                elif isinstance(a.converter, type):
1✔
178
                                        if a.converter is get_origin(a.type):
1✔
179
                                                annotations[arg_name] = a.type
1✔
180
                                        else:
181
                                                annotations[arg_name] = a.converter
1✔
182

183
                                else:
184
                                        signature = inspect.signature(a.converter)
1✔
185
                                        arg_type = next(iter(signature.parameters.items()))[1].annotation
1✔
186
                                        if arg_type is inspect.Signature.empty:
1✔
187
                                                annotations[arg_name] = a.type
1✔
188
                                        else:
189
                                                annotations[arg_name] = arg_type
1✔
190

191
        if hasattr(obj.__init__, "__annotations__"):
1✔
192
                obj.__init__.__annotations__.update(annotations)
1✔
193
        else:  # pragma: no cover
194
                obj.__init__.__annotations__ = annotations
195

196
        return cast(_C, obj)
1✔
197

198

199
def attrib(
1✔
200
                default=attr.NOTHING,
201
                validator=None,
202
                repr: bool = True,  # noqa: A002  # pylint: disable=redefined-builtin
203
                hash=None,  # noqa: A002  # pylint: disable=redefined-builtin
204
                init=True,
205
                metadata=None,
206
                annotation: Union[Type, object] = attr.NOTHING,
207
                converter=None,
208
                factory=None,
209
                kw_only: bool = False,
210
                eq=None,
211
                order=None,
212
                **kwargs,
213
                ):
214
        r"""
215
        Wrapper around :func:`attr.ib` which supports the ``annotation``
216
        keyword argument for use by :func:`~.add_init_annotations`.
217

218
        .. versionadded:: 0.2.0
219

220
        :param default:
221
        :param validator:
222
        :param repr:
223
        :param hash:
224
        :param init:
225
        :param metadata:
226
        :param annotation: The type to add to ``__init__.__annotations__``, if different to
227
                that the type taken as input to the converter function or the type hint of the attribute.
228
        :param converter:
229
        :param factory:
230
        :param kw_only:
231
        :param eq:
232
        :param order:
233

234
        See the documentation for :func:`attr.ib` for descriptions of the other arguments.
235
        """  # noqa: D400
236

237
        if annotation is not attr.NOTHING:
1✔
238
                if metadata is None:
1✔
239
                        metadata = {}
1✔
240

241
                metadata["annotation"] = annotation
1✔
242

243
        return attr.ib(
1✔
244
                        default=default,
245
                        validator=validator,
246
                        repr=repr,
247
                        hash=hash,
248
                        init=init,
249
                        metadata=metadata,
250
                        converter=converter,
251
                        factory=factory,
252
                        kw_only=kw_only,
253
                        eq=eq,
254
                        order=order,
255
                        **kwargs,
256
                        )
257

258

259
def attr_docstring_hook(obj: _A) -> _A:
1✔
260
        """
261
        Hook for :mod:`sphinx_toolbox.more_autodoc.typehints` to add annotations to the ``__init__`` method
262
        of attrs_ classes.
263

264
        .. _attrs: https://www.attrs.org/en/stable/
265

266
        :param obj: The object being documented.
267
        """  # noqa: D400
268

269
        if callable(obj):
1✔
270

271
                if inspect.isclass(obj):
1✔
272
                        obj = cast(_A, add_init_annotations(obj))
1✔
273

274
        return obj
1✔
275

276

277
def setup(app: "Sphinx") -> "SphinxExtMetadata":
1✔
278
        """
279
        Sphinx extension to populate ``__init__.__annotations__`` for attrs_ classes.
280

281
        .. _attrs: https://www.attrs.org/en/stable/
282

283
        :param app:
284
        """
285

286
        # 3rd party
287
        from sphinx_toolbox.more_autodoc.typehints import docstring_hooks  # nodep
1✔
288

289
        docstring_hooks.append((attr_docstring_hook, 50))
1✔
290

291
        app.setup_extension("sphinx_toolbox.more_autodoc.typehints")
1✔
292

293
        return {
1✔
294
                        "version": attr_utils.__version__,
295
                        "parallel_read_safe": True,
296
                        }
297

298

299
################################
300
# Demo
301
################################
302

303

304
def parse_occupations(  # pragma: no cover
305
                occupations: Iterable[str],
306
                ) -> Iterable[str]:
307

308
        if isinstance(occupations, str):
1✔
309
                return [x.strip() for x in occupations.split(',')]
×
310
        else:
311
                return [str(x) for x in occupations]
1✔
312

313

314
@attr.s
1✔
315
class AttrsClass:
1✔
316
        """
317
        Example of using :func:`~.add_init_annotations` for attrs_ classes with Sphinx documentation.
318

319
        .. _attrs: https://www.attrs.org/en/stable/
320

321
        :param name: The name of the person.
322
        :param age: The age of the person.
323
        :param occupations: The occupation(s) of the person.
324
        """
325

326
        name: str = attr.ib(converter=str)
1✔
327
        age: int = attr.ib(converter=int)
1✔
328
        occupations: List[str] = attr.ib(converter=parse_occupations)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc