Skip to content

Commit 9ff5608

Browse files
committed
Merge feature/release-1.6.5 into develop
Release version 1.6.5 with timeout functionality and comprehensive tests
2 parents cb9cf3f + f41b586 commit 9ff5608

4 files changed

Lines changed: 153 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.6.5] - 2026-05-26
11+
1012
### Added
13+
- Timeout support for `SyncReasoner.instances()` method with configurable timeout parameter (default: 1000 seconds)
14+
- Timeout support for `create_axiom_justifications()` method with cooperative cancellation via Java thread interruption
15+
- Timeout support for `create_laconic_axiom_justifications()` method
16+
- Java ExecutorService-based timeout mechanism for proper interruption of Java reasoning tasks
17+
- Comprehensive test suite for timeout functionality in `tests/test_reasoner_timeout.py`
1118
- CHANGELOG.md to track version history
1219
- CONTRIBUTING.md with contributor guidelines
1320
- CODE_OF_CONDUCT.md for community standards
@@ -20,10 +27,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2027
- Improved CI/CD pipeline with coverage reporting
2128
- Modernized Python packaging configuration
2229
- Reorganized Copilot agent files under .github/agents/owlapy/
30+
- Refactored reasoning methods to use shared single-threaded Java executor to prevent "ExtensionManager is not reentrant" errors
2331

2432
### Fixed
33+
- Resolved merge conflicts with develop branch maintaining timeout functionality
34+
- Fixed import ordering to comply with ruff linting standards
2535
- Resolved Python version inconsistencies in documentation and setup
2636
- Fixed coverage report generation in CI pipeline
37+
- Prevented concurrent Java thread access issues in HermiT and other reasoners
2738

2839
## [1.6.4] - 2025-05-20
2940

owlapy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from .parser import dl_to_owl_expression, manchester_to_owl_expression
33
from .render import owl_expression_to_dl, owl_expression_to_manchester
44

5-
__version__ = '1.6.4'
5+
__version__ = '1.6.5'
66

