1717from __future__ import annotations
1818
1919import os
20+ import sys
2021from datetime import datetime
2122from typing import Any
2223
2627# ---------------------------------------------------------------------------
2728
2829
29- def _env_lookup ( key : str , prefix : str ) -> str | None :
30- """Project-prefix-with-fallback lookup: try ``<prefix>_<key>`` first, then the
31- unprefixed ``<key>``. Returns the first set & non-empty (stripped) value, or
32- None. With no prefix this is a plain ``<key>`` lookup (backward compatible).
30+ # Tracks (prefix, key) pairs already warned about, so the migration warning (F-007)
31+ # fires once per process per variable rather than on every lookup. Exposed as a
32+ # module global so tests can reset it between cases.
33+ _WARNED_PREFIX_KEYS : set [ tuple [ str , str ]] = set ()
3334
34- This lets a family of routines share one environment: shared values
35- (credentials, tokens, common knobs) are set once unprefixed and reached via
36- the fallback, while per-routine values are set prefixed so they never collide.
35+
36+ def _env_lookup (key : str , prefix : str , shared : bool = False ) -> str | None :
37+ """Resolve a variable, honouring the project-prefix convention (F-001, F-007).
38+
39+ With no prefix this is a plain ``<key>`` lookup (backward compatible). With a
40+ prefix set, behaviour depends on ``shared``:
41+
42+ - ``shared=True`` — a family-shared credential/infra value: try
43+ ``<prefix>_<key>`` first, then fall back to the unprefixed ``<key>``, so a
44+ family of routines can share one environment with the shared values set once
45+ unprefixed.
46+ - ``shared=False`` (default) — a routine-own knob: read **only**
47+ ``<prefix>_<key>``. The unprefixed form is not read, so a knob set unprefixed
48+ in a shared environment cannot leak between sibling routines. If the
49+ unprefixed form *is* set while the prefixed one is not, emit a one-time
50+ stderr warning naming both forms (the stray value is still ignored).
51+
52+ Returns the first set & non-empty (stripped) value, or None.
3753 """
38- names = (f"{ prefix } _{ key } " , key ) if prefix else (key ,)
54+ if not prefix :
55+ names : tuple [str , ...] = (key ,)
56+ elif shared :
57+ names = (f"{ prefix } _{ key } " , key )
58+ else :
59+ names = (f"{ prefix } _{ key } " ,)
3960 for name in names :
4061 v = os .environ .get (name , "" ).strip ()
4162 if v :
4263 return v
64+ # Prefix-required miss: warn once if a stray unprefixed value is being ignored.
65+ if prefix and not shared and os .environ .get (key , "" ).strip ():
66+ pair = (prefix , key )
67+ if pair not in _WARNED_PREFIX_KEYS :
68+ _WARNED_PREFIX_KEYS .add (pair )
69+ print (
70+ f"WARNING: { prefix } _{ key } is not set; ignoring a plain { key } in the "
71+ f"environment. Routine-own keys are read only with the { prefix } "
72+ f"prefix — rename it to { prefix } _{ key } (shared family credentials "
73+ f"still allow the unprefixed form)." ,
74+ file = sys .stderr ,
75+ flush = True ,
76+ )
4377 return None
4478
4579
46- def env_required (key : str , * , prefix : str = "" ) -> str :
47- """Return a required env var, stripped, via the project-prefix-with-fallback
48- lookup. Aborts the run with a clear SystemExit naming the variable when
49- neither the prefixed nor the unprefixed form is set. A routine never falls
50- back to a placeholder for a credential or URL."""
51- v = _env_lookup (key , prefix )
80+ def env_required (key : str , * , prefix : str = "" , shared : bool = False ) -> str :
81+ """Return a required env var, stripped, via the project-prefix lookup. Aborts
82+ the run with a clear SystemExit naming the variable when it is not set. A
83+ routine never falls back to a placeholder for a credential or URL.
84+
85+ Pass ``shared=True`` for family-shared credentials/infra so the unprefixed
86+ form is still read; routine-own keys (the default) are prefix-only — see
87+ :func:`_env_lookup`."""
88+ v = _env_lookup (key , prefix , shared )
5289 if not v :
53- suffix = f" (or { prefix } _{ key } )" if prefix else ""
54- raise SystemExit (f"Missing required env var: { key } { suffix } " )
90+ suffix = f" (or { prefix } _{ key } )" if prefix and shared else ""
91+ name = f"{ prefix } _{ key } " if prefix and not shared else key
92+ raise SystemExit (f"Missing required env var: { name } { suffix } " )
5593 return v
5694
5795
58- def env_opt (key : str , default : str | None = None , * , prefix : str = "" ) -> str | None :
59- """Return an optional env var, stripped, via the project-prefix-with-fallback
60- lookup, or ``default`` when neither form is set/non-empty."""
61- v = _env_lookup (key , prefix )
96+ def env_opt (
97+ key : str , default : str | None = None , * , prefix : str = "" , shared : bool = False
98+ ) -> str | None :
99+ """Return an optional env var, stripped, via the project-prefix lookup, or
100+ ``default`` when not set. Pass ``shared=True`` for family-shared values (see
101+ :func:`_env_lookup`)."""
102+ v = _env_lookup (key , prefix , shared )
62103 return v if v is not None else default
63104
64105
@@ -67,16 +108,19 @@ def env_opt(key: str, default: str | None = None, *, prefix: str = "") -> str |
67108env_get = env_opt
68109
69110
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.
111+ def env_int (
112+ key : str , default : int | None = None , * , prefix : str = "" , shared : bool = False
113+ ) -> int | None :
114+ """Optional env var parsed as an ``int``, via the project-prefix lookup. Returns
115+ ``default`` when unset (``shared`` controls the unprefixed fallback, see
116+ :func:`_env_lookup`).
73117
74118 When the value **is** set but is not a valid integer, this aborts with a clear
75119 ``SystemExit`` naming the variable (``Invalid <KEY>='<value>': expected an
76120 integer.``) instead of raising an uncaught ``ValueError`` mid-run — so a typo in
77121 a numeric knob fails a routine's ``--dry-run`` config check cleanly. ``default``
78122 is returned as-is and never re-parsed (F-006)."""
79- raw = _env_lookup (key , prefix )
123+ raw = _env_lookup (key , prefix , shared )
80124 if raw is None :
81125 return default
82126 try :
@@ -85,11 +129,13 @@ def env_int(key: str, default: int | None = None, *, prefix: str = "") -> int |
85129 raise SystemExit (f"Invalid { key } ={ raw !r} : expected an integer." )
86130
87131
88- def env_float (key : str , default : float | None = None , * , prefix : str = "" ) -> float | None :
132+ def env_float (
133+ key : str , default : float | None = None , * , prefix : str = "" , shared : bool = False
134+ ) -> float | None :
89135 """Optional env var parsed as a ``float`` (see :func:`env_int`). Returns
90136 ``default`` when unset; aborts with a clear ``SystemExit`` naming the variable
91137 (``Invalid <KEY>='<value>': expected a number.``) when set but malformed (F-006)."""
92- raw = _env_lookup (key , prefix )
138+ raw = _env_lookup (key , prefix , shared )
93139 if raw is None :
94140 return default
95141 try :
0 commit comments