diff --git a/CHANGELOG.md b/CHANGELOG.md index 67419e5c..2c9daf83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Python-side type checks for `owlapy.iri.IRI` (`__init__`/`create`, converting bare `-O`-strippable `assert`s to clear `TypeError`/`ValueError`), `owlapy.owl_individual.OWLAnonymousIndividual` (`node_id`), and `owlapy.owl_ontology.OWLOntologyID` (`ontology_iri`/`version_iri`) -- a broader sweep beyond #271's explicitly-tracked scope, covering the same silent-Python-success-then-opaque-JVM-failure pattern for the identity type nearly every OWL entity is built from (#271) +- Python-side type checks for `owlapy.swrl` (`Variable`/`IVariable`/`DVariable`, `ClassAtom`, `DataRangeAtom`, `ObjectPropertyAtom`, `DataPropertyAtom`, `SameAsAtom`, `DifferentFromAtom`, `BuiltInAtom`, `Rule`), the last unvalidated Python-Java mapping flagged in #271. Also fixed a latent bug in `Atom.from_string`'s built-in-predicate parsing branch where a misplaced `return` inside the arg-conversion loop silently left every argument but the first as a raw, unconverted string, and non-variable arguments were wrapped as `OWLNamedIndividual` instead of `OWLLiteral` (#271) +- Python-side type checks for `owlapy.owl_data_ranges`'s `OWLNaryDataRange`/`OWLDataComplementOf`, deliberately left out of the two PRs above: operands are validated against `OWLPropertyRange` (the common base of `OWLDataRange` and `OWLClassExpression`) rather than `OWLDataRange` itself, since `owlapy.utils.nnf.NNF` deliberately reuses these constructors to wrap data-side class expressions (e.g. `OWLDataSomeValuesFrom`) during negation. This closes out the remaining scope of #271 (all Python-Java mappings now have Python-side validation) - Python-side type checks for the `owlapy.class_expression.restriction` constructors #272 missed: `OWLObjectSomeValuesFrom`/`OWLObjectAllValuesFrom` (property/filler), `OWLObjectHasSelf` (property), `OWLObjectHasValue` (property/individual), `OWLDataCardinalityRestriction` (`OWLDataMinCardinality`/`Max`/`Exact`, cardinality/property -- its `OWLObjectCardinalityRestriction` sibling already had this from #272), `OWLDataSomeValuesFrom`/`OWLDataAllValuesFrom` (property), `OWLDataHasValue` (property/value), `OWLObjectOneOf`/`OWLDataOneOf` (element type, now raising `TypeError` instead of a bare, `-O`-strippable `assert`), `OWLDatatypeRestriction` (datatype/facet restrictions), and `OWLFacetRestriction` (facet). `owlapy.owl_data_ranges`'s `OWLNaryDataRange`/`OWLDataComplementOf` were deliberately left unchanged -- they're reused by `owlapy.utils.nnf.NNF` to wrap data-side class expressions (e.g. `OWLDataSomeValuesFrom`) during negation, not just genuine `OWLDataRange` instances, so a strict `OWLDataRange` check there would break NNF (#271) - Python-side type checks across OWL construct constructors (`owlapy.owl_axiom`, `owlapy.class_expression`, `owlapy.owl_property`, `owlapy.owl_individual`, `owlapy.owl_datatype`, `owlapy.owl_literal`, `owlapy.namespaces`): constructing an OWL axiom/class expression/entity with an argument of the wrong type (e.g. an `OWLSubClassOfAxiom` given object properties instead of class expressions) now raises a clear `TypeError`/`ValueError` immediately, instead of succeeding silently and only failing later -- as an opaque Java exception -- once the construct is used with a JVM-backed ontology (#271, #272) - Additional regression tests for the `owlapy.owl_axiom` type checks above, covering `OWLSubClassOfAxiom`, `OWLClassAssertionAxiom`, `OWLEquivalentClassesAxiom`, `OWLObjectPropertyAssertionAxiom`, `OWLObjectPropertyDomainAxiom`, `OWLFunctionalObjectPropertyAxiom`, and `OWLDeclarationAxiom` (#271) diff --git a/owlapy/iri.py b/owlapy/iri.py index c2aad842..c862dd09 100644 --- a/owlapy/iri.py +++ b/owlapy/iri.py @@ -43,10 +43,15 @@ class IRI(OWLAnnotationSubject, OWLAnnotationValue, metaclass=_meta_IRI): def __init__(self, namespace: Union[str, Namespaces], remainder: str="", is_file_path=False): if isinstance(namespace, Namespaces): namespace = namespace.ns + elif not isinstance(namespace, str): + raise TypeError(f"Expected 'namespace' to be an instance of str or Namespaces, got {type(namespace).__name__} instead ({namespace!r}).") elif not is_file_path: - assert namespace[-1] in ("/", ":", "#"), ("It should be a valid IRI based on /, :, and #. " - "Are you saving a file? - then set is_file_path=True " - "to overcome this assertion.") + if not namespace or namespace[-1] not in ("/", ":", "#"): + raise ValueError("It should be a valid IRI based on /, :, and #. " + "Are you saving a file? - then set is_file_path=True " + "to overcome this assertion.") + if not isinstance(remainder, str): + raise TypeError(f"Expected 'remainder' to be an instance of str, got {type(remainder).__name__} instead ({remainder!r}).") import sys # https://docs.python.org/3.2/library/sys.html?highlight=sys.intern#sys.intern self._namespace = sys.intern(namespace) @@ -54,20 +59,23 @@ def __init__(self, namespace: Union[str, Namespaces], remainder: str="", is_file @staticmethod def create(iri:str | Namespaces, remainder:str=None, is_file_path=False) -> 'IRI': - assert isinstance(iri, str) | isinstance(iri, Namespaces), f"Input must be a string or an instance of Namespaces. Currently, {type(iri)}" + if not isinstance(iri, (str, Namespaces)): + raise TypeError(f"Expected 'iri' to be an instance of str or Namespaces, got {type(iri).__name__} instead ({iri!r}).") if is_file_path and iri != "": return IRI(iri, "", is_file_path) elif remainder is not None: - assert isinstance(remainder,str), f"Remainder must be string. Currently, {type(remainder)}" + if not isinstance(remainder, str): + raise TypeError(f"Expected 'remainder' to be an instance of str, got {type(remainder).__name__} instead ({remainder!r}).") return IRI(iri, remainder) else: - assert isinstance(iri, str) and remainder is None, \ - f"iri must be string if remainder is None. Currently, {type(iri)} and {type(remainder)}" + if not isinstance(iri, str): + raise TypeError(f"Expected 'iri' to be an instance of str when 'remainder' is None, got {type(iri).__name__} instead ({iri!r}).") # Extract remainder from input string - assert "/" in iri, (f"Input must contain /\tCurrently, {iri}. Are you saving a file? - then " - f"set is_file_path=True to overcome this assertion.") - # assert ":" in iri, "Input must contain :" - assert " " not in iri, f"Input must not contain whitespace. Currently:{iri}." + if "/" not in iri: + raise ValueError(f"Input must contain /\tCurrently, {iri}. Are you saving a file? - then " + f"set is_file_path=True to overcome this assertion.") + if " " in iri: + raise ValueError(f"Input must not contain whitespace. Currently:{iri}.") index = 1 + max(iri.rfind("/"), iri.rfind(":"), iri.rfind("#")) return IRI(iri[0:index], iri[index:]) diff --git a/owlapy/owl_data_ranges.py b/owlapy/owl_data_ranges.py index c230f895..955700b6 100644 --- a/owlapy/owl_data_ranges.py +++ b/owlapy/owl_data_ranges.py @@ -48,7 +48,17 @@ def __init__(self, operands: Iterable[OWLDataRange]): Args: operands: Data ranges. """ - self._operands = tuple(operands) + operands = tuple(operands) + for i, op in enumerate(operands): + # NB: checked against OWLPropertyRange (the common base of OWLDataRange and + # OWLClassExpression), not OWLDataRange itself -- owlapy.utils.nnf.NNF deliberately + # reuses this constructor to combine data-property restriction class expressions + # (e.g. OWLDataSomeValuesFrom) during negation, not just genuine data ranges. + if not isinstance(op, OWLPropertyRange): + raise TypeError( + f"Expected all operands to be instances of OWLPropertyRange, got {type(op).__name__} instead ({op!r}) at index {i}." + ) + self._operands = operands def operands(self) -> Iterable[OWLDataRange]: # documented in parent @@ -107,6 +117,11 @@ def __init__(self, data_range: OWLDataRange): Args: data_range: Data range to complement. """ + # NB: checked against OWLPropertyRange, not OWLDataRange -- see OWLNaryDataRange.__init__. + if not isinstance(data_range, OWLPropertyRange): + raise TypeError( + f"Expected 'data_range' to be an instance of OWLPropertyRange, got {type(data_range).__name__} instead ({data_range!r})." + ) self._data_range = data_range def get_data_range(self) -> OWLDataRange: diff --git a/owlapy/owl_individual.py b/owlapy/owl_individual.py index b4d2a682..b45697db 100644 --- a/owlapy/owl_individual.py +++ b/owlapy/owl_individual.py @@ -99,6 +99,8 @@ def __init__(self, node_id: Optional[str] = None): Returns: An OWLAnonymousIndividual identified by the given (or a freshly generated) node id. """ + if node_id is not None and not isinstance(node_id, str): + raise TypeError(f"Expected 'node_id' to be an instance of str or None, got {type(node_id).__name__} instead ({node_id!r}).") self._node_id = NodeID.get_node_id(node_id) @property diff --git a/owlapy/owl_ontology.py b/owlapy/owl_ontology.py index 644a41bf..c3e35c97 100644 --- a/owlapy/owl_ontology.py +++ b/owlapy/owl_ontology.py @@ -202,6 +202,10 @@ def __init__(self, ontology_iri: Optional[IRI] = None, version_iri: Optional[IRI ontology_iri: The ontology IRI (optional). version_iri: The version IRI (must be None if no ontology_iri is provided). """ + if ontology_iri is not None and not isinstance(ontology_iri, IRI): + raise TypeError(f"Expected 'ontology_iri' to be an instance of IRI or None, got {type(ontology_iri).__name__} instead ({ontology_iri!r}).") + if version_iri is not None and not isinstance(version_iri, IRI): + raise TypeError(f"Expected 'version_iri' to be an instance of IRI or None, got {type(version_iri).__name__} instead ({version_iri!r}).") self._ontology_iri = ontology_iri self._version_iri = version_iri diff --git a/owlapy/swrl.py b/owlapy/swrl.py index 42afdf74..89d734e1 100644 --- a/owlapy/swrl.py +++ b/owlapy/swrl.py @@ -30,8 +30,10 @@ class Variable(metaclass=ABCMeta): def __init__(self, iri:Union[IRI, str]): if isinstance(iri, str): self.iri = IRI.create(iri) - else: + elif isinstance(iri, IRI): self.iri = iri + else: + raise TypeError(f"Expected 'iri' to be an instance of IRI or str, got {type(iri).__name__} instead ({iri!r}).") def is_i_variable(self): if isinstance(self, IVariable): @@ -150,8 +152,8 @@ def from_string(atom_str: str, namespace: str, dp_predicates: List[str] = None): if "?" in args[i]: args[i] = DVariable(SWRL + args[i][1:]) else: - args[i] = OWLNamedIndividual(namespace + args[i]) - return BuiltInAtom(IRI.create(SWRLB + predicate), args) + args[i] = OWLLiteral(args[i]) + return BuiltInAtom(IRI.create(SWRLB + predicate), args) else: raise ValueError(f"Invalid SWRL atom: {atom_str}") @@ -191,6 +193,10 @@ class ClassAtom(Atom): cls: OWLClass def __init__(self, cls: OWLClass, argument1: Union[IVariable, OWLNamedIndividual]): + if not isinstance(cls, OWLClass): + raise TypeError(f"Expected 'cls' to be an instance of OWLClass, got {type(cls).__name__} instead ({cls!r}).") + if not isinstance(argument1, (IVariable, OWLNamedIndividual)): + raise TypeError(f"Expected 'argument1' to be an instance of IVariable or OWLNamedIndividual, got {type(argument1).__name__} instead ({argument1!r}).") self.cls = cls self.argument1 = argument1 @@ -225,10 +231,14 @@ def __eq__(self, other): class DataRangeAtom(Atom): """Represents a data range atom in SWRL syntax""" - argument1: DVariable + argument1: Union[DVariable, OWLLiteral] datatype: OWLDatatype - def __init__(self, datatype: OWLDatatype, argument1: DVariable): + def __init__(self, datatype: OWLDatatype, argument1: Union[DVariable, OWLLiteral]): + if not isinstance(datatype, OWLDatatype): + raise TypeError(f"Expected 'datatype' to be an instance of OWLDatatype, got {type(datatype).__name__} instead ({datatype!r}).") + if not isinstance(argument1, (DVariable, OWLLiteral)): + raise TypeError(f"Expected 'argument1' to be an instance of DVariable or OWLLiteral, got {type(argument1).__name__} instead ({argument1!r}).") self.datatype = datatype self.argument1 = argument1 @@ -296,6 +306,12 @@ class ObjectPropertyAtom(PropertyAtom): def __init__(self, prop: OWLObjectProperty, argument1: Union[OWLNamedIndividual, IVariable], argument2: Union[OWLNamedIndividual, IVariable]): + if not isinstance(prop, OWLObjectProperty): + raise TypeError(f"Expected 'prop' to be an instance of OWLObjectProperty, got {type(prop).__name__} instead ({prop!r}).") + if not isinstance(argument1, (OWLNamedIndividual, IVariable)): + raise TypeError(f"Expected 'argument1' to be an instance of OWLNamedIndividual or IVariable, got {type(argument1).__name__} instead ({argument1!r}).") + if not isinstance(argument2, (OWLNamedIndividual, IVariable)): + raise TypeError(f"Expected 'argument2' to be an instance of OWLNamedIndividual or IVariable, got {type(argument2).__name__} instead ({argument2!r}).") super().__init__(prop, argument1, argument2) def __repr__(self): @@ -318,6 +334,12 @@ class DataPropertyAtom(PropertyAtom): def __init__(self, prop: OWLDataProperty, argument1: Union[OWLNamedIndividual, IVariable], argument2: Union[OWLLiteral, DVariable]): + if not isinstance(prop, OWLDataProperty): + raise TypeError(f"Expected 'prop' to be an instance of OWLDataProperty, got {type(prop).__name__} instead ({prop!r}).") + if not isinstance(argument1, (OWLNamedIndividual, IVariable)): + raise TypeError(f"Expected 'argument1' to be an instance of OWLNamedIndividual or IVariable, got {type(argument1).__name__} instead ({argument1!r}).") + if not isinstance(argument2, (OWLLiteral, DVariable)): + raise TypeError(f"Expected 'argument2' to be an instance of OWLLiteral or DVariable, got {type(argument2).__name__} instead ({argument2!r}).") super().__init__(prop, argument1, argument2) def __repr__(self): @@ -337,6 +359,10 @@ class SameAsAtom(Atom): argument2: Union[IVariable, OWLNamedIndividual] def __init__(self, argument1: Union[IVariable, OWLNamedIndividual], argument2: Union[IVariable, OWLNamedIndividual]): + if not isinstance(argument1, (IVariable, OWLNamedIndividual)): + raise TypeError(f"Expected 'argument1' to be an instance of IVariable or OWLNamedIndividual, got {type(argument1).__name__} instead ({argument1!r}).") + if not isinstance(argument2, (IVariable, OWLNamedIndividual)): + raise TypeError(f"Expected 'argument2' to be an instance of IVariable or OWLNamedIndividual, got {type(argument2).__name__} instead ({argument2!r}).") self.argument1 = argument1 self.argument2 = argument2 @@ -376,6 +402,10 @@ class DifferentFromAtom(Atom): argument2: Union[IVariable, OWLNamedIndividual] def __init__(self, argument1: Union[IVariable, OWLNamedIndividual], argument2: Union[IVariable, OWLNamedIndividual]): + if not isinstance(argument1, (IVariable, OWLNamedIndividual)): + raise TypeError(f"Expected 'argument1' to be an instance of IVariable or OWLNamedIndividual, got {type(argument1).__name__} instead ({argument1!r}).") + if not isinstance(argument2, (IVariable, OWLNamedIndividual)): + raise TypeError(f"Expected 'argument2' to be an instance of IVariable or OWLNamedIndividual, got {type(argument2).__name__} instead ({argument2!r}).") self.argument1 = argument1 self.argument2 = argument2 @@ -412,9 +442,15 @@ def __eq__(self, other): class BuiltInAtom(Atom): """Represents a built-in atom in SWRL syntax""" predicate: IRI # should have the correct prefix, e.g: http://www.w3.org/2003/11/swrlb#divide - arguments: List[Union[DVariable, OWLLiteral]] - - def __init__(self, predicate: IRI, arguments: List[Union[DVariable, OWLLiteral]]): + arguments: List[Union[IVariable, DVariable, OWLLiteral]] + + def __init__(self, predicate: IRI, arguments: List[Union[IVariable, DVariable, OWLLiteral]]): + if not isinstance(predicate, IRI): + raise TypeError(f"Expected 'predicate' to be an instance of IRI, got {type(predicate).__name__} instead ({predicate!r}).") + arguments = list(arguments) + for i, arg in enumerate(arguments): + if not isinstance(arg, (IVariable, DVariable, OWLLiteral)): + raise TypeError(f"Expected all arguments to be instances of IVariable, DVariable or OWLLiteral, got {type(arg).__name__} instead ({arg!r}) at index {i}.") self.predicate = predicate self.arguments = arguments @@ -465,6 +501,15 @@ class Rule: head_atoms: Union[Atom, List[Atom]] def __init__(self, body_atoms: Union[Atom, List[Atom]], head_atoms: Union[Atom, List[Atom]]): + for name, atoms in (("body_atoms", body_atoms), ("head_atoms", head_atoms)): + if isinstance(atoms, Atom): + continue + if isinstance(atoms, list): + for i, a in enumerate(atoms): + if not isinstance(a, Atom): + raise TypeError(f"Expected all elements of '{name}' to be instances of Atom, got {type(a).__name__} instead ({a!r}) at index {i}.") + continue + raise TypeError(f"Expected '{name}' to be an instance of Atom or a list of Atom, got {type(atoms).__name__} instead ({atoms!r}).") self.body = body_atoms self.head = head_atoms diff --git a/tests/test_iri.py b/tests/test_iri.py new file mode 100644 index 00000000..020b55ef --- /dev/null +++ b/tests/test_iri.py @@ -0,0 +1,64 @@ +"""Python-side type-check coverage for owlapy.iri.IRI (#271 follow-up). + +IRI backs the identity of virtually every OWL entity and is mapped straight to Java's +org.semanticweb.owlapi.model.IRI in owlapi_mapper.py, so a bad argument here used to +either succeed silently (str-like duck typing) or fail with a bare, -O-strippable +AssertionError rather than a clear TypeError/ValueError. +""" +import pytest + +from owlapy.iri import IRI +from owlapy.namespaces import Namespaces + + +def test_init_rejects_non_str_non_namespaces_namespace(): + with pytest.raises(TypeError): + IRI(123, "x") + + +def test_init_rejects_non_str_remainder(): + with pytest.raises(TypeError): + IRI("http://example.com/", 456) + + +def test_init_rejects_namespace_without_valid_suffix(): + with pytest.raises(ValueError): + IRI("noSlashOrHashOrColon") + + +def test_init_accepts_namespaces_instance(): + ns = Namespaces("ex", "http://example.com/") + iri = IRI(ns, "x") + assert iri.get_namespace() == "http://example.com/" + assert iri.remainder == "x" + + +def test_init_accepts_file_path_without_suffix_check(): + iri = IRI("some/file.owl", is_file_path=True) + assert iri.get_namespace() == "some/file.owl" + + +def test_create_rejects_non_str_non_namespaces_iri(): + with pytest.raises(TypeError): + IRI.create(123) + + +def test_create_rejects_non_str_remainder(): + with pytest.raises(TypeError): + IRI.create("http://example.com/", 456) + + +def test_create_rejects_iri_without_slash(): + with pytest.raises(ValueError): + IRI.create("no-slash-here") + + +def test_create_rejects_whitespace(): + with pytest.raises(ValueError): + IRI.create("http://example.com/has space") + + +def test_create_splits_namespace_and_remainder(): + iri = IRI.create("http://example.com/onto#Class") + assert iri.get_namespace() == "http://example.com/onto#" + assert iri.remainder == "Class" diff --git a/tests/test_owl_anonymous_individual.py b/tests/test_owl_anonymous_individual.py index de32f351..0a52377e 100644 --- a/tests/test_owl_anonymous_individual.py +++ b/tests/test_owl_anonymous_individual.py @@ -2,6 +2,8 @@ import tempfile import unittest +import pytest + from owlapy.owl_axiom import OWLClassAssertionAxiom, OWLObjectPropertyAssertionAxiom from owlapy.owl_individual import OWLAnonymousIndividual, OWLNamedIndividual from owlapy.owl_ontology import SyncOntology @@ -56,6 +58,10 @@ def test_is_anonymous(self): def test_not_equal_to_named_individual(self): self.assertNotEqual(OWLAnonymousIndividual("b0"), OWLNamedIndividual("http://example.org/b0")) + def test_rejects_non_str_non_none_node_id(self): + with pytest.raises(TypeError): + OWLAnonymousIndividual(42) + class TestOntologyWithAnonymousIndividual(unittest.TestCase): """Regression test for issue #217: loading an ontology containing an anonymous diff --git a/tests/test_owl_data_ranges_type_checks.py b/tests/test_owl_data_ranges_type_checks.py new file mode 100644 index 00000000..47016c67 --- /dev/null +++ b/tests/test_owl_data_ranges_type_checks.py @@ -0,0 +1,54 @@ +"""Python-side type-check coverage for owlapy.owl_data_ranges (#271). + +OWLNaryDataRange (backing OWLDataIntersectionOf/OWLDataUnionOf) and OWLDataComplementOf +validate their operands against OWLPropertyRange rather than OWLDataRange itself, since +owlapy.utils.nnf.NNF deliberately reuses these constructors to wrap data-side class +expressions (e.g. OWLDataSomeValuesFrom) during negation, not just genuine data ranges. +""" +import pytest + +from owlapy.class_expression import OWLDataSomeValuesFrom +from owlapy.iri import IRI +from owlapy.owl_data_ranges import OWLDataComplementOf, OWLDataIntersectionOf, OWLDataUnionOf +from owlapy.owl_individual import OWLNamedIndividual +from owlapy.owl_literal import IntegerOWLDatatype, StringOWLDatatype +from owlapy.owl_property import OWLDataProperty + +NS = "http://example.com/data_range_test#" + + +def data_prop(name): + return OWLDataProperty(IRI.create(NS, name)) + + +def test_data_intersection_of_rejects_non_property_range_operand(): + with pytest.raises(TypeError): + OWLDataIntersectionOf((IntegerOWLDatatype, "not a data range")) + + +def test_data_union_of_rejects_non_property_range_operand(): + with pytest.raises(TypeError): + OWLDataUnionOf((IntegerOWLDatatype, 42)) + + +def test_data_complement_of_rejects_non_property_range(): + with pytest.raises(TypeError): + OWLDataComplementOf("not a data range") + + +def test_data_union_of_accepts_genuine_data_ranges(): + dr = OWLDataUnionOf((IntegerOWLDatatype, StringOWLDatatype)) + assert set(dr.operands()) == {IntegerOWLDatatype, StringOWLDatatype} + + +def test_data_complement_of_accepts_data_property_restriction_class_expression(): + # NNF deliberately wraps data-property restrictions (OWLClassExpression, not + # OWLDataRange) in OWLDataComplementOf during negation -- see owlapy.utils.nnf. + restriction = OWLDataSomeValuesFrom(data_prop("hasAge"), IntegerOWLDatatype) + complement = OWLDataComplementOf(restriction) + assert complement.get_data_range() is restriction + + +def test_data_complement_of_still_rejects_non_owl_object_types(): + with pytest.raises(TypeError): + OWLDataComplementOf(OWLNamedIndividual(IRI.create(NS, "bob"))) diff --git a/tests/test_owl_ontology_id.py b/tests/test_owl_ontology_id.py index 2b3209f5..2d0809d2 100644 --- a/tests/test_owl_ontology_id.py +++ b/tests/test_owl_ontology_id.py @@ -1,5 +1,7 @@ """Unit tests for owlapy.owl_ontology.OWLOntologyID -- a small, self-contained value class that existing ontology-management tests only touch incidentally.""" +import pytest + from owlapy.iri import IRI from owlapy.owl_ontology import OWLOntologyID @@ -46,3 +48,13 @@ def test_ontology_id_equality(): assert a != c # Comparing against an unrelated type falls back to NotImplemented -> False via Python's protocol. assert (a == "not an ontology id") is False + + +def test_ontology_id_rejects_non_iri_ontology_iri(): + with pytest.raises(TypeError): + OWLOntologyID(ontology_iri="not an iri") + + +def test_ontology_id_rejects_non_iri_version_iri(): + with pytest.raises(TypeError): + OWLOntologyID(ONTOLOGY_IRI, version_iri="not an iri") diff --git a/tests/test_swrl.py b/tests/test_swrl.py index cac53448..9c1651f1 100644 --- a/tests/test_swrl.py +++ b/tests/test_swrl.py @@ -51,7 +51,7 @@ def test_rule_creation_and_printing(self): def test_rule_parsing(self): rule = Rule.from_string("male(?x) ^ hasChild(?x, ?y) ^ hasChild(?y, ?z) ^ hasAge(?x, ?a) ^ integer(?a) ^ greaterThanOrEqual(?x, 40) -> Grandfather(?x)", namespace=self.NS, dp_predicates=["hasAge"]) - self.assertEqual(rule.__repr__(), """Rule(['ClassAtom(OWLClass(http://www.benchmark.org/family#male), IVariable(http://www.w3.org/2003/11/swrl#x))', 'ObjectPropertyAtom(OWLObjectProperty(http://www.benchmark.org/family#hasChild), IVariable(http://www.w3.org/2003/11/swrl#x), IVariable(http://www.w3.org/2003/11/swrl#y))', 'ObjectPropertyAtom(OWLObjectProperty(http://www.benchmark.org/family#hasChild), IVariable(http://www.w3.org/2003/11/swrl#y), IVariable(http://www.w3.org/2003/11/swrl#z))', 'DataPropertyAtom(OWLDataProperty(http://www.benchmark.org/family#hasAge), IVariable(http://www.w3.org/2003/11/swrl#x), DVariable(http://www.w3.org/2003/11/swrl#a))', 'DataRangeAtom(OWLDatatype(http://www.w3.org/2001/XMLSchema#integer) DVariable(http://www.w3.org/2003/11/swrl#a))', "BuiltInAtom(IRI.create(http://www.w3.org/2003/11/swrlb#greaterThanOrEqual), '40'])"], ['ClassAtom(OWLClass(http://www.benchmark.org/family#Grandfather), IVariable(http://www.w3.org/2003/11/swrl#x))'])""") + self.assertEqual(rule.__repr__(), """Rule(['ClassAtom(OWLClass(http://www.benchmark.org/family#male), IVariable(http://www.w3.org/2003/11/swrl#x))', 'ObjectPropertyAtom(OWLObjectProperty(http://www.benchmark.org/family#hasChild), IVariable(http://www.w3.org/2003/11/swrl#x), IVariable(http://www.w3.org/2003/11/swrl#y))', 'ObjectPropertyAtom(OWLObjectProperty(http://www.benchmark.org/family#hasChild), IVariable(http://www.w3.org/2003/11/swrl#y), IVariable(http://www.w3.org/2003/11/swrl#z))', 'DataPropertyAtom(OWLDataProperty(http://www.benchmark.org/family#hasAge), IVariable(http://www.w3.org/2003/11/swrl#x), DVariable(http://www.w3.org/2003/11/swrl#a))', 'DataRangeAtom(OWLDatatype(http://www.w3.org/2001/XMLSchema#integer) DVariable(http://www.w3.org/2003/11/swrl#a))', "BuiltInAtom(IRI.create(http://www.w3.org/2003/11/swrlb#greaterThanOrEqual), OWLLiteral(40, OWLDatatype(IRI('http://www.w3.org/2001/XMLSchema#', 'string')))])"], ['ClassAtom(OWLClass(http://www.benchmark.org/family#Grandfather), IVariable(http://www.w3.org/2003/11/swrl#x))'])""") class TestVariable(unittest.TestCase): @@ -282,3 +282,87 @@ def test_rule_from_string_with_same_as_and_different_from(self): self.assertEqual(len(rule.body), 3) self.assertIsInstance(rule.body[1], SameAsAtom) self.assertIsInstance(rule.body[2], DifferentFromAtom) + + +class TestSWRLTypeChecks(unittest.TestCase): + """Python-side type-check coverage for owlapy.swrl (#271).""" + + NS = "http://www.benchmark.org/family#" + x = IVariable(SWRL + "x") + y = IVariable(SWRL + "y") + a = DVariable(SWRL + "a") + ind1 = OWLNamedIndividual(NS + "matthias") + + def test_variable_rejects_non_iri_non_str(self): + with pytest.raises(TypeError): + IVariable(42) + + def test_class_atom_rejects_wrong_types(self): + male = OWLClass(self.NS + "male") + with pytest.raises(TypeError): + ClassAtom("not a class", self.x) + with pytest.raises(TypeError): + ClassAtom(male, "not a variable or individual") + + def test_data_range_atom_rejects_wrong_types(self): + with pytest.raises(TypeError): + DataRangeAtom("not a datatype", self.a) + with pytest.raises(TypeError): + DataRangeAtom(IntegerOWLDatatype, self.x) + + def test_data_range_atom_accepts_literal_argument(self): + atom = DataRangeAtom(IntegerOWLDatatype, OWLLiteral(5)) + self.assertEqual(atom.argument1, OWLLiteral(5)) + + def test_object_property_atom_rejects_wrong_types(self): + has_child = OWLObjectProperty(self.NS + "hasChild") + has_age = OWLDataProperty(self.NS + "hasAge") + with pytest.raises(TypeError): + ObjectPropertyAtom(has_age, self.x, self.y) + with pytest.raises(TypeError): + ObjectPropertyAtom(has_child, self.a, self.y) + + def test_data_property_atom_rejects_wrong_types(self): + has_child = OWLObjectProperty(self.NS + "hasChild") + has_age = OWLDataProperty(self.NS + "hasAge") + with pytest.raises(TypeError): + DataPropertyAtom(has_child, self.x, self.a) + with pytest.raises(TypeError): + DataPropertyAtom(has_age, self.x, self.y) + + def test_same_as_and_different_from_atom_reject_wrong_types(self): + with pytest.raises(TypeError): + SameAsAtom(self.a, self.x) + with pytest.raises(TypeError): + DifferentFromAtom(self.x, "not a variable or individual") + + def test_builtin_atom_rejects_wrong_types(self): + with pytest.raises(TypeError): + BuiltInAtom("not an iri", [self.x]) + with pytest.raises(TypeError): + BuiltInAtom(IRI.create(SWRLB + "add"), [self.x, "not a valid arg"]) + + def test_builtin_atom_accepts_individual_variables(self): + # BuiltInAtom arguments legitimately include IVariable, not just DVariable/OWLLiteral. + atom = BuiltInAtom(IRI.create(SWRLB + "equal"), [self.x, self.a]) + self.assertEqual(atom.arguments, [self.x, self.a]) + + def test_rule_rejects_non_atom_body_or_head(self): + male = OWLClass(self.NS + "male") + atom = ClassAtom(male, self.x) + with pytest.raises(TypeError): + Rule("not an atom", [atom]) + with pytest.raises(TypeError): + Rule([atom], [atom, "not an atom"]) + + def test_from_string_builtin_converts_all_arguments(self): + # Regression test: Atom.from_string used to `return` from inside the arg-conversion + # loop, so only the first argument of a multi-arg built-in predicate was converted + # (DVariable/OWLLiteral); every later one leaked through as a raw, un-typed string. + atom = Atom.from_string("greaterThanOrEqual(?x, 40)", namespace=self.NS) + self.assertIsInstance(atom, BuiltInAtom) + self.assertEqual(len(atom.arguments), 2) + for arg in atom.arguments: + self.assertIsInstance(arg, (DVariable, OWLLiteral)) + self.assertEqual(atom.arguments[0], DVariable(SWRL + "x")) + self.assertEqual(atom.arguments[1], OWLLiteral("40"))