Skip to content

Commit b4f1514

Browse files
mwebbersclaude
andcommitted
Added v0.2 — env_int/env_float numeric env helpers (F-006)
Numeric env helpers with project-prefix fallback that abort with a clear SystemExit naming the variable on a malformed value (Invalid <KEY>='<value>': expected an integer./a number.) instead of an uncaught ValueError. Folds the per-routine _int/_float helpers the WooCommerce family each duplicated (Margin F-032 / Sales F-021 / Stock F-023 / Marketing F-027) into the shared core — review Step 9. Additive; existing names unchanged. 28 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c8573e2 commit b4f1514

6 files changed

Lines changed: 107 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ adheres to semantic versioning.
88

99
## [Unreleased]
1010

11+
## [0.2.0] - 2026-06-01
12+
13+
### Added
14+
- **[F-006]** `env_int(key, default=None, *, prefix="")` and `env_float(...)`
15+
numeric env helpers using the same project-prefix-with-fallback lookup as
16+
`env_opt`, returning `default` when unset and aborting with a clear `SystemExit`
17+
naming the variable (`Invalid <KEY>='<value>': expected an integer.` /
18+
`… a number.`) when the value is set but malformed, instead of an uncaught
19+
`ValueError`. Folds the per-routine `_int`/`_float` helpers the WooCommerce
20+
family each carried into the shared core (review Step 9). Additive — existing
21+
names unchanged.
22+
1123
## [0.1.0] - 2026-05-31
1224

1325
Initial release: the vendor-agnostic, standard-library-only core, extracted from

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ Context for Claude Code in this repo. Keep this short — it loads on every sess
66

77
`claude-code-commons` is a **library**, not a routine. It holds the small,
88
vendor-agnostic, **standard-library-only** helpers shared by Claude routine
9-
repos: `env_required`/`env_opt`/`env_get`, `parse_num`, `currency_symbol`,
10-
`build_remote_path`, `log`. It has **no vendor or report logic and no third-party
11-
dependency**.
9+
repos: `env_required`/`env_opt`/`env_get`, `env_int`/`env_float`, `parse_num`,
10+
`currency_symbol`, `build_remote_path`, `log`. It has **no vendor or report logic
11+
and no third-party dependency**.
1212

