diff --git a/src/devana/preprocessing/components/property/parsers/attributeparser.py b/src/devana/preprocessing/components/property/parsers/attributeparser.py index be1ad47..ac25d80 100644 --- a/src/devana/preprocessing/components/property/parsers/attributeparser.py +++ b/src/devana/preprocessing/components/property/parsers/attributeparser.py @@ -1,10 +1,13 @@ -from typing import Type, Iterable, Optional +from typing import Iterable, Optional, Dict, Any +from itertools import count from devana.preprocessing.components.property.parsers.types import * -from devana.preprocessing.components.property.parsers.descriptions import IDescribedProperty, IDescribedType +from devana.preprocessing.components.property.parsers.descriptions import IDescribedProperty from devana.preprocessing.preprocessor import IGenerator from devana.syntax_abstraction.attribute import Attribute -from devana.preprocessing.components.property.parsers.result import Result +from devana.preprocessing.components.property.parsers.result import Result, Arguments, Value, PropertySignature from devana.preprocessing.components.property.parsers.configuration import Configuration +from devana.preprocessing.components.property.parsers.parser import ParsingBackend +from devana.preprocessing.components.property.parsers.types import parsable_element_from_described_type class AttributeParser(IGenerator): @@ -14,15 +17,19 @@ def __init__(self, properties: Optional[List[IDescribedProperty]] = None, configuration: Optional[Configuration] = None): self._properties = properties if properties is not None else [] self._configuration = configuration if configuration else Configuration(ignore_unknown=True) + self._backend = ParsingBackend() if self._configuration.ignore_unknown is False: raise ValueError("The attribute parser does not allow a restrictive approach to unknown attributes due to " "the existence of attributes that are compiler extensions.") - for p in self._properties: - self._validate_property(p) + for prop in self._properties: + self._validate_property(prop) + self._add_types(prop) + def add_property(self, prop: IDescribedProperty): self._validate_property(prop) self._properties.append(prop) + self._add_types(prop) @classmethod def get_required_type(cls) -> Type: @@ -33,7 +40,104 @@ def get_produced_type(cls) -> Type: return Result def generate(self, data: Iterable[Attribute]) -> Iterable[Result]: - return [] + for attr in data: + + def maybe_proto_gen(in_attr): + maybe_prop_result = filter(lambda e: e.namespace == in_attr.namespace, self._properties) + maybe_prop_result = list(filter(lambda e: e.name == in_attr.name, maybe_prop_result)) + return maybe_prop_result + + maybe_prop = maybe_proto_gen(attr) + if len(maybe_prop) == 0: + raise ValueError(f"Unknown property: {attr.name} in namespace: {attr.namespace}") + prop: IDescribedProperty = maybe_prop[0] + # matching property find as prop, so lets parse arguments + parsed_arguments = [] + if attr.arguments is not None: + parsed_arguments = [self._backend.parse_argument(a) for a in attr.arguments] + + # check if positional arguments are not mixed with named ones + is_positional_finished = False + for a in parsed_arguments: + if a.name is None and is_positional_finished: + raise ValueError(f"Mixed positional arguments and named is not allowed. Error in {prop.name}") + if a.name is not None: + is_positional_finished = True + + result_args = Arguments() + + result_positional_arguments = [] + result_named_arguments: Dict[str, Any] = {} + + # now try a match parsing result to expected arguments + + for i, a in zip(count(), parsed_arguments): + if a.name is None: + result_positional_arguments.append(a) + else: + if a.name in result_named_arguments: + raise ValueError(f"Duplicated argument name {a.name} while parsing property {prop.name}.") + result_named_arguments[a.name] = a + + # now we need to do the main validation - connect the paired parameters with the expected arguments + for i, a in zip(count(), result_positional_arguments): + if i >= len(prop.arguments): + raise ValueError("Too many arguments.") + expected_type = prop.arguments[i].type + given_type = a.type + if expected_type.name != given_type.name: # types have unique names + raise ValueError(f"Expected type {expected_type} but got {given_type}") + result_args.positional.append(Value(a.value)) + + for key in result_named_arguments: + + def expected_types_gen(expected_name: str, props: IDescribedProperty): + return list(filter(lambda e: e.name == expected_name, props.arguments)) + + expected_type_list = expected_types_gen(key, prop) + if len(expected_type_list) == 0: + raise ValueError(f"Unknown argument named {key} in {prop.name}") + expected_type = expected_type_list[0].type + given_type = result_named_arguments[key].type + if expected_type.name != given_type.name: # types have unique names + raise ValueError(f"Expected type {expected_type} but got {given_type}") + result_args.named[key] = Value(result_named_arguments[key].value) + + # now make a list of the remaining arguments based on the property description + remaining_expected_args = prop.arguments.copy() + del remaining_expected_args[: len(result_args.positional)] + + for key in result_args.named: + def elements_gen(key_name: str, expected): + return list(filter(lambda e: e.name == key_name, expected)) + + elements = elements_gen(key, remaining_expected_args) + if len(elements) == 0: + raise ValueError(f"Unknown argument named {key} in {prop.name}") + if len(elements) > 1: + raise ValueError(f"Multiple argument named {key} in {prop.name}") + element = elements[0] + remaining_expected_args.remove(element) + + # the parser does not provide default parameter values (because it is not its job) + # but we need to check if the function signature is satisfied - that is, + # if all parameters that do not + # have default values have been provided + if list(filter(lambda e: e.default_value is None, remaining_expected_args)): + raise ValueError(f"Not all parameters were provided for: {prop.name}") + + signature = PropertySignature( + name = prop.name, + namespaces = [prop.namespace] if prop.namespace else [], + arguments = [self._backend.types[n].result_type for n in [e.type.name for e in prop.arguments]] + ) + + yield Result( + property = signature, + arguments= result_args, + target=attr.parent + ) + @staticmethod def _validate_property(prop: IDescribedProperty): @@ -41,3 +145,14 @@ def _validate_property(prop: IDescribedProperty): if not prop.namespace: raise ValueError("Due to the existence of compiler extension attributes and standard standard " "attributes, it is required that preprocessor attributes use namespace.") + # check duplicates + for a1 in prop.arguments: + for a2 in prop.arguments: + if a1 != a2: + if (a1.name is not None or a2.name is not None) and a1.name == a2.name: + raise ValueError(f"Duplicated property name for property: {prop.name}") + + def _add_types(self, desc: IDescribedProperty): + """Add types to parser from IDescribedProperty.""" + for arg in desc.arguments: + self._backend.add_type(parsable_element_from_described_type(arg.type)) diff --git a/src/devana/preprocessing/components/property/parsers/descriptions.py b/src/devana/preprocessing/components/property/parsers/descriptions.py index f6062d6..99bbfba 100644 --- a/src/devana/preprocessing/components/property/parsers/descriptions.py +++ b/src/devana/preprocessing/components/property/parsers/descriptions.py @@ -50,6 +50,13 @@ def type(self) -> IDescribedType: def default_value(self) -> Optional[IDescribedValue]: """Default value of argument, if any.""" + @property + @abstractmethod + def name(self) -> Optional[str]: + """Name of argument. + If the argument is only and exclusively positional, it can be None. + Otherwise, the syntax name1 = val1, name2 = val, etc. is allowed.""" + class IDescribedProperty(ABC): """Description property for parsing and diagnostic messages.""" diff --git a/src/devana/preprocessing/components/property/parsers/parser.py b/src/devana/preprocessing/components/property/parsers/parser.py index e89c584..1943dba 100644 --- a/src/devana/preprocessing/components/property/parsers/parser.py +++ b/src/devana/preprocessing/components/property/parsers/parser.py @@ -1,7 +1,8 @@ -from typing import Optional, Any, Union, Type +import re +from typing import Optional, Any, Union, Type, Dict from abc import ABC, abstractmethod from dataclasses import dataclass - +from devana.preprocessing.components.property.parsers.descriptions import IDescribedType @dataclass class ParsableElementError: @@ -26,3 +27,70 @@ def parse(self, text: str) -> Union[ParsableElementError, Any]: @abstractmethod def result_type(self) -> Type: """Python type of result.""" + + +class IParsableType(IParsableElement, IDescribedType, ABC): + """Mix of two interfaces needed by parser.""" + + +class ParsingBackend: + """Core backend for all parser.""" + + @dataclass + class Argument: + """Class holding result arguments.""" + value: Any + type: IDescribedType + name: Optional[str] = None + + + def __init__(self): + self._types: Dict[str, IParsableElement] = {} + + @property + def types(self) -> Dict[str, IParsableElement]: + return self._types + + def add_type(self, element: IParsableType): + """Add a new type to parser. May raise exceptions for duplicate types.""" + if element.name not in self._types: + self._types[element.name] = element + if element != self._types[element.name]: + raise ValueError("Duplicate type name.") + + #__argument_pattern = re.compile(r'(^\s*(?P(\w|")+)\s*=\s*(?P\w+)\s*$)|^\s*(?P(\w|")+)\s*$') + + __argument_pattern = re.compile( + r'(^\s*(?P\w+)\s*=\s*(?P.+)\s*$)|^\s*(?P.+)\s*$') + + + @dataclass + class ParsedValue: + """Internal value parsing result.""" + text: str + type: IDescribedType + + + def _parse_value(self, text: str) -> ParsedValue: + for t in self._types.values(): + result = t.parse(text) + if isinstance(result, ParsableElementError): + if result.is_meaningless: + continue + raise ValueError(f"Parsing error: {result.what}") + return ParsingBackend.ParsedValue(result, t) + raise ValueError("Unable to find matching type.") + + def parse_argument(self, text: str) -> Argument: + match = self.__argument_pattern.match(text) + if match is None: + raise ValueError("Cannot parse argument.") + matches = match.groupdict() + if matches["value"] is not None: + parsing_result = self._parse_value(matches["value"]) + return self.Argument(parsing_result.text, parsing_result.type) + elif matches["named_value"] is not None and matches["name"] is not None: + parsing_result = self._parse_value(matches["named_value"]) + return self.Argument(parsing_result.text, parsing_result.type, matches["name"]) + else: + raise ValueError("Cannot parse argument.") diff --git a/src/devana/preprocessing/components/property/parsers/result.py b/src/devana/preprocessing/components/property/parsers/result.py index cc70739..027c6ed 100644 --- a/src/devana/preprocessing/components/property/parsers/result.py +++ b/src/devana/preprocessing/components/property/parsers/result.py @@ -19,15 +19,15 @@ def type(self) -> Type: class PropertySignature: """Invoking target property identification data.""" name: str - namespaces: List[str] = field(default_factory=lambda: []) - arguments: List[Type] = field(default_factory=lambda: []) + namespaces: List[str] = field(default_factory=list) + arguments: List[Type] = field(default_factory=list) @dataclass class Arguments: """Calling arguments.""" - positional: List[Value] - named: Dict[str, Value] + positional: List[Value] = field(default_factory=list) + named: Dict[str, Value] = field(default_factory=dict) @dataclass diff --git a/src/devana/preprocessing/components/property/parsers/types.py b/src/devana/preprocessing/components/property/parsers/types.py index 43ec47a..738aaa5 100644 --- a/src/devana/preprocessing/components/property/parsers/types.py +++ b/src/devana/preprocessing/components/property/parsers/types.py @@ -1,10 +1,14 @@ from typing import List, Union, Type import re from devana.preprocessing.components.property.parsers.descriptions import IDescribedType -from devana.preprocessing.components.property.parsers.parser import IParsableElement, ParsableElementError +from devana.preprocessing.components.property.parsers.parser import IParsableType, ParsableElementError +from devana.utility.typeregister import register -class IntegerType(IDescribedType, IParsableElement): +__register = [] + +@register(__register) +class IntegerType(IParsableType): """Representation of integer number.""" @property @@ -24,7 +28,8 @@ def result_type(self) -> Type: return int -class FloatType(IDescribedType, IParsableElement): +@register(__register) +class FloatType(IParsableType): """Representation of floating point number.""" @property @@ -44,7 +49,8 @@ def result_type(self) -> Type: return float -class StringType(IDescribedType, IParsableElement): +@register(__register) +class StringType(IParsableType): """Representation of text.""" @property @@ -64,7 +70,8 @@ def result_type(self) -> Type: return str -class BooleanType(IDescribedType, IParsableElement): +@register(__register) +class BooleanType(IParsableType): """Representation of true/false.""" @property @@ -105,7 +112,7 @@ def name(self) -> str: class _GenericType(IDescribedType): - """internal mixin for generic implementation.""" + """Internal mixin for generic implementation.""" def __init__(self, name: str, specialization: IDescribedType): self._name = name self._specialization = specialization @@ -120,19 +127,19 @@ def name(self) -> str: class ListType(_GenericType): - """internal of list like []""" + """Internal of list like []""" def __init__(self, specialization: IDescribedType): super().__init__("List", specialization) class OptionalType(_GenericType): - """internal of optional can be value of specialization type or none.""" + """Internal of optional can be value of specialization type or none.""" def __init__(self, specialization: IDescribedType): super().__init__("Optional", specialization) class UnionType(IDescribedType): - """internal of union type can be value of many types.""" + """Internal of union type can be value of many types.""" def __init__(self, types: List[IDescribedType]): self._types = types @@ -143,3 +150,15 @@ def types(self) -> List[IDescribedType]: @property def name(self) -> str: return f"Union<{'|'.join([s.name for s in self._types])}>" + + +def parsable_element_from_described_type(desc: IDescribedType) -> IParsableType: + """Create a parsing type from described.""" + if isinstance(desc, IParsableType): + return desc + matches = list(filter(lambda e: e.name == desc.name, __register)) + if len(matches) == 0: + raise ValueError(f"Could not find parser for type: {desc.name}") + if len(matches) > 1: + raise ValueError(f"Found more than one parser for type: {desc.name}") + return matches[0] diff --git a/src/devana/utility/__init__.py b/src/devana/utility/__init__.py index 0a86cca..7317aa0 100644 --- a/src/devana/utility/__init__.py +++ b/src/devana/utility/__init__.py @@ -5,3 +5,4 @@ from .lazy import LazyNotInit, lazy_invoke from .errors import CodeError, ParserError from .fakeenum import FakeEnum +from .typeregister import register diff --git a/src/devana/utility/lazy.py b/src/devana/utility/lazy.py index 7f66d89..dc488bb 100644 --- a/src/devana/utility/lazy.py +++ b/src/devana/utility/lazy.py @@ -2,8 +2,9 @@ class LazyNotInit: - """Value used by lazy_invoke to determine property is initialized or not. Set this type to value of property - in init function to inform that value must be initialized. For example, self._name = LazyNotInit""" + """The Value used by lazy_invoke to determine property is initialized or not. + Set this type to value of property in init function to inform that value must be initialized. + For example, self._name = LazyNotInit""" def __new__(cls): return cls diff --git a/src/devana/utility/typeregister.py b/src/devana/utility/typeregister.py new file mode 100644 index 0000000..5b3ddb9 --- /dev/null +++ b/src/devana/utility/typeregister.py @@ -0,0 +1,10 @@ +from typing import List, Type + +def register(current_register: List[Type]): + """Registers a type in the given variable - usually a global module variable. + Useful for automatically creating lists of default supported classes, etc.""" + + def wrapper(cls: Type): + current_register.append(cls()) + return cls + return wrapper diff --git a/tests/preprocessing/unit/components/property/parsers/test_attributes.py b/tests/preprocessing/unit/components/property/parsers/test_attributes.py index 38f7144..b77f72d 100644 --- a/tests/preprocessing/unit/components/property/parsers/test_attributes.py +++ b/tests/preprocessing/unit/components/property/parsers/test_attributes.py @@ -1,26 +1,222 @@ -# import unittest -# from dataclasses import dataclass, field -# import dataclasses -# from typing import Optional, List -# from devana.preprocessing.components.property.parsers.attributeparser import AttributeParser -# from devana.preprocessing.components.property.parsers.descriptions import IDescribedProperty, IDescribedArgument -# from devana.syntax_abstraction.attribute import Attribute -# -# -# @dataclass -# class DescribedProperty(IDescribedProperty): -# namespace: Optional[str] = dataclasses.MISSING -# name: str = dataclasses.MISSING -# arguments: List[IDescribedArgument] = field(default=lambda: []) -# -# class TestAttributesParserPositional(unittest.TestCase): -# -# def test_empty_attribute(self): -# attributes = [Attribute.from_text("devana::foo()")] -# properties = [DescribedProperty("devana", "foo")] -# parser = AttributeParser(properties) -# results = list(parser.generate(attributes)) -# # self.assertEqual(len(results), 1) -# # result = results[0] -# # self.assertEqual(len(result.arguments.positional), 0) -# # self.assertEqual(len(result.arguments.named), 0) +import unittest +from dataclasses import dataclass +import dataclasses +from typing import Optional, List, Any +from devana.preprocessing.components.property.parsers.attributeparser import AttributeParser +from devana.preprocessing.components.property.parsers.descriptions import ( + IDescribedProperty, IDescribedArgument, IDescribedType, IDescribedValue) +from devana.preprocessing.components.property.parsers.types import FloatType, StringType +from devana.syntax_abstraction.attribute import Attribute + + +@dataclass +class TestDescribedProperty(IDescribedProperty): + namespace: Optional[str] = dataclasses.MISSING + name: str = dataclasses.MISSING + arguments: List[IDescribedArgument] = dataclasses.MISSING + + +@dataclass +class TestDescribedType(IDescribedType): + name: str = dataclasses.MISSING + + +@dataclass +class TestDescribedValue(IDescribedValue): + type: IDescribedType = dataclasses.MISSING + content_as_string: str = dataclasses.MISSING + content: Optional[Any] = dataclasses.MISSING + + +@dataclass +class TestDescribedArgument(IDescribedArgument): + name: Optional[str] = dataclasses.MISSING + type: IDescribedType = dataclasses.MISSING + default_value: Optional[IDescribedValue] = dataclasses.MISSING + + +class TestAttributesParser(unittest.TestCase): + + def test_empty_attribute(self): + attributes = [Attribute.from_text("devana::foo()")] + properties = [TestDescribedProperty("devana", "foo", [])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 0) + self.assertEqual(len(result.arguments.named), 0) + + def test_attribute_string(self): + attributes = [Attribute.from_text('devana::foo("Text 1")')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, TestDescribedType("String"), None) + ])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 1) + self.assertEqual(len(result.arguments.named), 0) + arg = result.arguments.positional[0] + self.assertEqual(arg.type, str) + self.assertEqual(arg.content, "Text 1") + + def test_attribute_float(self): + attributes = [Attribute.from_text('devana::foo(7.5)')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, TestDescribedType("Float"), None) + ])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 1) + self.assertEqual(len(result.arguments.named), 0) + arg = result.arguments.positional[0] + self.assertEqual(arg.type, float) + self.assertEqual(arg.content, 7.5) + + def test_attribute_float_without_dot(self): + attributes = [Attribute.from_text('devana::foo(7)')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, TestDescribedType("Float"), None) + ])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 1) + self.assertEqual(len(result.arguments.named), 0) + arg = result.arguments.positional[0] + self.assertEqual(arg.type, float) + self.assertEqual(arg.content, 7) + + def test_attribute_named_string(self): + attributes = [Attribute.from_text('devana::foo(arg1="Text 1")')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument("arg1", TestDescribedType("String"), None) + ])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 0) + self.assertEqual(len(result.arguments.named), 1) + arg = result.arguments.named["arg1"] + self.assertEqual(arg.type, str) + self.assertEqual(arg.content, "Text 1") + + def test_attribute_two_named_arguments_position_unmatch(self): + attributes = [Attribute.from_text('devana::foo(arg2="Text 1", arg1=768)')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument("arg1", TestDescribedType("Float"), None), + TestDescribedArgument("arg2", TestDescribedType("String"), None) + ])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 0) + self.assertEqual(len(result.arguments.named), 2) + arg = result.arguments.named["arg1"] + self.assertEqual(arg.type, float) + self.assertEqual(arg.content, 768) + arg = result.arguments.named["arg2"] + self.assertEqual(arg.type, str) + self.assertEqual(arg.content, "Text 1") + + def test_attribute_two_one_positional_one_named(self): + attributes = [Attribute.from_text('devana::foo("Text 1", arg1="Text 2")')] + string_type = TestDescribedType("String") + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, string_type, None), + TestDescribedArgument("arg1", string_type, None) + ])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 1) + self.assertEqual(len(result.arguments.named), 1) + arg = result.arguments.positional[0] + self.assertEqual(arg.type, str) + self.assertEqual(arg.content, "Text 1") + arg = result.arguments.named["arg1"] + self.assertEqual(arg.type, str) + self.assertEqual(arg.content, "Text 2") + + def test_attribute_positional_wrong_type(self): + attributes = [Attribute.from_text('devana::foo(888, arg1=98)')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, StringType(), None), + TestDescribedArgument("arg1", FloatType(), None) + ])] + parser = AttributeParser(properties) + with self.assertRaises(ValueError): + list(parser.generate(attributes)) + + def test_attribute_named_wrong_type(self): + attributes = [Attribute.from_text('devana::foo(arg2=888, arg1=98)')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument("arg2", StringType(), None), + TestDescribedArgument("arg1", FloatType(), None) + ])] + parser = AttributeParser(properties) + with self.assertRaises(ValueError): + list(parser.generate(attributes)) + + def test_attribute_named_wrong_name(self): + attributes = [Attribute.from_text('devana::foo(arg=888)')] + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument("arg1", FloatType(), None) + ])] + parser = AttributeParser(properties) + with self.assertRaises(ValueError): + list(parser.generate(attributes)) + + def test_attribute_too_many_arguments(self): + attributes = [Attribute.from_text('devana::foo(0.1, 0.2, 0.3, 0.4)')] + float_type = TestDescribedType("Float") + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, float_type, None), + TestDescribedArgument(None, float_type, None), + ])] + parser = AttributeParser(properties) + with self.assertRaises(ValueError): + list(parser.generate(attributes)) + + def test_attribute_duplicated_named_argument(self): + attributes = [Attribute.from_text('devana::foo(arg=0.6, arg=5)')] + float_type = TestDescribedType("Float") + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument("arg", float_type, None) + ])] + parser = AttributeParser(properties) + with self.assertRaises(ValueError): + list(parser.generate(attributes)) + + def test_attribute_positional_default_values_used_declaration(self): + attributes = [Attribute.from_text('devana::foo(0.8)')] + float_type = TestDescribedType("Float") + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, float_type, None), + TestDescribedArgument(None, float_type, TestDescribedValue(float_type, "0.7", 0.7)), + ])] + AttributeParser(properties) + + + def test_attribute_positional_default_values_parsed(self): + attributes = [Attribute.from_text('devana::foo(0.8)')] + float_type = TestDescribedType("Float") + properties = [TestDescribedProperty("devana", "foo", [ + TestDescribedArgument(None, float_type, None), + TestDescribedArgument(None, float_type, TestDescribedValue(float_type, "0.7", 0.7)), + ])] + parser = AttributeParser(properties) + results = list(parser.generate(attributes)) + self.assertEqual(len(results), 1) + result = results[0] + self.assertEqual(len(result.arguments.positional), 1) + self.assertEqual(len(result.arguments.named), 0) + self.assertEqual(result.arguments.positional[0].content, 0.8) diff --git a/tests/preprocessing/unit/components/property/parsers/test_parser.py b/tests/preprocessing/unit/components/property/parsers/test_parser.py index edb9741..d127240 100644 --- a/tests/preprocessing/unit/components/property/parsers/test_parser.py +++ b/tests/preprocessing/unit/components/property/parsers/test_parser.py @@ -1,6 +1,6 @@ import unittest +from devana.preprocessing.components.property.parsers.parser import ParsingBackend from devana.preprocessing.components.property.parsers.types import * -from devana.preprocessing.components.property.parsers.parser import IParsableElement, ParsableElementError class TestPreprocessorParsingTypesBoolean(unittest.TestCase): @@ -139,3 +139,95 @@ def test_extract_space(self): text = ' " test with somme data " ' result = parser.parse(text) self.assertEqual(result, " test with somme data ") + + +class TestPreprocessorParsingBackendParseArgumentStructure(unittest.TestCase): + + def setUp(self): + self._parser = ParsingBackend() + self._parser.add_type(BooleanType()) + self._parser.add_type(StringType()) + self._parser.add_type(FloatType()) + + def test_parse_stand_alone_argument(self): + parser = self._parser + text = "true" + result = parser.parse_argument(text) + self.assertEqual(result.value, True) + self.assertEqual(result.name, None) + + def test_parse_named_argument(self): + parser = self._parser + text = "argument=true" + result = parser.parse_argument(text) + self.assertEqual(result.value, True) + self.assertEqual(result.name, "argument") + + def test_parse_named_argument_complex(self): + parser = self._parser + text = " argument = true " + result = parser.parse_argument(text) + self.assertEqual(result.value, True) + self.assertEqual(result.name, "argument") + + def test_parse_named_argument_double_eq(self): + parser = self._parser + text = " argument = = true " + with self.assertRaises(ValueError): + parser.parse_argument(text) + + def test_parse_named_argument_no_eq_eq(self): + parser = self._parser + text = " argument true " + with self.assertRaises(ValueError): + parser.parse_argument(text) + + def test_parse_positional_string_without_space(self): + parser = self._parser + text = '"Text"' + result = parser.parse_argument(text) + self.assertEqual(result.value, "Text") + + def test_parse_positional_string_with_space(self): + parser = self._parser + text = '"Text 1"' + result = parser.parse_argument(text) + self.assertEqual(result.value, "Text 1") + + def test_parse_named_string_without_space(self): + parser = self._parser + text = 'name= "Text"' + result = parser.parse_argument(text) + self.assertEqual(result.value, "Text") + + def test_parse_named_string_with_space(self): + parser = self._parser + text = 'name= "Text 2"' + result = parser.parse_argument(text) + self.assertEqual(result.value, "Text 2") + + def test_parse_only_space(self): + parser = self._parser + text = ' ' + with self.assertRaises(ValueError): + parser.parse_argument(text) + + def test_parse_float_with_dot(self): + parser = self._parser + text = '7.5' + result = parser.parse_argument(text) + self.assertEqual(result.value, 7.5) + + def test_parse_eq_inside_string(self): + parser = self._parser + text = '"arg = 7"' + result = parser.parse_argument(text) + self.assertEqual(result.value, "arg = 7") + self.assertEqual(result.name, None) + + def test_parse_eq_inside_string_named(self): + parser = self._parser + text = 'arg2 ="arg = 7"' + result = parser.parse_argument(text) + self.assertEqual(result.value, "arg = 7") + self.assertEqual(result.name, "arg2")