Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ REFACTOR: Replace attrs by dataclasses #557

Merged
merged 1 commit into from
Apr 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def run_apidoc(app):
# # 'inherited-members': True
# }
autodoc_member_order = "bysource"

nitpicky = True
nitpick_ignore = [
("py:class", "docutils.nodes.document"),
("py:class", "docutils.nodes.docinfo"),
Expand Down
142 changes: 142 additions & 0 deletions myst_parser/dc_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Validators for dataclasses, mirroring those of https://github.com/python-attrs/attrs."""
from __future__ import annotations

import dataclasses as dc
from typing import Any, Callable, Sequence, Type


def validate_fields(inst):
"""Validate the fields of a dataclass,
according to `validator` functions set in the field metadata.

This function should be called in the `__post_init__` of the dataclass.

The validator function should take as input (inst, field, value) and
raise an exception if the value is invalid.
"""
for field in dc.fields(inst):
if "validator" not in field.metadata:
continue
if isinstance(field.metadata["validator"], list):
for validator in field.metadata["validator"]:
validator(inst, field, getattr(inst, field.name))
else:
field.metadata["validator"](inst, field, getattr(inst, field.name))


ValidatorType = Callable[[Any, dc.Field, Any], None]


def instance_of(type: Type[Any] | tuple[Type[Any], ...]) -> ValidatorType:
"""
A validator that raises a `TypeError` if the initializer is called
with a wrong type for this particular attribute (checks are performed using
`isinstance` therefore it's also valid to pass a tuple of types).

:param type: The type to check for.
"""

def _validator(inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not isinstance(value, type):
raise TypeError(
f"'{attr.name}' must be {type!r} (got {value!r} that is a {value.__class__!r})."
)

return _validator


def optional(validator: ValidatorType) -> ValidatorType:
"""
A validator that makes an attribute optional. An optional attribute is one
which can be set to ``None`` in addition to satisfying the requirements of
the sub-validator.
"""

def _validator(inst, attr, value):
if value is None:
return

validator(inst, attr, value)

return _validator


def is_callable(inst, attr, value):
"""
A validator that raises a `TypeError` if the
initializer is called with a value for this particular attribute
that is not callable.
"""
if not callable(value):
raise TypeError(
f"'{attr.name}' must be callable "
f"(got {value!r} that is a {value.__class__!r})."
)


def in_(options: Sequence) -> ValidatorType:
"""
A validator that raises a `ValueError` if the initializer is called
with a value that does not belong in the options provided. The check is
performed using ``value in options``.

:param options: Allowed options.
"""

def _validator(inst, attr, value):
try:
in_options = value in options
except TypeError: # e.g. `1 in "abc"`
in_options = False

if not in_options:
raise ValueError(f"'{attr.name}' must be in {options!r} (got {value!r})")

return _validator


def deep_iterable(
member_validator: ValidatorType, iterable_validator: ValidatorType | None = None
) -> ValidatorType:
"""
A validator that performs deep validation of an iterable.

:param member_validator: Validator to apply to iterable members
:param iterable_validator: Validator to apply to iterable itself
"""

def _validator(inst, attr, value):
if iterable_validator is not None:
iterable_validator(inst, attr, value)

for member in value:
member_validator(inst, attr, member)

return _validator


def deep_mapping(
key_validator: ValidatorType,
value_validator: ValidatorType,
mapping_validator: ValidatorType | None = None,
) -> ValidatorType:
"""
A validator that performs deep validation of a dictionary.

:param key_validator: Validator to apply to dictionary keys
:param value_validator: Validator to apply to dictionary values
:param mapping_validator: Validator to apply to top-level mapping attribute (optional)
"""

def _validator(inst, attr, value):
if mapping_validator is not None:
mapping_validator(inst, attr, value)

for key in value:
key_validator(inst, attr, key)
value_validator(inst, attr, value[key])

return _validator
8 changes: 4 additions & 4 deletions myst_parser/docutils_.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
.. include:: path/to/file.md
:parser: myst_parser.docutils_
"""
from dataclasses import Field
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union

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


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


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

Expand Down