Files
CleverRDFlib/docs/class_hierarchy.md
CoreRasurae f14c0bad8c
CI / lint (pull_request) Successful in 1m35s
CI / typecheck (pull_request) Successful in 1m38s
CI / behave (3.11) (pull_request) Successful in 1m48s
CI / behave (3.12) (pull_request) Successful in 1m47s
CI / behave (3.13) (pull_request) Successful in 1m43s
CI / build (pull_request) Successful in 1m33s
CI / lint (push) Successful in 1m31s
CI / typecheck (push) Successful in 1m29s
CI / behave (3.11) (push) Successful in 1m37s
CI / behave (3.12) (push) Successful in 1m44s
CI / behave (3.13) (push) Successful in 1m36s
CI / build (push) Successful in 1m31s
docs: Initial documentation of CleverRDFLib
ISSUES CLOSED: #2
2025-12-22 10:50:03 +00:00

20 KiB

Class Hierarchy Navigation

The class hierarchy is the core feature of CleverRDFLib, providing object-oriented navigation of ontology class structures without requiring SPARQL queries.

Class Hierarchy Structure

The class hierarchy is built on a foundation of node types that represent different ontology elements:

  • Node: Abstract base class for all hierarchy nodes. Cannot be instantiated directly.

    • ClassNode: Represents OWL classes in the hierarchy
    • DataTypeNode: Represents datatypes (e.g., xsd:string, xsd:integer)
  • PropertyNode: Abstract base class for all properties. Cannot be instantiated directly.

    • ObjectPropertyNode: Represents OWL object properties that link classes to other classes
    • DatatypePropertyNode: Represents OWL datatype properties that link classes to datatypes

All nodes implement the OntologyElement interface and support the Visitor pattern through the accept() method.

Building the Hierarchy

The class hierarchy is automatically built when you access it:

from cleverrdf_lib import OntologyLoader

loader = OntologyLoader()
result = loader.load_ontology("ontology.ttl")

# Access the hierarchy (automatically builds if not already built)
hierarchy = result.class_hierarchy

Manual Building

You can also build the hierarchy manually from a graph:

from cleverrdf_lib.core.class_hierarchy import ClassHierarchy
from rdflib import Graph

graph = Graph()
# ... populate graph ...

hierarchy = ClassHierarchy(graph)
# Build with read-only nodes (default behavior)
hierarchy.build()

# Or explicitly build with mutable nodes if you need to modify them
hierarchy.build(read_only=False)

Navigating Classes

Getting All Classes

# Get all class IRIs
all_iris = hierarchy.get_all_classes_iris()
print(f"Total classes: {len(all_iris)}")

# Get all ClassNode instances as a dictionary (IRI -> ClassNode)
all_classes = hierarchy.get_all_classes()
for class_iri, class_node in all_classes.items():
    print(f"Class: {class_node.iri}")

# Or iterate over just the ClassNode instances
for class_node in hierarchy.get_all_classes().values():
    print(f"Class: {class_node.iri}")

# Or use get_all_classes_as_list() for a list
all_classes_list = hierarchy.get_all_classes_as_list()
for class_node in all_classes_list:
    print(f"Class: {class_node.iri}")

Finding a Specific Class

# Get a class by IRI from the dictionary
class_iri = "http://example.org/ontology#Person"
all_classes = hierarchy.get_all_classes()
class_node = all_classes.get(class_iri)  # Returns None if not found

if class_node:
    print(f"Found class: {class_node.iri}")
    print(f"Label: {class_node.label}")
    print(f"Comment: {class_node.comment}")
else:
    print(f"Class not found: {class_iri}")

Root and Leaf Classes

# Get root classes (classes with no parents)
root_classes = hierarchy.get_all_root_classes()
for root in root_classes:
    print(f"Root class: {root.iri}")

# Get leaf classes (classes with no children)
leaf_classes = hierarchy.get_all_leaf_classes()
for leaf in leaf_classes:
    print(f"Leaf class: {leaf.iri}")

ClassNode Properties

Each ClassNode provides rich navigation capabilities:

Basic Properties

