From dbeec413e7ef981096fef480cefd2e047626196c Mon Sep 17 00:00:00 2001 From: Caglar Demir Date: Mon, 24 Aug 2026 10:35:15 +0200 Subject: [PATCH] fix: close remaining #271 type-check gaps in class_expression.restriction #272 fixed OWLObjectCardinalityRestriction but left several parallel restriction constructors unvalidated: OWLObjectSomeValuesFrom/ OWLObjectAllValuesFrom (property/filler), OWLObjectHasSelf (property), OWLObjectHasValue (property/individual), OWLDataCardinalityRestriction (cardinality/property -- its Object-side sibling was already covered), OWLDataSomeValuesFrom/OWLDataAllValuesFrom (property), OWLDataHasValue (property/value), OWLDatatypeRestriction (datatype/facet restrictions), and OWLFacetRestriction (facet). OWLObjectOneOf/OWLDataOneOf's element checks are upgraded from a bare, -O-strippable assert to raise TypeError, matching #272's convention; this also fixes a latent bug where a one-shot iterable argument was exhausted by the validation loop before being materialized, silently producing an empty result. owlapy.owl_data_ranges's OWLNaryDataRange/OWLDataComplementOf were deliberately left unchanged: owlapy.utils.nnf.NNF reuses them to wrap data-side class expressions (e.g. OWLDataSomeValuesFrom) during negation, not just genuine OWLDataRange instances -- a strict OWLDataRange check there breaks NNF, as confirmed by the resulting test_owlapy_nnf.py failures during development. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + owlapy/class_expression/restriction.py | 94 +++++++++++++++++++++--- tests/test_restriction_extra_coverage.py | 84 ++++++++++++++++++++- 3 files changed, 168 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f92d7700..67419e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- 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/class_expression/restriction.py b/owlapy/class_expression/restriction.py index 1aaf76fa..a154a0e9 100644 --- a/owlapy/class_expression/restriction.py +++ b/owlapy/class_expression/restriction.py @@ -144,6 +144,10 @@ class OWLQuantifiedObjectRestriction(OWLQuantifiedRestriction[OWLClassExpression _filler: OWLClassExpression def __init__(self, filler: OWLClassExpression): + if not isinstance(filler, OWLClassExpression): + raise TypeError( + f"Expected 'filler' to be an instance of OWLClassExpression, got {type(filler).__name__} instead ({filler!r})." + ) self._filler = filler def get_filler(self) -> OWLClassExpression: @@ -286,6 +290,10 @@ def __init__(self, property: OWLObjectPropertyExpression, filler: OWLClassExpres An OWLObjectSomeValuesFrom restriction along the specified property with the specified filler. """ super().__init__(filler) + if not isinstance(property, OWLObjectPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLObjectPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) self._property = property def __repr__(self): @@ -313,6 +321,10 @@ class expression CE, and it contains all those individuals that are connected by type_index: Final = 3006 def __init__(self, property: OWLObjectPropertyExpression, filler: OWLClassExpression): super().__init__(filler) + if not isinstance(property, OWLObjectPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLObjectPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) self._property = property def __repr__(self): @@ -351,6 +363,10 @@ def __init__(self, property: OWLObjectPropertyExpression): Returns: A ObjectHasSelf class expression on the specified property. """ + if not isinstance(property, OWLObjectPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLObjectPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) self._property = property def get_property(self) -> OWLObjectPropertyExpression: @@ -390,6 +406,14 @@ def __init__(self, property: OWLObjectPropertyExpression, individual: OWLIndivid Returns: A HasValue restriction with specified property and value """ + if not isinstance(property, OWLObjectPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLObjectPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) + if not isinstance(individual, OWLIndividual): + raise TypeError( + f"Expected 'individual' to be an instance of OWLIndividual, got {type(individual).__name__} instead ({individual!r})." + ) super().__init__(individual) self._property = property @@ -426,13 +450,15 @@ class OWLObjectOneOf(OWLAnonymousClassExpression, HasOperands[OWLIndividual]): type_index: Final = 3004 def __init__(self, values: OWLIndividual | Iterable[OWLIndividual]): - #assert isinstance(values, OWLIndividual) | isinstance(values, set) - # f"The input of OWLObjectOneOf must be either an OWLIndividual or a set of OWLIndividual. Currently, {type(values)}!" if isinstance(values, OWLIndividual): self._values = values, else: - for _ in values: - assert isinstance(_, OWLIndividual) + values = tuple(values) + for i, v in enumerate(values): + if not isinstance(v, OWLIndividual): + raise TypeError( + f"Expected all values to be instances of OWLIndividual, got {type(v).__name__} instead ({v!r}) at index {i}." + ) self._values = frozenset(values) def individuals(self) -> Iterable[OWLIndividual]: @@ -496,7 +522,10 @@ class OWLQuantifiedDataRestriction(OWLQuantifiedRestriction[OWLDataRange], _filler: OWLDataRange def __init__(self, filler: OWLDataRange): - assert isinstance(filler, OWLDataRange), "filler must be an OWLDataRange" + if not isinstance(filler, OWLDataRange): + raise TypeError( + f"Expected 'filler' to be an instance of OWLDataRange, got {type(filler).__name__} instead ({filler!r})." + ) self._filler = filler # @TODO:CD: define it as @property @@ -515,7 +544,18 @@ class OWLDataCardinalityRestriction(OWLCardinalityRestriction[OWLDataRange], @abstractmethod def __init__(self, cardinality: int, property: OWLDataPropertyExpression, filler: OWLDataRange): - assert isinstance(filler, OWLDataRange), "filler must be an OWLDataRange" + if not isinstance(cardinality, int) or cardinality < 0: + raise ValueError( + f"Expected 'cardinality' to be a non-negative integer, got {cardinality!r} instead." + ) + if not isinstance(property, OWLDataPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLDataPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) + if not isinstance(filler, OWLDataRange): + raise TypeError( + f"Expected 'filler' to be an instance of OWLDataRange, got {type(filler).__name__} instead ({filler!r})." + ) super().__init__(cardinality, filler) self._property = property @@ -641,6 +681,10 @@ def __init__(self, property: OWLDataPropertyExpression, filler: OWLDataRange): An OWLDataSomeValuesFrom restriction along the specified property with the specified filler. """ super().__init__(filler) + if not isinstance(property, OWLDataPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLDataPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) self._property = property def __repr__(self): @@ -685,6 +729,10 @@ def __init__(self, property: OWLDataPropertyExpression, filler: OWLDataRange): An OWLDataAllValuesFrom restriction along the specified property with the specified filler. """ super().__init__(filler) + if not isinstance(property, OWLDataPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLDataPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) self._property = property def __repr__(self): @@ -730,6 +778,14 @@ def __init__(self, property: OWLDataPropertyExpression, value: OWLLiteral): Returns: An OWLDataHasValue restriction along the specified property with the specified literal. """ + if not isinstance(property, OWLDataPropertyExpression): + raise TypeError( + f"Expected 'property' to be an instance of OWLDataPropertyExpression, got {type(property).__name__} instead ({property!r})." + ) + if not isinstance(value, OWLLiteral): + raise TypeError( + f"Expected 'value' to be an instance of OWLLiteral, got {type(value).__name__} instead ({value!r})." + ) super().__init__(value) self._property = property @@ -769,9 +825,13 @@ def __init__(self, values: Union[OWLLiteral, Iterable[OWLLiteral]]): if isinstance(values, OWLLiteral): self._values = values, else: - for _ in values: - assert isinstance(_, OWLLiteral) - self._values = tuple(values) + values = tuple(values) + for i, v in enumerate(values): + if not isinstance(v, OWLLiteral): + raise TypeError( + f"Expected all values to be instances of OWLLiteral, got {type(v).__name__} instead ({v!r}) at index {i}." + ) + self._values = values def __repr__(self): return f'OWLDataOneOf({self._values})' @@ -815,10 +875,20 @@ class OWLDatatypeRestriction(OWLDataRange): def __init__(self, type_: OWLDatatype, facet_restrictions: Union['OWLFacetRestriction', Iterable['OWLFacetRestriction']]): + if not isinstance(type_, OWLDatatype): + raise TypeError( + f"Expected 'type_' to be an instance of OWLDatatype, got {type(type_).__name__} instead ({type_!r})." + ) self._type = type_ if isinstance(facet_restrictions, OWLFacetRestriction): facet_restrictions = facet_restrictions, - self._facet_restrictions = tuple(facet_restrictions) + facet_restrictions = tuple(facet_restrictions) + for i, fr in enumerate(facet_restrictions): + if not isinstance(fr, OWLFacetRestriction): + raise TypeError( + f"Expected all facet_restrictions to be instances of OWLFacetRestriction, got {type(fr).__name__} instead ({fr!r}) at index {i}." + ) + self._facet_restrictions = facet_restrictions def get_datatype(self) -> OWLDatatype: return self._type @@ -851,6 +921,10 @@ class OWLFacetRestriction(OWLObject): _literal: 'OWLLiteral' def __init__(self, facet: OWLFacet, literal: Literals): + if not isinstance(facet, OWLFacet): + raise TypeError( + f"Expected 'facet' to be an instance of OWLFacet, got {type(facet).__name__} instead ({facet!r})." + ) self._facet = facet if isinstance(literal, OWLLiteral): self._literal = literal diff --git a/tests/test_restriction_extra_coverage.py b/tests/test_restriction_extra_coverage.py index feb6cff1..3eb8a7ce 100644 --- a/tests/test_restriction_extra_coverage.py +++ b/tests/test_restriction_extra_coverage.py @@ -5,6 +5,8 @@ couple of default marker methods (is_data_restriction/is_object_restriction) and convenience methods (as_intersection_of_min_max). Pure Python, no JVM involved. """ +import pytest + from owlapy.class_expression import ( OWLDataAllValuesFrom, OWLDataExactCardinality, @@ -15,6 +17,7 @@ OWLDataSomeValuesFrom, OWLDatatypeRestriction, OWLFacetRestriction, + OWLObjectAllValuesFrom, OWLObjectExactCardinality, OWLObjectHasSelf, OWLObjectHasValue, @@ -23,6 +26,7 @@ OWLObjectMinCardinality, OWLObjectOneOf, OWLObjectSomeValuesFrom, + OWLThing, ) from owlapy.iri import IRI from owlapy.owl_individual import OWLNamedIndividual @@ -169,4 +173,82 @@ def test_facet_restriction_eq_false_for_different_type(): assert a == b assert a != c assert (a == "not a facet restriction") is False - assert hash(a) == hash(b) + + +# --------------------------------------------------------------------------- +# Python-side type checks (owlapy#271): restriction constructors given an +# argument of the wrong OWL construct type must fail immediately with a clear +# TypeError/ValueError, not silently succeed and only blow up once the JVM +# gets involved. These close gaps #272 left in restriction.py -- it fixed +# OWLObjectCardinalityRestriction but missed several parallel constructs. +# --------------------------------------------------------------------------- + +def test_object_some_values_from_rejects_data_property(): + with pytest.raises(TypeError): + OWLObjectSomeValuesFrom(data_prop("age"), OWLThing) + + +def test_object_some_values_from_rejects_non_class_expression_filler(): + with pytest.raises(TypeError): + OWLObjectSomeValuesFrom(obj_prop("p"), ind("alice")) + + +def test_object_all_values_from_rejects_non_class_expression_filler(): + with pytest.raises(TypeError): + OWLObjectAllValuesFrom(obj_prop("p"), ind("alice")) + + +def test_object_has_self_rejects_data_property(): + with pytest.raises(TypeError): + OWLObjectHasSelf(data_prop("age")) + + +def test_object_has_value_rejects_swapped_arguments(): + with pytest.raises(TypeError): + OWLObjectHasValue(ind("alice"), obj_prop("p")) + + +def test_data_some_values_from_rejects_object_property(): + with pytest.raises(TypeError): + OWLDataSomeValuesFrom(obj_prop("p"), IntegerOWLDatatype) + + +def test_data_all_values_from_rejects_object_property(): + with pytest.raises(TypeError): + OWLDataAllValuesFrom(obj_prop("p"), IntegerOWLDatatype) + + +def test_data_has_value_rejects_non_literal_value(): + with pytest.raises(TypeError): + OWLDataHasValue(data_prop("age"), ind("alice")) + + +def test_data_min_cardinality_rejects_object_property(): + with pytest.raises(TypeError): + OWLDataMinCardinality(1, obj_prop("p"), IntegerOWLDatatype) + + +def test_data_min_cardinality_rejects_negative_cardinality(): + with pytest.raises(ValueError): + OWLDataMinCardinality(-1, data_prop("age"), IntegerOWLDatatype) + + +def test_object_one_of_rejects_non_individual(): + with pytest.raises(TypeError): + OWLObjectOneOf([ind("alice"), obj_prop("p")]) + + +def test_data_one_of_rejects_non_literal(): + with pytest.raises(TypeError): + OWLDataOneOf([OWLLiteral(1), ind("alice")]) + + +def test_datatype_restriction_rejects_non_datatype(): + facet = OWLFacetRestriction(OWLFacet.MIN_INCLUSIVE, OWLLiteral(0)) + with pytest.raises(TypeError): + OWLDatatypeRestriction(ind("alice"), facet) + + +def test_facet_restriction_rejects_non_facet(): + with pytest.raises(TypeError): + OWLFacetRestriction("not a facet", OWLLiteral(0))