params-proto 3.0.0rc25__py3-none-any.whl → 3.0.0rc27__py3-none-any.whl

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.
params_proto/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
1
  from .app import app
2
- from .proto import EnvVar, Field, get_var, proto
2
+ from .envvar import EnvVar, Field, get_var
3
+ from .proto import proto
3
4
  from .hyper import Sweep
params_proto/envvar.py CHANGED
@@ -25,24 +25,50 @@ class _EnvVar:
25
25
  db_url: str = EnvVar("DATABASE_URL", default="localhost")
26
26
  data_dir: str = EnvVar("$DATA_DIR/models", default="/tmp/models")
27
27
 
28
+ 4. OR operation with multiple env var names (tries each in order):
29
+ api_key: str = EnvVar @ "API_KEY" @ "SECRET_KEY" | "default"
30
+ # Or function syntax:
31
+ api_key: str = EnvVar("API_KEY", "SECRET_KEY", default="default")
32
+
28
33
  The pipe operator (|) allows clean chaining of env var name with fallback value.
34
+ Values are loaded lazily - environment variables are read at access time, not definition time.
29
35
  """
30
36
 
31
- def __init__(self, template: str = None, *, default: Any = None):
37
+ def __init__(self, *templates: str, default: Any = None, dtype: type = None):
32
38
  """
33
39
  Create an environment variable reader.
34
40
 
35
41
  Args:
36
- template: Environment variable name or template string (e.g., "$VAR" or "VAR")
37
- default: Default value if environment variable is not set
42
+ *templates: One or more environment variable names or template strings.
43
+ When multiple are provided, they are checked in order (OR operation).
44
+ default: Default value if no environment variable is set
45
+ dtype: Optional type to convert the value to (overrides annotation inference)
38
46
  """
39
- self.template = template
47
+ # Store templates as a tuple for immutability
48
+ self.templates = tuple(t for t in templates if t is not None)
40
49
  self.default = default
50
+ self.dtype = dtype
41
51
  self._env_name = None
52
+ # For lazy loading - cached value
53
+ self._cached_value = None
54
+ self._is_cached = False
55
+
56
+ @property
57
+ def template(self):
58
+ """Backward compatibility: return first template or None."""
59
+ return self.templates[0] if self.templates else None
42
60
 
43
61
  def __matmul__(self, other: Any):
44
62
  """
45
- Support EnvVar @ "VAR_NAME" or EnvVar @ default_value syntax.
63
+ Support EnvVar @ "VAR_NAME" syntax, chainable for OR operation.
64
+
65
+ Examples:
66
+ EnvVar @ "VAR_NAME" # Single env var
67
+ EnvVar @ "VAR1" @ "VAR2" # OR: try VAR1, then VAR2
68
+ EnvVar @ "VAR1" @ "VAR2" | default # With fallback
69
+
70
+ Note: The | operator has lower precedence than @, so
71
+ `EnvVar @ "A" @ "B" | default` is parsed as `(EnvVar @ "A" @ "B") | default`.
46
72
 
47
73
  Args:
48
74
  other: Either an environment variable name (str) or a default value
@@ -51,11 +77,11 @@ class _EnvVar:
51
77
  New _EnvVar instance configured with the given parameter
52
78
  """
53
79
  if isinstance(other, str):
54
- # EnvVar @ "VAR_NAME" - treat as env var name
55
- return _EnvVar(template=other, default=None)
80
+ # EnvVar @ "VAR_NAME" - add to templates list (OR operation)
81
+ return _EnvVar(*self.templates, other, default=self.default, dtype=self.dtype)
56
82
  else:
57
83
  # EnvVar @ some_value - treat as default value
58
- return _EnvVar(template=None, default=other)
84
+ return _EnvVar(*self.templates, default=other, dtype=self.dtype)
59
85
 
60
86
  def __or__(self, other: Any):
61
87
  """
@@ -63,46 +89,56 @@ class _EnvVar:
63
89
 
64
90
  Syntax: EnvVar @ "VAR_NAME" | default_value
65
91
 
92
+ Note: Using `or` instead of `|` will NOT work because `or` has lower
93
+ precedence than @ and evaluates truthiness instead of calling __or__.
94
+
66
95
  Args:
67
96
  other: Default value to use if env var is not set
