Skip to content

Commit fff28a4

Browse files
authored
♻️ REFACTOR: Replace attrs by dataclasses (#557)
Removes dependency on attrs
1 parent 719de0a commit fff28a4

File tree

6 files changed

+329
-136
lines changed

6 files changed

+329
-136
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def run_apidoc(app):
156156
# # 'inherited-members': True
157157
# }
158158
autodoc_member_order = "bysource"
159-
159+
nitpicky = True
160160
nitpick_ignore = [
161161
("py:class", "docutils.nodes.document"),
162162
("py:class", "docutils.nodes.docinfo"),

myst_parser/dc_validators.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Validators for dataclasses, mirroring those of https://github.com/python-attrs/attrs."""
2+
from __future__ import annotations
3+
4+
import dataclasses as dc
5+
from typing import Any, Callable, Sequence, Type
6+
7+
8+
def validate_fields(inst):
9+
"""Validate the fields of a dataclass,
10+
according to `validator` functions set in the field metadata.
11+
12+
This function should be called in the `__post_init__` of the dataclass.
13+
14+
The validator function should take as input (inst, field, value) and
15+
raise an exception if the value is invalid.
16+
"""
17+
for field in dc.fields(inst):
18+
if "validator" not in field.metadata:
19+
continue
20+
if isinstance(field.metadata["validator"], list):
21+
for validator in field.metadata["validator"]:
22+
validator(inst, field, getattr(inst, field.name))
23+
else:
24+
field.metadata["validator"](inst, field, getattr(inst, field.name))
25+
26+
27+
ValidatorType = Callable[[Any, dc.Field, Any], None]
28+
29+
30+
def instance_of(type: Type[Any] | tuple[Type[Any], ...]) -> ValidatorType:
31+
"""
32+
A validator that raises a `TypeError` if the initializer is called
33+
with a wrong type for this particular attribute (checks are performed using
34+
`isinstance` therefore it's also valid to pass a tuple of types).
35+
36+
:param type: The type to check for.
37+
"""
38+
39+
def _validator(inst, attr, value):
40+
"""
41+
We use a callable class to be able to change the ``__repr__``.
42+
"""
43+
if not isinstance(value, type):
44+
raise TypeError(
45+
f"'{attr.name}' must be {type!r} (got {value!r} that is a {value.__class__!r})."
46+
)
47+
48+
return _validator
49+
50+
51+
def optional(validator: ValidatorType) -> ValidatorType:
52+
"""
53+
A validator that makes an attribute optional. An optional attribute is one
54+
which can be set to ``None`` in addition to satisfying the requirements of
55+
the sub-validator.
56+
"""
57+
58+
def _validator(inst, attr, value):
59+
if value is None:
60+
return
61+
62+
validator(inst, attr, value)
63+
64+
return _validator
65+
66+
67+
def is_callable(inst, attr, value):
68+
"""
69+
A validator that raises a `TypeError` if the
70+
initializer is called with a value for this particular attribute
71+
that is not callable.
72+
"""
73+
if not callable(value):
74+
raise TypeError(
75+
f"'{attr.name}' must be callable "
76+
f"(got {value!r} that is a {value.__class__!r})."
77+
)
78+
79+
80+
def in_(options: Sequence) -> ValidatorType:
81+
"""
82+
A validator that raises a `ValueError` if the initializer is called
83+
with a value that does not belong in the options provided. The check is
84+
performed using ``value in options``.
85+
86+
:param options: Allowed options.
87+
"""
88+
89+
def _validator(inst, attr, value):
90+
try:
91+
in_options = value in options
92+
except TypeError: # e.g. `1 in "abc"`
93+
in_options = False
94+
95+
if not in_options:
96+
raise ValueError(f"'{attr.name}' must be in {options!r} (got {value!r})")
97+
98+
return _validator
99+
100+
101+
def deep_iterable(
102+
member_validator: ValidatorType, iterable_validator: ValidatorType | None = None
103+
) -> ValidatorType:
104+
"""
105+
A validator that performs deep validation of an iterable.
106+
107+
:param member_validator: Validator to apply to iterable members
108+
:param iterable_validator: Validator to apply to iterable itself
109+
"""
110+
111+
def _validator(inst, attr, value):
112+
if iterable_validator is not None:
113+
iterable_validator(inst, attr, value)
114+
115+
for member in value:
116+
member_validator(inst, attr, member)
117+
118+
return _validator
119+
120+
121+
def deep_mapping(
122+
key_validator: ValidatorType,
123+
value_validator: ValidatorType,
124+
mapping_validator: ValidatorType | None = None,
125+
) -> ValidatorType:
126+
"""
127+
A validator that performs deep validation of a dictionary.
128+
129+
:param key_validator: Validator to apply to dictionary keys
130+
:param value_validator: Validator to apply to dictionary values
131+
:param mapping_validator: Validator to apply to top-level mapping attribute (optional)
132+
"""
133+
134+
def _validator(inst, attr, value):
135+
if mapping_validator is not None:
136+
mapping_validator(inst, attr, value)
137+
138+
for key in value:
139+
key_validator(inst, attr, key)
140+
value_validator(inst, attr, value[key])
141+
142+
return _validator

myst_parser/docutils_.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
.. include:: path/to/file.md
44
:parser: myst_parser.docutils_
55
"""
6+
from dataclasses import Field
67
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
78

8-
from attr import Attribute
99
from docutils import frontend, nodes
1010
from docutils.core import default_description, publish_cmdline
1111
from docutils.parsers.rst import Parser as RstParser
@@ -69,8 +69,8 @@ def __repr__(self):
6969
"""Names of settings that cannot be set in docutils.conf."""
7070

7171

72-
def _attr_to_optparse_option(at: Attribute, default: Any) -> Tuple[dict, str]:
73-
"""Convert an ``attrs.Attribute`` into a Docutils optparse options dict."""
72+
def _attr_to_optparse_option(at: Field, default: Any) -> Tuple[dict, str]:
73+
"""Convert a field into a Docutils optparse options dict."""
7474
if at.type is int:
7575
return {"metavar": "<int>", "validator": _validate_int}, f"(default: {default})"
7676
if at.type is bool:
@@ -118,7 +118,7 @@ def _attr_to_optparse_option(at: Attribute, default: Any) -> Tuple[dict, str]:
118118

119119

120120
def attr_to_optparse_option(
121-
attribute: Attribute, default: Any, prefix: str = "myst_"
121+
attribute: Field, default: Any, prefix: str = "myst_"
122122
) -> Tuple[str, List[str], Dict[str, Any]]:
123123
"""Convert an ``MdParserConfig`` attribute into a Docutils setting tuple.
124124

0 commit comments

Comments
 (0)