diff --git a/docs/source/userdoc/overview.md b/docs/source/userdoc/overview.md index 88ef7b3..04ae1bd 100644 --- a/docs/source/userdoc/overview.md +++ b/docs/source/userdoc/overview.md @@ -11,6 +11,8 @@ In Pyerk there are the following kinds of keys: - d) prefixed name-labeled key like `"bi__R1234__my_relation"` - e) index-labeld key like `"R1234['my relation']"` +Note: prefixed and name-labled keys can optionally have a language indicator. Examples: ``"bi__R1__de"`` or `"R1__has_label__fr"`. + Also, the leading character indicates the entity type (called `EType` in the code): `I` → item, `R` → relation. The usage of these syntax variants depens on the context. diff --git a/src/pyerk/auxiliary.py b/src/pyerk/auxiliary.py index 35b7481..83dc907 100644 --- a/src/pyerk/auxiliary.py +++ b/src/pyerk/auxiliary.py @@ -118,7 +118,10 @@ class PyERKError(Exception): """ raised in situations where some ERK-specific conditions are violated """ + pass + +class MultilingualityError(PyERKError): pass @@ -151,6 +154,10 @@ class InvalidGeneralKeyError(PyERKError): pass +class InconsistentLabelError(PyERKError): + pass + + # used for syntactically correct keys which could not be found class ShortKeyNotFoundError(PyERKError): pass diff --git a/src/pyerk/builtin_entities.py b/src/pyerk/builtin_entities.py index c44e9a2..d5a7fdd 100644 --- a/src/pyerk/builtin_entities.py +++ b/src/pyerk/builtin_entities.py @@ -1282,7 +1282,12 @@ def _get_subscope(self, name: str): assert isinstance(name, str) scope_rels: list = self.get_inv_relations("R21__is_scope_of", return_subj=True) - res = [rel for rel in scope_rels if rel.R1 == name or rel.R1 == f"scp__{name}"] + res = [] + for rel in scope_rels: + assert isinstance(rel.R1, core.Literal) + r1 = rel.R1.value + if r1 == name or r1 == f"scp__{name}": + res.append(rel) if len(res) == 0: msg = f"no scope with name {name} could be found" @@ -2301,7 +2306,7 @@ def set_multiple_statements(subjects: Union[list, tuple], predicate: Relation, o "specifies that the subject should be threated according to the mode (int number) when constructing the " "prototype graph of an I41__semantic_rule; Modes: 0 -> normal; 1 -> ignore node, 2 -> relation statement, " "3 -> variable literal, 4 -> function-anchor; 5 -> create_asserted_statement_only_if_new; " - "currently '2' is not implemented.", + "currently '2' is not implemented." ), R8__has_domain_of_argument_1=I1["general item"], R11__has_range_of_result=int, diff --git a/src/pyerk/core.py b/src/pyerk/core.py index b22b090..94fb7d0 100644 --- a/src/pyerk/core.py +++ b/src/pyerk/core.py @@ -178,7 +178,7 @@ def idoc(self, adhoc_label: str): if adhoc_label != self.R1: # due to multilinguality there might be multiple labels. As adhoc label we accept any language all_labels = self.get_relations("R1", return_obj=True) - all_labels_dict = dict((str(label), None) for label in all_labels) + all_labels_dict = dict((str(label.value), None) for label in all_labels) adhoc_label_str = str(adhoc_label) if adhoc_label_str not in all_labels_dict: @@ -205,11 +205,11 @@ def __getattr__(self, attr_name): return self.__dict__[attr_name] except KeyError: pass - processed_key = self.__process_attribute_name(attr_name) + processed_key: ProcessedStmtKey = self.__process_attribute_name(attr_name) try: # TODO: introduce prefixes here, which are mapped to uris - etyrel = self._get_relation_contents(rel_uri=processed_key.uri) + etyrel = self._get_relation_contents(rel_uri=processed_key.uri, lang_indicator=processed_key.lang_indicator) except KeyError: msg = f"'{type(self)}' object has no attribute '{processed_key.short_key}'" raise AttributeError(msg) @@ -294,7 +294,7 @@ def _perform_instantiation(self): for func in parent_class._method_prototypes: self.add_method(func) - def _get_relation_contents(self, rel_uri: str): + def _get_relation_contents(self, rel_uri: str, lang_indicator=None): aux.ensure_valid_uri(rel_uri) statements: List[Statement] = ds.get_statements(self.uri, rel_uri) @@ -311,7 +311,7 @@ def _get_relation_contents(self, rel_uri: str): # (note that R22 itself is also a functional relation: only one of {True, False} is meaningful, same holds for # R32["is functional for each language"]). R32 also must be handled separately - relation = ds.relations[rel_uri] + relation: Relation = ds.relations[rel_uri] hardcoded_functional_relations = [ aux.make_uri(settings.BUILTINS_URI, "R22"), aux.make_uri(settings.BUILTINS_URI, "R32"), @@ -330,22 +330,25 @@ def _get_relation_contents(self, rel_uri: str): # is a similar situation # if rel_key == "R32" this means that self 'is functional for each language' - elif rel_uri in hardcoded_functional_fnc4elang_relations or relation.R32: - # TODO: handle multilingual situations more flexible + if lang_indicator is not None and lang_indicator not in settings.SUPPORTED_LANGUAGES: + msg = f"unsupported language ({lang_indicator}) while accessing {self}.{relation.short_key}." + raise aux.MultilingualityError(msg) - # todo: specify currently relevant language here (influences the return value); for now: using default - language = settings.DEFAULT_DATA_LANGUAGE + if lang_indicator is None: + lang_indicator = settings.DEFAULT_DATA_LANGUAGE filtered_res_explicit_lang = [] filtered_res_without_lang = [] + + # TODO: simplify this since now we can be sure that we have only Literal-instances in res for elt in res: # if no language is defined (e.g. ordinary string) -> use interpret this as match # (but only if no other result with matching language attribute is available) lng = getattr(elt, "language", None) if lng is None: filtered_res_without_lang.append(elt) - elif lng == language: + elif lng == lang_indicator: filtered_res_explicit_lang.append(elt) if filtered_res_explicit_lang: @@ -360,10 +363,10 @@ def _get_relation_contents(self, rel_uri: str): else: msg = ( f"unexpectedly found more then one object for relation {relation.short_key} " - f"and language {language}." + f"and language {lang_indicator}." ) - raise ValueError(msg) + raise aux.MultilingualityError(msg) else: return res @@ -463,6 +466,11 @@ def set_relation( return None if isinstance(relation, Relation): + + # handle R32__is_functional_for_each_language + if relation.R32 and not isinstance(obj, Literal): + obj = Literal(obj, lang=settings.DEFAULT_DATA_LANGUAGE) + if isinstance(obj, (Entity, *allowed_literal_types)) or obj in allowed_literal_types: return self._set_relation(relation.uri, obj, scope=scope, qualifiers=qualifiers, proxyitem=proxyitem) else: @@ -801,7 +809,7 @@ def get_item_by_label(self, label) -> Entity: Useful during interactive debugging. Not useful for production! """ for uri, itm in self.items.items(): - if itm.R1 == label: + if itm.R1.value == label: return itm def get_entity_by_key_str(self, key_str, mod_uri=None) -> Entity: @@ -934,13 +942,14 @@ def set_statement(self, stm: "Statement") -> None: ) raise aux.FunctionalRelationError(msg) elif relation.R32 and not exception_flag: + if not isinstance(stm.object, Literal): + stm.object = Literal(stm.object, settings.DEFAULT_DATA_LANGUAGE) lang_list = [get_language_of_str_literal(s.object) for s in stm_list] - new_lang = get_language_of_str_literal(stm.object) - if new_lang in lang_list: + if stm.object.language in lang_list: msg = ( f"for subject {subj_uri} ({subj_label}) there already exists statements for relation " f"{stm.predicate} with the object languages {lang_list}. This relation is functional for " - f"each language (R32). Thus another statement with language `{new_lang}` is not allowed." + f"each language (R32). Thus another statement with language `{stm.object.language}` is not allowed." ) raise aux.FunctionalRelationError(msg) stm_list.append(stm) @@ -983,8 +992,12 @@ def preprocess_query(self, query): label = description.replace("_", " ") - msg = f"Entity label '{entity.R1}' for entity '{e}' and given label '{label}' do not match!" - assert entity.R1 == label, msg + assert isinstance(entity.R1, Literal) + r1 = entity.R1.value + + if r1 != label: + msg = f"Entity label '{r1}' for entity '{e}' and given label '{label}' do not match!" + raise aux.InconsistentLabelError(msg) new_query = re.sub(r"__[\w]+(?:–_instance)?", "", query) else: @@ -1076,6 +1089,7 @@ class ProcessedStmtKey: label: str = None prefix: str = None uri: str = None + lang_indicator: str = None original_key_str: str = None @@ -1177,6 +1191,16 @@ def process_key_str( if resolve_prefix: _resolve_prefix(res, passed_mod_uri=mod_uri) + if res.label: + match_list = langcode_end_pattern.findall(res.label) + if match_list: + assert len(match_list) == 1 + match, = match_list + assert match.startswith("__") + res.label = langcode_end_pattern.sub("", res.label) + + res.lang_indicator = match[2:] + if check: aux.ensure_valid_short_key(res.short_key) check_processed_key_label(res) @@ -1261,6 +1285,8 @@ def _resolve_prefix(pr_key: ProcessedStmtKey, passed_mod_uri: str = None) -> Non pr_key.uri = aux.make_uri(mod_uri, pr_key.short_key) +# regex pattern which represents a language indicator +langcode_end_pattern = re.compile("__[a-z]{2}$") def check_processed_key_label(pkey: ProcessedStmtKey) -> None: """ @@ -1280,10 +1306,21 @@ def check_processed_key_label(pkey: ProcessedStmtKey) -> None: except KeyError: # entity does not exist -> no label to compare with return + + if entity.R1 is None: + # no label was set for the default language -> nothing to compare + return + + # note: this includes Literal + assert isinstance(entity.R1, str) + label_compare_str1 = entity.R1 label_compare_str2 = ilk2nlk(entity.R1) - if pkey.label.lower() not in (label_compare_str1.lower(), label_compare_str2.lower()): + label = pkey.label.lower() + + error_condition = label not in (label_compare_str1.lower(), label_compare_str2.lower()) + if error_condition: msg = ( f"check of label consistency failed for key {pkey.original_key_str}. Expected: one of " f'("{label_compare_str1}", "{label_compare_str2}") but got "{pkey.label}". ' @@ -1359,38 +1396,109 @@ def get_active_mod_uri(strict: bool = True) -> Union[str, None]: return res -# noinspection PyShadowingNames -def create_item(key_str: str = "", **kwargs) -> Item: +def process_kwargs_for_entity_creation(entity_key: str, kwargs: dict) ->(dict, dict): """ - - :param key_str: "" or unique key of this item (something like `I1234`) - :param kwargs: further relations - - :return: newly created item + :return: return new_kwargs, lang_related_kwargs """ - if key_str == "": - item_key = get_key_str_by_inspection() - else: - item_key = key_str - mod_uri = get_active_mod_uri() new_kwargs = {} + lang_related_kwargs = defaultdict(list) # prepare the kwargs to set relations for dict_key, value in kwargs.items(): processed_key = process_key_str(dict_key) if processed_key.etype != EType.RELATION: - msg = f"unexpected key: {dict_key} during creation of item {item_key}." + msg = f"unexpected key: {dict_key} during creation of item {entity_key}." raise ValueError(msg) if processed_key.prefix: new_key = f"{processed_key.prefix}__{processed_key.short_key}" else: new_key = processed_key.short_key + + # handle those relations which might come with multiple languages + # (related to R32__is_functional_for_each_language) + if new_key in ("R1", "R2"): + value_list = lang_related_kwargs[new_key] + if len(value_list) == 0: + valid_languages = (None, settings.DEFAULT_DATA_LANGUAGE) + if processed_key.lang_indicator not in valid_languages: + msg = ( + f"while creating {entity_key}: the first {new_key}-argument must be with " + "lang_indicator `None` or explicitly using the default language. " + f"Got {processed_key.lang_indicator} instead." + ) + raise aux.MultilingualityError(msg) + value_lang = getattr(value, "language", None) + if value_lang not in valid_languages: + msg = ( + f"while creating {entity_key}: the first {new_key}-argument must be " + f"a flat string or a literal with the default language ({settings.DEFAULT_DATA_LANGUAGE})" + f"Got {value_lang} instead." + ) + raise aux.MultilingualityError(msg) + + if not isinstance(value, Literal): + if not isinstance(value, str): + item_uri = aux.make_uri(mod_uri, entity_key) + msg = ( + f"While creating {item_uri}: the {new_key}-argument must be a string. " + f"Got {type(value)} instead." + ) + raise TypeError(msg) + value = Literal(value, lang=settings.DEFAULT_DATA_LANGUAGE) + value_list.append((processed_key.lang_indicator, value)) + else: + value_list.append((processed_key.lang_indicator, value)) + # do not pass this key-value-pair to the Item-constructor + # it will be handled later + continue + new_kwargs[new_key] = value + return new_kwargs, lang_related_kwargs + + +def process_lang_related_kwargs_for_entity_creation(entity: Entity, short_key: str, lang_related_kwargs: dict) -> None: + for rel_key, value_list in lang_related_kwargs.items(): + # omit the first argument as it was already passed to the Item-constructor + for lang_indicator, value in value_list[1:]: + if isinstance(value, Literal): + if value.language != lang_indicator: + msg = ( + f"while creating {short_key} ({rel_key}-argument) got inconsistent language indicators: " + f"in argument_name: {lang_indicator} but in value (Literal-instance) {value.language}" + ) + raise aux.MultilingualityError(msg) + elif isinstance(value, str): + value = Literal(value, lang=lang_indicator) + else: + msg = f"unexpected type ({type(value)}) while creating {short_key} ({rel_key}-argument)" + raise TypeError(msg) + + entity.set_relation(rel_key, value) + + +def create_item(key_str: str = "", **kwargs) -> Item: + """ + + :param key_str: "" or unique key of this item (something like `I1234`) + :param kwargs: further relations + + :return: newly created item + """ + + if key_str == "": + item_key = get_key_str_by_inspection() + else: + item_key = key_str + + mod_uri = get_active_mod_uri() + + new_kwargs, lang_related_kwargs = process_kwargs_for_entity_creation(item_key, kwargs) + itm = Item(base_uri=mod_uri, key_str=item_key, **new_kwargs) assert itm.uri not in ds.items, f"Problematic (duplicated) uri: {itm.uri}" ds.items[itm.uri] = itm @@ -1398,6 +1506,8 @@ def create_item(key_str: str = "", **kwargs) -> Item: # acces the defaultdict(list) ds.entities_created_in_mod[mod_uri].append(itm.uri) + process_lang_related_kwargs_for_entity_creation(itm, item_key, lang_related_kwargs) + run_hooks(itm, phase="post-create") return itm @@ -1717,7 +1827,7 @@ def get_first_qualifier_obj_with_rel(self, key=None, uri=None, tolerate_key_erro if key: try: - uri = process_key_str(key).uri + uri = process_key_str(key, check=False).uri except aux.ShortKeyNotFoundError: if tolerate_key_error: # this allows to ask for qualifiers before they are created @@ -1834,19 +1944,12 @@ def create_relation(key_str: str = "", **kwargs) -> Relation: mod_uri = get_active_mod_uri() + # TODO: obsolete? default_relations = { # "R22": None, # R22__is_functional } - new_kwargs = {**default_relations} - for key, value in kwargs.items(): - processed_key = process_key_str(key) - - if processed_key.etype != EType.RELATION: - msg = f"unexpected key: {key} during creation of item {rel_key}." - raise ValueError(msg) - - new_kwargs[processed_key.short_key] = value + new_kwargs, lang_related_kwargs = process_kwargs_for_entity_creation(rel_key, kwargs) rel = Relation(mod_uri, rel_key, **new_kwargs) if rel.uri in ds.relations: @@ -1855,6 +1958,8 @@ def create_relation(key_str: str = "", **kwargs) -> Relation: ds.relations[rel.uri] = rel ds.entities_created_in_mod[mod_uri].append(rel.uri) + process_lang_related_kwargs_for_entity_creation(rel, rel_key, lang_related_kwargs) + run_hooks(rel, phase="post-create") return rel @@ -2309,7 +2414,7 @@ def end_mod(): _uri_stack.pop() assert len(_uri_stack) == 0 - +# TODO: obsolete? def get_language_of_str_literal(obj: Union[str, Literal]): if isinstance(obj, Literal): return obj.language @@ -2331,13 +2436,16 @@ def __rmatmul__(self, arg: str) -> Literal: :return: Literal instance with `.lang` attribute set """ - assert isinstance(arg, str) + + # note that Literal is a subclass of str + assert not isinstance(arg, Literal) and isinstance(arg, str) res = Literal(arg, lang=self.langtag) return res +df = LangaguageCode(settings.DEFAULT_DATA_LANGUAGE) en = LangaguageCode("en") de = LangaguageCode("de") fr = LangaguageCode("fr") diff --git a/src/pyerk/release.py b/src/pyerk/release.py index 1f4c4d4..ae6db5f 100644 --- a/src/pyerk/release.py +++ b/src/pyerk/release.py @@ -1 +1 @@ -__version__ = "0.10.1" +__version__ = "0.11.0" diff --git a/tests/test_core.py b/tests/test_core.py index 3c317f5..00dee10 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -257,9 +257,9 @@ def test_a03_tear_down(self): # noinspection PyUnresolvedReferences # (above noinspection is necessary because of the @-operator which is undecleared for strings) - def test_b01__core1_basics(self): + def test_b00__core1_basics(self): mod1 = p.erkloader.load_mod_from_path(TEST_DATA_PATH2, prefix="ct") - self.assertEqual(mod1.I3749.R1, "Cayley-Hamilton theorem") + self.assertEqual(mod1.I3749.R1.value, "Cayley-Hamilton theorem") def_eq_item = mod1.I6886.R6__has_defining_mathematical_relation self.assertEqual(def_eq_item.R4__is_instance_of, p.I18["mathematical expression"]) @@ -268,14 +268,14 @@ def test_b01__core1_basics(self): mod_uri = p.ds.uri_prefix_mapping.b["ct"] p.unload_mod(mod_uri) - def test_b02_builtins1(self): + def test_b01_builtins1(self): """ Test the mechanism to endow the Entity class with custom methods (on class and on instance level) :return: """ # class level def example_func(slf, a): - return f"{slf.R1}--{a}" + return f"{slf.R1.value}--{a}" p.Entity.add_method_to_class(example_func) @@ -291,7 +291,7 @@ def example_func(slf, a): itm2 = p.create_item(key_str=p.pop_uri_based_key("I"), R1="unit test item2") def example_func2(slf, a): - return f"{slf.R1}::{a}" + return f"{slf.R1.value}::{a}" itm.add_method(example_func2) @@ -314,6 +314,17 @@ def test_a02__load_settings(self): # self.assertTrue(len(conf) != 0) self.assertTrue(len(conf) >= 0) + def test_b02_builtins2(self): + + rel = p.R65 + self.assertIsInstance(rel.R1, p.Literal) + + # do not allow Literal here + self.assertEqual(type(rel.R1.value), str) + + k = "R65__allows_alternative_functional_value" + pk = p.process_key_str(k) + def test_c01__ct_loads_math(self): """ test if the control_theory module successfully loads the math module @@ -325,112 +336,6 @@ def test_c01__ct_loads_math(self): itm1 = p.ds.get_entity_by_key_str("ma__I5000__scalar_zero") self.assertEqual(itm1, mod1.ma.I5000["scalar zero"]) - def test_c02__multilingual_relations(self): - """ - test how to create items with labels in multiple languages - """ - - with p.uri_context(uri=TEST_BASE_URI): - itm = p.create_item( - key_str=p.pop_uri_based_key("I"), - # multiple values to R1 can be passed using a list - R1__has_label=[ - "test-label in english" @ p.en, # `@p.en` is recommended, if you use multiple languages - "test-label auf deutsch" @ p.de, - ], - R2__has_description="test-description in english", - ) - - # this returns only one label according to the default language - default_label = itm.R1 - - # to access all labels use this: - label1, label2 = itm.get_relations("R1", return_obj=True) - self.assertEqual(default_label, label1) - self.assertEqual(label1.value, "test-label in english") - self.assertEqual(label1.language, "en") - self.assertEqual(label2.value, "test-label auf deutsch") - self.assertEqual(label2.language, "de") - - # add another language later - - with p.uri_context(uri=TEST_BASE_URI): - itm.set_relation(p.R2, "test-beschreibung auf deutsch" @ p.de) - - desc1, desc2 = itm.get_relations("R2", return_obj=True) - - self.assertTrue(isinstance(desc1, str)) - self.assertTrue(isinstance(desc2, p.Literal)) - - self.assertEqual(desc2.language, "de") - - # use the labels of different languages in index-labeld key notation - - # first: without explicitly specifying the language - tmp1 = itm["test-label in english"] - self.assertTrue(tmp1 is itm) - - tmp2 = itm["test-label auf deutsch"] - self.assertTrue(tmp2 is itm) - - # second: with explicitly specifying the language - tmp3 = itm["test-label in english" @ p.en] - self.assertTrue(tmp3 is itm) - - tmp4 = itm["test-label auf deutsch" @ p.de] - self.assertTrue(tmp4 is itm) - - with self.assertRaises(ValueError): - tmp5 = itm["wrong label"] - - with self.assertRaises(ValueError): - tmp5 = itm["wrong label" @ p.de] - - with self.assertRaises(ValueError): - tmp5 = itm["wrong label" @ p.en] # noqa - - # change the default language - - p.settings.DEFAULT_DATA_LANGUAGE = "de" - - new_default_label = itm.R1 - self.assertEqual(new_default_label, label2) - self.assertEqual(new_default_label.language, "de") - - new_default_description = itm.R2 - self.assertEqual(new_default_description, "test-beschreibung auf deutsch" @ p.de) - - with p.uri_context(uri=TEST_BASE_URI): - itm2 = p.create_item( - key_str=p.pop_uri_based_key("I"), - # multiple values to R1 can be passed using a list - R1__has_label=["test-label2", "test-label2-de" @ p.de], - R2__has_description="test-description2 in english", - ) - - # in case of ordinary strings they should be used if no value is available for current language - - self.assertEqual(p.settings.DEFAULT_DATA_LANGUAGE, "de") - self.assertEqual(itm2.R1, "test-label2-de" @ p.de) - self.assertEqual(itm2.R2, "test-description2 in english") - - p.settings.DEFAULT_DATA_LANGUAGE = "en" - self.assertEqual(itm2.R1, "test-label2") - self.assertEqual(itm2.R2, "test-description2 in english") - - p.settings.DEFAULT_DATA_LANGUAGE = "en" - - # test for correct error message - with p.uri_context(uri=TEST_BASE_URI, prefix="ut"): - - itm1 = p.instance_of(p.I1["general item"]) - - # this should cause no error (because of differnt language) - itm1.set_relation(p.R1["has label"], "neues Label" @ p.de) - - with self.assertRaises(p.aux.FunctionalRelationError): - itm1.set_relation(p.R1["has label"], "new label") - def test_c03__nontrivial_metaclasses(self): with p.uri_context(uri=TEST_BASE_URI): i1 = p.instance_of(p.I34["complex number"]) @@ -506,7 +411,7 @@ def test_c07__scope_vars(self): _ = p.erkloader.load_mod_from_path(TEST_DATA_PATH2, prefix="ct") def_itm = p.ds.get_entity_by_key_str("ct__I9907__definition_of_square_matrix") matrix_instance = def_itm.M - self.assertEqual(matrix_instance.R1, "M") + self.assertEqual(matrix_instance.R1.value, "M") def test_c07b__nested_scopes_of_propositions(self): """ @@ -663,7 +568,7 @@ def test_c07c__scope_copying(self): stm_subjects = I0222_setting.get_inv_relations("R20__has_defining_scope", return_subj=True) x2, y2, z2, V2, f2, LfV2 = stm_subjects[:6] - labels = [obj.R1 for obj in stm_subjects[:3]] + labels = [obj.R1.value for obj in stm_subjects[:3]] self.assertEqual(labels, ["x", "y", "z"]) self.assertNotEqual(x.uri, x2.uri) @@ -861,7 +766,7 @@ def test_c12a__process_key_str2(self): ) # this is the verbose way to adress a builtin relation - self.assertEqual(e1.bi__R1, "some label") + self.assertEqual(e1.bi__R1.value, "some label") # this is the (necessary) way to adress a relation from an external module self.assertEqual(e1.ct__R7641[0], e0) @@ -1425,16 +1330,6 @@ def test_c06__ruleengine05(self): class Test_03_Multilinguality(HouskeeperMixin, unittest.TestCase): def test_a01__label(self): - with p.uri_context(uri=TEST_BASE_URI): - - I900 = p.create_item( - R1__has_label="test item mit label auf deutsch" @ p.de, - R2__has_description="used for testing during development", - R4__is_instance_of=p.I2["Metaclass"], - R18__has_usage_hint="This item serves only for unittesting labels in different languages", - ) - - I900.set_relation(p.R1["has label"], "test item with english label" @ p.en) teststring1 = "this is english text" @ p.en teststring2 = "das ist deutsch" @ p.de @@ -1442,18 +1337,63 @@ def test_a01__label(self): self.assertIsInstance(teststring1, rdflib.Literal) self.assertIsInstance(teststring2, rdflib.Literal) + # this test will break if the default language is not "en" + # (it would need some further work to make it independent of the concrete default lang) + self.assertEqual(p.settings.DEFAULT_DATA_LANGUAGE, "en") + + with p.uri_context(uri=TEST_BASE_URI): + + with self.assertRaises(p.aux.MultilingualityError): + # the following is not allowed because the R1 argument is a non-default-language literal + # but R1-key has no language indicator + I901 = p.create_item( + R1__has_label="deutsches label" @ p.de, + ) + + with self.assertRaises(p.aux.MultilingualityError): + # the following is not allowed because the R1__de argument comes before the default R1 argument + I901 = p.create_item( + R1__has_label__de="deutsches label" @ p.de, + R1__has_label="default label", + ) + + with self.assertRaises(p.aux.MultilingualityError): + # the following is not allowed because of inconsistent language specifications + I901 = p.create_item( + R1__has_label="default label", + R1__has_label__es="deutsches label" @ p.de, + ) + + with self.assertRaises(TypeError): + # the following ensures that some old syntax is correctly reported as error + I902 = p.create_item( + R1__has_label=["default label", "deutsches label" @ p.de], + ) + + I900 = p.create_item( + R1__has_label="default label", + R1__has_label__de="deutsches label" @ p.de, + ) + + if p.settings.DEFAULT_DATA_LANGUAGE == "en": + with self.assertRaises(p.aux.FunctionalRelationError): + # we already have an english label set by specifying R1 without lang_indicator above + I900.set_relation(p.R1["has label"], "english label" @ p.en) + # R1 should return the default - self.assertEqual(I900.R1.language, p.settings.DEFAULT_DATA_LANGUAGE) + self.assertEqual(I900.R1.value, "default label") stored_default_lang = p.settings.DEFAULT_DATA_LANGUAGE p.settings.DEFAULT_DATA_LANGUAGE = "de" r1_de = I900.R1__has_label.value - self.assertEqual(r1_de, "test item mit label auf deutsch") + self.assertEqual(r1_de, "deutsches label") - p.settings.DEFAULT_DATA_LANGUAGE = "en" - r1_en = I900.R1__has_label.value - self.assertEqual(r1_en, "test item with english label") + + if p.settings.DEFAULT_DATA_LANGUAGE == "en": + p.settings.DEFAULT_DATA_LANGUAGE = "en" + r1_en = I900.R1__has_label.value + self.assertEqual(r1_en, "default_label") p.settings.DEFAULT_DATA_LANGUAGE = stored_default_lang @@ -1461,6 +1401,161 @@ def test_a01__label(self): self.assertNotIsInstance(p.I12.R2, list) self.assertNotIsInstance(I900.R2, list) + # test convenient notation + with p.uri_context(uri=TEST_BASE_URI): + + I1001 = p.create_item( + R1__has_label="default label", + R1__has_label__de="deutsches label" @ p.de, + + # we do not need to pass a Literal-instance (it is created automatically) + R1__has_label__fr="dénomination française", + ) + I1001.set_relation(p.R1["has label"], "nombre español" @ p.es) + + labels = I1001.get_relations("R1", return_obj=True) + + self.assertEqual(labels, ["default label"@p.df, "deutsches label"@p.de, "dénomination française"@p.fr, "nombre español"@p.es]) + + r1_default = I1001.R1__has_label + r1_de = I1001.R1__has_label__de + r1_es = I1001.R1__has_label__es + + self.assertEqual(r1_default, "default label"@p.df) + self.assertEqual(r1_de, "deutsches label"@p.de) + self.assertEqual(r1_es, "nombre español"@p.es) + + def test_b1__multilingual_relations1(self): + """ + test how to create items with labels in multiple languages + """ + + # this test will break if the default language is not "en" + # (it would need some further work to make it independent of the concrete default lang) + self.assertEqual(p.settings.DEFAULT_DATA_LANGUAGE, "en") + + self.assertTrue(isinstance(p.R2.R1, str)) + + with p.uri_context(uri=TEST_BASE_URI): + itm = p.create_item( + key_str=p.pop_uri_based_key("I"), + # multiple values to R1 can be passed using a list + R1__has_label="test-label in english", # specify default language + R1__has_label__de="test-label auf deutsch", + R2__has_description="test-description in english", + ) + + # this returns only one label according to the default language + default_label = itm.R1 + + # to access all labels use this: + label1, label2 = itm.get_relations("R1", return_obj=True) + self.assertEqual(default_label, label1) + self.assertEqual(label1.value, "test-label in english") + self.assertEqual(label1.language, "en") + self.assertEqual(label2.value, "test-label auf deutsch") + self.assertEqual(label2.language, "de") + + # add another language later + + with p.uri_context(uri=TEST_BASE_URI): + itm.set_relation(p.R2, "test-beschreibung auf deutsch" @ p.de) + + desc1, desc2 = itm.get_relations("R2", return_obj=True) + + self.assertTrue(isinstance(desc1, str)) + self.assertTrue(isinstance(desc2, p.Literal)) + + self.assertEqual(desc2.language, "de") + + # use the labels of different languages in index-labeld key notation + + # first: without explicitly specifying the language + tmp1 = itm["test-label in english"] + self.assertTrue(tmp1 is itm) + + tmp2 = itm["test-label auf deutsch"] + self.assertTrue(tmp2 is itm) + + # second: with explicitly specifying the language + tmp3 = itm["test-label in english" @ p.en] + self.assertTrue(tmp3 is itm) + + tmp4 = itm["test-label auf deutsch" @ p.de] + self.assertTrue(tmp4 is itm) + + with self.assertRaises(ValueError): + tmp5 = itm["wrong label"] + + with self.assertRaises(ValueError): + tmp5 = itm["wrong label" @ p.de] + + with self.assertRaises(ValueError): + tmp5 = itm["wrong label" @ p.en] # noqa + + # change the default language + p.settings.DEFAULT_DATA_LANGUAGE = "de" + + new_default_label = itm.R1 + self.assertEqual(new_default_label, label2) + self.assertEqual(new_default_label.language, "de") + + new_default_description = itm.R2 + self.assertEqual(new_default_description, "test-beschreibung auf deutsch" @ p.de) + + with p.uri_context(uri=TEST_BASE_URI): + with self.assertRaises(p.aux.FunctionalRelationError): + itm_fail = p.create_item( + key_str=p.pop_uri_based_key("I"), + # multiple values to R1 can be passed using a list + R1__has_label="test-label2", # this is now interpreted as de-label + R1__has_label__de="test-label2-de", # this causes an error + ) + + itm2 = p.create_item( + key_str=p.pop_uri_based_key("I"), + # multiple values to R1 can be passed using a list + R1__has_label="test-label2", # this is now interpreted as de-label + R1__has_label__en="test-label2-en", + R2__has_description="test beschreibung auf deutsch", + ) + + # in case of ordinary strings they should be used if no value is available for current language + + self.assertEqual(p.settings.DEFAULT_DATA_LANGUAGE, "de") + self.assertEqual(itm2.R1, "test-label2" @ p.de) + self.assertEqual(itm2.R2, "test beschreibung auf deutsch" @ p.de) + + p.settings.DEFAULT_DATA_LANGUAGE = "en" + self.assertEqual(itm2.R1.value, "test-label2-en") + + # no other description is available + self.assertEqual(itm2.R2, None) + + # TODO: decide whether this behavior (returning some other lang) would be better + # self.assertEqual(itm2.R2, "test beschreibung auf deutsch" @ p.de) + + # test for correct error message + with p.uri_context(uri=TEST_BASE_URI, prefix="ut"): + + itm1 = p.instance_of(p.I1["general item"]) + + # this should cause no error (because of differnt language) + itm1.set_relation(p.R1["has label"], "neues Label" @ p.de) + + with self.assertRaises(p.aux.FunctionalRelationError): + itm1.set_relation(p.R1["has label"], "new label") + + def test_b2__multilingual_relations2(self): + with p.uri_context(uri=TEST_BASE_URI, prefix="ut"): + R300 = p.create_relation( + R1__has_label="default rel-label", + R1__has_label__de="deutsches rel-label", + ) + + labels = R300.get_relations("R1", return_obj=True) + self.assertEqual(labels, ["default rel-label"@p.df, "deutsches rel-label"@p.de]) + class Test_Z_Core(HouskeeperMixin, unittest.TestCase): """ @@ -1565,7 +1660,7 @@ def test_c02__sparql_zz_preprocessing(self): {condition} }} """ - with self.assertRaises(AssertionError) as cm: + with self.assertRaises(p.aux.InconsistentLabelError) as cm: p.ds.preprocess_query(qsrc_incorr_1) self.assertEqual(cm.exception.args[0], msg) @@ -1649,11 +1744,12 @@ def test_b02__rdf_import(self): c = p.io.import_stms_from_rdf_triples(fpath) - c.new_items.sort(key=lambda itm: itm.R1__has_label) + self.assertIsInstance(c.new_items[0].R1, p.Literal) + c.new_items.sort(key=lambda itm: itm.R1__has_label.value) x0, x1, x2 = c.new_items # test that overwriting worked - self.assertEqual(R301.R1__has_label, "relation1") + self.assertEqual(R301.R1__has_label.value, "relation1") # test that a statement has been created self.assertEqual(x0.R301__relation1, [x1]) diff --git a/tests/test_rulebased_reasoning.py b/tests/test_rulebased_reasoning.py index faa81a3..9245b21 100644 --- a/tests/test_rulebased_reasoning.py +++ b/tests/test_rulebased_reasoning.py @@ -108,7 +108,7 @@ def test_d01__zebra_puzzle_stage02(self): zp = p.erkloader.load_mod_from_path(TEST_DATA_PATH_ZEBRA02, prefix="zp") # test base data - self.assertEqual(zp.zb.I4037["Englishman"].zb__R8098__has_house_color.R1, "red") + self.assertEqual(zp.zb.I4037["Englishman"].zb__R8098__has_house_color.R1.value, "red") # test hints self.assertEqual(zp.zb.I9848["Norwegian"].zb__R3606__lives_next_to[0], zp.person12) @@ -406,7 +406,7 @@ def test_d06__zebra_puzzle_stage02(self): cm.uses_external_entities(I702) with I702.scope("premise") as cm: - cm.new_rel(cm.rel1, p.R1["has label"], "another relation", overwrite=True) + cm.new_rel(cm.rel1, p.R1["has label"], "another relation"@p.df, overwrite=True) with I702.scope("assertion") as cm: cm.new_rel(cm.rel1, p.R54["is matched by rule"], I702) diff --git a/tests/test_script.py b/tests/test_script.py index 2b82616..85fd95e 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -53,7 +53,7 @@ def test_a01__insert_keys(self): itm2 = itm3.R4__is_instance_of itm1 = itm2.R3__is_subclass_of - self.assertEqual(itm1.R1__has_label, "some new item") + self.assertEqual(itm1.R1__has_label.value, "some new item") @unittest.skipIf(os.environ.get("CI"), "Skipping visualization test on CI to prevent graphviz-dependency")