From f88c83de38ba6390b5a3eeb695166615a7c05afb Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 20 Nov 2021 19:05:40 +0100 Subject: [PATCH 01/14] Added support for evaluating Manchester expression to owlready2 --- README.md | 1 + ontopy/manchester.py | 104 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + tests/test_manchester.py | 43 ++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 ontopy/manchester.py create mode 100644 tests/test_manchester.py diff --git a/README.md b/README.md index 1f80715a7..2a9c3817e 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ pip install EMMOntoPy - [semver]: Required for `ontoversion`-tool. - [pydot]: Used for generating graphs. Will be deprecated. + - [pyparsing](https://github.com/pyparsing/pyparsing): Used for parsing Manchester syntax See [docs/docker-instructions.md](docs/docker-instructions.md) for how to build a docker image. diff --git a/ontopy/manchester.py b/ontopy/manchester.py new file mode 100644 index 000000000..f9e2fedd1 --- /dev/null +++ b/ontopy/manchester.py @@ -0,0 +1,104 @@ +"""Evaluate Manchester syntax.""" +import pyparsing as pp +import ontopy # noqa F401 -- ontopy must be imported before owlready2 +import owlready2 + + +GRAMMAR = None # Global cache + + +def manchester_expression(): + """Returns pyparsing grammer for a Manchester expression. + + See also: https://www.w3.org/TR/owl2-manchester-syntax/ + """ + global GRAMMAR + if GRAMMAR: + return GRAMMAR + + ident = pp.Word(pp.alphas + '_', pp.alphanums + '_', asKeyword=True) + uint = pp.Word(pp.nums) + logOp = pp.oneOf(['and', 'or'], asKeyword=True) + expr = pp.Forward() + restriction = pp.Forward() + primary = pp.Keyword('not')[...] + ( + restriction | ident('cls') | pp.nestedExpr('(', ')', expr)) + objPropExpr = ( + pp.Literal('inverse') + pp.Suppress('(') + ident('objProp') + + pp.Suppress(')') | + pp.Literal('inverse') + ident('objProp') | + ident('objProp')) + restriction << ( + objPropExpr + pp.Keyword('some') + expr | + objPropExpr + pp.Keyword('only') + expr | + objPropExpr + pp.Keyword('Self') | + objPropExpr + pp.Keyword('value') + ident('individual') | + objPropExpr + pp.Keyword('min') + uint + expr | + objPropExpr + pp.Keyword('max') + uint + expr | + objPropExpr + pp.Keyword('exactly') + uint + expr) + expr << primary + (logOp('op') + expr)[...] + GRAMMAR = expr + return expr + + +class ManchesterError(Exception): + """Raised on invalid Manchester notation.""" + + +def evaluate(ontology, expr): + """ + """ + def _eval(r): + """Evaluate parsed expression.""" + def fneg(x): + return owlready2.Not(x) if neg else x + + if isinstance(r, str): + return ontology[r] + + neg = False + while r[0] == 'not': + r.pop(0) + neg = not neg + + if len(r) == 1: + if isinstance(r[0], str): + return fneg(ontology[r[0]]) + else: + return fneg(_eval(r[0])) + elif r.op: + ops = {'and': owlready2.And, 'or': owlready2.Or} + if r.op not in ops: + raise ManchesterError(f'unexpected logical operator: {r.op}') + op = ops[r.op] + if len(r) == 3: + return op([fneg(_eval(r[0])), _eval(r[2])]) + else: + arg1 = fneg(_eval(r[0])) + r.pop(0) + r.pop(0) + return op([arg1, _eval(r)]) + elif r.objProp: + if r[0] == 'inverse': + r.pop(0) + prop = owlready2.Inverse(ontology[r[0]]) + else: + prop = ontology[r[0]] + rtype = r[1] + if rtype == 'Self': + return fneg(prop.has_self()) + r.pop(0) + r.pop(0) + f = getattr(prop, rtype) + if rtype in ('some', 'only'): + return fneg(f(_eval(r))) + elif rtype in ('min', 'max', 'exactly'): + cardinality = r.pop() + return fneg(f(cardinality, _eval(r))) + else: + raise ManchesterError(f'invalid restriction type: {rtype}') + else: + raise ManchesterError(f'invalid expression: {r}') + + grammar = manchester_expression() + return _eval(grammar.parseString(expr)) diff --git a/requirements.txt b/requirements.txt index 84aac94d2..ed4f956e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ Pygments>=2.7.4,<3 PyYAML>=5.4.1,<7 rdflib>=4.2.1,<7 semver>=2.8.1,<3 +pyparsing>=2.4.7 diff --git a/tests/test_manchester.py b/tests/test_manchester.py new file mode 100644 index 000000000..c6f083950 --- /dev/null +++ b/tests/test_manchester.py @@ -0,0 +1,43 @@ +from ontopy import get_ontology +from ontopy.manchester import evaluate +from owlready2 import And, Or, Not, Inverse + + +def test(s, expected, no_traceback=True): + if no_traceback: + try: + r = evaluate(emmo, s) + except pp.ParseException as e: + print('**', s, f'-- ParseError: {e}') + except Exception as e: + print('**', s, f'-- EvalError: {e}') + else: + r = evaluate(emmo, s) + + if repr(r) == repr(expected): + print(s, f'--> {r}') + else: + print('**', s, f'-- Failed: {r} != {expected}') + + +emmo = get_ontology().load() + +test('Item', emmo.Item) +test('not Item', Not(emmo.Item)) +test('not not Item', emmo.Item) +test('hasPart some Atom', emmo.hasPart.some(emmo.Atom)) +test('Atom and not Molecule', emmo.Atom & Not(emmo.Molecule)) +test('Atom and (not Molecule)', emmo.Atom & Not(emmo.Molecule)) +test('not Atom and Molecule', Not(emmo.Atom) & emmo.Molecule) +test('(not Atom) and Molecule', Not(emmo.Atom) & emmo.Molecule) +test('inverse hasPart some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) +test('inverse(hasPart) some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) +test('not hasPart some Atom', Not(emmo.hasPart.some(emmo.Atom))) +test('not (hasPart some Atom)', Not(emmo.hasPart.some(emmo.Atom))) +test('hasPart some (not Atom)', emmo.hasPart.some(Not(emmo.Atom))) +test('hasPart some not Atom', emmo.hasPart.some(Not(emmo.Atom))) +test('not hasPart some not Atom', Not(emmo.hasPart.some(Not(emmo.Atom)))) +test('hasPart only (inverse hasPart some not Atom)', + emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) +test('hasPart only inverse hasPart some not Atom', + emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) From da6c9e571321437e3f9c9ff6268a13ee1124ec1f Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 20 Nov 2021 19:30:48 +0100 Subject: [PATCH 02/14] Corrected bug in __init__ modules in emmopy and ontopy and cleaned up test_manchester.py --- emmopy/__init__.py | 2 +- ontopy/__init__.py | 2 +- tests/test_manchester.py | 53 ++++++++++++++++------------------------ 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/emmopy/__init__.py b/emmopy/__init__.py index 0b12928f1..a2e8df3f9 100644 --- a/emmopy/__init__.py +++ b/emmopy/__init__.py @@ -10,7 +10,7 @@ raise RuntimeError("emmopy requires Python 3.6 or later") # Ensure emmopy is imported before owlready2... -if "owlready2" in sys.modules and "ontopy" not in sys.modules: +if "owlready2" in sys.modules and "emmopy" not in sys.modules: raise RuntimeError("emmopy must be imported before owlready2") # Import functions from emmopy diff --git a/ontopy/__init__.py b/ontopy/__init__.py index 4c348e8da..1f0a0302f 100644 --- a/ontopy/__init__.py +++ b/ontopy/__init__.py @@ -9,7 +9,7 @@ raise RuntimeError("ontopy requires Python 3.6 or later") # Ensure ontopy is imported before owlready2... -if "owlready2" in sys.modules and "emmopy" not in sys.modules: +if "owlready2" in sys.modules and "ontopy" not in sys.modules: raise RuntimeError("ontopy must be imported before owlready2") # Monkey patch Owlready2 by injecting some methods diff --git a/tests/test_manchester.py b/tests/test_manchester.py index c6f083950..054a48e6b 100644 --- a/tests/test_manchester.py +++ b/tests/test_manchester.py @@ -3,41 +3,30 @@ from owlready2 import And, Or, Not, Inverse -def test(s, expected, no_traceback=True): - if no_traceback: - try: - r = evaluate(emmo, s) - except pp.ParseException as e: - print('**', s, f'-- ParseError: {e}') - except Exception as e: - print('**', s, f'-- EvalError: {e}') - else: - r = evaluate(emmo, s) - - if repr(r) == repr(expected): - print(s, f'--> {r}') - else: - print('**', s, f'-- Failed: {r} != {expected}') +def check(s, expected): + r = evaluate(emmo, s) + print(s, '-->', r) + assert repr(r) == repr(expected) emmo = get_ontology().load() -test('Item', emmo.Item) -test('not Item', Not(emmo.Item)) -test('not not Item', emmo.Item) -test('hasPart some Atom', emmo.hasPart.some(emmo.Atom)) -test('Atom and not Molecule', emmo.Atom & Not(emmo.Molecule)) -test('Atom and (not Molecule)', emmo.Atom & Not(emmo.Molecule)) -test('not Atom and Molecule', Not(emmo.Atom) & emmo.Molecule) -test('(not Atom) and Molecule', Not(emmo.Atom) & emmo.Molecule) -test('inverse hasPart some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) -test('inverse(hasPart) some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) -test('not hasPart some Atom', Not(emmo.hasPart.some(emmo.Atom))) -test('not (hasPart some Atom)', Not(emmo.hasPart.some(emmo.Atom))) -test('hasPart some (not Atom)', emmo.hasPart.some(Not(emmo.Atom))) -test('hasPart some not Atom', emmo.hasPart.some(Not(emmo.Atom))) -test('not hasPart some not Atom', Not(emmo.hasPart.some(Not(emmo.Atom)))) -test('hasPart only (inverse hasPart some not Atom)', +check('Item', emmo.Item) +check('not Item', Not(emmo.Item)) +check('not not Item', emmo.Item) +check('hasPart some Atom', emmo.hasPart.some(emmo.Atom)) +check('Atom and not Molecule', emmo.Atom & Not(emmo.Molecule)) +check('Atom and (not Molecule)', emmo.Atom & Not(emmo.Molecule)) +check('not Atom and Molecule', Not(emmo.Atom) & emmo.Molecule) +check('(not Atom) and Molecule', Not(emmo.Atom) & emmo.Molecule) +check('inverse hasPart some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) +check('inverse(hasPart) some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) +check('not hasPart some Atom', Not(emmo.hasPart.some(emmo.Atom))) +check('not (hasPart some Atom)', Not(emmo.hasPart.some(emmo.Atom))) +check('hasPart some (not Atom)', emmo.hasPart.some(Not(emmo.Atom))) +check('hasPart some not Atom', emmo.hasPart.some(Not(emmo.Atom))) +check('not hasPart some not Atom', Not(emmo.hasPart.some(Not(emmo.Atom)))) +check('hasPart only (inverse hasPart some not Atom)', emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) -test('hasPart only inverse hasPart some not Atom', +check('hasPart only inverse hasPart some not Atom', emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) From 4607e8c9225bb613805e1907a166544dfc294306 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 20 Nov 2021 19:50:23 +0100 Subject: [PATCH 03/14] Added docstring to manchester.evaluate() --- ontopy/manchester.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index f9e2fedd1..7bdbc40e7 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -45,8 +45,25 @@ class ManchesterError(Exception): """Raised on invalid Manchester notation.""" -def evaluate(ontology, expr): - """ +def evaluate(ontology : owlready2.Ontology, expr : str): + """Evaluate expression in Manchester syntax. + + Args: + ontology: The ontology within which the expression will be evaluated. + expr: Manchester expression to be evaluated. + + Returns: + An Owlready2 construct that corresponds to the expression. + + Example: + >>> from ontopy.manchester import evaluate + >>> from ontopy import get_ontology + >>> emmo = get_ontology.load() + + >>> restriction = evaluate(emmo, 'hasPart some Atom') + >>> cls = evaluate(emmo, 'Atom') + >>> expr = evaluate(emmo, 'Atom or Molecule') + """ def _eval(r): """Evaluate parsed expression.""" From c5e115e40ba0d09274969631fd1d06aae94b668d Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 20 Nov 2021 23:43:26 +0100 Subject: [PATCH 04/14] Updated tests --- tests/test_manchester.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_manchester.py b/tests/test_manchester.py index 054a48e6b..18a666603 100644 --- a/tests/test_manchester.py +++ b/tests/test_manchester.py @@ -14,6 +14,7 @@ def check(s, expected): check('Item', emmo.Item) check('not Item', Not(emmo.Item)) check('not not Item', emmo.Item) +check('not (not Item)', Not(Not(emmo.Item))) check('hasPart some Atom', emmo.hasPart.some(emmo.Atom)) check('Atom and not Molecule', emmo.Atom & Not(emmo.Molecule)) check('Atom and (not Molecule)', emmo.Atom & Not(emmo.Molecule)) @@ -27,6 +28,12 @@ def check(s, expected): check('hasPart some not Atom', emmo.hasPart.some(Not(emmo.Atom))) check('not hasPart some not Atom', Not(emmo.hasPart.some(Not(emmo.Atom)))) check('hasPart only (inverse hasPart some not Atom)', - emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) + emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) check('hasPart only inverse hasPart some not Atom', - emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) + emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) +check('Atom and Molecule and Proton', + emmo.Atom & (emmo.Molecule & emmo.Proton)) +check('Atom and (Molecule and Proton)', + emmo.Atom & (emmo.Molecule & emmo.Proton)) +check('(Atom and Molecule) or Proton', + (emmo.Atom & emmo.Molecule) | emmo.Proton) From 822e9d1bec11c5c1c3542a78850d5e0389ec7ae1 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 21 Nov 2021 12:42:29 +0100 Subject: [PATCH 05/14] Added docs/api_reference/ontopy/manchester.md --- docs/api_reference/ontopy/manchester.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/api_reference/ontopy/manchester.md diff --git a/docs/api_reference/ontopy/manchester.md b/docs/api_reference/ontopy/manchester.md new file mode 100644 index 000000000..27c2200c0 --- /dev/null +++ b/docs/api_reference/ontopy/manchester.md @@ -0,0 +1,6 @@ +# manchester + +::: ontopy.manchester + +A module for compiling restrictions and logical constructs in +Manchester syntax into Owlready2 classes. From 2dc38fdf7cd6e3de0bacd5ed6c07986e344b08ec Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 21 Nov 2021 13:08:04 +0100 Subject: [PATCH 06/14] Sorted requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed4f956e3..080db63a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ Owlready2>=0.28,<0.36,!=0.32,!=0.34 packaging>=21.0<22 pydot>=1.4.1,<2 Pygments>=2.7.4,<3 +pyparsing>=2.4.7 PyYAML>=5.4.1,<7 rdflib>=4.2.1,<7 semver>=2.8.1,<3 -pyparsing>=2.4.7 From 6ec1483cfa85be0ac489567182642f90bc5ee354 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Mon, 22 Nov 2021 23:50:39 +0100 Subject: [PATCH 07/14] Added more comments as suggested by Francesca Also polluted it with a lot of "pylint: disable=..." in order to commit... --- docs/api_reference/ontopy/manchester.md | 3 - ontopy/manchester.py | 94 ++++++++++++++++--------- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/docs/api_reference/ontopy/manchester.md b/docs/api_reference/ontopy/manchester.md index 27c2200c0..4ed8fc363 100644 --- a/docs/api_reference/ontopy/manchester.md +++ b/docs/api_reference/ontopy/manchester.md @@ -1,6 +1,3 @@ # manchester ::: ontopy.manchester - -A module for compiling restrictions and logical constructs in -Manchester syntax into Owlready2 classes. diff --git a/ontopy/manchester.py b/ontopy/manchester.py index 7bdbc40e7..e9f575169 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -1,4 +1,12 @@ -"""Evaluate Manchester syntax.""" +"""Evaluate Manchester syntax + +This module compiles restrictions and logical constructs in Manchester +syntax into Owlready2 classes. The main function in this module is +`manchester.evaluate()`, see its docstring for usage example. + +Pyparsing is used under the hood for parsing. +""" +# pylint: disable=unused-import,wrong-import-order import pyparsing as pp import ontopy # noqa F401 -- ontopy must be imported before owlready2 import owlready2 @@ -10,33 +18,45 @@ def manchester_expression(): """Returns pyparsing grammer for a Manchester expression. + This function is mostly for internal use. + See also: https://www.w3.org/TR/owl2-manchester-syntax/ """ + # pylint: disable=global-statement,invalid-name,expression-not-assigned global GRAMMAR if GRAMMAR: return GRAMMAR - ident = pp.Word(pp.alphas + '_', pp.alphanums + '_', asKeyword=True) + # Subset of the Manchester grammar for expressions + # It is based on https://www.w3.org/TR/owl2-manchester-syntax/ + # but allows logical constructs within restrictions (like Protege) + ident = pp.Word(pp.alphas + "_", pp.alphanums + "_", asKeyword=True) uint = pp.Word(pp.nums) - logOp = pp.oneOf(['and', 'or'], asKeyword=True) + logOp = pp.oneOf(["and", "or"], asKeyword=True) expr = pp.Forward() restriction = pp.Forward() - primary = pp.Keyword('not')[...] + ( - restriction | ident('cls') | pp.nestedExpr('(', ')', expr)) + primary = pp.Keyword("not")[...] + ( + restriction | ident("cls") | pp.nestedExpr("(", ")", expr) + ) objPropExpr = ( - pp.Literal('inverse') + pp.Suppress('(') + ident('objProp') + - pp.Suppress(')') | - pp.Literal('inverse') + ident('objProp') | - ident('objProp')) + pp.Literal("inverse") + + pp.Suppress("(") + + ident("objProp") + + pp.Suppress(")") + | pp.Literal("inverse") + ident("objProp") + | ident("objProp") + ) restriction << ( - objPropExpr + pp.Keyword('some') + expr | - objPropExpr + pp.Keyword('only') + expr | - objPropExpr + pp.Keyword('Self') | - objPropExpr + pp.Keyword('value') + ident('individual') | - objPropExpr + pp.Keyword('min') + uint + expr | - objPropExpr + pp.Keyword('max') + uint + expr | - objPropExpr + pp.Keyword('exactly') + uint + expr) - expr << primary + (logOp('op') + expr)[...] + objPropExpr + pp.Keyword("some") + expr + | objPropExpr + pp.Keyword("only") + expr + | objPropExpr + pp.Keyword("Self") + | objPropExpr + pp.Keyword("value") + ident("individual") + | objPropExpr + pp.Keyword("min") + uint + expr + | objPropExpr + pp.Keyword("max") + uint + expr + | objPropExpr + pp.Keyword("exactly") + uint + expr + ) + expr << primary + (logOp("op") + expr)[...] + GRAMMAR = expr return expr @@ -45,7 +65,7 @@ class ManchesterError(Exception): """Raised on invalid Manchester notation.""" -def evaluate(ontology : owlready2.Ontology, expr : str): +def evaluate(ontology: owlready2.Ontology, expr: str): """Evaluate expression in Manchester syntax. Args: @@ -65,28 +85,34 @@ def evaluate(ontology : owlready2.Ontology, expr : str): >>> expr = evaluate(emmo, 'Atom or Molecule') """ + # pylint: disable=invalid-name,no-else-return,too-many-return-statements + # pylint: disable=too-many-branches def _eval(r): - """Evaluate parsed expression.""" + """Recursively evaluate expression produced by pyparsing into an + Owlready2 construct.""" + def fneg(x): + """Negates the argument if `neg` is true.""" return owlready2.Not(x) if neg else x - if isinstance(r, str): + if isinstance(r, str): # r is atomic, returns its owlready2 repr return ontology[r] - neg = False - while r[0] == 'not': - r.pop(0) + neg = False # whether the expression starts with "not" + while r[0] == "not": + r.pop(0) # strip off the "not" and proceed neg = not neg - if len(r) == 1: + if len(r) == 1: # r is either a atomic or a parenthesised + # subexpression that should be further evaluated if isinstance(r[0], str): return fneg(ontology[r[0]]) else: return fneg(_eval(r[0])) - elif r.op: - ops = {'and': owlready2.And, 'or': owlready2.Or} + elif r.op: # r contains a logical operator: and/or + ops = {"and": owlready2.And, "or": owlready2.Or} if r.op not in ops: - raise ManchesterError(f'unexpected logical operator: {r.op}') + raise ManchesterError(f"unexpected logical operator: {r.op}") op = ops[r.op] if len(r) == 3: return op([fneg(_eval(r[0])), _eval(r[2])]) @@ -95,27 +121,27 @@ def fneg(x): r.pop(0) r.pop(0) return op([arg1, _eval(r)]) - elif r.objProp: - if r[0] == 'inverse': + elif r.objProp: # r is a restriction + if r[0] == "inverse": r.pop(0) prop = owlready2.Inverse(ontology[r[0]]) else: prop = ontology[r[0]] rtype = r[1] - if rtype == 'Self': + if rtype == "Self": return fneg(prop.has_self()) r.pop(0) r.pop(0) f = getattr(prop, rtype) - if rtype in ('some', 'only'): + if rtype in ("some", "only"): return fneg(f(_eval(r))) - elif rtype in ('min', 'max', 'exactly'): + elif rtype in ("min", "max", "exactly"): cardinality = r.pop() return fneg(f(cardinality, _eval(r))) else: - raise ManchesterError(f'invalid restriction type: {rtype}') + raise ManchesterError(f"invalid restriction type: {rtype}") else: - raise ManchesterError(f'invalid expression: {r}') + raise ManchesterError(f"invalid expression: {r}") grammar = manchester_expression() return _eval(grammar.parseString(expr)) From 1992e6aaf5b40af53bac090136f16f1acd3fba0b Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Tue, 23 Nov 2021 00:24:53 +0100 Subject: [PATCH 08/14] Added a test_manchester() function --- ontopy/manchester.py | 8 ++--- tests/test_manchester.py | 70 +++++++++++++++++++++++----------------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index e9f575169..2d1edd441 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -16,13 +16,13 @@ def manchester_expression(): - """Returns pyparsing grammer for a Manchester expression. + """Returns pyparsing grammar for a Manchester expression. This function is mostly for internal use. See also: https://www.w3.org/TR/owl2-manchester-syntax/ """ - # pylint: disable=global-statement,invalid-name,expression-not-assigned + # pylint: disable=global-statement,invalid-name global GRAMMAR if GRAMMAR: return GRAMMAR @@ -46,7 +46,7 @@ def manchester_expression(): | pp.Literal("inverse") + ident("objProp") | ident("objProp") ) - restriction << ( + restriction <<= ( objPropExpr + pp.Keyword("some") + expr | objPropExpr + pp.Keyword("only") + expr | objPropExpr + pp.Keyword("Self") @@ -55,7 +55,7 @@ def manchester_expression(): | objPropExpr + pp.Keyword("max") + uint + expr | objPropExpr + pp.Keyword("exactly") + uint + expr ) - expr << primary + (logOp("op") + expr)[...] + expr <<= primary + (logOp("op") + expr)[...] GRAMMAR = expr return expr diff --git a/tests/test_manchester.py b/tests/test_manchester.py index 18a666603..a329fb2aa 100644 --- a/tests/test_manchester.py +++ b/tests/test_manchester.py @@ -3,37 +3,49 @@ from owlready2 import And, Or, Not, Inverse +emmo = get_ontology().load() + + def check(s, expected): r = evaluate(emmo, s) - print(s, '-->', r) + print(s, "-->", r) assert repr(r) == repr(expected) -emmo = get_ontology().load() - -check('Item', emmo.Item) -check('not Item', Not(emmo.Item)) -check('not not Item', emmo.Item) -check('not (not Item)', Not(Not(emmo.Item))) -check('hasPart some Atom', emmo.hasPart.some(emmo.Atom)) -check('Atom and not Molecule', emmo.Atom & Not(emmo.Molecule)) -check('Atom and (not Molecule)', emmo.Atom & Not(emmo.Molecule)) -check('not Atom and Molecule', Not(emmo.Atom) & emmo.Molecule) -check('(not Atom) and Molecule', Not(emmo.Atom) & emmo.Molecule) -check('inverse hasPart some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) -check('inverse(hasPart) some Atom', Inverse(emmo.hasPart).some(emmo.Atom)) -check('not hasPart some Atom', Not(emmo.hasPart.some(emmo.Atom))) -check('not (hasPart some Atom)', Not(emmo.hasPart.some(emmo.Atom))) -check('hasPart some (not Atom)', emmo.hasPart.some(Not(emmo.Atom))) -check('hasPart some not Atom', emmo.hasPart.some(Not(emmo.Atom))) -check('not hasPart some not Atom', Not(emmo.hasPart.some(Not(emmo.Atom)))) -check('hasPart only (inverse hasPart some not Atom)', - emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) -check('hasPart only inverse hasPart some not Atom', - emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom)))) -check('Atom and Molecule and Proton', - emmo.Atom & (emmo.Molecule & emmo.Proton)) -check('Atom and (Molecule and Proton)', - emmo.Atom & (emmo.Molecule & emmo.Proton)) -check('(Atom and Molecule) or Proton', - (emmo.Atom & emmo.Molecule) | emmo.Proton) +def test_manchester(): + check("Item", emmo.Item) + check("not Item", Not(emmo.Item)) + check("not not Item", emmo.Item) + check("not (not Item)", Not(Not(emmo.Item))) + check("hasPart some Atom", emmo.hasPart.some(emmo.Atom)) + check("Atom and not Molecule", emmo.Atom & Not(emmo.Molecule)) + check("Atom and (not Molecule)", emmo.Atom & Not(emmo.Molecule)) + check("not Atom and Molecule", Not(emmo.Atom) & emmo.Molecule) + check("(not Atom) and Molecule", Not(emmo.Atom) & emmo.Molecule) + check("inverse hasPart some Atom", Inverse(emmo.hasPart).some(emmo.Atom)) + check("inverse(hasPart) some Atom", Inverse(emmo.hasPart).some(emmo.Atom)) + check("not hasPart some Atom", Not(emmo.hasPart.some(emmo.Atom))) + check("not (hasPart some Atom)", Not(emmo.hasPart.some(emmo.Atom))) + check("hasPart some (not Atom)", emmo.hasPart.some(Not(emmo.Atom))) + check("hasPart some not Atom", emmo.hasPart.some(Not(emmo.Atom))) + check("not hasPart some not Atom", Not(emmo.hasPart.some(Not(emmo.Atom)))) + check( + "hasPart only (inverse hasPart some not Atom)", + emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom))), + ) + check( + "hasPart only inverse hasPart some not Atom", + emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom))), + ) + check( + "Atom and Molecule and Proton", + emmo.Atom & (emmo.Molecule & emmo.Proton), + ) + check( + "Atom and (Molecule and Proton)", + emmo.Atom & (emmo.Molecule & emmo.Proton), + ) + check( + "(Atom and Molecule) or Proton", + (emmo.Atom & emmo.Molecule) | emmo.Proton, + ) From 41813844ec0f2e221bb2c1873e5e056c3cac03f7 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Tue, 23 Nov 2021 00:43:25 +0100 Subject: [PATCH 09/14] Fixed issues blocking pre-commit --- demo/horizontal/step1_generate_metadata.py | 2 +- docs/index.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/horizontal/step1_generate_metadata.py b/demo/horizontal/step1_generate_metadata.py index 6f5bb2831..609eaa2bf 100755 --- a/demo/horizontal/step1_generate_metadata.py +++ b/demo/horizontal/step1_generate_metadata.py @@ -52,4 +52,4 @@ print("Generated metadata for the usercase ontology:") print(f" {e.coll.count()} instances") -print(f" {len(list(e.coll.relations()))} relations") +print(f" {len(list(e.coll.get_relations()))} relations") diff --git a/docs/index.md b/docs/index.md index 912c21e4e..4f1cbf154 100644 --- a/docs/index.md +++ b/docs/index.md @@ -120,6 +120,7 @@ pip install EMMOntoPy - [semver]: Required for `ontoversion`-tool. - [pydot]: Used for generating graphs. Will be deprecated. + - [pyparsing](https://github.com/pyparsing/pyparsing): Used for parsing Manchester syntax See [docker-instructions.md](docker-instructions.md) for how to build a docker image. From a546ae205c9b8bc08331d54b931af1e5bbe824ab Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Tue, 23 Nov 2021 00:52:10 +0100 Subject: [PATCH 10/14] Added return type annotation to ontopy.manchester.evaluate() --- ontopy/manchester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index 2d1edd441..950a10bb9 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -65,7 +65,7 @@ class ManchesterError(Exception): """Raised on invalid Manchester notation.""" -def evaluate(ontology: owlready2.Ontology, expr: str): +def evaluate(ontology: owlready2.Ontology, expr: str) -> owlready2.Construct: """Evaluate expression in Manchester syntax. Args: From 746db6316510424d1a2aea05db16a518a920ba9e Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 24 Nov 2021 10:52:35 +0100 Subject: [PATCH 11/14] Update manchester.py Added parseAll=True to pyparsing parseString() to get an exception for invalid expressions --- ontopy/manchester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index 950a10bb9..0c92df67c 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -144,4 +144,4 @@ def fneg(x): raise ManchesterError(f"invalid expression: {r}") grammar = manchester_expression() - return _eval(grammar.parseString(expr)) + return _eval(grammar.parseString(expr, parseAll=True)) From fec8970d48322cdc676252bc2fa5e3c3d5590293 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 25 Nov 2021 08:35:56 +0100 Subject: [PATCH 12/14] Added support for data properties, except that language strings are not parsed correctly yet... --- ontopy/manchester.py | 53 +++++++++++++++++++++++++++++++++++++--- tests/test_manchester.py | 26 +++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index 0c92df67c..4879d3843 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -22,7 +22,7 @@ def manchester_expression(): See also: https://www.w3.org/TR/owl2-manchester-syntax/ """ - # pylint: disable=global-statement,invalid-name + # pylint: disable=global-statement,invalid-name,too-many-locals global GRAMMAR if GRAMMAR: return GRAMMAR @@ -32,6 +32,17 @@ def manchester_expression(): # but allows logical constructs within restrictions (like Protege) ident = pp.Word(pp.alphas + "_", pp.alphanums + "_", asKeyword=True) uint = pp.Word(pp.nums) + string = pp.Word(pp.alphanums + ":") + quotedString = ( + pp.QuotedString('"""', multiline=True) | pp.QuotedString('"') + )("string") + typedLiteral = pp.Combine(quotedString + "^^" + string("datatype")) + stringLiteral = quotedString + stringLanguageLiteral = pp.Combine(quotedString + "@" + string("language")) + numberLiteral = pp.pyparsing_common.number("number") + literal = ( + typedLiteral | stringLiteral | stringLanguageLiteral | numberLiteral + ) logOp = pp.oneOf(["and", "or"], asKeyword=True) expr = pp.Forward() restriction = pp.Forward() @@ -46,6 +57,7 @@ def manchester_expression(): | pp.Literal("inverse") + ident("objProp") | ident("objProp") ) + dataPropExpr = ident("dataProp") restriction <<= ( objPropExpr + pp.Keyword("some") + expr | objPropExpr + pp.Keyword("only") + expr @@ -54,6 +66,7 @@ def manchester_expression(): | objPropExpr + pp.Keyword("min") + uint + expr | objPropExpr + pp.Keyword("max") + uint + expr | objPropExpr + pp.Keyword("exactly") + uint + expr + | dataPropExpr + pp.Keyword("value") + literal ) expr <<= primary + (logOp("op") + expr)[...] @@ -65,6 +78,7 @@ class ManchesterError(Exception): """Raised on invalid Manchester notation.""" +# pylint: disable=too-many-statements def evaluate(ontology: owlready2.Ontology, expr: str) -> owlready2.Construct: """Evaluate expression in Manchester syntax. @@ -85,6 +99,23 @@ def evaluate(ontology: owlready2.Ontology, expr: str) -> owlready2.Construct: >>> expr = evaluate(emmo, 'Atom or Molecule') """ + + # pylint: disable=invalid-name + def _parse_literal(r): + """Compiles literal to Owlready2 type.""" + print("*** repr:", repr(r)) + if r.language: + print(" language:", r.language) + v = owlready2.locstr(r.string, r.language) + elif r.number: + print(" number:", repr(r.number), type(r.number)) + v = r.number + else: + print(" string:", r.string) + v = r.string + print(" v:", repr(v)) + return v + # pylint: disable=invalid-name,no-else-return,too-many-return-statements # pylint: disable=too-many-branches def _eval(r): @@ -111,8 +142,6 @@ def fneg(x): return fneg(_eval(r[0])) elif r.op: # r contains a logical operator: and/or ops = {"and": owlready2.And, "or": owlready2.Or} - if r.op not in ops: - raise ManchesterError(f"unexpected logical operator: {r.op}") op = ops[r.op] if len(r) == 3: return op([fneg(_eval(r[0])), _eval(r[2])]) @@ -133,13 +162,29 @@ def fneg(x): r.pop(0) r.pop(0) f = getattr(prop, rtype) - if rtype in ("some", "only"): + if rtype == "value": + return fneg(f(_eval(r))) + elif rtype in ("some", "only"): return fneg(f(_eval(r))) elif rtype in ("min", "max", "exactly"): cardinality = r.pop() return fneg(f(cardinality, _eval(r))) else: raise ManchesterError(f"invalid restriction type: {rtype}") + elif r.dataProp: # r is a data property restriction + prop = ontology[r[0]] + rtype = r[1] + r.pop(0) + r.pop(0) + f = getattr(prop, rtype) + if rtype == "value": + print("===", _parse_literal(r)) + return f(_parse_literal(r)) + else: + raise ManchesterError( + f"unimplemented data property restriction: " + f"{prop} {rtype} {r}" + ) else: raise ManchesterError(f"invalid expression: {r}") diff --git a/tests/test_manchester.py b/tests/test_manchester.py index a329fb2aa..25943aa0d 100644 --- a/tests/test_manchester.py +++ b/tests/test_manchester.py @@ -1,6 +1,6 @@ from ontopy import get_ontology from ontopy.manchester import evaluate -from owlready2 import And, Or, Not, Inverse +from owlready2 import And, Or, Not, Inverse, locstr emmo = get_ontology().load() @@ -49,3 +49,27 @@ def test_manchester(): "(Atom and Molecule) or Proton", (emmo.Atom & emmo.Molecule) | emmo.Proton, ) + check( + "(Atom and Molecule) or Proton", + (emmo.Atom & emmo.Molecule) | emmo.Proton, + ) + check( + "inverse(hasPart) value Universe", + Inverse(emmo.hasPart).value(emmo.Universe), + ) + # literal data restriction + check('hasSymbolData value "hello"', emmo.hasSymbolData.value("hello")) + check("hasSymbolData value 42", emmo.hasSymbolData.value(42)) + check("hasSymbolData value 3.14", emmo.hasSymbolData.value(3.14)) + check( + 'hasSymbolData value "abc"^^xsd:string', + emmo.hasSymbolData.value("abc"), + ) + # check( + # 'hasSymbolData value "hello"@en', + # emmo.hasSymbolData.value(locstr("hello", "en")), + # ) + + +if __name__ == "__main__": + test_manchester() From 72c0248a24e46d8ad072add549b675689921ccab Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 25 Nov 2021 22:34:37 +0100 Subject: [PATCH 13/14] Added support for data property literals --- ontopy/manchester.py | 14 +++++++------- tests/test_manchester.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index 4879d3843..6a9c0da48 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -32,16 +32,17 @@ def manchester_expression(): # but allows logical constructs within restrictions (like Protege) ident = pp.Word(pp.alphas + "_", pp.alphanums + "_", asKeyword=True) uint = pp.Word(pp.nums) + alphas = pp.Word(pp.alphas) string = pp.Word(pp.alphanums + ":") quotedString = ( pp.QuotedString('"""', multiline=True) | pp.QuotedString('"') )("string") typedLiteral = pp.Combine(quotedString + "^^" + string("datatype")) + stringLanguageLiteral = pp.Combine(quotedString + "@" + alphas("language")) stringLiteral = quotedString - stringLanguageLiteral = pp.Combine(quotedString + "@" + string("language")) numberLiteral = pp.pyparsing_common.number("number") literal = ( - typedLiteral | stringLiteral | stringLanguageLiteral | numberLiteral + typedLiteral | stringLanguageLiteral | stringLiteral | numberLiteral ) logOp = pp.oneOf(["and", "or"], asKeyword=True) expr = pp.Forward() @@ -98,22 +99,21 @@ def evaluate(ontology: owlready2.Ontology, expr: str) -> owlready2.Construct: >>> cls = evaluate(emmo, 'Atom') >>> expr = evaluate(emmo, 'Atom or Molecule') + Note: + Logical expressions (with `not`, `and` and `or`) are supported as + well as object property restrictions as well as data property value + restrictions. Other data property restrictions are not yet supported. """ # pylint: disable=invalid-name def _parse_literal(r): """Compiles literal to Owlready2 type.""" - print("*** repr:", repr(r)) if r.language: - print(" language:", r.language) v = owlready2.locstr(r.string, r.language) elif r.number: - print(" number:", repr(r.number), type(r.number)) v = r.number else: - print(" string:", r.string) v = r.string - print(" v:", repr(v)) return v # pylint: disable=invalid-name,no-else-return,too-many-return-statements diff --git a/tests/test_manchester.py b/tests/test_manchester.py index 25943aa0d..8280dfaba 100644 --- a/tests/test_manchester.py +++ b/tests/test_manchester.py @@ -65,10 +65,10 @@ def test_manchester(): 'hasSymbolData value "abc"^^xsd:string', emmo.hasSymbolData.value("abc"), ) - # check( - # 'hasSymbolData value "hello"@en', - # emmo.hasSymbolData.value(locstr("hello", "en")), - # ) + check( + 'hasSymbolData value "hello"@en', + emmo.hasSymbolData.value(locstr("hello", "en")), + ) if __name__ == "__main__": From 0bb20a8b87650dbbcd14ae45142225ed6a7d7115 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 25 Nov 2021 22:45:10 +0100 Subject: [PATCH 14/14] Corrected the english in docstring --- ontopy/manchester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index 6a9c0da48..5b59cdff6 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -101,8 +101,8 @@ def evaluate(ontology: owlready2.Ontology, expr: str) -> owlready2.Construct: Note: Logical expressions (with `not`, `and` and `or`) are supported as - well as object property restrictions as well as data property value - restrictions. Other data property restrictions are not yet supported. + well as object property restrictions. For data properterties are + only value restrictions supported so far. """ # pylint: disable=invalid-name