68
97
 
69
98
  Returns:
70
- New _EnvVar instance with both template and default
99
+ New _EnvVar instance with both template(s) and default
71
100
  """
72
- return _EnvVar(template=self.template, default=other)
101
+ return _EnvVar(*self.templates, default=other, dtype=self.dtype)
73
102
 
74
- def __call__(self, template: str, *, default: Any = None):
103
+ def __call__(self, *templates: str, default: Any = None, dtype: type = None):
75
104
  """
76
- Support EnvVar("VAR_NAME", default=...) function call syntax.
105
+ Support EnvVar("VAR_NAME", ..., default=...) function call syntax.
106
+
107
+ Examples:
108
+ EnvVar("DATABASE_URL", default="localhost")
109
+ EnvVar("API_KEY", "SECRET_KEY", default="fallback") # OR operation
110
+ EnvVar("PORT", dtype=int, default=8080) # With type conversion
77
111
 
78
112
  Args:
79
- template: Environment variable name or template string
80
- default: Default value if environment variable is not set
113
+ *templates: One or more environment variable names or template strings
114
+ default: Default value if no environment variable is set
115
+ dtype: Optional type to convert the value to
81
116
 
82
117
  Returns:
83
118
  New _EnvVar instance configured with the given parameters
84
119
  """
85
- return _EnvVar(template=template, default=default)
120
+ return _EnvVar(*templates, default=default, dtype=dtype)
86
121
 
87
- def get(self) -> Any:
122
+ def _resolve_single(self, name: str) -> tuple[Any, bool]:
88
123
  """
89
- Get the value from environment variable.
124
+ Resolve a single environment variable name/template.
125
+
126
+ Args:
127
+ name: Environment variable name or template string
90
128
 
91
129
  Returns:
