From cd0b442671726efce594ee4502b9a3c9eafc50d0 Mon Sep 17 00:00:00 2001 From: Iwan Aucamp Date: Fri, 19 May 2023 12:17:29 +0200 Subject: [PATCH] fix: bugs with `rdflib.extras.infixowl` (#2390) Fix the following issues in `rdflib.extras.infixowl`: - getting and setting of max cardinality only considered identifiers and not other RDF terms. - The return value of `manchesterSyntax` was wrong for some cases. - The way that `BooleanClass` was generating its string representation (i.e. `BooleanClass.__repr__`) was wrong for some cases. Other changes: - Added an example for using infixowl to create an ontology. - Updated infixowl tests. - Updated infixowl documentation. This code is based on code from: - Changes are primarily authored by . --------- Co-authored-by: Graham Higgins --- examples/infixowl_ontology_creation.py | 280 ++++++++++++++++++ rdflib/extras/infixowl.py | 150 ++++++---- .../test_infixowl/test_booleanclass.py | 8 +- .../test_infixowl/test_restriction.py | 143 +++++++-- 4 files changed, 488 insertions(+), 93 deletions(-) create mode 100644 examples/infixowl_ontology_creation.py diff --git a/examples/infixowl_ontology_creation.py b/examples/infixowl_ontology_creation.py new file mode 100644 index 000000000..8efeb69ca --- /dev/null +++ b/examples/infixowl_ontology_creation.py @@ -0,0 +1,280 @@ +from rdflib import Graph, Literal, Namespace, URIRef +from rdflib.extras.infixowl import Class, Ontology, Property, min, only, some + +CPR = Namespace("http://purl.org/cpr/0.75#") +INF = Namespace("http://www.loa-cnr.it/ontologies/InformationObjects.owl#") +EDNS = Namespace("http://www.loa-cnr.it/ontologies/ExtendedDnS.owl#") +DOLCE = Namespace("http://www.loa-cnr.it/ontologies/DOLCE-Lite.owl#") +REL = Namespace("http://www.geneontology.org/owl#") +GALEN = Namespace("http://www.co-ode.org/ontologies/galen#") +TIME = Namespace("http://www.w3.org/2006/time#") +CYC = Namespace("http://sw.cyc.com/2006/07/27/cyc/") + + +def infixowl_example(): + g = Graph() + g.bind("cpr", CPR, override=False) + g.bind("ro", REL, override=False) + g.bind("inf", INF, override=False) + g.bind("edns", EDNS, override=False) + g.bind("dol", DOLCE, override=False) + g.bind("time", TIME, override=False) + g.bind("galen", GALEN, override=False) + + Class.factoryGraph = g + Property.factoryGraph = g + Ontology.factoryGraph = g + + cprOntology = Ontology(URIRef("http://purl.org/cpr/owl")) # noqa: N806 + cprOntology.imports = [ + URIRef("http://obo.sourceforge.net/relationship/relationship.owl"), + URIRef(DOLCE), + URIRef(EDNS), + URIRef("http://www.w3.org/2006/time#"), + ] + cprOntology.comment = [ + Literal( + """This OWL ontology was generated by Fuxi 0.85b.dev-r107 + (with newly added Infix OWL syntax library). It imports the + OBO relationship ontology, DOLCE, and OWL time. It formally + defines a focused, core set of archetypes [Jung, C.] + replicated in various patient record terminology. This core is + defined in RDF and follows the normalization principles + of "rigorous formal ontologies" [Rector, A.].""" + ) + ] + cprOntology.setVersion(Literal("0.75")) + + # Relations + # represented-by + representationOf = Property( # noqa: N806 + CPR["representation-of"], + inverseOf=Property(CPR["represented-by"]), + comment=[ + Literal( + """Patient records stand in the cpr:representation-of relation + with patients""" + ) + ], + ) + representedBy = Property( # noqa: F841, N806 + CPR["represented-by"], inverseOf=representationOf + ) + # description-of + descrOf = Property( # noqa: N806 + CPR["description-of"], + comment=[ + Literal( + """Clinical descriptions stand in the cpr:description-of + relation with various clinical phenomenon""" + ) + ], + domain=[Class(CPR["clinical-description"])], + ) + # cpr:interpreted-by + interpretedBy = Property( # noqa: F841, N806 + CPR["interpreted-by"], + comment=[ + Literal( + """Signs and symptoms are interpreted by rational physical + objects (people)""" + ) + ], + domain=[Class(CPR["medical-sign"]) | Class(CPR["symptom"])], + range=[Class(CPR.person)], + ) + # cpr:realized-by + realizedBy = Property( # noqa: N806 + CPR["realized-by"], + comment=[ + Literal( + """The epistemological relation in which screening acts and + the problems they realize stand to each other""" + ) + ], + inverseOf=Property(CPR["realizes"]), + domain=[Class(CPR["medical-problem"])], + range=[Class(CPR["screening-act"])], + ) + # cpr:realizes + realizes = Property(CPR["realizes"], inverseOf=realizedBy) # noqa: F841 + + # Classes + # cpr:person + person = Class(CPR.person) + person.comment = [ + Literal( + """A class which directly corresponds with the “Person” class in + both GALEN and Cyc""" + ) + ] + person.subClassOf = [Class(EDNS["rational-physical-object"])] + person.equivalentClass = [Class(GALEN.Person), Class(CYC.Person)] + + # cpr:patient + patient = Class(CPR.patient) + patient.comment = [ + Literal( + """A class which directly corresponds with the “Patient” + and “MedicalPatient” classes in GALEN / Cyc""" + ) + ] + # patient.equivalentClass = [Class(GALEN.Patient),Class(CYC.MedicalPatient)] + patient.subClassOf = [CPR["represented-by"] @ some @ Class(CPR["patient-record"])] + person += patient + + # cpr:clinician + clinician = Class(CPR.person) + clinician.comment = [ + Literal( + """A person who plays the clinician role (typically Nurse, + Physician / Doctor, etc.)""" + ) + ] + person += clinician + + # bytes + bytes = Class(CPR.bytes) + bytes.comment = [ + Literal( + """The collection of physical objects which constitute a stream of + bytes in memory, disk, etc.""" + ) + ] + bytes.subClassOf = [DOLCE["non-agentive-physical-object"]] + + # cpr:patient-record + patientRecord = Class(CPR["patient-record"]) # noqa: N806 + patientRecord.comment = [ + Literal( + """a class (a representational artifact [REFTERM]) depicting + relevant clinical information about a specific patient and is + primarily comprised of one or more + cpr:clinical-descriptions.""" + ) + ] + patientRecord.seeAlso = [URIRef("")] + patientRecord.subClassOf = [ + bytes, + # Class(CYC.InformationBearingThing), + CPR["representation-of"] @ only @ patient, + REL.OBO_REL_has_proper_part @ some @ Class(CPR["clinical-description"]), + ] + + # cpr:medical-problem + problem = Class( + CPR["medical-problem"], + subClassOf=[ + Class(DOLCE.quality), + realizedBy @ only @ Class(CPR["screening-act"]), + ], + ) + problem.comment = [ + Literal( + """.. problems that clearly require the intervention of a health + care professional. These include acute problems requiring + hospitalization and chronic problems requiring long-term + management.""" + ) + ] + + # cpr:clinical-description + clinDescr = Class(CPR["clinical-description"]) # noqa: N806 + clinDescr.disjointWith = [CPR["patient-record"]] + clinDescr.comment = [ + Literal( + """A class which corresponds (at least syntactically) with the HL7 + RIM Act Class, insofar as its members consist of clinical + recordings (representational artifacts) of natural phenomena + of clinical significance""" + ) + ] + clinDescr.subClassOf = [ + bytes, + # Class(CYC.InformationBearingThing), + DOLCE["has-quality"] @ some @ Class(TIME.TemporalEntity), + descrOf @ min @ Literal(1), + ] + + # cpr:medical-sign + sign = Class( + CPR["medical-sign"], + subClassOf=[ + problem, + Property(CPR["interpreted-by"]) @ only @ clinician, + Property(CPR["interpreted-by"]) @ some @ clinician, + ], + disjointWith=[CPR.symptom], + ) + sign.comment = [ + Literal( + """A cpr:medical-problem which are specifically interpreted by a + clinician. As such, this class is informally defined as an + objective indication of a quality typically detected by a + physician during a physical examination of a patient.""" + ) + ] + + symptom = Class( + CPR["symptom"], + subClassOf=[ + problem, + Property(CPR["interpreted-by"]) @ only @ patient, + Property(CPR["interpreted-by"]) @ some @ patient, + ], + disjointWith=[sign], + ) + symptom.comment = [ + Literal( + """(Medicine) any sensation or change in bodily function that is + experienced by a patient and is associated with a particular + disease.""" + ) + ] + + # clinical-act heriarchy + clinicalAct = Class( # noqa: N806 + CPR["clinical-act"], subClassOf=[Class(EDNS.activity)] + ) + + therapy = Class(CPR["therapeutic-act"], subClassOf=[clinicalAct]) + therapy += Class(CPR["physical-therapy"], disjointWith=[CPR["medical-therapy"]]) + therapy += Class( + CPR["psychological-therapy"], + disjointWith=[CPR["medical-therapy"], CPR["physical-therapy"]], + ) + + medicalTherapy = Class( # noqa: N806 + CPR["medical-therapy"], + disjointWith=[CPR["physical-therapy"], CPR["psychological-therapy"]], + ) + therapy += medicalTherapy + medicalTherapy += Class(CPR["substance-administration"]) + + diagnosticAct = Class(CPR["diagnostic-act"], subClassOf=[clinicalAct]) # noqa: N806 + diagnosticAct.disjointWith = [CPR["therapeutic-act"]] + + screeningAct = Class(CPR["screening-act"]) # noqa: N806 + screeningAct += Class(CPR["laboratory-test"]) + + diagnosticAct += screeningAct + + screeningAct += Class( + CPR["medical-history-screening-act"], + disjointWith=[CPR["clinical-examination"], CPR["laboratory-test"]], + ) + + screeningAct += Class( + CPR["clinical-examination"], + disjointWith=[CPR["laboratory-test"], CPR["medical-history-screening-act"]], + ) + + device = Class( # noqa: F841 + CPR["medical-device"], subClassOf=[Class(GALEN.Device)] + ) + + print(g.serialize(format="turtle")) + + +if __name__ == "__main__": + infixowl_example() diff --git a/rdflib/extras/infixowl.py b/rdflib/extras/infixowl.py index 9c75345bb..dadc6324e 100644 --- a/rdflib/extras/infixowl.py +++ b/rdflib/extras/infixowl.py @@ -2,6 +2,19 @@ __doc__ = """RDFLib Python binding for OWL Abstract Syntax +OWL Constructor DL Syntax Manchester OWL Syntax Example +==================================================================================== +intersectionOf C ∩ D C AND D Human AND Male +unionOf C ∪ D C OR D Man OR Woman +complementOf ¬ C NOT C NOT Male +oneOf {a} ∪ {b}... {a b ...} {England Italy Spain} +someValuesFrom ∃ R C R SOME C hasColleague SOME Professor +allValuesFrom ∀ R C R ONLY C hasColleague ONLY Professor +minCardinality ≥ N R R MIN 3 hasColleague MIN 3 +maxCardinality ≤ N R R MAX 3 hasColleague MAX 3 +cardinality = N R R EXACTLY 3 hasColleague EXACTLY 3 +hasValue ∃ R {a} R VALUE a hasColleague VALUE Matthew + see: http://www.w3.org/TR/owl-semantics/syntax.html http://owl-workshop.man.ac.uk/acceptedLong/submission_9.pdf @@ -12,12 +25,9 @@ Uses Manchester Syntax for __repr__ ->>> exNs = Namespace('http://example.com/') ->>> namespace_manager = NamespaceManager(Graph()) ->>> namespace_manager.bind('ex', exNs, override=False) ->>> namespace_manager.bind('owl', OWL, override=False) +>>> exNs = Namespace("http://example.com/") >>> g = Graph() ->>> g.namespace_manager = namespace_manager +>>> g.bind("ex", exNs, override=False) Now we have an empty graph, we can construct OWL classes in it using the Python classes defined in this module @@ -39,8 +49,6 @@ This can also be used against already populated graphs: >>> owlGraph = Graph().parse(str(OWL)) ->>> namespace_manager.bind('owl', OWL, override=False) ->>> owlGraph.namespace_manager = namespace_manager >>> list(Class(OWL.Class, graph=owlGraph).subClassOf) [Class: rdfs:Class ] @@ -97,13 +105,13 @@ Restrictions can also be created using Manchester OWL syntax in 'colloquial' Python ->>> exNs.hasParent << some >> Class(exNs.Physician, graph=g) +>>> exNs.hasParent @ some @ Class(exNs.Physician, graph=g) ( ex:hasParent SOME ex:Physician ) ->>> Property(exNs.hasParent, graph=g) << max >> Literal(1) +>>> Property(exNs.hasParent, graph=g) @ max @ Literal(1) ( ex:hasParent MAX 1 ) ->>> print(g.serialize(format='pretty-xml')) #doctest: +SKIP +>>> print(g.serialize(format='pretty-xml')) # doctest: +SKIP """ @@ -170,9 +178,7 @@ # definition of an Infix operator class # this recipe also works in jython -# calling sequence for the infix is either: -# x << op >> y -# or: +# calling sequence for the infix is: # x @ op @ y @@ -332,7 +338,8 @@ def castToQName(x): # noqa: N802 except Exception: if isinstance(thing, BNode): return thing.n3() - return "<" + thing + ">" + # Expect the unexpected + return thing.identifier if not isinstance(thing, str) else thing label = first(Class(thing, graph=store).label) if label: return label @@ -359,7 +366,8 @@ def _remover(inst): class Individual: """ - A typed individual + A typed individual, the base class of the InfixOWL classes. + """ factoryGraph = Graph() # noqa: N815 @@ -383,16 +391,45 @@ def __init__(self, identifier=None, graph=None): pass # pragma: no cover def clearInDegree(self): # noqa: N802 + """ + Remove references to this individual as an object in the + backing store. + """ self.graph.remove((None, None, self.identifier)) def clearOutDegree(self): # noqa: N802 + """ + Remove all statements to this individual as a subject in the + backing store. Note that this only removes the statements + themselves, not the blank node closure so there is a chance + that this will cause orphaned blank nodes to remain in the + graph. + """ self.graph.remove((self.identifier, None, None)) def delete(self): + """ + Delete the individual from the graph, clearing the in and + out degrees. + """ self.clearInDegree() self.clearOutDegree() def replace(self, other): + """ + Replace the individual in the graph with the given other, + causing all triples that refer to it to be changed and then + delete the individual. + + >>> g = Graph() + >>> b = Individual(OWL.Restriction, g) + >>> b.type = RDFS.Resource + >>> len(list(b.type)) + 1 + >>> del b.type + >>> len(list(b.type)) + 0 + """ for s, p, _o in self.graph.triples((None, None, self.identifier)): self.graph.add((s, p, classOrIdentifier(other))) self.delete() @@ -829,26 +866,23 @@ def DeepClassClear(class_to_prune): # noqa: N802 Recursively clear the given class, continuing where any related class is an anonymous class - >>> EX = Namespace('http://example.com/') - >>> namespace_manager = NamespaceManager(Graph()) - >>> namespace_manager.bind('ex', EX, override=False) - >>> namespace_manager.bind('owl', OWL, override=False) + >>> EX = Namespace("http://example.com/") >>> g = Graph() - >>> g.namespace_manager = namespace_manager + >>> g.bind("ex", EX, override=False) >>> Individual.factoryGraph = g >>> classB = Class(EX.B) >>> classC = Class(EX.C) >>> classD = Class(EX.D) >>> classE = Class(EX.E) >>> classF = Class(EX.F) - >>> anonClass = EX.someProp << some >> classD + >>> anonClass = EX.someProp @ some @ classD >>> classF += anonClass >>> list(anonClass.subClassOf) [Class: ex:F ] >>> classA = classE | classF | anonClass >>> classB += classA >>> classA.equivalentClass = [Class()] - >>> classB.subClassOf = [EX.someProp << some >> classC] + >>> classB.subClassOf = [EX.someProp @ some @ classC] >>> classA ( ex:E OR ex:F OR ( ex:someProp SOME ex:D ) ) >>> DeepClassClear(classA) @@ -1113,20 +1147,16 @@ def __and__(self, other): Construct an anonymous class description consisting of the intersection of this class and 'other' and return it - >>> exNs = Namespace('http://example.com/') - >>> namespace_manager = NamespaceManager(Graph()) - >>> namespace_manager.bind('ex', exNs, override=False) - >>> namespace_manager.bind('owl', OWL, override=False) - >>> g = Graph() - >>> g.namespace_manager = namespace_manager - Chaining 3 intersections + >>> exNs = Namespace("http://example.com/") + >>> g = Graph() + >>> g.bind("ex", exNs, override=False) >>> female = Class(exNs.Female, graph=g) >>> human = Class(exNs.Human, graph=g) >>> youngPerson = Class(exNs.YoungPerson, graph=g) >>> youngWoman = female & human & youngPerson - >>> youngWoman #doctest: +SKIP + >>> youngWoman # doctest: +SKIP ex:YoungPerson THAT ( ex:Female AND ex:Human ) >>> isinstance(youngWoman, BooleanClass) True @@ -1230,11 +1260,8 @@ def _get_parents(self): >>> from rdflib.util import first >>> exNs = Namespace('http://example.com/') - >>> namespace_manager = NamespaceManager(Graph()) - >>> namespace_manager.bind('ex', exNs, override=False) - >>> namespace_manager.bind('owl', OWL, override=False) >>> g = Graph() - >>> g.namespace_manager = namespace_manager + >>> g.bind("ex", exNs, override=False) >>> Individual.factoryGraph = g >>> brother = Class(exNs.Brother) >>> sister = Class(exNs.Sister) @@ -1462,25 +1489,21 @@ class EnumeratedClass(OWLRDFListProxy, Class): axiom ::= 'EnumeratedClass(' classID ['Deprecated'] { annotation } { individualID } ')' - - >>> exNs = Namespace('http://example.com/') - >>> namespace_manager = NamespaceManager(Graph()) - >>> namespace_manager.bind('ex', exNs, override=False) - >>> namespace_manager.bind('owl', OWL, override=False) + >>> exNs = Namespace("http://example.com/") >>> g = Graph() - >>> g.namespace_manager = namespace_manager + >>> g.bind("ex", exNs, override=False) >>> Individual.factoryGraph = g >>> ogbujiBros = EnumeratedClass(exNs.ogbujicBros, ... members=[exNs.chime, ... exNs.uche, ... exNs.ejike]) - >>> ogbujiBros #doctest: +SKIP + >>> ogbujiBros # doctest: +SKIP { ex:chime ex:uche ex:ejike } >>> col = Collection(g, first( ... g.objects(predicate=OWL.oneOf, subject=ogbujiBros.identifier))) >>> sorted([g.qname(item) for item in col]) ['ex:chime', 'ex:ejike', 'ex:uche'] - >>> print(g.serialize(format='n3')) #doctest: +SKIP + >>> print(g.serialize(format='n3')) # doctest: +SKIP @prefix ex: . @prefix owl: . @prefix rdf: . @@ -1531,16 +1554,14 @@ class BooleanClassExtentHelper: >>> testGraph = Graph() >>> Individual.factoryGraph = testGraph >>> EX = Namespace("http://example.com/") - >>> namespace_manager = NamespaceManager(Graph()) - >>> namespace_manager.bind('ex', EX, override=False) - >>> testGraph.namespace_manager = namespace_manager + >>> testGraph.bind("ex", EX, override=False) >>> fire = Class(EX.Fire) >>> water = Class(EX.Water) >>> testClass = BooleanClass(members=[fire, water]) >>> testClass2 = BooleanClass( ... operator=OWL.unionOf, members=[fire, water]) >>> for c in BooleanClass.getIntersections(): - ... print(c) #doctest: +SKIP + ... print(c) # doctest: +SKIP ( ex:Fire AND ex:Water ) >>> for c in BooleanClass.getUnions(): ... print(c) #doctest: +SKIP @@ -1560,7 +1581,10 @@ def _getExtent(): # noqa: N802 class Callable: def __init__(self, anycallable): - self.__call__ = anycallable + self._callfn = anycallable + + def __call__(self, *args, **kwargs): + return self._callfn(*args, **kwargs) class BooleanClass(OWLRDFListProxy, Class): @@ -1602,9 +1626,7 @@ def __init__( rdf_list = list(self.graph.objects(predicate=operator, subject=self.identifier)) assert ( not members or not rdf_list - ), "This is a previous boolean class description!" + repr( - Collection(self.graph, rdf_list[0]).n3() - ) + ), "This is a previous boolean class description." OWLRDFListProxy.__init__(self, rdf_list, members) def copy(self): @@ -1637,13 +1659,10 @@ def changeOperator(self, newOperator): # noqa: N802, N803 Converts a unionOf / intersectionOf class expression into one that instead uses the given operator - >>> testGraph = Graph() >>> Individual.factoryGraph = testGraph >>> EX = Namespace("http://example.com/") - >>> namespace_manager = NamespaceManager(Graph()) - >>> namespace_manager.bind('ex', EX, override=False) - >>> testGraph.namespace_manager = namespace_manager + >>> testGraph.bind("ex", EX, override=False) >>> fire = Class(EX.Fire) >>> water = Class(EX.Water) >>> testClass = BooleanClass(members=[fire,water]) @@ -1655,7 +1674,7 @@ def changeOperator(self, newOperator): # noqa: N802, N803 >>> try: ... testClass.changeOperator(OWL.unionOf) ... except Exception as e: - ... print(e) #doctest: +SKIP + ... print(e) # doctest: +SKIP The new operator is already being used! """ @@ -1668,7 +1687,11 @@ def __repr__(self): """ Returns the Manchester Syntax equivalent for this class """ - return manchesterSyntax(self._rdfList.uri, self.graph, boolean=self._operator) + return manchesterSyntax( + self._rdfList.uri if isinstance(self._rdfList, Collection) else BNode(), + self.graph, + boolean=self._operator, + ) def __or__(self, other): """ @@ -1704,6 +1727,7 @@ class Restriction(Class): OWL.allValuesFrom, OWL.someValuesFrom, OWL.hasValue, + OWL.cardinality, OWL.maxCardinality, OWL.minCardinality, ] @@ -1774,16 +1798,14 @@ def serialize(self, graph): >>> g1 = Graph() >>> g2 = Graph() >>> EX = Namespace("http://example.com/") - >>> namespace_manager = NamespaceManager(g1) - >>> namespace_manager.bind('ex', EX, override=False) - >>> namespace_manager = NamespaceManager(g2) - >>> namespace_manager.bind('ex', EX, override=False) + >>> g1.bind("ex", EX, override=False) + >>> g2.bind("ex", EX, override=False) >>> Individual.factoryGraph = g1 >>> prop = Property(EX.someProp, baseType=OWL.DatatypeProperty) >>> restr1 = (Property( ... EX.someProp, - ... baseType=OWL.DatatypeProperty)) << some >> (Class(EX.Foo)) - >>> restr1 #doctest: +SKIP + ... baseType=OWL.DatatypeProperty)) @ some @ (Class(EX.Foo)) + >>> restr1 # doctest: +SKIP ( ex:someProp SOME ex:Foo ) >>> restr1.serialize(g2) >>> Individual.factoryGraph = g2 @@ -1917,7 +1939,7 @@ def _get_cardinality(self): def _set_cardinality(self, other): if not other: return - triple = (self.identifier, OWL.cardinality, classOrIdentifier(other)) + triple = (self.identifier, OWL.cardinality, classOrTerm(other)) if triple in self.graph: return else: @@ -1939,7 +1961,7 @@ def _get_maxcardinality(self): def _set_maxcardinality(self, other): if not other: return - triple = (self.identifier, OWL.maxCardinality, classOrIdentifier(other)) + triple = (self.identifier, OWL.maxCardinality, classOrTerm(other)) if triple in self.graph: return else: diff --git a/test/test_extras/test_infixowl/test_booleanclass.py b/test/test_extras/test_infixowl/test_booleanclass.py index 86f7a223e..62153ce06 100644 --- a/test/test_extras/test_infixowl/test_booleanclass.py +++ b/test/test_extras/test_infixowl/test_booleanclass.py @@ -17,7 +17,7 @@ def graph(): del g -@pytest.mark.xfail(reason="assert len(props) == 1, repr(props), so AssertionError: []") +@pytest.mark.xfail(reason="AssertionError, len(props) != 1", raises=AssertionError) def test_booleanclass_operator_as_none(graph): fire = Class(EXNS.Fire) water = Class(EXNS.Water) @@ -63,16 +63,10 @@ def test_booleanclass_with_or_operator(graph): assert str(c) == "( ex:Fire OR ex:Water )" -@pytest.mark.xfail( - reason="BooleanClass.getIntersections() - TypeError: 'Callable' object is not callable" -) def test_getintersections(graph): _ = BooleanClass.getIntersections() -@pytest.mark.xfail( - reason="BooleanClass.getUnions() - TypeError: 'Callable' object is not callable" -) def test_getunions(graph): _ = BooleanClass.getUnions() diff --git a/test/test_extras/test_infixowl/test_restriction.py b/test/test_extras/test_infixowl/test_restriction.py index c57cacb2c..94ffc36f5 100644 --- a/test/test_extras/test_infixowl/test_restriction.py +++ b/test/test_extras/test_infixowl/test_restriction.py @@ -1,6 +1,6 @@ import pytest -from rdflib import OWL, XSD, BNode, Graph, Literal, Namespace, URIRef +from rdflib import OWL, RDF, XSD, BNode, Graph, Literal, Namespace, URIRef from rdflib.extras.infixowl import Class, Individual, Property, Restriction, some EXNS = Namespace("http://example.org/vocab/") @@ -21,11 +21,7 @@ def graph(): def test_restriction_str_and_hash(graph): - r1 = ( - (Property(EXNS.someProp, baseType=OWL.DatatypeProperty)) - @ some - @ (Class(EXNS.Foo)) - ) + r1 = Property(EXNS.someProp, baseType=OWL.DatatypeProperty) @ some @ Class(EXNS.Foo) assert str(r1) == "( ex:someProp SOME ex:Foo )" @@ -236,34 +232,40 @@ def test_restriction_cardinality_value(graph): assert str(r.cardinality) == "Some Class " -@pytest.mark.xfail(reason="_set_cardinality fails to handle Literal") def test_restriction_cardinality_set_value(graph): r = Restriction( onProperty=EXNS.hasChild, graph=graph, - cardinality=OWL.cardinality, + cardinality=Literal("0", datatype=XSD.nonNegativeInteger), + identifier=URIRef(EXNS.r1), ) + assert str(r) == "( ex:hasChild EQUALS 0 )" + assert graph.serialize(format="ttl") == ( "@prefix ex: .\n" "@prefix owl: .\n" + "@prefix xsd: .\n" "\n" - "[] a owl:Restriction ;\n" - " owl:cardinality owl:cardinality ;\n" + "ex:r1 a owl:Restriction ;\n" + ' owl:cardinality "0"^^xsd:nonNegativeInteger ;\n' " owl:onProperty ex:hasChild .\n" "\n" ) - assert r.cardinality is not None - - assert str(r) == "( ex:hasChild EQUALS http://www.w3.org/2002/07/owl#cardinality )" - - assert str(r.cardinality) == "Class: owl:cardinality " + r.cardinality = Literal("1", datatype=XSD.nonNegativeInteger) - r.cardinality = Literal("0", datatype=XSD.nonNegativeInteger) + assert str(r) == "( ex:hasChild EQUALS 1 )" - assert ( - str(r) == '( ex:hasChild EQUALS owl:cardinality "0"^^xsd:nonNegativeInteger )' + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "@prefix xsd: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + ' owl:cardinality "1"^^xsd:nonNegativeInteger ;\n' + " owl:onProperty ex:hasChild .\n" + "\n" ) @@ -271,21 +273,114 @@ def test_restriction_maxcardinality(graph): r = Restriction( onProperty=EXNS.hasChild, graph=graph, - maxCardinality=OWL.maxCardinality, + maxCardinality=Literal("0", datatype=XSD.nonNegativeInteger), + identifier=URIRef(EXNS.r1), ) - assert str(r.maxCardinality) == "Class: owl:maxCardinality " + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "@prefix xsd: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + ' owl:maxCardinality "0"^^xsd:nonNegativeInteger ;\n' + " owl:onProperty ex:hasChild .\n" + "\n" + ) + + # FIXME: Don't do this, it changes the value!! + assert str(r.maxCardinality) == "Some Class " + + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "@prefix xsd: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + ' owl:maxCardinality "0"^^xsd:nonNegativeInteger ;\n' + " owl:onProperty ex:hasChild .\n" + "\n" + "[] a owl:Class .\n" + "\n" + ) r.maxCardinality = OWL.maxCardinality + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + " owl:maxCardinality owl:maxCardinality ;\n" + " owl:onProperty ex:hasChild .\n" + "\n" + "[] a owl:Class .\n" + "\n" + ) + + # Ignored r.maxCardinality = None - r.maxCardinality = EXNS.foo + assert graph.serialize(format="ttl") != "" + + superfluous_assertion_subject = list(graph.subjects(RDF.type, OWL.Class))[0] + + assert isinstance(superfluous_assertion_subject, BNode) + + graph.remove((superfluous_assertion_subject, RDF.type, OWL.Class)) + + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + " owl:maxCardinality owl:maxCardinality ;\n" + " owl:onProperty ex:hasChild .\n" + "\n" + ) + + r.maxCardinality = EXNS.maxkids + + assert str(r) == "( ex:hasChild MAX http://example.org/vocab/maxkids )" + + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + " owl:maxCardinality ex:maxkids ;\n" + " owl:onProperty ex:hasChild .\n" + "\n" + ) del r.maxCardinality + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + " owl:onProperty ex:hasChild .\n" + "\n" + ) + assert r.maxCardinality is None + r.maxCardinality = Literal("2", datatype=XSD.nonNegativeInteger) + + assert str(r) == "( ex:hasChild MAX 2 )" + + assert graph.serialize(format="ttl") == ( + "@prefix ex: .\n" + "@prefix owl: .\n" + "@prefix xsd: .\n" + "\n" + "ex:r1 a owl:Restriction ;\n" + ' owl:maxCardinality "2"^^xsd:nonNegativeInteger ;\n' + " owl:onProperty ex:hasChild .\n" + "\n" + ) + def test_restriction_mincardinality(graph): r = Restriction( @@ -300,12 +395,16 @@ def test_restriction_mincardinality(graph): r.minCardinality = None - r.minCardinality = EXNS.foo + r.minCardinality = EXNS.minkids + + assert str(r) == "( ex:hasChild MIN http://example.org/vocab/minkids )" del r.minCardinality assert r.minCardinality is None + r.minCardinality = Literal("0", datatype=XSD.nonNegativeInteger) + def test_restriction_kind(graph): r = Restriction(