# Get a class by IRI
class_iri = "http://example.org/ontology#Person"
class_node = hierarchy.get_all_classes().get(class_iri)

if class_node:
    # IRI
    print(f"IRI: {class_node.iri}")

    # Label and comment
    print(f"Label: {class_node.label}")
    print(f"Comment: {class_node.comment}")

    # Check if root or leaf
    print(f"Is root: {class_node.is_root()}")
    print(f"Is leaf: {class_node.is_leaf()}")
    print(f"Is datatype: {class_node.is_datatype()}")

Parent-Child Relationships

# Get parents
parents = class_node.parents  # Returns frozenset[ClassNode]
print(f"Parent classes:")
for parent in parents:
    print(f"  - {parent.iri}")

# Get children
children = class_node.children  # Returns frozenset[ClassNode]
print(f"Child classes:")
for child in children:
    print(f"  - {child.iri}")

Object Properties

Object properties link classes to other classes:

# Properties where this class is the domain
domain_properties = class_node.object_properties_as_domain
print(f"Properties with this class as domain:")
for prop_iri, prop_node in domain_properties.items():
    print(f"  - {prop_iri}")
    print(f"    Range: {prop_node.range.iri if prop_node.range else 'None'}")

# Properties where this class is the range
range_properties = class_node.object_properties_as_range
print(f"Properties with this class as range:")
for prop_iri, prop_node in range_properties.items():
    print(f"  - {prop_iri}")
    print(f"    Domain: {prop_node.domain.iri if prop_node.domain else 'None'}")

Datatype Properties

Datatype properties link classes to datatypes:

# Datatype properties where this class is the domain
datatype_props = class_node.datatype_properties_as_domain
print(f"Datatype properties:")
for prop_iri, prop_node in datatype_props.items():
    print(f"  - {prop_iri}")
    print(f"    Range: {prop_node.range.iri if prop_node.range else 'None'}")

PropertyNode Properties

Properties themselves can be navigated. CleverRDFLib distinguishes between object properties and datatype properties:

Object Properties

# Get all object properties from the hierarchy
object_properties = hierarchy.get_all_object_properties()  # Returns frozendict[str, ObjectPropertyNode]
object_properties_list = hierarchy.get_all_object_properties_as_list()  # Returns list[ObjectPropertyNode]

# Iterate over dictionary values
for prop in object_properties.values():
    print(f"Property: {prop.iri}")
    print(f"  Type: {prop.ptype}")  # PropertyType.Object
    print(f"  Type IRI: {prop.type_iri}")
    print(f"  Label: {prop.label}")
    print(f"  Comment: {prop.comment}")
    
    if prop.domain:
        print(f"  Domain: {prop.domain.iri}")
    if prop.range:
        print(f"  Range: {prop.range.iri}")  # Range is a ClassNode for object properties

Datatype Properties

# Get all datatype properties from the hierarchy
datatype_properties = hierarchy.get_all_datatype_properties()  # Returns frozendict[str, DatatypePropertyNode]
datatype_properties_list = hierarchy.get_all_datatype_properties_as_list()  # Returns list[DatatypePropertyNode]

# Iterate over dictionary values
for prop in datatype_properties.values():
    print(f"Property: {prop.iri}")
    print(f"  Type: {prop.ptype}")  # PropertyType.DataType
    print(f"  Type IRI: {prop.type_iri}")
    print(f"  Label: {prop.label}")
    print(f"  Comment: {prop.comment}")
    
    if prop.domain:
        print(f"  Domain: {prop.domain.iri}")
    if prop.range:
        print(f"  Range: {prop.range.iri}")  # Range is a DataTypeNode for datatype properties

Getting All Properties

If you need both object and datatype properties together:

# Get both types separately and combine
object_props = hierarchy.get_all_object_properties()
datatype_props = hierarchy.get_all_datatype_properties()

# Combine if needed
all_properties = {**object_props, **datatype_props}

Property Node Types

CleverRDFLib provides two concrete property node types:

  • ObjectPropertyNode: Represents OWL object properties that link classes to other classes
  • DatatypePropertyNode: Represents OWL datatype properties that link classes to datatypes