92
- Value from environment or default
130
+ Tuple of (value, found) where found indicates if the env var was set
93
131
  """
94
- # Use only the explicitly set template
95
- # NO auto-inference for security reasons
96
- if not self.template:
97
- return self.default
98
-
99
- name = self.template
100
-
101
132
  # Handle template strings with $ prefix or ${} syntax
102
133
  if name.startswith("$") or "${" in name:
103
134
  # Parse and expand template
104
135
  vars_in_template = parse_env_template(name)
105
136
  if vars_in_template:
137
+ # Check if any variable in template is actually set
138
+ any_set = any(var in os.environ for var in vars_in_template)
139
+ if not any_set:
140
+ return None, False
141
+
106
142
  # Expansion for both $VAR and ${VAR} syntax
107
143
  expanded = name
108
144
  for var in vars_in_template:
@@ -110,14 +146,69 @@ class _EnvVar:
110
146
  # Replace both ${VAR} and $VAR forms
111
147
  expanded = expanded.replace(f"${{{var}}}", var_value)
112
148
  expanded = expanded.replace(f"${var}", var_value)
113
- return expanded if expanded != name else self.default
149
+ return expanded, True
114
150
 
115
151
  # Simple env var lookup
116
- return os.environ.get(name, self.default)
152
+ if name in os.environ:
153
+ return os.environ[name], True
154
+ return None, False
155
+
156
+ def get(self, *, lazy: bool = True) -> Any:
157
+ """
158
+ Get the value from environment variable(s).
159
+
160
+ When multiple templates are specified (OR operation), tries each in order
161
+ and returns the first one that is set in the environment.
162
+
163
+ Args:
164
+ lazy: If True (default), cache the result for subsequent calls.
165
+ Set to False to always re-read from environment.
166
+
167
+ Returns:
168
+ Value from environment or default, converted to dtype if specified
169
+ """
170
+ from params_proto.type_utils import _convert_type
171
+
172
+ # Return cached value if lazy loading is enabled and we have a cached value
173
+ if lazy and self._is_cached:
174
+ return self._cached_value
175
+
176
+ # No templates means return default
177
+ if not self.templates:
178
+ result = self.default
179
+ else:
180
+ # Try each template in order (OR operation)
181
+ result = None
182
+ for name in self.templates:
183
+ value, found = self._resolve_single(name)
184
+ if found:
185
+ result = value
186
+ break
187
+ else:
188
+ # No env var was found, use default
189
+ result = self.default
190
+
191
+ # Apply dtype conversion if specified and we have a value
192
+ if result is not None and self.dtype is not None:
193
+ result = _convert_type(result, self.dtype)
194
+
195
+ # Cache the result for lazy loading
196
+ if lazy:
197
+ self._cached_value = result
198
+ self._is_cached = True
199
+
200
+ return result
201
+
202
+ def invalidate_cache(self):
203
+ """Clear the cached value, forcing re-read from environment on next get()."""
204
+ self._cached_value = None
205
+ self._is_cached = False
117
206
 
118
207
  def __repr__(self):
119
- if self.template:
120
- return f"EnvVar({self.template!r}, default={self.default!r})"
208
+ if self.templates:
209
+ if len(self.templates) == 1:
210
+ return f"EnvVar({self.templates[0]!r}, default={self.default!r})"
211
+ return f"EnvVar({', '.join(repr(t) for t in self.templates)}, default={self.default!r})"
121
212
  return f"EnvVar(default={self.default!r})"
122
213
 
123
214
 
params_proto/proto.py CHANGED
@@ -755,6 +755,30 @@ def proto(
755
755
  klass_annotations = getattr(klass, "__annotations__", {})
756
756
  annotations.update(klass_annotations)
757
757
 
758
+ # Infer types for untyped class attributes from their default values
759
+ # This allows `untyped_attr = "hello"` to be treated as `untyped_attr: str = "hello"`
760
+ # For None defaults, use Any since NoneType isn't useful as a type hint
761
+ from typing import Any
762
+
763
+ for klass in reversed(obj.__mro__):
764
+ if klass is object:
765
+ continue
766
+ for name, value in vars(klass).items():
767
+ # Skip if already annotated, private/dunder, or is a method/descriptor
768
+ if name in annotations:
769
+ continue
770
+ if name.startswith("_"):
771
+ continue
772
+ if callable(value) and not (hasattr(value, "__class__") and value.__class__.__name__ == "_EnvVar"):
773
+ continue
774
+ if isinstance(value, (classmethod, staticmethod, property)):
775
+ continue
776
+ # Infer type from the default value, use Any for None
777
+ if value is None:
778
+ annotations[name] = Any
779
+ else:
780
+ annotations[name] = type(value)
781
+
758
782
  for name in annotations.keys():
759
783
  if hasattr(obj, name):
760
784
  value = getattr(obj, name)
@@ -1093,166 +1117,4 @@ def partial(config_class: Type, method: bool = False):
1093
1117
 
1094
1118
  proto.partial = partial
1095
1119
 
1096
-
1097
- class _EnvVar:
1098
- """
1099
- Environment variable reader that supports three syntaxes:
1100
-
1101
- 1. Matmul operator with env var name:
1102
- batch_size: int = EnvVar @ "BATCH_SIZE"
1103
-
1104
- 2. Matmul operator with pipe for default:
1105
- learning_rate: float = EnvVar @ "LR" | 0.001
1106
-
1107
- 3. Function call syntax:
1108
- db_url: str = EnvVar("DATABASE_URL", default="localhost")
1109
- data_dir: str = EnvVar("$DATA_DIR/models", default="/tmp/models")
1110
- port: int = EnvVar("PORT", dtype=int, default=8080)
1111
-
1112
- The pipe operator (|) allows clean chaining of env var name with fallback value.
1113
- """
1114
-
1115
- def __init__(self, template: str = None, *, default: Any = None, dtype: type = None):
1116
- """
1117
- Create an environment variable reader.
1118
-
1119
- Args:
1120
- template: Environment variable name or template string (e.g., "$VAR" or "VAR")
1121
- default: Default value if environment variable is not set
1122
- dtype: Optional type to convert the value to (overrides annotation inference)
1123
- """
1124
- self.template = template
1125
- self.default = default
1126
- self.dtype = dtype
1127
- self._env_name = None
1128
-
1129
- def __matmul__(self, other: Any):
1130
- """
1131
- Support EnvVar @ "VAR_NAME" or EnvVar @ default_value syntax.
1132
-
1133
- Args:
1134
- other: Either an environment variable name (str) or a default value
1135
-
1136
- Returns:
1137
- New _EnvVar instance configured with the given parameter
1138
- """
1139
- if isinstance(other, str):
1140
- # EnvVar @ "VAR_NAME" - treat as env var name
1141
- return _EnvVar(template=other, default=None)
1142
- else:
1143
- # EnvVar @ some_value - treat as default value
1144
- return _EnvVar(template=None, default=other)
1145
-
1146
- def __or__(self, other: Any):
1147
- """
1148
- Support chaining with | to specify default value.
1149
-
1150
- Syntax: EnvVar @ "VAR_NAME" | default_value
1151
-
1152
- Args:
1153
- other: Default value to use if env var is not set
1154
-
1155
- Returns:
1156
- New _EnvVar instance with both template and default
1157
- """
1158
- return _EnvVar(template=self.template, default=other, dtype=self.dtype)
1159
-
1160
- def __call__(self, template: str, *, default: Any = None, dtype: type = None):
1161
- """
1162
- Support EnvVar("VAR_NAME", default=..., dtype=...) function call syntax.
1163
-
1164
- Args:
1165
- template: Environment variable name or template string
1166
- default: Default value if environment variable is not set
1167
- dtype: Optional type to convert the value to
1168
-
1169
- Returns:
1170
- New _EnvVar instance configured with the given parameters
1171
- """
1172
- return _EnvVar(template=template, default=default, dtype=dtype)
1173
-
1174
- def get(self) -> Any:
1175
- """
1176
- Get the value from environment variable.
1177
-
1178
- Returns:
1179
- Value from environment or default, converted to dtype if specified
1180
- """
1181
- import os
1182
-
1183
- from params_proto.parse_env_template import parse_env_template
1184
- from params_proto.type_utils import _convert_type
1185
-
1186
- # Use only the explicitly set template
1187
- # NO auto-inference for security reasons
1188
- if not self.template:
1189
- return self.default
1190
-
1191
- name = self.template
1192
-
1193
- # Handle template strings with $ prefix or ${} syntax
1194
- if name.startswith("$") or "${" in name:
1195
- # Parse and expand template
1196
- vars_in_template = parse_env_template(name)
1197
- if vars_in_template:
1198
- # Expansion for both $VAR and ${VAR} syntax
1199
- expanded = name
1200
- for var in vars_in_template:
1201
- var_value = os.environ.get(var, "")
1202
- # Replace both ${VAR} and $VAR forms
1203
- expanded = expanded.replace(f"${{{var}}}", var_value)
1204
- expanded = expanded.replace(f"${var}", var_value)
1205
- value = expanded if expanded != name else self.default
1206
- # Apply dtype conversion if specified
1207
- if value is not None and self.dtype is not None:
1208
- return _convert_type(value, self.dtype)
1209
- return value
1210
-
1211
- # Simple env var lookup
1212
- value = os.environ.get(name)
1213
- if value is not None:
1214
- # Apply dtype conversion if specified
1215
- if self.dtype is not None:
1216
- return _convert_type(value, self.dtype)
1217
- return value
1218
- return self.default
1219
-
1220
- def __repr__(self):
1221
- if self.template:
1222
- return f"EnvVar({self.template!r}, default={self.default!r})"
1223
- return f"EnvVar(default={self.default!r})"
1224
-
1225
-
1226
- # Create singleton instance for @ syntax, but also keep class available
1227
- EnvVar = _EnvVar()
1228
-
1229
-
1230
- def get_var(default: Any = None, *, env: str = None):
1231
- """
1232
- Mark a field as an environment variable.
1233
-
1234
- Args:
1235
- default: Default value if env var not set
1236
- env: Environment variable name (defaults to field name)
1237
- """
1238
- import os
1239
-
1240
- def getter():
1241
- env_name = env or "FIELD_NAME" # Will be replaced at decoration time
1242
- return os.environ.get(env_name, default)
1243
-
1244
- return getter
1245
-
1246
-
1247
- def Field(fn: Callable):
1248
- """
1249
- Decorator to mark a method as a computed field.
1250
-
1251
- Args:
1252
- fn: The method to decorate
1253
-
1254
- Returns:
1255
- The decorated method
1256
- """
1257
- fn._is_field = True
1258
- return fn
1120
+ # EnvVar, get_var, and Field are now imported from params_proto.envvar
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: params-proto
3
- Version: 3.0.0rc25
3
+ Version: 3.0.0rc27
4
4
  Summary: Modern Hyper Parameter Management for Machine Learning
5
5
  Project-URL: Homepage, https://github.com/geyang/params-proto
6
6
  Project-URL: Documentation, https://params-proto.readthedocs.io
@@ -119,7 +119,7 @@ uv run python scratch/demo_v3.py 42
119
119
  ## Installation
120
120
 
121
121
  ```bash
122
- pip install params-proto==3.0.0-rc7
122
+ pip install params-proto==3.0.0-rc25
123
123
  ```
124
124
 
125
125
  ## Key Features
@@ -1,9 +1,9 @@
1
- params_proto/__init__.py,sha256=3TxfXQRi2wHNfNjD-pKcPO6xyAC5j2RY24VH3oul1hg,95
1
+ params_proto/__init__.py,sha256=JJfjHf6dpkNpLDGOyH0KBWFo44nQoxSIzmwfGZb36ZU,114
2
2
  params_proto/app.py,sha256=UySpd1op3M44Szk6Ekyn0fJcnZsQvMTMPdaEybwWsLE,19
3
3
  params_proto/documentation.py,sha256=mIqmcwGWo8tM1BuNzLIwVTzdbQ3qyPus7yWTaOce4dM,8091
4
- params_proto/envvar.py,sha256=Gui4AJGPiBSDSyIC3KjmXGmewFmgihAF0QEOm-ufFVo,4323
4
+ params_proto/envvar.py,sha256=PxSgNkk-GHksMiAYhRozSjCX3aCinfaPfCpuhuEbeho,7964
5
5
  params_proto/parse_env_template.py,sha256=mXTvKpNhT2jGr3HpwKw42shd18O0QACmSJn6yWMDdKA,1298
6
- params_proto/proto.py,sha256=oMTHLNtAMgU7qWLNOSjssGEUP0il4mt4rWdmUwfzEBg,40898
6
+ params_proto/proto.py,sha256=h-igiykTm1AwJrwUFa6rJaLZ3u4-8p6fAE7k_4hTo5w,37117
7
7
  params_proto/type_utils.py,sha256=x68rL5m76ZFRKsCRgH_i_4vLpt6ldWEsEAalgacFIH8,7364
8
8
  params_proto/cli/__init__.py,sha256=sLpN3GmaBqd_d0J0nvUNOeGlV74_-jQGW0nDUU34tjA,493
9
9
  params_proto/cli/ansi_help.py,sha256=-1gzbvOpi9GjPlqgiINOYQAfIstzg0-ukv1se88TYCQ,10967
@@ -20,7 +20,7 @@ params_proto/v2/hyper.py,sha256=onBAkT8Ja8IkeHEOq1AwCdTuBzAnthIe766ZE0lAy-M,1146
20
20
  params_proto/v2/partial.py,sha256=_ovi4NY8goYgHurfYt1OV0E9DSMXGYucjMVIyG1Q_xc,983
21
21
  params_proto/v2/proto.py,sha256=KvinzgzwRQr2bHDNtrU7App2kgAyB-SEfBe4SNYceh0,18995
22
22
  params_proto/v2/utils.py,sha256=5EWvwboZDTsCYfzSED_J6RVFyNLIlf95nIu4p_ZSVxA,3540
23
- params_proto-3.0.0rc25.dist-info/METADATA,sha256=4e6-lb-i_mmv-p53IlmqyBKW9-1V8EPassIBo4ZHVik,8994
24
- params_proto-3.0.0rc25.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
- params_proto-3.0.0rc25.dist-info/licenses/LICENSE.md,sha256=c2qSYi9tUMZtzj9SEsMeKhub5LJUmHwBtDLiIMM5b6U,1526
26
- params_proto-3.0.0rc25.dist-info/RECORD,,
23
+ params_proto-3.0.0rc27.dist-info/METADATA,sha256=LOsTpFkjezao5QzAyBeoo0uyHTbk9hcwbjudxu5-n5Q,8995
24
+ params_proto-3.0.0rc27.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
+ params_proto-3.0.0rc27.dist-info/licenses/LICENSE.md,sha256=c2qSYi9tUMZtzj9SEsMeKhub5LJUmHwBtDLiIMM5b6U,1526
26
+ params_proto-3.0.0rc27.dist-info/RECORD,,