Skip to content

Commit

Permalink
feat(grammar)!: generalized grammar type class
Browse files Browse the repository at this point in the history
  • Loading branch information
syu-w committed Apr 9, 2024
1 parent a1317b9 commit fadbea4
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 296 deletions.
293 changes: 104 additions & 189 deletions craft_grammar/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

import abc
import re
from typing import Any, Dict, List, Union
from typing import Any, Dict, List, get_args, get_origin

from pydantic import BaseConfig, PydanticTypeError
from pydantic.validators import find_validators

from overrides import overrides

Expand All @@ -33,6 +36,8 @@

_GrammarType = Dict[str, Any]

_CONFIG = BaseConfig()


class _GrammarBase(abc.ABC):
@classmethod
Expand All @@ -53,196 +58,106 @@ def _grammar_append(cls, entry: List, item: Any) -> None:
_mark_and_append(entry, {key: cls.validate(value)})


# Public types for grammar-enabled attributes
class GrammarBool(_GrammarBase):
"""Grammar-enabled bool field."""

__root__: Union[bool, _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarBool entry can be a list if it contains clauses
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
else:
raise TypeError(f"value must be a list of bool: {entry!r}")
return new_entry

if isinstance(entry, bool):
return entry

raise TypeError(f"value must be a bool: {entry!r}")


class GrammarInt(_GrammarBase):
"""Grammar-enabled integer field."""

__root__: Union[int, _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarInt entry can be a list if it contains clauses
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
else:
raise TypeError(f"value must be a list of integer: {entry!r}")
return new_entry

if isinstance(entry, int):
return entry

raise TypeError(f"value must be a integer: {entry!r}")


class GrammarFloat(_GrammarBase):
"""Grammar-enabled float field."""

__root__: Union[float, _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarFloat entry can be a list if it contains clauses
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
else:
raise TypeError(f"value must be a list of float: {entry!r}")
return new_entry

if isinstance(entry, (int, float)):
return float(entry)

raise TypeError(f"value must be a float: {entry!r}")


class GrammarStr(_GrammarBase):
"""Grammar-enabled string field."""

__root__: Union[str, _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarStr entry can be a list if it contains clauses
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
else:
raise TypeError(f"value must be a string: {entry!r}")
return new_entry

if isinstance(entry, str):
return entry

raise TypeError(f"value must be a string: {entry!r}")


class GrammarStrList(_GrammarBase):
"""Grammar-enabled list of strings field."""

__root__: Union[List[Union[str, _GrammarType]], _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarStrList will always be a list
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
elif isinstance(item, str):
new_entry.append(item)
else:
raise TypeError(f"value must be a list of string: {entry!r}")
return new_entry

raise TypeError(f"value must be a list of string: {entry!r}")


class GrammarSingleEntryDictList(_GrammarBase):
"""Grammar-enabled list of dictionaries field."""

__root__: Union[List[Dict[str, Any]], _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarSingleEntryDictList will always be a list
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
elif isinstance(item, dict) and len(item) == 1:
new_entry.append(item)
else:
raise TypeError(
f"value must be a list of single-entry dictionaries: {entry!r}"
)
return new_entry

raise TypeError(f"value must be a list of single-entry dictionaries: {entry!r}")


class GrammarDict(_GrammarBase):
"""Grammar-enabled dictionary field."""

__root__: Union[Dict[str, Any], _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarDict entry can be a list if it contains clauses
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
else:
raise TypeError(f"value must be a list of dictionaries: {entry!r}")
return new_entry

if isinstance(entry, dict):
return entry
def _format_type_error(type_: type, entry: Any) -> str:
"""Format a type error message."""
origin = get_origin(type_)
args = get_args(type_)

raise TypeError(f"value must be a dictionary: {entry!r}")
# handle primitive types which origin is None
if not origin:
origin = type_


class GrammarDictList(_GrammarBase):
"""Grammar-enabled list of dictionary field."""

__root__: Union[List[Dict[str, Any]], _GrammarType]

@classmethod
@overrides
def validate(cls, entry):
# GrammarDictList will always be a list
if isinstance(entry, list):
new_entry = []
for item in entry:
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
elif isinstance(item, dict):
new_entry.append(item)
else:
raise TypeError(f"value must be a list of dictionaries: {entry!r}")
return new_entry

raise TypeError(f"value must be a list of dictionary: {entry!r}")
if issubclass(origin, list):
if args:
return f"value must be a list of {args[0].__name__}: {entry!r}"
else:
return f"value must be a list: {entry!r}"
elif issubclass(origin, dict):
if len(args) == 2:
return f"value must be a dict of {args[0].__name__} and {args[1].__name__}: {entry!r}"
else:
return f"value must be a dict: {entry!r}"
else:
return f"value must be a {type_.__name__}: {entry!r}"


class GrammarGeneratorMetaClass(type):
# Define __getitem__ method to be able to use index
def __getitem__(self, type_):
# Define Main Class
class GrammarScalar(_GrammarBase):

_type = type_

@classmethod
@overrides
def validate(cls, entry):
# Grammar[T] entry can be a list if it contains clauses
if isinstance(entry, list):
# Check if the type_ supposed to be a list
sub_type = get_args(cls._type)

# handle typed list
if sub_type:
sub_type = sub_type[0]
if sub_type is Any:
sub_type = None

new_entry = []
for item in entry:
# Check if the item is a valid grammar clause
if _is_grammar_clause(item):
cls._grammar_append(new_entry, item)
else:
# Check if the item is a valid type if not a grammar clause
if sub_type and isinstance(item, sub_type):
new_entry.append(item)
else:
raise TypeError(_format_type_error(cls._type, entry))

return new_entry

# Not a valid grammar, check if it is a dict
if isinstance(entry, dict):
# Check if the type_ supposed to be a dict
if get_origin(cls._type) is not dict:
raise TypeError(_format_type_error(cls._type, entry))

sub_type = get_args(cls._type)
# The dict is not a typed dict
if not sub_type:
return entry

sub_key_type = sub_type[0] if sub_type else Any
sub_value_type = sub_type[1] if sub_type else Any

# validate the dict
for k, v in entry.items():
if (sub_key_type is Any or isinstance(k, sub_key_type)) and (
sub_value_type is Any or isinstance(v, sub_value_type)
):
# we do not need the return value if it is a valid dict
pass
else:
raise TypeError(_format_type_error(cls._type, entry))

return entry

# handle standard types with pydantic validators
try:
for validator in find_validators(cls._type, _CONFIG):
# we do not need the return value of the validator
validator(entry)
except PydanticTypeError:
raise TypeError(_format_type_error(cls._type, entry))

return entry

return GrammarScalar


class GrammarGenerator(metaclass=GrammarGeneratorMetaClass):
pass


def _ensure_selector_valid(selector: str, *, clause: str) -> None:
Expand Down
Loading

0 comments on commit fadbea4

Please sign in to comment.