▶ runtime-narrative

Auto-Instrumentation

Auto-instrumentation removes per-function boilerplate by wrapping public callables as stages at the class, module, or import level.

@narrative_class

Wraps every public instance method of a class as a stage. The stage name is "ClassName.method_name".

python
from runtime_narrative import narrative_class, story

@narrative_class
class OrderService:
    def validate(self, order: dict) -> None:        # → stage "OrderService.validate"
        if "amount" not in order:
            raise ValueError("missing amount")

    def charge(self, order: dict) -> str:           # → stage "OrderService.charge"
        return f"charged ${order['amount']:.2f}"

    def fulfill(self, order: dict) -> str:          # → stage "OrderService.fulfill"
        return f"fulfilled order {order.get('id')}"

svc = OrderService()

with story("Process Order", total_stages=3):
    svc.validate({"id": "ORD-1", "amount": 49.99})
    receipt = svc.charge({"id": "ORD-1", "amount": 49.99})
    svc.fulfill({"id": "ORD-1", "amount": 49.99})

What @narrative_class skips by default:

  • Methods whose names start with _ (private/dunder)
  • Methods decorated with @no_stage
  • @classmethod descriptors
  • @staticmethod descriptors
  • @property descriptors
  • Methods inherited from parent classes (only vars(cls) is inspected)

Opt in to classmethods and staticmethods:

python
@narrative_class(instrument_classmethods=True, instrument_staticmethods=True)
class Factory:
    @classmethod
    def create(cls, **kw):            # → stage "Factory.create"
        return cls(**kw)

    @staticmethod
    def validate_schema(data: dict):  # → stage "Factory.validate_schema"
        assert "type" in data

@narrative_stage

A per-function or per-method decorator for explicit stage naming. When name is omitted, the function name is title-cased. This decorator also works inside @narrative_class to override the default ClassName.method_name stage name — @narrative_class detects the _narrative_stage_name sentinel and skips re-wrapping to prevent double-instrumentation.

python
from runtime_narrative import narrative_stage, narrative_class, story

@narrative_stage("Validate Payment")   # explicit custom name
def check_payment(card: str) -> bool:
    return card.startswith("4")

@narrative_class
class PaymentService:
    @narrative_stage("Charge Customer")   # overrides default "PaymentService.charge"
    def charge(self, amount: float) -> str:
        return f"charged ${amount:.2f}"

    def refund(self, amount: float) -> str:  # default: "PaymentService.refund"
        return f"refunded ${amount:.2f}"

with story("Checkout"):
    check_payment("4111111111111111")
    svc = PaymentService()
    svc.charge(49.99)

@no_stage

An opt-out marker that excludes a method or function from all auto-instrumentation — @narrative_class, instrument_module, and auto_instrument all respect it.

python
from runtime_narrative import narrative_class, no_stage

@narrative_class
class ReportService:
    def generate(self, data: list) -> str:    # → stage "ReportService.generate"
        return self._format(data)

    @no_stage
    def _format(self, data: list) -> str:     # private; @no_stage for explicit clarity
        return "\n".join(str(d) for d in data)

    @no_stage
    def debug_dump(self) -> None:             # public but not a meaningful stage
        print(repr(self))

@no_stage sets fn._narrative_skip = True. It does not change the function's behavior.

instrument_module

Instruments all public callables defined in an already-imported module in-place:

  • Classes whose __module__ matches the module name get @narrative_class applied
  • Top-level functions whose __module__ matches are wrapped directly
  • Symbols imported from other modules are not touched
python
import runtime_narrative
from runtime_narrative import story
import myapp.services as services

runtime_narrative.instrument_module(services)  # call once after import

svc = services.CustomerService()

with story("Sync Customers", total_stages=3):
    rows = svc.load("crm.csv")
    svc.validate(rows)
    count = svc.save(rows)

This is the right tool when retrofitting existing code you do not own or do not want to modify.

auto_instrument

Registers a sys.meta_path import hook that instruments every app module on import. Only modules whose source file starts with one of the app_roots paths are instrumented — stdlib and installed packages are unaffected.

python
import sys
import runtime_narrative
from runtime_narrative import story
from pathlib import Path

# Register the hook BEFORE importing any app modules
finder = runtime_narrative.auto_instrument(
    app_roots=[str(Path(__file__).resolve().parent)]
)

# These modules are instrumented on the way in
import myapp.orders
import myapp.payments

with story("Process Order"):
    svc = myapp.orders.OrderService()
    svc.validate({"amount": 49.99})
    svc.charge({"amount": 49.99})

# Remove the hook when you no longer need it
sys.meta_path.remove(finder)

auto_instrument defaults app_roots to the current working directory when called with no arguments:

python
runtime_narrative.auto_instrument()  # instruments all modules under os.getcwd()

Next step

Continue to Failure Diagnostics to see how lean and rich modes capture the failure data these stages report.