varar 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
varar-0.5.2/.gitignore ADDED
@@ -0,0 +1,32 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+ *.tsbuildinfo
5
+ coverage
6
+ *.timestamp-*
7
+ .superpowers
8
+ .var/
9
+
10
+ # varar.lock.json baselines written by var-cli integration tests (temp fixtures)
11
+ typescript/packages/cli/tests/fixtures/run-basic/varar.lock.json
12
+ typescript/packages/cli/tests/fixtures/drift-tmp-*/
13
+ # varar.lock.json written by the Python conformance dogfood command
14
+ python/varar.lock.json
15
+ # varar.lock.json written into the Kotest fixture roots on every test run
16
+ # (the drift fixture's baseline IS committed and is not listed here)
17
+ java/kotest/src/test/resources/kotest-smoke/varar.lock.json
18
+ java/kotest/src/test/resources/kotest-failing/varar.lock.json
19
+ .claude/*
20
+ !.claude/skills/
21
+
22
+ # Python
23
+ .venv/
24
+ __pycache__/
25
+ *.pyc
26
+ .pytest_cache/
27
+ .ruff_cache/
28
+ .coverage
29
+ htmlcov/
30
+ coverage.lcov
31
+
32
+ release/dist/
varar-0.5.2/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: varar
3
+ Version: 0.5.2
4
+ Summary: Markdown-native BDD — pure Python core (port of @varar/varar)
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: cucumber-expressions==20.0.0
8
+ Requires-Dist: varar-core==0.5.2
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "varar"
3
+ version = "0.5.2"
4
+ description = "Markdown-native BDD — pure Python core (port of @varar/varar)"
5
+ requires-python = ">=3.11"
6
+ license = "MIT"
7
+ dependencies = [
8
+ "cucumber-expressions==20.0.0",
9
+ "varar-core==0.5.2",
10
+ ]
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/varar"]
@@ -0,0 +1,7 @@
1
+ """Author facade for var: steps over the pure var-core engine."""
2
+
3
+ __version__ = "0.0.0"
4
+
5
+ from varar.internal import steps
6
+
7
+ __all__ = ["steps"]
@@ -0,0 +1,170 @@
1
+ """Author-facing API for declaring step state — port of var/src/internal.ts (steps)."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from re import Pattern
6
+ from typing import Any, Callable, Optional
7
+
8
+ from varar_core.registry import Registry, add_step, create_registry, define_parameter_type
9
+ from varar_core.step_role import StepKind
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Module-level mutable builder state (mirrors the module-scope vars in internal.ts)
13
+ # ---------------------------------------------------------------------------
14
+
15
+ _steps: list[dict[str, Any]] = []
16
+ # One factory per step-file; keyed by the FACTORY's __code__.co_filename
17
+ _context_factories_by_file: dict[str, Callable[[], Any]] = {}
18
+ _custom_types: list[dict[str, Any]] = []
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Public API
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ def steps(
27
+ factory: Optional[Callable[[], Any]] = None,
28
+ ) -> tuple[
29
+ Callable[..., None],
30
+ Callable[[str], Callable[[Callable], Callable]],
31
+ Callable[[str], Callable[[Callable], Callable]],
32
+ ]:
33
+ """Register *factory* as the context-state constructor for its step file.
34
+
35
+ *factory* is optional: a step file whose steps are pure (nothing to
36
+ arrange, nothing to evolve) calls ``steps()`` bare and its handlers
37
+ receive an empty ``dict`` as state.
38
+
39
+ Returns ``(param, stimulus, sensor)``:
40
+
41
+ - ``param(name, regexp, parse=None, format=None)`` declares a custom
42
+ cucumber-expression parameter type. ``parse`` is a varargs function over
43
+ the capture groups (``parse(*groups)``); omitted, the parameter stays the
44
+ matched text. ``format`` is the display-only inverse.
45
+ - ``stimulus`` / ``sensor`` are decorator factories:
46
+ ``@stimulus("expression")`` registers the decorated function as a step.
47
+
48
+ Source location is captured from the decorated function's ``__code__``
49
+ attributes (``co_filename`` / ``co_firstlineno``).
50
+
51
+ Raises ``RuntimeError`` if called more than once for the same source file
52
+ (keyed by *factory*'s ``co_filename``, or the caller's file when *factory*
53
+ is omitted), mirroring ``internal.ts``.
54
+ """
55
+ if factory is None:
56
+ # No factory code object to key by — use the calling step file itself,
57
+ # which is where an inline factory would have been defined anyway.
58
+ source_file: str = sys._getframe(1).f_code.co_filename
59
+ factory = dict
60
+ else:
61
+ source_file = factory.__code__.co_filename
62
+ if source_file in _context_factories_by_file:
63
+ raise RuntimeError(
64
+ f"steps() called more than once in {source_file}"
65
+ )
66
+ _context_factories_by_file[source_file] = factory
67
+
68
+ def param(
69
+ name: str,
70
+ regexp: Any,
71
+ parse: Optional[Callable[..., Any]] = None,
72
+ format: Optional[Callable[[Any], str]] = None,
73
+ ) -> None:
74
+ _custom_types.append(
75
+ {"name": name, "regexp": regexp, "parse": parse, "format": format}
76
+ )
77
+
78
+ def _make_decorator(kind: StepKind) -> Callable[[str], Callable[[Callable], Callable]]:
79
+ def decorator_factory(expression: str) -> Callable[[Callable], Callable]:
80
+ def decorator(fn: Callable) -> Callable:
81
+ _steps.append(
82
+ {
83
+ "expression": expression,
84
+ "source_file": fn.__code__.co_filename,
85
+ "source_line": fn.__code__.co_firstlineno,
86
+ "handler": fn,
87
+ "kind": kind,
88
+ }
89
+ )
90
+ return fn
91
+
92
+ return decorator
93
+
94
+ return decorator_factory
95
+
96
+ return param, _make_decorator("stimulus"), _make_decorator("sensor")
97
+
98
+
99
+ def context_factory() -> Callable[[str], Any]:
100
+ """Return a callable ``(step_file: str) -> state`` that invokes the
101
+ registered factory for *step_file*, or ``{}`` if none is registered.
102
+
103
+ Mirrors ``contextFactory()`` in ``internal.ts``.
104
+ """
105
+ # Snapshot at call time (mirrors TS semantics where the map is closed over)
106
+ factories = dict(_context_factories_by_file)
107
+
108
+ def _invoke(step_file: str) -> Any:
109
+ f = factories.get(step_file)
110
+ return f() if f is not None else {}
111
+
112
+ return _invoke
113
+
114
+
115
+ def build_registry() -> Registry:
116
+ """Build and return a ``Registry`` from the accumulated steps and custom types.
117
+
118
+ Mirrors ``buildRegistry()`` in ``internal.ts``: custom parameter types are
119
+ registered first, then steps are compiled in registration order.
120
+ """
121
+ r = create_registry()
122
+ for t in _custom_types:
123
+ kwargs: dict[str, Any] = {"name": t["name"], "regexp": t["regexp"]}
124
+ if t.get("parse") is not None:
125
+ kwargs["parse"] = t["parse"]
126
+ if t.get("format") is not None:
127
+ kwargs["format"] = t["format"]
128
+ r = define_parameter_type(r, **kwargs)
129
+ for e in _steps:
130
+ r = add_step(
131
+ r,
132
+ expression=e["expression"],
133
+ expression_source_file=e["source_file"],
134
+ expression_source_line=e["source_line"],
135
+ handler=e["handler"],
136
+ kind=e["kind"],
137
+ )
138
+ return r
139
+
140
+
141
+ def _reset_builder() -> None:
142
+ """Clear all accumulated state. Use in tests between isolated scenarios."""
143
+ global _steps, _custom_types
144
+ _steps = []
145
+ _context_factories_by_file.clear()
146
+ _custom_types = []
147
+
148
+
149
+ def _custom_parameter_types() -> list[dict[str, str]]:
150
+ """Conformance-harness accessor: the custom parameter types accumulated by
151
+ ``param`` since the last ``_reset_builder``, projected to the
152
+ ``{"name", "regexp"}`` wire shape ``to_registry_artifact`` serializes.
153
+
154
+ ``regexp`` is the bare pattern source (``re.Pattern.pattern`` or the string
155
+ as authored — no flags/delimiters), the cross-port convention every
156
+ language's registry golden uses. Internal-only, mirrors the TS
157
+ ``_customParameterTypes``.
158
+ """
159
+ out: list[dict[str, str]] = []
160
+ for t in _custom_types:
161
+ rx = t["regexp"]
162
+ if isinstance(rx, Pattern):
163
+ rx = rx.pattern
164
+ elif not isinstance(rx, str):
165
+ raise TypeError(
166
+ f"parameter type {t['name']!r}: regexp lists are not supported by the "
167
+ "conformance projection yet"
168
+ )
169
+ out.append({"name": t["name"], "regexp": rx})
170
+ return out
@@ -0,0 +1,6 @@
1
+ """Adapter-only glue (mirrors @varar/varar/registry): build the registry and
2
+ context factory from the module-scope accumulator, and reset it between runs."""
3
+
4
+ from varar.internal import _custom_parameter_types, _reset_builder, build_registry, context_factory
5
+
6
+ __all__ = ["build_registry", "context_factory", "_reset_builder", "_custom_parameter_types"]
@@ -0,0 +1,117 @@
1
+ """test_conformance.py — parametrized var-doc, registry, plan, and trace conformance harness.
2
+
3
+ For each bundle under conformance/bundles/, parses example.md, projects it
4
+ to the var-doc artifact, and asserts byte-for-byte equality with the
5
+ committed golden/var-doc.json.
6
+
7
+ The registry stage imports each bundle's steps.py (after resetting the builder),
8
+ builds the registry, projects it, and asserts byte-for-byte equality with
9
+ golden/registry.json.
10
+
11
+ The plan stage builds the execution plan and asserts equality with golden/plan.json.
12
+
13
+ The trace stage runs all examples via run_conformance and asserts equality with
14
+ golden/trace.json for all four artifacts simultaneously.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import importlib.util
20
+ from pathlib import Path
21
+
22
+ import pytest
23
+
24
+ from varar_core.canonical_json import canonical_stringify
25
+ from varar_core.conformance import run_conformance, to_plan_artifact, to_registry_artifact, to_var_doc_artifact
26
+ from varar.registry import _custom_parameter_types, _reset_builder, build_registry, context_factory
27
+ from varar_core.parse import parse
28
+ from varar_core.plan import plan as build_plan
29
+
30
+ # python/packages/varar/tests/ -> parents[4] = repo root
31
+ BUNDLES_DIR = Path(__file__).resolve().parents[4] / "conformance" / "bundles"
32
+ BUNDLES = sorted(p for p in BUNDLES_DIR.iterdir() if p.is_dir())
33
+
34
+
35
+ @pytest.mark.parametrize("bundle", BUNDLES, ids=lambda b: b.name)
36
+ def test_var_doc_matches_golden(bundle: Path) -> None:
37
+ source = (bundle / "example.md").read_text(encoding="utf-8")
38
+ doc = parse("example.md", source)
39
+ artifact = to_var_doc_artifact(doc)
40
+ actual = canonical_stringify(artifact)
41
+ expected = (bundle / "golden" / "var-doc.json").read_text(encoding="utf-8")
42
+ assert actual == expected
43
+
44
+
45
+ def _find_steps_file(bundle: Path) -> Path | None:
46
+ """Return the canonical step file for *bundle*.
47
+
48
+ Prefers ``<name>.steps.py`` (mirroring the TypeScript ``<name>.steps.ts``
49
+ convention) so the stem matches the golden ``stepFile`` values. Falls back
50
+ to the legacy ``steps.py`` name so existing bundles without a renamed file
51
+ still work.
52
+ """
53
+ candidates = sorted(bundle.glob("*.steps.py"))
54
+ if candidates:
55
+ return candidates[0]
56
+ legacy = bundle / "steps.py"
57
+ return legacy if legacy.exists() else None
58
+
59
+
60
+ def _import_steps(bundle: Path) -> None:
61
+ """Import the step file from *bundle* as a fresh module (unique name each call).
62
+
63
+ Using ``spec_from_file_location`` with a unique module name ensures
64
+ ``steps`` sees a different ``co_filename`` per import, and also
65
+ prevents Python's import cache from returning a stale module across bundles.
66
+ """
67
+ steps_py = _find_steps_file(bundle)
68
+ assert steps_py is not None, f"Cannot find a step file in {bundle}"
69
+ module_name = f"_steps_{bundle.name}"
70
+ spec = importlib.util.spec_from_file_location(module_name, steps_py)
71
+ assert spec is not None and spec.loader is not None, f"Cannot load {steps_py}"
72
+ mod = importlib.util.module_from_spec(spec)
73
+ spec.loader.exec_module(mod) # type: ignore[union-attr]
74
+
75
+
76
+ # Every bundle in the corpus is gated. A bundle without a Python step fixture is a
77
+ # hole in the gate, not a bundle to skip: parametrize over all of them and let
78
+ # _import_steps fail loudly, the way the Java/Kotlin/Rust/Go/TS harnesses do.
79
+
80
+
81
+ @pytest.mark.parametrize("bundle", BUNDLES, ids=lambda b: b.name)
82
+ def test_registry_matches_golden(bundle: Path) -> None:
83
+ _reset_builder()
84
+ _import_steps(bundle)
85
+ registry = build_registry()
86
+ artifact = to_registry_artifact(registry, _custom_parameter_types())
87
+ actual = canonical_stringify(artifact)
88
+ expected = (bundle / "golden" / "registry.json").read_text(encoding="utf-8")
89
+ assert actual == expected, f"registry.json mismatch for {bundle.name}"
90
+
91
+
92
+ @pytest.mark.parametrize("bundle", BUNDLES, ids=lambda b: b.name)
93
+ def test_plan_matches_golden(bundle: Path) -> None:
94
+ _reset_builder()
95
+ _import_steps(bundle)
96
+ registry = build_registry()
97
+ source = (bundle / "example.md").read_text(encoding="utf-8")
98
+ doc = parse("example.md", source)
99
+ execution = build_plan(doc, registry)
100
+ artifact = to_plan_artifact(execution)
101
+ actual = canonical_stringify(artifact)
102
+ expected = (bundle / "golden" / "plan.json").read_text(encoding="utf-8")
103
+ assert actual == expected, f"plan.json mismatch for {bundle.name}"
104
+
105
+
106
+ @pytest.mark.parametrize("bundle", BUNDLES, ids=lambda b: b.name)
107
+ def test_trace_matches_golden(bundle: Path) -> None:
108
+ _reset_builder()
109
+ _import_steps(bundle)
110
+ registry = build_registry()
111
+ create_ctx = context_factory()
112
+ source = (bundle / "example.md").read_text(encoding="utf-8")
113
+ doc = parse("example.md", source)
114
+ artifacts = run_conformance(doc, registry, create_ctx, tuple(_custom_parameter_types()))
115
+ actual = canonical_stringify(artifacts.trace)
116
+ expected = (bundle / "golden" / "trace.json").read_text(encoding="utf-8")
117
+ assert actual == expected, f"trace.json mismatch for {bundle.name}"
@@ -0,0 +1,32 @@
1
+ import re
2
+
3
+ import pytest
4
+
5
+ from varar import steps
6
+ from varar.registry import _custom_parameter_types, _reset_builder
7
+
8
+
9
+ def test_projects_name_and_pattern_source():
10
+ _reset_builder()
11
+ param, _stimulus, _sensor = steps(lambda: {})
12
+ param("airport", re.compile(r"[A-Z]{3}"), parse=lambda code: code.lower())
13
+ assert _custom_parameter_types() == [{"name": "airport", "regexp": "[A-Z]{3}"}]
14
+ _reset_builder()
15
+ assert _custom_parameter_types() == []
16
+
17
+
18
+ def test_string_regexp_passes_through_verbatim():
19
+ _reset_builder()
20
+ param, _stimulus, _sensor = steps(lambda: {})
21
+ param("airport", "[A-Z]{3}")
22
+ assert _custom_parameter_types() == [{"name": "airport", "regexp": "[A-Z]{3}"}]
23
+ _reset_builder()
24
+
25
+
26
+ def test_list_form_regexp_is_rejected():
27
+ _reset_builder()
28
+ param, _stimulus, _sensor = steps(lambda: {})
29
+ param("code", ["[A-Z]{3}", "[0-9]{3}"])
30
+ with pytest.raises(TypeError, match="not supported"):
31
+ _custom_parameter_types()
32
+ _reset_builder()
@@ -0,0 +1,5 @@
1
+ import varar
2
+
3
+
4
+ def test_version():
5
+ assert varar.__version__ == "0.0.0"
@@ -0,0 +1,149 @@
1
+ """Tests for internal.py steps() — translated from var/src/internal.ts tests."""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+
6
+ from varar.registry import _reset_builder, build_registry, context_factory
7
+ from varar import steps
8
+
9
+
10
+ def test_two_decorators_register_with_correct_kinds() -> None:
11
+ _reset_builder()
12
+
13
+ def factory():
14
+ return {}
15
+
16
+ _param, stimulus, sensor = steps(factory)
17
+
18
+ @stimulus("I have set up the world")
19
+ def setup(state):
20
+ pass
21
+
22
+ @stimulus("I do something")
23
+ def do_something(state):
24
+ pass
25
+
26
+ @sensor("I check the result")
27
+ def check(state):
28
+ pass
29
+
30
+ r = build_registry()
31
+ assert len(r.steps) == 3
32
+ kinds = {s.expression: s.kind for s in r.steps}
33
+ assert kinds["I have set up the world"] == "stimulus"
34
+ assert kinds["I do something"] == "stimulus"
35
+ assert kinds["I check the result"] == "sensor"
36
+
37
+
38
+ def test_decorators_capture_source_file_and_line() -> None:
39
+ _reset_builder()
40
+
41
+ def factory():
42
+ return {}
43
+
44
+ _param, stimulus, _sensor = steps(factory)
45
+
46
+ @stimulus("a step")
47
+ def my_step(state):
48
+ pass
49
+
50
+ r = build_registry()
51
+ step = r.steps[0]
52
+ assert step.expression_source_line >= 1
53
+ assert "test_steps" in step.expression_source_file
54
+
55
+
56
+ def test_second_steps_in_same_module_raises() -> None:
57
+ _reset_builder()
58
+
59
+ def factory1():
60
+ return {}
61
+
62
+ def factory2():
63
+ return {}
64
+
65
+ steps(factory1)
66
+ with pytest.raises(Exception, match=r"steps.*called more than once"):
67
+ steps(factory2)
68
+
69
+
70
+ def test_build_registry_returns_steps_in_registration_order() -> None:
71
+ _reset_builder()
72
+
73
+ def factory():
74
+ return {}
75
+
76
+ _param, stimulus, sensor = steps(factory)
77
+
78
+ @stimulus("step one")
79
+ def s1(state):
80
+ pass
81
+
82
+ @stimulus("step two")
83
+ def s2(state):
84
+ pass
85
+
86
+ @sensor("step three")
87
+ def s3(state):
88
+ pass
89
+
90
+ r = build_registry()
91
+ assert [s.expression for s in r.steps] == ["step one", "step two", "step three"]
92
+
93
+
94
+ def test_context_factory_returns_callable_that_invokes_registered_factory() -> None:
95
+ _reset_builder()
96
+
97
+ def factory():
98
+ return {"count": 42}
99
+
100
+ _param, stimulus, _sensor = steps(factory)
101
+
102
+ @stimulus("some step")
103
+ def step(state):
104
+ pass
105
+
106
+ r = build_registry()
107
+ # context_factory() returns a callable(step_file) -> state
108
+ cf = context_factory()
109
+ step_file = r.steps[0].expression_source_file
110
+ state = cf(step_file)
111
+ assert state == {"count": 42}
112
+
113
+
114
+ def test_context_factory_returns_empty_dict_for_unknown_file() -> None:
115
+ _reset_builder()
116
+
117
+ cf = context_factory()
118
+ result = cf("nonexistent_file.py")
119
+ assert result == {}
120
+
121
+
122
+ def test_steps_without_factory_registers_steps_against_empty_state() -> None:
123
+ _reset_builder()
124
+
125
+ _param, stimulus, sensor = steps()
126
+
127
+ @stimulus("I warm up my mental math")
128
+ def warm_up(state):
129
+ pass
130
+
131
+ @sensor("the square of {int} is {int}")
132
+ def square(state, n, expected):
133
+ return [n, n * n]
134
+
135
+ r = build_registry()
136
+ assert len(r.steps) == 2
137
+ # The factory is keyed by THIS file (the caller), and produces a fresh {}.
138
+ cf = context_factory()
139
+ state = cf(r.steps[0].expression_source_file)
140
+ assert state == {}
141
+ assert cf(r.steps[0].expression_source_file) is not state
142
+
143
+
144
+ def test_steps_without_factory_still_enforces_once_per_file() -> None:
145
+ _reset_builder()
146
+
147
+ steps()
148
+ with pytest.raises(Exception, match=r"steps.*called more than once"):
149
+ steps()