Both inherit from the abstract PropertyNode base class. The PropertyNode class cannot be instantiated directly - use the concrete subclasses instead.

Traversing the Hierarchy

Depth-First Traversal

def traverse_depth_first(class_node, visited=None):
    """Traverse the hierarchy depth-first."""
    if visited is None:
        visited = set()
    
    if class_node.iri in visited:
        return
    
    visited.add(class_node.iri)
    print(f"Visiting: {class_node.iri}")
    
    # Traverse children
    for child in class_node.children:
        traverse_depth_first(child, visited)

# Start from root classes
for root in hierarchy.get_all_root_classes():
    traverse_depth_first(root)

Breadth-First Traversal

from collections import deque

def traverse_breadth_first(root_class):
    """Traverse the hierarchy breadth-first."""
    queue = deque([root_class])
    visited = set()
    
    while queue:
        current = queue.popleft()
        
        if current.iri in visited:
            continue
        
        visited.add(current.iri)
        print(f"Visiting: {current.iri}")
        
        # Add children to queue
        for child in current.children:
            queue.append(child)

# Start from root classes
for root in hierarchy.get_all_root_classes():
    traverse_breadth_first(root)

Finding Ancestors

def get_ancestors(class_node):
    """Get all ancestor classes (parents, grandparents, etc.)."""
    ancestors = set()
    
    def collect_ancestors(node):
        for parent in node.parents:
            if parent.iri not in ancestors:
                ancestors.add(parent.iri)
                collect_ancestors(parent)
    
    collect_ancestors(class_node)
    return ancestors

# Get a class by IRI
class_iri = "http://example.org/ontology#Employee"
class_node = hierarchy.get_all_classes().get(class_iri)

if class_node:
    ancestors = get_ancestors(class_node)
    print(f"Ancestors: {ancestors}")

Finding Descendants

def get_descendants(class_node):
    """Get all descendant classes (children, grandchildren, etc.)."""
    descendants = set()
    
    def collect_descendants(node):
        for child in node.children:
            if child.iri not in descendants:
                descendants.add(child.iri)
                collect_descendants(child)
    
    collect_descendants(class_node)
    return descendants

# Get a class by IRI
class_iri = "http://example.org/ontology#Person"
class_node = hierarchy.get_all_classes().get(class_iri)

if class_node:
    descendants = get_descendants(class_node)
    print(f"Descendants: {descendants}")

Visitor Pattern

The class hierarchy supports the Visitor pattern for traversing and processing ontology elements. All elements (ClassNode, ObjectPropertyNode, DatatypePropertyNode, DataTypeNode) implement the OntologyElement interface with an accept method.

Visitor Interface

The ClassHierarchyVisitor interface defines the visitor methods:

from cleverrdf_lib.core.interfaces.interfaces import ClassHierarchyVisitor, OntologyElement

class MyVisitor(ClassHierarchyVisitor):
    """Custom visitor for processing ontology elements."""
    
    def visit_class(self, cls: OntologyElement) -> None:
        """Called when visiting a ClassNode."""
        print(f"Visiting class: {cls.iri}")
        # Process the class node
    
    def visit_datatype(self, dt: OntologyElement) -> None:
        """Called when visiting a DataTypeNode."""
        print(f"Visiting datatype: {dt.iri}")
        # Process the datatype node
    
    def visit_object_property(self, prop: OntologyElement) -> None:
        """Called when visiting an ObjectPropertyNode."""
        print(f"Visiting object property: {prop.iri}")
        # Process the object property node
    
    def visit_datatype_property(self, prop: OntologyElement) -> None:
        """Called when visiting a DatatypePropertyNode."""
        print(f"Visiting datatype property: {prop.iri}")
        # Process the datatype property node

Using the Visitor

from cleverrdf_lib.core.interfaces.interfaces import ClassHierarchyVisitor