1313
There is no scheduled-task/operator section here — this package does not run on
1414
its own. It is consumed by the routine repos and by vendor toolkits (e.g.

SCOPE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ Each feature is testable. The ID in brackets is referenced by tests via
6262
- **[F-005] Timestamped run log.** `log(msg)` prints a flushed `[HH:MM:SS] msg`
6363
line to stdout — the shared run-log format the routines use.
6464

65+
- **[F-006] Numeric environment helpers with clear errors.** `env_int(key,
66+
default=None, *, prefix="")` and `env_float(key, default=None, *, prefix="")`
67+
resolve a variable via the same project-prefix-with-fallback lookup as `env_opt`,
68+
return `default` when unset, and parse the value as an `int`/`float`. When the
69+
value is **set but malformed**, they abort with a clear `SystemExit` naming the
70+
variable (`Invalid <KEY>='<value>': expected an integer.` / `… a number.`)
71+
instead of raising an uncaught `ValueError` mid-run — so a typo in a numeric knob
72+
fails a routine's `--dry-run` config validation cleanly. Replaces the per-routine
73+
`_int`/`_float` helpers the WooCommerce family each carried (review Step 9).
74+
6575
## Out of scope
6676

6777
Anything vendor-specific (a WooCommerce/Shopify/… client, shop meta keys);

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "claude-code-commons"
7-
version = "0.1.0"
7+
version = "0.2.0"
88
description = "Vendor-agnostic, standard-library-only helpers (env lookup, tolerant parsing, currency symbols, remote-path builder, run log) shared by Claude routine repos"
99
# Standard library only — a broad floor so it installs in any routine runtime,
1010
# including a Python 3.11 scheduled-task sandbox. Verified on 3.9 and 3.12.

src/code_commons.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,37 @@ def env_opt(key: str, default: str | None = None, *, prefix: str = "") -> str |
6767
env_get = env_opt
6868

6969

70+
def env_int(key: str, default: int | None = None, *, prefix: str = "") -> int | None:
71+
"""Optional env var parsed as an ``int``, via the project-prefix-with-fallback
72+
lookup. Returns ``default`` when neither form is set.
73+
74+
When the value **is** set but is not a valid integer, this aborts with a clear
75+
``SystemExit`` naming the variable (``Invalid <KEY>='<value>': expected an
76+
integer.``) instead of raising an uncaught ``ValueError`` mid-run — so a typo in
77+
a numeric knob fails a routine's ``--dry-run`` config check cleanly. ``default``
78+
is returned as-is and never re-parsed (F-006)."""
79+
raw = _env_lookup(key, prefix)
80+
if raw is None:
81+
return default
82+
try:
83+
return int(raw)
84+
except (TypeError, ValueError):
85+
raise SystemExit(f"Invalid {key}={raw!r}: expected an integer.")
86+
87+
88+
def env_float(key: str, default: float | None = None, *, prefix: str = "") -> float | None:
89+
"""Optional env var parsed as a ``float`` (see :func:`env_int`). Returns
90+
``default`` when unset; aborts with a clear ``SystemExit`` naming the variable
91+
(``Invalid <KEY>='<value>': expected a number.``) when set but malformed (F-006)."""
92+
raw = _env_lookup(key, prefix)
93+
if raw is None:
94+
return default
95+
try:
96+
return float(raw)
97+
except (TypeError, ValueError):
98+
raise SystemExit(f"Invalid {key}={raw!r}: expected a number.")
99+
100+
70101
# ---------------------------------------------------------------------------
71102
# Tolerant numeric parsing
72103
# ---------------------------------------------------------------------------

tests/test_code_commons.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,56 @@ def test_env_required_missing_names_both_forms(monkeypatch):
6767
assert "WC_URL" in msg and "STOCK_WC_URL" in msg
6868

6969

70+
# ---------------------------------------------------------------------------
71+
# F-006 Numeric environment helpers with clear errors
72+
# ---------------------------------------------------------------------------
73+
74+
75+
@pytest.mark.feature("F-006")
76+
def test_env_int_float_parse_with_default_and_prefix(monkeypatch):
77+
for k in ("PAGES", "STOCK_PAGES", "RATE", "STOCK_RATE"):
78+
monkeypatch.delenv(k, raising=False)
79+
# Unset → default returned as-is (an int/float, not re-parsed); no default → None.
80+
assert C.env_int("PAGES", 50, prefix="STOCK") == 50
81+
assert C.env_float("RATE", 1.5, prefix="STOCK") == 1.5
82+
assert C.env_int("PAGES") is None
83+
assert C.env_float("RATE") is None
84+
# Set & valid: parsed; the prefixed form wins over the unprefixed fallback.
85+
monkeypatch.setenv("PAGES", "10")
86+
monkeypatch.setenv("STOCK_PAGES", "200")
87+
assert C.env_int("PAGES", 50, prefix="STOCK") == 200
88+
monkeypatch.delenv("STOCK_PAGES", raising=False)
89+
assert C.env_int("PAGES", 50, prefix="STOCK") == 10
90+
monkeypatch.setenv("RATE", "2.5")
91+
assert C.env_float("RATE", 1.0) == 2.5
92+
93+
94+
@pytest.mark.feature("F-006")
95+
def test_env_int_malformed_aborts_naming_the_variable(monkeypatch):
96+
monkeypatch.setenv("PAGES", "abc")
97+
with pytest.raises(SystemExit) as exc:
98+
C.env_int("PAGES")
99+
msg = str(exc.value)
100+
assert "PAGES" in msg and "abc" in msg and "integer" in msg
101+
102+
103+
@pytest.mark.feature("F-006")
104+
def test_env_int_rejects_a_non_integer_number(monkeypatch):
105+
# "10.5" is a valid float but not an int — env_int must reject it, not truncate.
106+
monkeypatch.setenv("PAGES", "10.5")
107+
with pytest.raises(SystemExit):
108+
C.env_int("PAGES")
109+
110+
111+
@pytest.mark.feature("F-006")
112+
def test_env_float_malformed_aborts_naming_the_variable(monkeypatch):
113+
monkeypatch.setenv("RATE", "x")
114+
with pytest.raises(SystemExit) as exc:
115+
C.env_float("RATE")
116+
msg = str(exc.value)
117+
assert "RATE" in msg and "number" in msg
118+
119+
70120
# ---------------------------------------------------------------------------
71121
# F-002 Tolerant number parsing
72122
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)