77
__all__ = [
88
'owl_expression_to_dl', 'owl_expression_to_manchester',

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
2-
from setuptools import setup, find_packages
2+
3+
from setuptools import find_packages, setup
34

45
_deps = [
56
"scikit-learn>=1.5.2",
@@ -54,7 +55,7 @@ def deps_list(*pkgs):
5455
setup(
5556
name="owlapy",
5657
description="OWLAPY is a Python Framework for creating and manipulating OWL Ontologies.",
57-
version="1.6.4",
58+
version="1.6.5",
5859
packages=find_packages(),
5960
include_package_data=True,
6061
package_data={'owlapy': ['jar_dependencies/*.jar', 'py.typed'],},

tests/test_reasoner_timeout.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Tests for timeout functionality in SyncReasoner methods."""
2+
import os
3+
import unittest
4+
5+
from owlapy import dl_to_owl_expression
6+
from owlapy.class_expression import OWLClass
7+
from owlapy.iri import IRI
8+
from owlapy.owl_axiom import OWLClassAssertionAxiom
9+
from owlapy.owl_individual import OWLNamedIndividual
10+
from owlapy.owl_ontology import SyncOntology
11+
from owlapy.owl_reasoner import SyncReasoner
12+
13+
14+
class TestSyncReasonerTimeout(unittest.TestCase):
15+
"""Test timeout functionality in SyncReasoner."""
16+
17+
@classmethod
18+
def setUpClass(cls):
19+
"""Set up test fixtures."""
20+
cls.ontology_path = None
21+
for root, dirs, files in os.walk("."):
22+
for file in files:
23+
if file == "family-benchmark_rich_background.owl":
24+
cls.ontology_path = os.path.abspath(os.path.join(root, file))
25+
break
26+
if cls.ontology_path:
27+
break
28+
29+
if cls.ontology_path is None:
30+
raise FileNotFoundError("Could not locate 'family-benchmark_rich_background.owl' within project structure.")
31+
32+
cls.namespace = "http://www.benchmark.org/family#"
33+
34+
try:
35+
cls.ontology = SyncOntology(cls.ontology_path)
36+
cls.reasoner = SyncReasoner(cls.ontology, reasoner="Pellet")
37+
except Exception as e:
38+
raise RuntimeError(f"Failed to load ontology or initialize reasoner: {e}")
39+
40+
def test_instances_with_short_timeout(self):
41+
"""Test instances() method completes within reasonable timeout."""
42+
ce = OWLClass(IRI.create(self.namespace + "Male"))
43+
44+
# Should complete successfully with a reasonable timeout (10 seconds)
45+
individuals = set(self.reasoner.instances(ce, timeout=10))
46+
47+
self.assertIsInstance(individuals, set, "instances() should return a set")
48+
self.assertGreater(len(individuals), 0, "Should find Male individuals")
49+
50+
def test_instances_with_complex_expression(self):
51+
"""Test instances() with complex class expression and timeout."""
52+
dl_expr = "∃ hasChild.Male"
53+
ce = dl_to_owl_expression(dl_expr, self.namespace)
54+
55+
# Complex expression with reasonable timeout
56+
individuals = set(self.reasoner.instances(ce, timeout=15))
57+
58+
self.assertIsInstance(individuals, set)
59+
# We expect some individuals to satisfy this expression
60+
61+
def test_create_axiom_justifications_default_timeout(self):
62+
"""Test create_axiom_justifications with default timeout parameter."""
63+
individual = OWLNamedIndividual(IRI.create(self.namespace + "F10M171"))
64+
dl_expr_str = "∃ hasChild.Male"
65+
target_class = dl_to_owl_expression(dl_expr_str, self.namespace)
66+
67+
axiom = OWLClassAssertionAxiom(individual, target_class)
68+
69+
# Should work with default timeout
70+
justifications = self.reasoner.create_axiom_justifications(axiom, save=False)
71+
72+
self.assertIsInstance(justifications, list)
73+
self.assertGreater(len(justifications), 0, "Should find at least one justification")
74+
75+
for justification in justifications:
76+
self.assertIsInstance(justification, set)
77+
self.assertGreater(len(justification), 0, "Each justification should contain axioms")
78+
79+
def test_create_axiom_justifications_custom_timeout(self):
80+
"""Test create_axiom_justifications with custom timeout."""
81+
individual = OWLNamedIndividual(IRI.create(self.namespace + "F10M171"))
82+
dl_expr_str = "∃ hasChild.Male"
83+
target_class = dl_to_owl_expression(dl_expr_str, self.namespace)
84+
85+
axiom = OWLClassAssertionAxiom(individual, target_class)
86+
87+
# Test with custom timeout of 20 seconds
88+
justifications = self.reasoner.create_axiom_justifications(
89+
axiom,
90+
n_max_justifications=5,
91+
timeout=20,
92+
save=False
93+
)
94+
95+
self.assertIsInstance(justifications, list)
96+
# Should get at most 5 justifications
97+
self.assertLessEqual(len(justifications), 5)
98+
99+
def test_instances_parameter_validation(self):
100+
"""Test that instances method accepts timeout parameter correctly."""
101+
ce = OWLClass(IRI.create(self.namespace + "Female"))
102+
103+
# Test with various timeout values
104+
individuals_short = set(self.reasoner.instances(ce, timeout=5))
105+
individuals_long = set(self.reasoner.instances(ce, timeout=30))
106+
107+
# Both should return the same results (just different timeouts)
108+
self.assertEqual(individuals_short, individuals_long,
109+
"Different timeouts should return same results for simple queries")
110+
111+
def test_instances_direct_parameter_with_timeout(self):
112+
"""Test instances method with both direct and timeout parameters."""
113+
ce = OWLClass(IRI.create(self.namespace + "Person"))
114+
115+
# Test with direct=False and timeout
116+
all_instances = set(self.reasoner.instances(ce, direct=False, timeout=10))
117+
118+
# Test with direct=True and timeout
119+
direct_instances = set(self.reasoner.instances(ce, direct=True, timeout=10))
120+
121+
self.assertIsInstance(all_instances, set)
122+
self.assertIsInstance(direct_instances, set)
123+
# Direct instances should be a subset of all instances
124+
self.assertTrue(direct_instances.issubset(all_instances) or len(direct_instances) == 0)
125+
126+
@classmethod
127+
def tearDownClass(cls):
128+
"""Clean up resources."""
129+
if hasattr(cls, 'reasoner'):
130+
try:
131+
from owlapy.static_funcs import stopJVM
132+
stopJVM()
133+
except Exception:
134+
pass # JVM might already be stopped
135+
136+
137+
if __name__ == '__main__':
138+
unittest.main()

0 commit comments

Comments
 (0)