class StatisticsVisitor(ClassHierarchyVisitor):
    """Visitor that collects statistics about the hierarchy."""
    
    def __init__(self):
        self.class_count = 0
        self.object_property_count = 0
        self.datatype_property_count = 0
        self.datatype_count = 0
    
    def visit_class(self, cls: OntologyElement) -> None:
        self.class_count += 1
    
    def visit_datatype(self, dt: OntologyElement) -> None:
        self.datatype_count += 1
    
    def visit_object_property(self, prop: OntologyElement) -> None:
        self.object_property_count += 1
    
    def visit_datatype_property(self, prop: OntologyElement) -> None:
        self.datatype_property_count += 1

# Create visitor
visitor = StatisticsVisitor()

# Visit all classes (iterate over dictionary values)
for class_node in hierarchy.get_all_classes().values():
    class_node.accept(visitor)

# Visit all object properties
for prop in hierarchy.get_all_object_properties().values():
    prop.accept(visitor)

# Visit all datatype properties
for prop in hierarchy.get_all_datatype_properties().values():
    prop.accept(visitor)

# Access collected statistics
print(f"Classes: {visitor.class_count}")
print(f"Object properties: {visitor.object_property_count}")
print(f"Datatype properties: {visitor.datatype_property_count}")
print(f"Datatypes: {visitor.datatype_count}")

Traversing with Visitor

You can combine the visitor pattern with hierarchy traversal:

from cleverrdf_lib.core.interfaces.interfaces import ClassHierarchyVisitor, OntologyElement

class HierarchyTraversingVisitor(ClassHierarchyVisitor):
    """Visitor that traverses the entire hierarchy."""
    
    def __init__(self):
        self.visited = set()
    
    def visit_class(self, cls: OntologyElement) -> None:
        if cls.iri in self.visited:
            return
        
        self.visited.add(cls.iri)
        print(f"Visiting class: {cls.iri}")
        
        # Visit children recursively
        for child in cls.children:
            child.accept(self)
    
    def visit_datatype(self, dt: OntologyElement) -> None:
        print(f"Visiting datatype: {dt.iri}")
    
    def visit_object_property(self, prop: OntologyElement) -> None:
        print(f"Visiting object property: {prop.iri}")
    
    def visit_datatype_property(self, prop: OntologyElement) -> None:
        print(f"Visiting datatype property: {prop.iri}")

# Start traversal from root classes
visitor = HierarchyTraversingVisitor()
for root in hierarchy.get_all_root_classes():
    root.accept(visitor)

The Visitor pattern provides a clean way to separate traversal logic from element processing, making it easy to implement different operations on the hierarchy without modifying the element classes.

Hierarchy Statistics

from cleverrdf_lib.core.class_hierarchy import PropertyType

# Get hierarchy statistics
all_classes = hierarchy.get_all_classes()  # Returns frozendict[str, ClassNode]
root_classes = hierarchy.get_all_root_classes()  # Returns list[ClassNode]
leaf_classes = hierarchy.get_all_leaf_classes()  # Returns list[ClassNode]
object_properties = hierarchy.get_all_object_properties()  # Returns frozendict[str, ObjectPropertyNode]
datatype_properties = hierarchy.get_all_datatype_properties()  # Returns frozendict[str, DatatypePropertyNode]
all_datatypes = hierarchy.get_all_datatypes_as_list()  # Returns list[DataTypeNode]

print(f"Total classes: {len(all_classes)}")
print(f"Root classes: {len(root_classes)}")
print(f"Leaf classes: {len(leaf_classes)}")
print(f"Object properties: {len(object_properties)}")
print(f"Datatype properties: {len(datatype_properties)}")
print(f"Total datatypes: {len(all_datatypes)}")

Read-Only Mode

Default Read-Only Behavior

By default, the hierarchy is built with all nodes as read-only to prevent accidental modifications:

from cleverrdf_lib.core.class_hierarchy import ClassHierarchy
from rdflib import Graph

graph = Graph()
# ... populate graph ...

hierarchy = ClassHierarchy(graph)
# Build with read-only=True by default
hierarchy.build()

# All class nodes and property nodes are automatically read-only
# Attempting to modify will raise ReadOnlyPropertyException
class_node = hierarchy.get_all_classes().values().__iter__().__next__()
# class_node.add_parent(other_class)  # Raises ReadOnlyPropertyException
# class_node.set_label("new label")  # Raises ReadOnlyPropertyException

object_prop_node = hierarchy.get_all_object_properties().values().__iter__().__next__()
# object_prop_node.set_label("new label")  # Raises ReadOnlyPropertyException

datatype_prop_node = hierarchy.get_all_datatype_properties().values().__iter__().__next__()
# datatype_prop_node.set_label("new label")  # Raises ReadOnlyPropertyException

Building with Mutable Nodes

If you need to modify nodes after building, you can explicitly build with mutable nodes:

from cleverrdf_lib.core.class_hierarchy import ClassHierarchy
from rdflib import Graph

graph = Graph()
# ... populate graph ...

hierarchy = ClassHierarchy(graph)
# Build with mutable nodes
hierarchy.build(read_only=False)

# Now you can modify nodes
class_node = hierarchy.get_all_classes().values().__iter__().__next__()
class_node.set_label("Modified Label")  # Works fine

# You can later make individual nodes read-only if needed
class_node.make_read_only()
# class_node.set_label("Another Label")  # Now raises ReadOnlyPropertyException

Manually Making Nodes Read-Only

Even when building with mutable nodes, you can selectively make individual nodes read-only:

from cleverrdf_lib.core.class_hierarchy import ClassHierarchy, PropertyType
from rdflib import Graph

graph = Graph()
# ... populate graph ...

hierarchy = ClassHierarchy(graph)
hierarchy.build(read_only=False)

# Make specific class nodes read-only
for class_node in hierarchy.get_all_classes().values():
    if class_node.iri.startswith("http://example.org/protected/"):
        class_node.make_read_only()

# Make specific property nodes read-only
for prop_node in hierarchy.get_all_object_properties().values():
    prop_node.make_read_only()

for prop_node in hierarchy.get_all_datatype_properties().values():
    prop_node.make_read_only()

Read-Only Operations

When a node is read-only, the following operations will raise ReadOnlyPropertyException:

For ClassNode:

  • add_parent(parent) - Cannot add parent relationships
  • add_child(child) - Cannot add child relationships
  • set_label(label) - Cannot modify label
  • set_comment(comment) - Cannot modify comment
  • add_object_property_as_domain(property_node) - Cannot add object properties
  • add_object_property_as_range(property_node) - Cannot add object properties
  • add_datatype_property_as_domain(property_node) - Cannot add datatype properties

For ObjectPropertyNode:

  • set_domain(domain_class) - Cannot modify domain
  • set_range(range_class) - Cannot modify range (ClassNode)
  • set_type_iri(type_iri) - Cannot modify type IRI
  • set_label(label) - Cannot modify label
  • set_comment(comment) - Cannot modify comment

For DatatypePropertyNode:

  • set_domain(domain_class) - Cannot modify domain
  • set_range(range_datatype) - Cannot modify range (DataTypeNode)
  • set_type_iri(type_iri) - Cannot modify type IRI
  • set_label(label) - Cannot modify label
  • set_comment(comment) - Cannot modify comment

Note: Read-only mode only prevents modifications to the node structure. You can still navigate and read all properties of read-only nodes.

Memory Management

To free memory, you can forget the underlying graph:

# After building, you can forget the graph to save memory
hierarchy.forget_graph()

# The hierarchy remains usable, but the graph is no longer accessible

Best Practices

  1. Lazy building: The hierarchy builds automatically when accessed
  2. Use frozensets: Parent and child sets are immutable for safety
  3. Check for None: Always check if .get(iri) returns None when looking up classes by IRI
  4. Use the dictionary API: Access classes efficiently using hierarchy.get_all_classes().get(iri)
  5. Traverse efficiently: Use appropriate traversal algorithms for your use case
  6. Read-only by default: Hierarchies are built as read-only by default to prevent accidental modifications. Use build(read_only=False) only when you specifically need to modify the hierarchy structure
  7. Selective mutability: If you need to modify only specific nodes, build with read_only=False and then selectively call make_read_only() on nodes that should be protected

Next Steps