"""Step definitions for the UKO Analyzer Plugin Framework feature.""" from __future__ import annotations from typing import Any from behave import given, then, when from cleveragents.domain.models.acms.analyzers import ( AnalyzerProtocol, AnalyzerRegistry, UKOTriple, ) from cleveragents.domain.models.acms.markdown_analyzer import MarkdownAnalyzer from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer __all__: list[str] = [] # --------------------------------------------------------------------------- # UKOTriple steps # --------------------------------------------------------------------------- @given('a UKOTriple with subject "{subj}" predicate "{pred}" and object_uri "{obj}"') def step_given_triple_with_obj_uri( context: Any, subj: str, pred: str, obj: str ) -> None: context.triple = UKOTriple(subject_uri=subj, predicate=pred, object_uri=obj) @given('a UKOTriple with subject "{subj}" predicate "{pred}" and object_value "{val}"') def step_given_triple_with_obj_value( context: Any, subj: str, pred: str, val: str ) -> None: context.triple = UKOTriple(subject_uri=subj, predicate=pred, object_value=val) @then('the triple subject_uri should be "{expected}"') def step_then_triple_subject(context: Any, expected: str) -> None: assert context.triple.subject_uri == expected @then('the triple predicate should be "{expected}"') def step_then_triple_predicate(context: Any, expected: str) -> None: assert context.triple.predicate == expected @then('the triple object_uri should be "{expected}"') def step_then_triple_object_uri(context: Any, expected: str) -> None: assert context.triple.object_uri == expected @then('the triple object_value should be "{expected}"') def step_then_triple_object_value(context: Any, expected: str) -> None: assert context.triple.object_value == expected @then("the triple confidence should be {expected:g}") def step_then_triple_confidence(context: Any, expected: float) -> None: assert context.triple.confidence == expected @then("creating a UKOTriple with empty subject_uri should raise ValueError") def step_then_triple_empty_subject(context: Any) -> None: try: UKOTriple(subject_uri="", predicate="rdf:type", object_uri="uko:X") raise AssertionError("Expected ValueError") except ValueError: pass @then("creating a UKOTriple with empty predicate should raise ValueError") def step_then_triple_empty_predicate(context: Any) -> None: try: UKOTriple(subject_uri="uko://a", predicate="", object_uri="uko:X") raise AssertionError("Expected ValueError") except ValueError: pass @then("creating a UKOTriple with confidence {score:g} should raise ValueError") def step_then_triple_bad_confidence(context: Any, score: float) -> None: try: UKOTriple( subject_uri="uko://a", predicate="rdf:type", object_uri="uko:X", confidence=score, ) raise AssertionError("Expected ValueError") except ValueError: pass @then("modifying the triple confidence should raise an error") def step_then_triple_immutable(context: Any) -> None: try: context.triple.confidence = 0.5 # type: ignore[misc] raise AssertionError("Expected frozen error") except (TypeError, ValueError, AttributeError): pass # --------------------------------------------------------------------------- # AnalyzerRegistry steps # --------------------------------------------------------------------------- @given("an AnalyzerRegistry with a PythonAnalyzer registered") def step_given_registry_with_python(context: Any) -> None: context.registry = AnalyzerRegistry() context.registry.register(PythonAnalyzer()) @given("an empty AnalyzerRegistry") def step_given_empty_registry(context: Any) -> None: context.registry = AnalyzerRegistry() @given("an AnalyzerRegistry with both Python and Markdown analyzers") def step_given_registry_with_both(context: Any) -> None: context.registry = AnalyzerRegistry() context.registry.register(PythonAnalyzer()) context.registry.register(MarkdownAnalyzer()) @when('I look up the analyzer for extension "{ext}"') def step_when_lookup_extension(context: Any, ext: str) -> None: context.lookup_result = context.registry.get_for_extension(ext) @when('I register another analyzer for "{ext}"') def step_when_register_another(context: Any, ext: str) -> None: """Register a second MarkdownAnalyzer that also claims .py.""" class _DummyAnalyzer: @property def supported_extensions(self) -> frozenset[str]: return frozenset({ext}) @property def domain(self) -> str: return "dummy" def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]: return [] context.registry.register(_DummyAnalyzer()) @then('the analyzer domain should be "{expected}"') def step_then_analyzer_domain(context: Any, expected: str) -> None: assert context.lookup_result is not None assert context.lookup_result.domain == expected @then("the lookup result should be None") def step_then_lookup_none(context: Any) -> None: assert context.lookup_result is None @then("registering a plain object should raise TypeError") def step_then_register_plain_object(context: Any) -> None: try: context.registry.register("not an analyzer") # type: ignore[arg-type] raise AssertionError("Expected TypeError") except TypeError: pass @then("registering an analyzer with no extensions should raise ValueError") def step_then_register_no_extensions(context: Any) -> None: class _Empty: @property def supported_extensions(self) -> frozenset[str]: return frozenset() @property def domain(self) -> str: return "empty" def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]: return [] try: context.registry.register(_Empty()) raise AssertionError("Expected ValueError") except ValueError: pass @then('the registry should list extensions including "{ext1}" and "{ext2}"') def step_then_list_extensions(context: Any, ext1: str, ext2: str) -> None: exts = context.registry.list_extensions() assert ext1 in exts, f"{ext1} not in {exts}" assert ext2 in exts, f"{ext2} not in {exts}" @then("the registry length should be {expected:d}") def step_then_registry_length(context: Any, expected: int) -> None: assert len(context.registry) == expected # --------------------------------------------------------------------------- # PythonAnalyzer steps # --------------------------------------------------------------------------- @given("a PythonAnalyzer instance") def step_given_python_analyzer(context: Any) -> None: context.analyzer = PythonAnalyzer() @then("it should satisfy the AnalyzerProtocol") def step_then_satisfies_protocol(context: Any) -> None: assert isinstance(context.analyzer, AnalyzerProtocol) @then('its supported extensions should include "{ext1}" and "{ext2}"') def step_then_supported_extensions(context: Any, ext1: str, ext2: str) -> None: exts = context.analyzer.supported_extensions assert ext1 in exts, f"{ext1} not in {exts}" assert ext2 in exts, f"{ext2} not in {exts}" @when('I analyze Python content "{content}" with resource URI "{uri}"') def step_when_analyze_python(context: Any, content: str, uri: str) -> None: context.triples = context.analyzer.analyze(content.replace("\\n", "\n"), uri) @when('I analyze Python content with a class "{cls_name}"') def step_when_analyze_python_class(context: Any, cls_name: str) -> None: code = f"class {cls_name}:\n pass\n" context.triples = context.analyzer.analyze(code, "src/test.py") @when('I analyze Python content with a function "{func_name}"') def step_when_analyze_python_function(context: Any, func_name: str) -> None: code = f"def {func_name}():\n pass\n" context.triples = context.analyzer.analyze(code, "src/test.py") @when('I analyze Python content with class "{cls_name}" and method "{method_name}"') def step_when_analyze_python_method( context: Any, cls_name: str, method_name: str ) -> None: code = f"class {cls_name}:\n def {method_name}(self):\n pass\n" context.triples = context.analyzer.analyze(code, "src/test.py") @when('I analyze Python content with import "{mod_name}"') def step_when_analyze_python_import(context: Any, mod_name: str) -> None: code = f"import {mod_name}\n" context.triples = context.analyzer.analyze(code, "src/test.py") @when('I analyze Python content with from-import "{mod_name}"') def step_when_analyze_python_from_import(context: Any, mod_name: str) -> None: code = f"from {mod_name} import Path\n" context.triples = context.analyzer.analyze(code, "src/test.py") @when('I analyze Python content with docstring "{docstring}"') def step_when_analyze_python_docstring(context: Any, docstring: str) -> None: code = f'"""{docstring}"""\nx = 1\n' context.triples = context.analyzer.analyze(code, "src/test.py") @when("I analyze Python content with class docstring") def step_when_analyze_python_class_docstring(context: Any) -> None: code = 'class Foo:\n """Class doc."""\n pass\n' context.triples = context.analyzer.analyze(code, "src/test.py") @when("I analyze malformed Python content") def step_when_analyze_malformed_python(context: Any) -> None: context.triples = context.analyzer.analyze("def (\n broken", "src/bad.py") @when('I analyze Python content with a class inheriting from "{base}"') def step_when_analyze_python_inheritance(context: Any, base: str) -> None: code = f"class Child({base}):\n pass\n" context.triples = context.analyzer.analyze(code, "src/test.py") @then( 'the triples should contain a triple with predicate "{pred}" and object_uri "{obj}"' ) def step_then_triples_contain_pred_obj(context: Any, pred: str, obj: str) -> None: found = any(t.predicate == pred and t.object_uri == obj for t in context.triples) assert found, ( f"No triple with predicate={pred!r} object_uri={obj!r} in {context.triples}" ) @then( 'the triples should contain a triple with predicate "{pred}" and object_value "{val}"' ) def step_then_triples_contain_pred_val(context: Any, pred: str, val: str) -> None: found = any(t.predicate == pred and t.object_value == val for t in context.triples) assert found, ( f"No triple with predicate={pred!r} object_value={val!r} in {context.triples}" ) @then( "the triples should contain a triple with predicate" ' "uko:contains" linking module to class' ) def step_then_triples_contain_module_contains_class( context: Any, ) -> None: found = any( t.predicate == "uko:contains" and "module" in t.subject_uri and "class" in t.object_uri for t in context.triples ) assert found, f"No module-contains-class triple in {context.triples}" @then( 'the triples should contain a method triple with predicate "{pred}" and object_value "{val}"' ) def step_then_triples_contain_method(context: Any, pred: str, val: str) -> None: found = any(t.predicate == pred and t.object_value == val for t in context.triples) assert found, ( f"No method triple with predicate={pred!r} val={val!r} in {context.triples}" ) @then( 'the triples should contain a triple with predicate "uko:references" referencing "{mod_name}"' ) def step_then_triples_contain_import_ref(context: Any, mod_name: str) -> None: found = any( t.predicate == "uko:references" and mod_name in t.object_uri for t in context.triples ) assert found, f"No import reference to {mod_name!r} in {context.triples}" @then("the triples should contain a docstring triple for the class") def step_then_triples_contain_class_docstring(context: Any) -> None: found = any( t.predicate == "uko-doc:hasDocstring" and "class" in t.subject_uri.lower() for t in context.triples ) assert found, f"No class docstring triple in {context.triples}" @then("the analyzer result should be an empty triple list") def step_then_result_empty(context: Any) -> None: assert context.triples == [] @then("analyzing empty content should raise ValueError") def step_then_analyze_empty_content(context: Any) -> None: try: context.analyzer.analyze("", "src/test.py") raise AssertionError("Expected ValueError") except ValueError: pass @then("analyzing with empty resource_uri should raise ValueError") def step_then_analyze_empty_uri(context: Any) -> None: try: context.analyzer.analyze("x = 1\n", "") raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # MarkdownAnalyzer steps # --------------------------------------------------------------------------- @given("a MarkdownAnalyzer instance") def step_given_markdown_analyzer(context: Any) -> None: context.analyzer = MarkdownAnalyzer() @when('I analyze Markdown content "{content}" with resource URI "{uri}"') def step_when_analyze_markdown(context: Any, content: str, uri: str) -> None: context.triples = context.analyzer.analyze(content.replace("\\n", "\n"), uri) @when('I analyze Markdown content with heading "{heading}"') def step_when_analyze_markdown_heading(context: Any, heading: str) -> None: content = f"# {heading}\n\nSome text here.\n" context.triples = context.analyzer.analyze(content, "docs/test.md") @when("I analyze Markdown with nested headings") def step_when_analyze_markdown_nested(context: Any) -> None: content = "# Top\n\n## Sub\n\nBody text.\n" context.triples = context.analyzer.analyze(content, "docs/test.md") @when("I analyze Markdown content with a Python code block") def step_when_analyze_markdown_codeblock(context: Any) -> None: content = "# Code\n\n```python\nprint('hello')\n```\n" context.triples = context.analyzer.analyze(content, "docs/test.md") @when('I analyze Markdown content with a link to "{url}"') def step_when_analyze_markdown_link(context: Any, url: str) -> None: content = f"See [example]({url}) for details.\n" context.triples = context.analyzer.analyze(content, "docs/test.md") @when("I analyze Markdown content with a bare code fence") def step_when_analyze_markdown_bare_fence(context: Any) -> None: content = "# Title\n\n```\nsome code\n```\n" context.triples = context.analyzer.analyze(content, "docs/test.md") @then("the triples should contain section containment for nested sections") def step_then_nested_containment(context: Any) -> None: # Document should contain Top section, Top section should contain Sub doc_contains = [ t for t in context.triples if t.predicate == "uko:contains" and "document" in t.subject_uri and "section" in t.object_uri ] section_contains = [ t for t in context.triples if t.predicate == "uko:contains" and "section" in t.subject_uri and "section" in t.object_uri ] assert len(doc_contains) >= 1, f"Expected doc-contains-section, got {doc_contains}" assert len(section_contains) >= 1, ( f"Expected section-contains-section, got {section_contains}" ) @then("the triples should contain a code block triple") def step_then_contains_codeblock(context: Any) -> None: found = any( t.predicate == "rdf:type" and t.object_uri == "uko-code:CodeBlock" for t in context.triples ) assert found, f"No code block triple in {context.triples}" @then("analyzing empty markdown content should raise ValueError") def step_then_md_analyze_empty_content(context: Any) -> None: try: context.analyzer.analyze("", "docs/test.md") raise AssertionError("Expected ValueError") except ValueError: pass @then("analyzing markdown with empty resource_uri should raise ValueError") def step_then_md_analyze_empty_uri(context: Any) -> None: try: context.analyzer.analyze("# Title\n", "") raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # Coverage-gap steps # --------------------------------------------------------------------------- @when("I look up the analyzer for an empty extension") def step_when_lookup_empty_extension(context: Any) -> None: context.lookup_result = context.registry.get_for_extension("") @then("the registry list_analyzers should return {expected:d} analyzers") def step_then_list_analyzers(context: Any, expected: int) -> None: result = context.registry.list_analyzers() assert len(result) == expected, f"Expected {expected} analyzers, got {len(result)}" @when("I analyze Python content with a function with docstring") def step_when_analyze_python_func_docstring(context: Any) -> None: code = 'def greet():\n """Say hello."""\n pass\n' context.triples = context.analyzer.analyze(code, "src/test.py") @then( 'the triples should contain a triple with predicate "uko-doc:hasDocstring" for a function' ) def step_then_triples_func_docstring(context: Any) -> None: found = any( t.predicate == "uko-doc:hasDocstring" and "function" in t.subject_uri for t in context.triples ) assert found, f"No function docstring triple in {context.triples}" @when("I analyze Python content with class and method with docstring") def step_when_analyze_python_method_docstring(context: Any) -> None: code = ( "class Svc:\n" " def run(self):\n" ' """Execute the service."""\n' " pass\n" ) context.triples = context.analyzer.analyze(code, "src/test.py") @then( 'the triples should contain a triple with predicate "uko-doc:hasDocstring" for a method' ) def step_then_triples_method_docstring(context: Any) -> None: found = any( t.predicate == "uko-doc:hasDocstring" and "function" in t.subject_uri and "Svc" in t.subject_uri for t in context.triples ) assert found, f"No method docstring triple in {context.triples}" @then('the analyzer domain property should be "{expected}"') def step_then_analyzer_domain_prop(context: Any, expected: str) -> None: assert context.analyzer.domain == expected @when("I analyze Markdown with sibling headings at the same level") def step_when_analyze_md_sibling_headings(context: Any) -> None: content = "# A\n\n## B\n\n## C\n\nText\n" context.triples = context.analyzer.analyze(content, "docs/test.md") @then("each sibling section should be contained by the document") def step_then_sibling_sections_contained_by_doc(context: Any) -> None: # Sections B and C are both at level 2 under A; the second ## pops the # first ## off the stack. Both should be contained by the # A section # (which is contained by the document). section_contains = [ t for t in context.triples if t.predicate == "uko:contains" and "section" in t.object_uri ] assert len(section_contains) >= 3, ( f"Expected at least 3 contains-section triples, got {section_contains}" )