@@ -600,6 +600,151 @@ def test_multivalued_slot_exact_cardinality(input_path):
600600 ) in g
601601
602602
603+ def test_zero_maximum_cardinality_emits_maxcount (input_path ):
604+ """Test that maximum_cardinality: 0 correctly emits sh:maxCount 0.
605+
606+ Regression test for the bug where Python truthiness check
607+ `if s.maximum_cardinality:` would skip the value 0 (falsy),
608+ failing to emit sh:maxCount 0 in the generated SHACL shape.
609+ The fix uses `if s.maximum_cardinality is not None:` instead.
610+
611+ This is the primary mechanism for suppressing inherited slots on
612+ subclasses via slot_usage (e.g., OWL maxCardinality 0 pattern).
613+ """
614+ shacl = ShaclGenerator (input_path ("shaclgen/cardinality.yaml" ), mergeimports = True ).serialize ()
615+
616+ g = rdflib .Graph ()
617+ g .parse (data = shacl )
618+
619+ # Find the ChildWithZeroMaxCard shape
620+ child_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/ChildWithZeroMaxCard" )
621+ restricted_slot_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/restricted_slot" )
622+
623+ # Get all property shapes for the child class
624+ prop_nodes = list (g .objects (child_uri , SH .property ))
625+ assert prop_nodes , "ChildWithZeroMaxCard should have property shapes"
626+
627+ # Find the property shape for restricted_slot
628+ restricted_prop_node = None
629+ for pn in prop_nodes :
630+ if (pn , SH .path , restricted_slot_uri ) in g :
631+ restricted_prop_node = pn
632+ break
633+ assert restricted_prop_node is not None , "Should have a property shape for restricted_slot"
634+
635+ # The critical assertion: sh:maxCount 0 must be emitted
636+ max_count_values = list (g .objects (restricted_prop_node , SH .maxCount ))
637+ assert len (max_count_values ) == 1 , f"Expected exactly one sh:maxCount, got { max_count_values } "
638+ assert max_count_values [0 ] == rdflib .term .Literal (
639+ 0 , datatype = rdflib .term .URIRef ("http://www.w3.org/2001/XMLSchema#integer" )
640+ ), f"sh:maxCount should be 0, got { max_count_values [0 ]} "
641+
642+
643+ def test_zero_exact_cardinality_emits_both_counts (input_path ):
644+ """Test that exact_cardinality: 0 emits both sh:minCount 0 and sh:maxCount 0.
645+
646+ Same truthiness bug as maximum_cardinality: `if s.exact_cardinality:`
647+ skips value 0 (falsy). The fix uses `is not None` instead.
648+ """
649+ shacl = ShaclGenerator (input_path ("shaclgen/cardinality.yaml" ), mergeimports = True ).serialize ()
650+
651+ g = rdflib .Graph ()
652+ g .parse (data = shacl )
653+
654+ child_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/ChildWithZeroExactCard" )
655+ restricted_slot_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/restricted_slot" )
656+
657+ prop_nodes = list (g .objects (child_uri , SH .property ))
658+ assert prop_nodes , "ChildWithZeroExactCard should have property shapes"
659+
660+ restricted_prop_node = None
661+ for pn in prop_nodes :
662+ if (pn , SH .path , restricted_slot_uri ) in g :
663+ restricted_prop_node = pn
664+ break
665+ assert restricted_prop_node is not None , "Should have a property shape for restricted_slot"
666+
667+ XSD_INT = rdflib .term .URIRef ("http://www.w3.org/2001/XMLSchema#integer" )
668+
669+ min_count_values = list (g .objects (restricted_prop_node , SH .minCount ))
670+ assert len (min_count_values ) == 1 , f"Expected exactly one sh:minCount, got { min_count_values } "
671+ assert min_count_values [0 ] == rdflib .term .Literal (0 , datatype = XSD_INT )
672+
673+ max_count_values = list (g .objects (restricted_prop_node , SH .maxCount ))
674+ assert len (max_count_values ) == 1 , f"Expected exactly one sh:maxCount, got { max_count_values } "
675+ assert max_count_values [0 ] == rdflib .term .Literal (0 , datatype = XSD_INT )
676+
677+
678+ def test_zero_minimum_cardinality_emits_mincount (input_path ):
679+ """Test that minimum_cardinality: 0 emits sh:minCount 0.
680+
681+ Same truthiness bug as maximum_cardinality: `if s.minimum_cardinality:`
682+ skips value 0 (falsy). The fix uses `is not None` instead. sh:minCount 0
683+ is vacuously satisfied (W3C SHACL 4.2.2) but is emitted for consistency
684+ with owlgen (owl:minCardinality 0) and to faithfully reflect the schema.
685+ """
686+ shacl = ShaclGenerator (input_path ("shaclgen/cardinality.yaml" ), mergeimports = True ).serialize ()
687+
688+ g = rdflib .Graph ()
689+ g .parse (data = shacl )
690+
691+ child_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/ChildWithZeroMinCard" )
692+ restricted_slot_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/restricted_slot" )
693+
694+ prop_nodes = list (g .objects (child_uri , SH .property ))
695+ assert prop_nodes , "ChildWithZeroMinCard should have property shapes"
696+
697+ restricted_prop_node = None
698+ for pn in prop_nodes :
699+ if (pn , SH .path , restricted_slot_uri ) in g :
700+ restricted_prop_node = pn
701+ break
702+ assert restricted_prop_node is not None , "Should have a property shape for restricted_slot"
703+
704+ XSD_INT = rdflib .term .URIRef ("http://www.w3.org/2001/XMLSchema#integer" )
705+
706+ min_count_values = list (g .objects (restricted_prop_node , SH .minCount ))
707+ assert len (min_count_values ) == 1 , f"Expected exactly one sh:minCount, got { min_count_values } "
708+ assert min_count_values [0 ] == rdflib .term .Literal (0 , datatype = XSD_INT )
709+
710+
711+ def test_explicit_minimum_cardinality_overrides_required (input_path ):
712+ """An explicit minimum_cardinality: 0 takes precedence over required: true.
713+
714+ The generator resolves min-count with an ``elif`` cascade in which an
715+ explicit ``minimum_cardinality`` wins and ``required`` is only the fallback.
716+ This mirrors owlgen.py (``if slot.minimum_cardinality is not None ... elif
717+ slot.required``), so ``required: true`` + ``minimum_cardinality: 0`` yields
718+ ``sh:minCount 0`` (not 1). The combination is a schema contradiction; the
719+ explicit, more specific constraint is emitted.
720+ """
721+ shacl = ShaclGenerator (input_path ("shaclgen/cardinality.yaml" ), mergeimports = True ).serialize ()
722+
723+ g = rdflib .Graph ()
724+ g .parse (data = shacl )
725+
726+ child_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/ChildWithRequiredAndZeroMinCard" )
727+ restricted_slot_uri = URIRef ("https://w3id.org/linkml/examples/cardinality/restricted_slot" )
728+
729+ prop_nodes = list (g .objects (child_uri , SH .property ))
730+ assert prop_nodes , "ChildWithRequiredAndZeroMinCard should have property shapes"
731+
732+ restricted_prop_node = None
733+ for pn in prop_nodes :
734+ if (pn , SH .path , restricted_slot_uri ) in g :
735+ restricted_prop_node = pn
736+ break
737+ assert restricted_prop_node is not None , "Should have a property shape for restricted_slot"
738+
739+ XSD_INT = rdflib .term .URIRef ("http://www.w3.org/2001/XMLSchema#integer" )
740+
741+ min_count_values = list (g .objects (restricted_prop_node , SH .minCount ))
742+ assert len (min_count_values ) == 1 , f"Expected exactly one sh:minCount, got { min_count_values } "
743+ assert min_count_values [0 ] == rdflib .term .Literal (0 , datatype = XSD_INT ), (
744+ f"explicit minimum_cardinality: 0 should override required: true (minCount 0, not 1), got { min_count_values [0 ]} "
745+ )
746+
747+
603748def test_exclude_imports (input_path ):
604749 shacl = ShaclGenerator (
605750 input_path ("shaclgen/exclude_imports.yaml" ), mergeimports = True , exclude_imports = True
0 commit comments