Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 19 additions & 11 deletions owlapy/iri.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,39 @@ 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)
self._remainder = remainder

@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:])

Expand Down
17 changes: 16 additions & 1 deletion owlapy/owl_data_ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions owlapy/owl_individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions owlapy/owl_ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
61 changes: 53 additions & 8 deletions owlapy/swrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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}")

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
64 changes: 64 additions & 0 deletions tests/test_iri.py
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading