diff --git a/chatette/__main__.py b/chatette/__main__.py index 3379ba94..922dc0ca 100644 --- a/chatette/__main__.py +++ b/chatette/__main__.py @@ -75,8 +75,7 @@ def _add_optional_arguments(argument_parser): argument_parser.add_argument( "-a", "--adapter", dest="adapter", required=False, type=str, default="rasa", - help="Write adapter. " + \ - "Possible values: ['rasa', 'rasamd' or 'rasa-md', 'jsonl']" + help="Write adapter. Possible values: ['rasa', 'jsonl', 'rasamd' or 'rasa-md', 'rasayml' or 'rasayaml']" ) argument_parser.add_argument( "--base-file", dest="base_filepath", diff --git a/chatette/adapters/_base.py b/chatette/adapters/_base.py index 2bd056f5..1e8c4139 100644 --- a/chatette/adapters/_base.py +++ b/chatette/adapters/_base.py @@ -53,7 +53,7 @@ def write(self, output_directory, examples, synonyms): self.__get_file_name( batch, output_directory, single_file_output ) - with io.open(output_file_path, 'w') as output_file: + with io.open(output_file_path, 'w', encoding='utf-8') as output_file: self._write_batch(output_file, batch) @classmethod diff --git a/chatette/adapters/factory.py b/chatette/adapters/factory.py index d7426c7b..7b71246b 100644 --- a/chatette/adapters/factory.py +++ b/chatette/adapters/factory.py @@ -6,6 +6,7 @@ from chatette.adapters.jsonl import JsonListAdapter from chatette.adapters.rasa import RasaAdapter from chatette.adapters.rasa_md import RasaMdAdapter +from chatette.adapters.rasa_yml import RasaYMLAdapter def create_adapter(adapter_name, base_filepath=None): @@ -23,6 +24,8 @@ def create_adapter(adapter_name, base_filepath=None): return RasaAdapter(base_filepath) elif adapter_name in ('rasa-md', 'rasamd'): return RasaMdAdapter(base_filepath) + elif adapter_name in ('rasa-yml', 'rasayml'): + return RasaYMLAdapter(base_filepath) elif adapter_name == 'jsonl': return JsonListAdapter(base_filepath) raise ValueError("Unknown adapter was selected.") diff --git a/chatette/adapters/rasa.py b/chatette/adapters/rasa.py index c5b63570..2ffe4547 100644 --- a/chatette/adapters/rasa.py +++ b/chatette/adapters/rasa.py @@ -38,12 +38,18 @@ def _write_batch(self, output_file_handle, batch): def prepare_example(self, example): def entity_to_rasa(entity): - return { + entity_dict = { "entity": entity.slot_name, "value": entity.value, "start": entity._start_index, "end": entity._start_index + entity._len, } + if entity.role is not None: + entity_dict['role'] = entity.role + if entity.group is not None: + entity_dict['group'] = entity.group + + return entity_dict return { "intent": example.intent_name, @@ -68,7 +74,7 @@ def _get_base_to_extend(self): if self._base_file_contents is None: if self._base_filepath is None: return self._get_empty_base() - with io.open(self._base_filepath, 'r') as base_file: + with io.open(self._base_filepath, 'r', encoding='utf-8') as base_file: self._base_file_contents = json.load(base_file) self.check_base_file_contents() return self._base_file_contents diff --git a/chatette/adapters/rasa_md.py b/chatette/adapters/rasa_md.py index 22f80c25..aa0fc881 100644 --- a/chatette/adapters/rasa_md.py +++ b/chatette/adapters/rasa_md.py @@ -76,11 +76,18 @@ def prepare_example(self, example): ) result = example.text[:] for entity in sorted_entities: + entity_annotation_text = ']{"entity": "' + entity.slot_name + entity_text = result[entity._start_index:entity._start_index + entity._len] + if entity_text != entity.value: + entity_annotation_text += '", "value": "{}'.format(entity.value) + if entity.role is not None: + entity_annotation_text += '", "role": "{}'.format(entity.role) + if entity.group is not None: + entity_annotation_text += '", "group": "{}'.format(entity.group) result = \ result[:entity._start_index] + "[" + \ - result[entity._start_index:entity._start_index + entity._len] + \ - "](" + entity.slot_name + ")" + \ - result[entity._start_index + entity._len:] + entity_text + entity_annotation_text + '"}' + \ + result[entity._start_index + entity._len:] # New rasa entity format return result @@ -105,7 +112,7 @@ def _get_base_to_extend(self): if self._base_file_contents is None: if self._base_filepath is None: return self._get_empty_base() - with io.open(self._base_filepath, 'r') as base_file: + with io.open(self._base_filepath, 'r', encoding='utf-8') as base_file: self._base_file_contents = ''.join(base_file.readlines()) self.check_base_file_contents() return self._base_file_contents diff --git a/chatette/adapters/rasa_yml.py b/chatette/adapters/rasa_yml.py new file mode 100644 index 00000000..8b5acc07 --- /dev/null +++ b/chatette/adapters/rasa_yml.py @@ -0,0 +1,189 @@ +import os +import io +import ruamel.yaml as yaml +from ruamel.yaml.scalarstring import DoubleQuotedScalarString +from ruamel.yaml.error import YAMLError +from ruamel.yaml.constructor import DuplicateKeyError +from ruamel.yaml.comments import CommentedMap as OrderedDict +from chatette.adapters._base import Adapter +from chatette.utils import append_to_list_in_dict, cast_to_unicode + +YAML_VERSION = (1, 2) + +def intent_dict_to_list_of_dict(data): + list_data = [] + for key, values in data.items(): + list_data.append( + { + "intent": key, + "examples": '\n'.join(['- ' + v for v in values]) + '\n' + } + ) + + return list_data + +def fix_yaml_loader() -> None: + """Ensure that any string read by yaml is represented as unicode.""" + """Code from Rasa yaml reader""" + def construct_yaml_str(self, node): + # Override the default string handling function + # to always return unicode objects + return self.construct_scalar(node) + + yaml.Loader.add_constructor("tag:yaml.org,2002:str", construct_yaml_str) + yaml.SafeLoader.add_constructor("tag:yaml.org,2002:str", construct_yaml_str) + + +class RasaYMLAdapter(Adapter): + def __init__(self, base_filepath=None): + super(RasaYMLAdapter, self).__init__(base_filepath, None) + self._base_file_contents = None + + @classmethod + def _get_file_extension(cls): + return "yml" + + def __get_file_name(self, batch, output_directory, single_file): + if single_file: + return \ + os.path.join( + output_directory, "nlu." + self._get_file_extension() + ) + raise ValueError( + "Tried to generate several files with Rasa YAML adapter." + ) + + def _write_batch(self, output_file_handle, batch): + data = self._get_base_to_extend() + prepared_examples = dict() + for example in batch.examples: + append_to_list_in_dict( + prepared_examples, + example.intent_name, self.prepare_example(example) + ) + prepared_examples = intent_dict_to_list_of_dict(prepared_examples) + prepared_examples.extend( + self.__format_synonyms(batch.synonyms) + ) + data['nlu'] = prepared_examples + data = cast_to_unicode(data) + + yaml.scalarstring.walk_tree(data) + yaml.round_trip_dump(data, output_file_handle, default_flow_style=False, allow_unicode=True) + + + def prepare_example(self, example): + if len(example.entities) == 0: + return example.text + + sorted_entities = \ + sorted( + example.entities, + reverse=True, + key=lambda entity: entity._start_index + ) + result = example.text[:] + for entity in sorted_entities: + entity_annotation_text = ']{"entity": "' + entity.slot_name + entity_text = result[entity._start_index:entity._start_index + entity._len] + if entity_text != entity.value: + entity_annotation_text += '", "value": "{}'.format(entity.value) + if entity.role is not None: + entity_annotation_text += '", "role": "{}'.format(entity.role) + if entity.group is not None: + entity_annotation_text += '", "group": "{}'.format(entity.group) + result = \ + result[:entity._start_index] + "[" + \ + entity_text + entity_annotation_text + '"}' + \ + result[entity._start_index + entity._len:] # New rasa entity format + return result + + @classmethod + def __format_synonyms(cls, synonyms): + # {str: [str]} -> [{"value": str, "synonyms": [str]}] + return [ + { + "synonym": slot_name, + "examples": '\n'.join(['- ' + s for s in synonyms[slot_name]]) + '\n' + } + for slot_name in synonyms + if len(synonyms[slot_name]) > 1 + ] + + def _read_yaml(self, content): + fix_yaml_loader() + yaml_parser = yaml.YAML(typ='safe') + yaml_parser.version = YAML_VERSION + yaml_parser.preserve_quotes = True + yaml.allow_duplicate_keys = False + + return yaml_parser.load(content) + + def _get_base_to_extend(self): + if self._base_file_contents is None: + if self._base_filepath is None: + return self._get_empty_base() + with io.open(self._base_filepath, 'r', encoding='utf-8') as base_file: + try: + self._base_file_contents = self._read_yaml(base_file.read()) + except (YAMLError, DuplicateKeyError) as e: + raise YamlSyntaxException(self._base_filepath, e) + self.check_base_file_contents() + return self._base_file_contents + + def _get_empty_base(self): + base = OrderedDict() + base['version'] = DoubleQuotedScalarString('2.0') + base['nlu'] = list() + return base + + def check_base_file_contents(self): + """ + Checks that `self._base_file_contents` contains well formatted NLU dictionary. + Throws a `SyntaxError` if the data is incorrect. + """ + if self._base_file_contents is None: + return + if not isinstance(self._base_file_contents, dict): + self._base_file_contents = None + raise SyntaxError( + "Couldn't load valid data from base file '" + \ + self._base_filepath + "'" + ) + else: + if "nlu" not in self._base_file_contents: + self._base_file_contents = None + raise SyntaxError( + "Expected 'nlu' as a root of base file '" + \ + self._base_filepath + "'") + + +class YamlSyntaxException(Exception): + """Raised when a YAML file can not be parsed properly due to a syntax error.""" + """code from rasa.shared.exceptions.YamlSyntaxException""" + + def __init__(self, filename, underlying_yaml_exception): + self.filename = filename + self.underlying_yaml_exception = underlying_yaml_exception + + def __str__(self): + if self.filename: + exception_text = "Failed to read '{}'.".format(self.filename) + else: + exception_text = "Failed to read YAML." + + if self.underlying_yaml_exception: + self.underlying_yaml_exception.warn = None + self.underlying_yaml_exception.note = None + exception_text += " {}".format(self.underlying_yaml_exception) + + if self.filename: + exception_text = exception_text.replace( + 'in ""', 'in "{}"'.format(self.filename) + ) + + exception_text += ( + "\n\nYou can use https://yamlchecker.com/ to validate the " + "YAML syntax of your file." + ) + return exception_text \ No newline at end of file diff --git a/chatette/parsing/__init__.py b/chatette/parsing/__init__.py index f570877c..753b1c46 100644 --- a/chatette/parsing/__init__.py +++ b/chatette/parsing/__init__.py @@ -16,7 +16,7 @@ from future.utils import with_metaclass from chatette.units.modifiable.choice import Choice -from chatette.units.modifiable.unit_reference import UnitReference +from chatette.units.modifiable.unit_reference import UnitReference, SlotRoleGroupReference from chatette.units.modifiable.definitions.alias import AliasDefinition from chatette.units.modifiable.definitions.slot import SlotDefinition from chatette.units.modifiable.definitions.intent import IntentDefinition @@ -91,6 +91,7 @@ def __init__(self): self.identifier = None self.variation = None self.arg_value = None + self.slot_rolegroup = None def _check_information(self): super(UnitRefBuilder, self)._check_information() @@ -108,6 +109,12 @@ def _build_modifiers_repr(self): def create_concrete(self): self._check_information() + if self.slot_rolegroup is not None: + return SlotRoleGroupReference( + self.identifier, self.type, + self.leading_space, self._build_modifiers_repr(), + self.slot_rolegroup + ) return UnitReference( self.identifier, self.type, self.leading_space, self._build_modifiers_repr() diff --git a/chatette/parsing/lexing/rule_unit_ref.py b/chatette/parsing/lexing/rule_unit_ref.py index 9a0a3c8f..3a7f1e4d 100644 --- a/chatette/parsing/lexing/rule_unit_ref.py +++ b/chatette/parsing/lexing/rule_unit_ref.py @@ -11,6 +11,7 @@ extract_identifier, \ CASE_GEN_SYM, UNIT_END_SYM +from chatette.parsing.lexing.rule_annotation import RuleAnnotation from chatette.parsing.lexing.rule_unit_start import RuleUnitStart from chatette.parsing.lexing.rule_variation import RuleVariation from chatette.parsing.lexing.rule_rand_gen import RuleRandGen @@ -55,11 +56,13 @@ def _apply_strategy(self, **kwargs): "using character '" + UNIT_END_SYM + "')." return False + is_slot = False # TODO maybe making a function for this would be useful if self._tokens[0].type == TerminalType.alias_ref_start: unit_end_type = TerminalType.alias_ref_end elif self._tokens[0].type == TerminalType.slot_ref_start: unit_end_type = TerminalType.slot_ref_end + is_slot = True elif self._tokens[0].type == TerminalType.intent_ref_start: unit_end_type = TerminalType.intent_ref_end else: # Should never happen @@ -72,5 +75,15 @@ def _apply_strategy(self, **kwargs): self._next_index += 1 self._update_furthest_matched_index() self._tokens.append(LexicalToken(unit_end_type, UNIT_END_SYM)) - + + # This is for adding new rasa training mode that has role and group entity + # Reference: https://rasa.com/docs/rasa/nlu-training-data/#entities-roles-and-groups + annotation_rule = RuleAnnotation(self._text, self._next_index) + + # ? Should we raise error if RuleAnnotation doesn't match, i.e. wrong pattern + if is_slot and annotation_rule.matches(): + self._next_index = annotation_rule.get_next_index_to_match() + self._update_furthest_matched_index() + self._tokens.extend(annotation_rule.get_lexical_tokens()) + return True diff --git a/chatette/parsing/line_count_file_wrapper.py b/chatette/parsing/line_count_file_wrapper.py index ace354f1..60fd29f1 100644 --- a/chatette/parsing/line_count_file_wrapper.py +++ b/chatette/parsing/line_count_file_wrapper.py @@ -15,7 +15,7 @@ class LineCountFileWrapper(object): def __init__(self, filepath, mode='r'): self.name = cast_to_unicode(filepath) - self.f = io.open(filepath, mode) + self.f = io.open(filepath, mode, encoding='utf-8') self.line_nb = 0 def close(self): diff --git a/chatette/parsing/parser.py b/chatette/parsing/parser.py index e2563bdf..3707d3ce 100644 --- a/chatette/parsing/parser.py +++ b/chatette/parsing/parser.py @@ -30,7 +30,7 @@ from chatette.units.rule import Rule from chatette.parsing import \ - ChoiceBuilder, UnitRefBuilder, \ + ChoiceBuilder, UnitRefBuilder,\ AliasDefBuilder, SlotDefBuilder, IntentDefBuilder @@ -398,12 +398,18 @@ def _parse_rule(self, tokens): elif ( token.type in \ (TerminalType.alias_ref_end, - TerminalType.slot_ref_end, TerminalType.intent_ref_end) ): rule_contents.append(current_builder.create_concrete()) current_builder = None leading_space = False + elif token.type == TerminalType.slot_ref_end: + # checking annotation after slot reference + rolegroup_annotation, i = self._check_for_annotations(tokens, i) + current_builder.slot_rolegroup = rolegroup_annotation + rule_contents.append(current_builder.create_concrete()) + current_builder = None + leading_space = False elif token.type == TerminalType.unit_identifier: current_builder.identifier = token.text elif token.type == TerminalType.choice_start: @@ -505,3 +511,28 @@ def _parse_choice(self, tokens): ) return rules + + def _check_for_annotations(self, tokens, i): + if ( + i+1 == len(tokens) + or tokens[i+1].type != TerminalType.annotation_start + ): + return None, i + + annotation = {} + current_key = None + for j, token in enumerate(tokens[i+1:]): + if token.type == TerminalType.annotation_end: + i += j+1 + break + elif token.type == TerminalType.key: + current_key = token.text + elif token.type == TerminalType.value: + if current_key in annotation: + self.input_file_manager.syntax_error( + "Annotation contained the key '" + current_key + \ + "' twice." + ) + annotation[current_key] = token.text + + return annotation, i diff --git a/chatette/units/__init__.py b/chatette/units/__init__.py index 9236ab71..21593273 100644 --- a/chatette/units/__init__.py +++ b/chatette/units/__init__.py @@ -125,11 +125,13 @@ class Entity(object): Represents an entity as it will be contained in examples (instances of `Example`). """ - def __init__(self, name, length, value=None, start_index=0): + def __init__(self, name, length, value=None, start_index=0, role=None, group=None): self.slot_name = name # name of the entity (not the associated text) self.value = value self._len = length self._start_index = start_index + self.role = role + self.group = group def _remove_leading_space(self): """ @@ -146,17 +148,27 @@ def _remove_leading_space(self): return True def as_dict(self): - return { + entity_dict = { "slot-name": self.slot_name, "value": self.value, "start-index": self._start_index, "end-index": self._start_index + self._len } + if self.role is not None: + entity_dict['role'] = self.role + if self.group is not None: + entity_dict['group'] = self.group + return entity_dict def __repr__(self): representation = "entity '" + self.slot_name + "'" if self.value is not None: representation += ":'" + self.value + "'" + # ? There might be better representation format? + if self.role is not None: + representation += ", 'role' :'" + self.role + "'" + if self.group is not None: + representation += ", 'group' :'" + self.group + "'" return representation def __str__(self): return \ diff --git a/chatette/units/modifiable/unit_reference.py b/chatette/units/modifiable/unit_reference.py index 02b1c5e6..726746a1 100644 --- a/chatette/units/modifiable/unit_reference.py +++ b/chatette/units/modifiable/unit_reference.py @@ -126,3 +126,53 @@ def as_template_str(self): if self._leading_space: result = ' ' + result return result + + +class SlotRoleGroupReference(UnitReference): + """ + Represents a reference to a unit definition that can be contained + in a template rule. + """ + def __init__(self, identifier, unit_type, leading_space, modifiers, rolegroup): + super(SlotRoleGroupReference, self).__init__( + identifier, unit_type, leading_space, modifiers + ) + + # dictionary {"role": "value"}, or {"group": "value"}, or both + self._role = rolegroup.get('role', None) + self._group = rolegroup.get('group', None) + + def _generate_random_strategy(self): + generated_example = super()._generate_random_strategy() + + for ent in generated_example.entities: + if self._role is not None: + ent.role = self._role + if self._group is not None: + ent.group = self._group + return generated_example + + + def _generate_all_strategy(self): + generated_examples = super()._generate_all_strategy() + + for ex in generated_examples: + for ent in ex.entities: + if self._role is not None: + ent.role = self._role + if self._group is not None: + ent.group = self._group + + return generated_examples + + def _generate_n_strategy(self, n): + generated_examples = super()._generate_n_strategy() + + for ex in generated_examples: + for ent in ex.entities: + if self._role is not None: + ent.role = self._role + if self._group is not None: + ent.group = self._group + + return generated_examples \ No newline at end of file diff --git a/examples/simple/airport/aliases.chatette b/examples/simple/airport/aliases.chatette index 4e5fa41e..12ccfd89 100644 --- a/examples/simple/airport/aliases.chatette +++ b/examples/simple/airport/aliases.chatette @@ -23,6 +23,6 @@ register ~[from airport] - from @[source-airport] + from @[airport#source]("role":"source") ~[to airport] - [to go?] to @[source-airport] + [to go?] to @[airport#dest]('role':'destination') diff --git a/examples/simple/airport/slots/cities.chatette b/examples/simple/airport/slots/cities.chatette index 526d6877..a69bd5fb 100644 --- a/examples/simple/airport/slots/cities.chatette +++ b/examples/simple/airport/slots/cities.chatette @@ -1,10 +1,10 @@ // Lists of cities that are available as source airports and destination airports -@[source-airport] +@[airport#source] Brussels Paris Amsterdam -@[destination-airport] +@[airport#dest] Paris Amsterdam London diff --git a/requirements/common.txt b/requirements/common.txt index ea4305f6..42892472 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,3 +1,4 @@ enum-compat future six +ruamel.yaml diff --git a/requirements/develop.txt b/requirements/develop.txt index cd362625..b1d16c2b 100644 --- a/requirements/develop.txt +++ b/requirements/develop.txt @@ -1,4 +1,4 @@ -r test.txt tox pylint - +ruamel.yaml diff --git a/setup.py b/setup.py index b4e99e1a..62d861ea 100644 --- a/setup.py +++ b/setup.py @@ -33,5 +33,6 @@ "enum-compat", "future", "six", + "ruamel.yaml", ] ) diff --git a/tests/system-testing/inputs/generate-all/slotrolegroup.chatette b/tests/system-testing/inputs/generate-all/slotrolegroup.chatette new file mode 100644 index 00000000..2900e8ea --- /dev/null +++ b/tests/system-testing/inputs/generate-all/slotrolegroup.chatette @@ -0,0 +1,25 @@ +%[single_role] + @[slot]("role":"role") + +%[single_group] + @[slot]("group":"group") + +%[single_role_group] + @[slot]("role":"role", "group":"group") + +%[multiple_role] + @[slot]("role":"role1") @[slot]("role":"role2") + +%[multiple_group] + @[slot]("group":"group1") @[slot]("group":"group2") + +%[multiple_role_group] + @[slot]("role":"role1", "group":"group1") @[slot]("role":"role2", "group":"group2") + +%[not_role_group] + @[slot]("role":"role" + +@[slot] + slot one + slot1 + diff --git a/tests/system-testing/inputs/generate-all/slotrolegroup.solution b/tests/system-testing/inputs/generate-all/slotrolegroup.solution new file mode 100644 index 00000000..d12aece8 --- /dev/null +++ b/tests/system-testing/inputs/generate-all/slotrolegroup.solution @@ -0,0 +1,22 @@ +# Contains all possible examples for the Chatette template file with the same name. +single_role>>>slot one +single_role>>>slot1 +single_group>>>slot one +single_group>>>slot1 +single_role_group>>>slot one +single_role_group>>>slot1 +multiple_role>>>slot one slot one +multiple_role>>>slot one slot1 +multiple_role>>>slot1 slot one +multiple_role>>>slot1 slot1 +multiple_group>>>slot one slot one +multiple_group>>>slot one slot1 +multiple_group>>>slot1 slot one +multiple_group>>>slot1 slot1 +multiple_role_group>>>slot one slot one +multiple_role_group>>>slot one slot1 +multiple_role_group>>>slot1 slot one +multiple_role_group>>>slot1 slot1 + +not_role_group>>>slot one("role":"role" +not_role_group>>>slot1("role":"role" \ No newline at end of file diff --git a/tests/system-testing/test_system.py b/tests/system-testing/test_system.py index 5a25eefb..7744ec23 100644 --- a/tests/system-testing/test_system.py +++ b/tests/system-testing/test_system.py @@ -157,7 +157,7 @@ def test_generate_all_training(self): input_filenames = [ "simplest.chatette", "only-words.chatette", "words-and-groups.chatette", "alias.chatette", "include.chatette", - "slot.chatette" + "slot.chatette", "slotrolegroup.chatette" ] for filename in input_filenames: file_path = os.path.join(input_dir_path, filename) diff --git a/tests/unit-testing/parsing/test_init.py b/tests/unit-testing/parsing/test_init.py index bf791e68..e0a4ea4f 100644 --- a/tests/unit-testing/parsing/test_init.py +++ b/tests/unit-testing/parsing/test_init.py @@ -15,7 +15,7 @@ from chatette.modifiers.representation import ModifiersRepresentation from chatette.units.modifiable.choice import Choice -from chatette.units.modifiable.unit_reference import UnitReference +from chatette.units.modifiable.unit_reference import UnitReference, SlotRoleGroupReference from chatette.units.modifiable.definitions.alias import AliasDefinition from chatette.units.modifiable.definitions.slot import SlotDefinition from chatette.units.modifiable.definitions.intent import IntentDefinition @@ -90,6 +90,32 @@ def test_create_concrete(self): assert unit_ref._unit_type == UnitType.alias assert unit_ref._name == "id" + def test_create_concrete_rolegroup_ref(self): + builder = UnitRefBuilder() + builder.identifier = "id" + + with pytest.raises(ValueError): + builder.create_concrete() + + builder.type = UnitType.slot + modifiers = builder._build_modifiers_repr() + assert isinstance(modifiers, ModifiersRepresentation) + assert not modifiers.casegen + assert not modifiers.randgen + assert modifiers.randgen.name is None + assert modifiers.randgen.percentage == 50 + assert not modifiers.randgen.opposite + + annotation = {'role': 'role', 'group': 'group'} + builder.slot_rolegroup = annotation + unit_ref = builder.create_concrete() + assert isinstance(unit_ref, SlotRoleGroupReference) + assert not unit_ref._leading_space + assert unit_ref._unit_type == UnitType.slot + assert unit_ref._name == "id" + assert unit_ref._role == annotation['role'] + assert unit_ref._group == annotation['group'] + class TestUnitDefBuilder(object): def test_creation(self): with pytest.raises(TypeError):