From 5c2901ae27004a799e18cd3d6dfcdb9edcf524da Mon Sep 17 00:00:00 2001 From: pravit-amp <43916793+pravit-amp@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:19:32 -0700 Subject: [PATCH] docs(context): fix unrunnable ContextGraph docstring example (#921) * docs(context): fix unrunnable ContextGraph docstring example The module docstring's Example Usage block called add_node/add_edge with keyword arguments they do not accept. add_node(node_id, node_type, ...) takes node_type positionally and has no properties parameter, so the documented call raised TypeError; add_edge's parameter is edge_type, so type= fell through to **properties and polluted edge metadata while appearing to work. Two of the three broken forms failed silently rather than raising, storing a nested properties dict or a stray type key instead of erroring. Add regression tests that execute the documented calls and assert the docstring itself does not reintroduce the invalid kwargs. Co-Authored-By: Claude Opus 5 * test(context): close two blind spots in the docstring regression guards The guards added in the previous commit could pass while checking nothing. _example_block() terminated the capture at the first "\n\n". The Example Usage block already contains ">>> " spacer lines, so any reformatting that turned one into a bare blank line would truncate the capture -- potentially to empty -- and the guards would then scan a block that no longer held the add_node/add_edge calls they exist to police. Both guards also iterated over re.findall() without asserting a match. Zero matches meant zero assertions and a green test, so the two failure modes compounded: a truncated block produced no matches, and no matches produced a pass. Terminate the block at the next top-level section header (^\S) or end of docstring instead, so blank lines inside the example are harmless, and assert the captured block, the parsed statement list, and each guard's match list are all non-empty. Extract statements with doctest.DocTestParser rather than a line regex. This also catches a call reformatted across "..." continuation lines, which the ">>> graph.add_node(.*" pattern silently skipped, and lets test_documented_calls_execute exec the docstring's own statements instead of a retyped copy that could drift from it. Full doctest.testmod isn't usable here: add_node/add_edge return True and the docs carry no expected-output lines, so it reports 4 spurious failures. Narrow the kwarg check to (? * fix(context): correct precedent lookup in docstring example --------- Co-authored-by: Pravit Ampapathini Co-authored-by: Claude Opus 5 Co-authored-by: Sameer Kadam Co-authored-by: Mohd Kaif <98801504+KaifAhmad1@users.noreply.github.com> --- semantica/context/context_graph.py | 8 +- .../test_context_graph_docstring_example.py | 142 ++++++++++++++++++ 2 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 tests/context/test_context_graph_docstring_example.py diff --git a/semantica/context/context_graph.py b/semantica/context/context_graph.py index 28b431ef..ad6ecf3f 100644 --- a/semantica/context/context_graph.py +++ b/semantica/context/context_graph.py @@ -72,9 +72,9 @@ Example Usage: ... node_embeddings=True) >>> >>> # Basic graph operations - >>> graph.add_node("Python", type="language", properties={"popularity": "high"}) - >>> graph.add_node("Programming", type="concept") - >>> graph.add_edge("Python", "Programming", type="related_to") + >>> graph.add_node("Python", "language", popularity="high") + >>> graph.add_node("Programming", "concept") + >>> graph.add_edge("Python", "Programming", "related_to") >>> centrality = graph.get_node_centrality("Python") >>> similar = graph.find_similar_nodes("Python", similarity_type="content") >>> analysis = graph.analyze_graph_with_kg() @@ -88,7 +88,7 @@ Example Usage: ... confidence=0.95, ... entities=["customer_123", "property_456"] ... ) - >>> precedents = graph.find_precedents("loan_approval", limit=5) + >>> precedents = graph.find_precedents(decision_id, limit=5) >>> influence = graph.analyze_decision_influence(decision_id) >>> insights = graph.get_decision_insights() >>> causality = graph.trace_decision_causality(decision_id) diff --git a/tests/context/test_context_graph_docstring_example.py b/tests/context/test_context_graph_docstring_example.py new file mode 100644 index 00000000..8dcd4039 --- /dev/null +++ b/tests/context/test_context_graph_docstring_example.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Regression tests for the ContextGraph module docstring example. + +The "Example Usage" block in ``semantica/context/context_graph.py`` previously +called ``add_node``/``add_edge`` with keyword arguments those methods do not +accept (``type=`` and ``properties=``), so the documented example raised +``TypeError`` -- and the near-miss variants silently nested the properties dict +instead of failing. + +These tests keep the documented example executable and pin the two behaviours +that made the original mistake easy to miss. +""" + +import doctest +import re +from typing import Dict, List + +import pytest + +import semantica.context.context_graph as context_graph_module +from semantica.context.context_graph import ContextGraph + +# The example block runs to the next top-level section header (a line starting +# in column 0, e.g. "Production Use Cases:") or the end of the docstring. +# Terminating on the next header rather than on a blank line keeps the capture +# intact when the example gains blank lines or extra paragraphs. +_EXAMPLE_BLOCK_RE = re.compile(r"^Example Usage:\n(.*?)(?=^\S|\Z)", re.DOTALL | re.MULTILINE) + +# ``type=`` as its own keyword, but not the legitimate ``node_type=``/``edge_type=``. +_BARE_TYPE_KWARG_RE = re.compile(r"(? str: + """Return the 'Example Usage' block from the module docstring.""" + doc = context_graph_module.__doc__ or "" + match = _EXAMPLE_BLOCK_RE.search(doc) + assert match, "module docstring no longer contains an 'Example Usage:' block" + block = match.group(1).strip() + assert block, "the 'Example Usage:' block in the module docstring is empty" + return block + + +def _example_statements() -> List[str]: + """Return the documented ``>>>`` statements, continuation lines included.""" + statements = [example.source for example in doctest.DocTestParser().get_examples(_example_block())] + assert statements, "the 'Example Usage:' block no longer contains any '>>>' statements" + return statements + + +def _statements_calling(method: str) -> List[str]: + """Return the documented statements that call ``graph.(``.""" + return [stmt for stmt in _example_statements() if "graph.{}(".format(method) in stmt] + + +def _run_example() -> Dict[str, object]: + """Execute the documented example verbatim and return its namespace.""" + source = "".join(_example_statements()) + namespace: Dict[str, object] = {} + exec(compile(source, "", "exec"), namespace) + return namespace + + +class TestDocstringExampleIsRunnable: + """The documented example must execute exactly as written.""" + + def test_documented_calls_execute(self): + # Run the docstring text itself so this test cannot drift from the docs. + ns = _run_example() + graph = ns["graph"] + + assert "Python" in graph.nodes + assert "Programming" in graph.nodes + assert graph.nodes["Python"].node_type == "language" + assert graph.nodes["Programming"].node_type == "concept" + + neighbors = graph.get_neighbors("Python", hops=1) + assert any(n["id"] == "Programming" for n in neighbors) + + # record_decision must return a non-empty string ID. + assert isinstance(ns["decision_id"], str) and ns["decision_id"] + # find_precedents must be called with that ID and return a list. + assert isinstance(ns["precedents"], list) + + def test_node_properties_are_stored_flat(self): + """``popularity`` must land as a top-level property, not nested. + + Passing the previously documented ``properties={...}`` does not raise -- + it stores a dict *inside* the properties dict, which is why the original + docs bug could reach a user's graph unnoticed. + """ + graph = ContextGraph(advanced_analytics=False) + graph.add_node("Python", "language", popularity="high") + + assert graph.nodes["Python"].properties == {"popularity": "high"} + assert graph.find_node("Python")["metadata"]["popularity"] == "high" + assert "properties" not in graph.nodes["Python"].properties + + def test_edge_type_is_positional_not_a_property(self): + """``related_to`` must be the edge type, not a stray metadata key.""" + graph = ContextGraph(advanced_analytics=False) + graph.add_node("Python", "language") + graph.add_node("Programming", "concept") + graph.add_edge("Python", "Programming", "related_to") + + edge = graph.edges[0] + assert edge.edge_type == "related_to" + assert "type" not in edge.metadata + + +class TestDocstringExampleDoesNotRegress: + """Guard the docstring text itself, not just equivalent code.""" + + def test_add_node_example_supplies_node_type_positionally(self): + calls = _statements_calling("add_node") + assert calls, "the 'Example Usage:' block no longer calls graph.add_node()" + for call in calls: + assert not _BARE_TYPE_KWARG_RE.search(call), ( + f"add_node example passes type= as a keyword: {call!r}. " + "node_type is positional-required; type= falls through to " + "**properties and the call raises TypeError." + ) + assert "properties=" not in call, ( + f"add_node example passes properties=: {call!r}. " + "add_node has no properties parameter; extra properties are " + "passed as **kwargs." + ) + + def test_add_edge_example_supplies_edge_type_positionally(self): + calls = _statements_calling("add_edge") + assert calls, "the 'Example Usage:' block no longer calls graph.add_edge()" + for call in calls: + assert not _BARE_TYPE_KWARG_RE.search(call), ( + f"add_edge example passes type= as a keyword: {call!r}. " + "The parameter is edge_type; type= is silently absorbed into " + "**properties and pollutes edge metadata." + ) + + def test_broken_form_still_raises(self): + """Pin the signature contract the example has to respect.""" + graph = ContextGraph(advanced_analytics=False) + with pytest.raises(TypeError, match="node_type"): + graph.add_node("Python", type="language", properties={"popularity": "high"})