diff --git a/craft_grammar/_processor.py b/craft_grammar/_processor.py index cff30ac..7c3ff25 100644 --- a/craft_grammar/_processor.py +++ b/craft_grammar/_processor.py @@ -120,6 +120,9 @@ def process( if statement is None: primitives.append(section) + elif isinstance(section, (int | float | bool | list)): + # If the section is a number, boolean, or list, it's a primitive. + primitives.append(section) else: # jsonschema should never let us get here. raise GrammarSyntaxError( diff --git a/craft_grammar/models.py b/craft_grammar/models.py index 694b758..f06dbe9 100644 --- a/craft_grammar/models.py +++ b/craft_grammar/models.py @@ -31,6 +31,9 @@ _TRY = "try" +_GrammarType = Dict[str, Any] + + class _GrammarBase(abc.ABC): @classmethod def __get_validators__(cls): @@ -50,10 +53,77 @@ def _grammar_append(cls, entry: List, item: Any) -> None: _mark_and_append(entry, {key: cls.validate(value)}) -_GrammarType = Dict[str, Any] +# Public types for grammar-enabled attributes +class GrammarBool(_GrammarBase): + """Grammar-enabled bool field.""" + __root__: Union[bool, _GrammarType] -# Public types for grammar-enabled attributes + @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 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): @@ -128,6 +198,55 @@ def validate(cls, 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): + # 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): + new_entry.append(item) + else: + raise TypeError(f"value must be a list of dictionaries: {entry!r}") + return new_entry + + if isinstance(entry, dict): + return entry + + raise TypeError(f"value must be a dictionary: {entry!r}") + + +class GrammarDictList(_GrammarBase): + """Grammar-enabled list of dictionary 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): + 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}") + + def _ensure_selector_valid(selector: str, *, clause: str) -> None: """Verify selector syntax.""" # spaces are not allowed in selector diff --git a/tests/unit/test_compound.py b/tests/unit/test_compound.py index 55d7f25..94187ac 100644 --- a/tests/unit/test_compound.py +++ b/tests/unit/test_compound.py @@ -159,7 +159,6 @@ @pytest.mark.parametrize("scenario", scenarios) def test_compound_statement(scenario): - processor = GrammarProcessor( arch=scenario["arch"], target_arch="armhf", diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 365c4c1..868cc6c 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -21,16 +21,30 @@ import pytest import yaml -from craft_grammar.models import GrammarSingleEntryDictList, GrammarStr, GrammarStrList +from craft_grammar.models import ( + GrammarBool, + GrammarDict, + GrammarDictList, + GrammarFloat, + GrammarInt, + GrammarSingleEntryDictList, + GrammarStr, + GrammarStrList, +) class ValidationTest(pydantic.BaseModel): """A test model containing all types of grammar-aware types.""" control: str + grammar_bool: GrammarBool + grammar_int: GrammarInt + grammar_float: GrammarFloat grammar_str: GrammarStr grammar_strlist: GrammarStrList + grammar_dict: GrammarDict grammar_single_entry_dictlist: GrammarSingleEntryDictList + grammar_dictlist: GrammarDictList def test_validate_grammar_trivial(): @@ -38,26 +52,45 @@ def test_validate_grammar_trivial(): textwrap.dedent( """ control: a string + grammar_bool: true + grammar_int: 42 + grammar_float: 3.14 grammar_str: another string grammar_strlist: - a - string - list + grammar_dict: + key: value + other_key: other_value grammar_single_entry_dictlist: - key: value - other_key: other_value + grammar_dictlist: + - key: value + other_key: other_value + - key2: value + other_key2: other_value """ ) ) v = ValidationTest(**data) assert v.control == "a string" + assert v.grammar_bool is True + assert v.grammar_int == 42 + assert v.grammar_float == 3.14 assert v.grammar_str == "another string" assert v.grammar_strlist == ["a", "string", "list"] + assert v.grammar_dict == {"key": "value", "other_key": "other_value"} assert v.grammar_single_entry_dictlist == [ {"key": "value"}, {"other_key": "other_value"}, ] + assert v.grammar_dictlist == [ + {"key": "value", "other_key": "other_value"}, + {"key2": "value", "other_key2": "other_value"}, + ] def test_validate_grammar_simple(): @@ -65,26 +98,59 @@ def test_validate_grammar_simple(): textwrap.dedent( """ control: a string + grammar_bool: + - on amd64: true + - else: false + grammar_int: + - on amd64: 42 + - else: 23 + grammar_float: + - on amd64: 3.14 + - else: 2.71 grammar_str: - on amd64: another string - else: something different grammar_strlist: - to amd64,arm64: - - a - - string - - list + - a + - string + - list + - else fail + grammar_dict: + - on amd64: + key: value + other_key: other_value - else fail grammar_single_entry_dictlist: - on arch: - key: value - other_key: other_value - else fail + grammar_dictlist: + - on arch: + - key: value + other_key: other_value + - key2: value + other_key2: other_value + - else fail """ ) ) v = ValidationTest(**data) assert v.control == "a string" + assert v.grammar_bool == [ + {"*on amd64": True}, + {"*else": False}, + ] + assert v.grammar_int == [ + {"*on amd64": 42}, + {"*else": 23}, + ] + assert v.grammar_float == [ + {"*on amd64": 3.14}, + {"*else": 2.71}, + ] assert v.grammar_str == [ {"*on amd64": "another string"}, {"*else": "something different"}, @@ -93,10 +159,23 @@ def test_validate_grammar_simple(): {"*to amd64,arm64": ["a", "string", "list"]}, "*else fail", ] + assert v.grammar_dict == [ + {"*on amd64": {"key": "value", "other_key": "other_value"}}, + "*else fail", + ] assert v.grammar_single_entry_dictlist == [ {"*on arch": [{"key": "value"}, {"other_key": "other_value"}]}, "*else fail", ] + assert v.grammar_dictlist == [ + { + "*on arch": [ + {"key": "value", "other_key": "other_value"}, + {"key2": "value", "other_key2": "other_value"}, + ] + }, + "*else fail", + ] def test_validate_grammar_recursive(): @@ -104,6 +183,21 @@ def test_validate_grammar_recursive(): textwrap.dedent( """ control: a string + grammar_bool: + - on amd64: true + - else: + - to arm64: false + - else fail + grammar_int: + - on amd64: 42 + - else: + - to arm64: 23 + - else fail + grammar_float: + - on amd64: 3.14 + - else: + - to arm64: 2.71 + - else fail grammar_str: - on amd64: another string - else: @@ -127,6 +221,16 @@ def test_validate_grammar_recursive(): - else: - other - stuff + grammar_dict: + - on amd64,arm64: + key: value + other_key: other_value + - else: + - on other_arch: + - to yet_another_arch: + yet_another_key: yet_another_value + - else fail + - else fail grammar_single_entry_dictlist: - on arch,other_arch: - on other_arch: @@ -137,12 +241,37 @@ def test_validate_grammar_recursive(): - else: - yet_another_key: yet_another_value - else fail + grammar_dictlist: + - on arch,other_arch: + - on other_arch: + - to yet_another_arch: + - key: value + other_key: other_value + - key2: value + other_key2: other_value + - else fail + - else: + - yet_another_key: yet_another_value + - yet_another_key2: yet_another_value2 + - else fail """ ) ) v = ValidationTest(**data) assert v.control == "a string" + assert v.grammar_bool == [ + {"*on amd64": True}, + {"*else": [{"*to arm64": False}, "*else fail"]}, + ] + assert v.grammar_int == [ + {"*on amd64": 42}, + {"*else": [{"*to arm64": 23}, "*else fail"]}, + ] + assert v.grammar_float == [ + {"*on amd64": 3.14}, + {"*else": [{"*to arm64": 2.71}, "*else fail"]}, + ] assert v.grammar_str == [ {"*on amd64": "another string"}, { @@ -173,6 +302,25 @@ def test_validate_grammar_recursive(): }, {"*else": ["other", "stuff"]}, ] + assert v.grammar_dict == [ + {"*on amd64,arm64": {"key": "value", "other_key": "other_value"}}, + { + "*else": [ + { + "*on other_arch": [ + { + "*to yet_another_arch": { + "yet_another_key": "yet_another_value" + } + }, + "*else fail", + ] + }, + "*else fail", + ] + }, + ] + assert v.grammar_single_entry_dictlist == [ { "*on arch,other_arch": [ @@ -193,6 +341,35 @@ def test_validate_grammar_recursive(): "*else fail", ] + assert v.grammar_dictlist == [ + { + "*on arch,other_arch": [ + { + "*on other_arch": [ + { + "*to yet_another_arch": [ + {"key": "value", "other_key": "other_value"}, + {"key2": "value", "other_key2": "other_value"}, + ] + }, + "*else fail", + ] + }, + { + "*else": [ + { + "yet_another_key": "yet_another_value", + }, + { + "yet_another_key2": "yet_another_value2", + }, + ] + }, + ] + }, + "*else fail", + ] + @pytest.mark.parametrize( "value", diff --git a/tests/unit/test_processor.py b/tests/unit/test_processor.py index 68ffa17..7434656 100644 --- a/tests/unit/test_processor.py +++ b/tests/unit/test_processor.py @@ -338,11 +338,6 @@ def transformer(call_stack, package_name, target_arch): "grammar_entry": ["else fail"], "expected_exception": ".*'else' doesn't seem to correspond.*", }, - # unexpected type - { - "grammar_entry": [5], - "expected_exception": ".*expected grammar section.*but got.*", - }, ]