params-proto 3.1.0__tar.gz → 3.1.1__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.
Files changed (129) hide show
  1. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/workspace.xml +2 -1
  2. {params_proto-3.1.0 → params_proto-3.1.1}/PKG-INFO +1 -1
  3. {params_proto-3.1.0 → params_proto-3.1.1}/docs/release_notes.md +21 -4
  4. {params_proto-3.1.0 → params_proto-3.1.1}/pyproject.toml +1 -1
  5. {params_proto-3.1.0 → params_proto-3.1.1}/skills/params-proto/SKILL.md +2 -2
  6. params_proto-3.1.1/skills/params-proto.skill +0 -0
  7. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/envvar.py +32 -18
  8. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/proto.py +18 -3
  9. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_proto_envvar.py +145 -39
  10. params_proto-3.1.0/skills/params-proto.skill +0 -0
  11. {params_proto-3.1.0 → params_proto-3.1.1}/.claude-plugin/marketplace.json +0 -0
  12. {params_proto-3.1.0 → params_proto-3.1.1}/.claude-plugin/plugin.json +0 -0
  13. {params_proto-3.1.0 → params_proto-3.1.1}/.editorconfig +0 -0
  14. {params_proto-3.1.0 → params_proto-3.1.1}/.gitignore +0 -0
  15. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/.gitignore +0 -0
  16. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/codeStyles/codeStyleConfig.xml +0 -0
  17. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/inspectionProfiles/Project_Default.xml +0 -0
  18. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  19. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/markdown.xml +0 -0
  20. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/misc.xml +0 -0
  21. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/modules.xml +0 -0
  22. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/params-proto.iml +0 -0
  23. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/ruff.xml +0 -0
  24. {params_proto-3.1.0 → params_proto-3.1.1}/.idea/vcs.xml +0 -0
  25. {params_proto-3.1.0 → params_proto-3.1.1}/.readthedocs.yaml +0 -0
  26. {params_proto-3.1.0 → params_proto-3.1.1}/.run/pytest for test_neo_proto_cli.run.xml +0 -0
  27. {params_proto-3.1.0 → params_proto-3.1.1}/.run/pytest in test_params_proto.run.xml +0 -0
  28. {params_proto-3.1.0 → params_proto-3.1.1}/ANSI_HELP_CONSIDERATIONS.md +0 -0
  29. {params_proto-3.1.0 → params_proto-3.1.1}/CLAUDE.md +0 -0
  30. {params_proto-3.1.0 → params_proto-3.1.1}/LICENSE.md +0 -0
  31. {params_proto-3.1.0 → params_proto-3.1.1}/Makefile +0 -0
  32. {params_proto-3.1.0 → params_proto-3.1.1}/README +0 -0
  33. {params_proto-3.1.0 → params_proto-3.1.1}/README.md +0 -0
  34. {params_proto-3.1.0 → params_proto-3.1.1}/docs/Makefile +0 -0
  35. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/api/hyper.md +0 -0
  36. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/api/proto.md +0 -0
  37. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/api/utils.md +0 -0
  38. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/examples/advanced_features.md +0 -0
  39. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/examples/basic_usage.md +0 -0
  40. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/examples/environment_variables.md +0 -0
  41. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/examples/hyperparameter_sweeps.md +0 -0
  42. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/examples/index.md +0 -0
  43. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/examples/nested_configs.md +0 -0
  44. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_archive_v2/quick_start.md +0 -0
  45. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_static/ansi.css +0 -0
  46. {params_proto-3.1.0 → params_proto-3.1.1}/docs/_static/custom.css +0 -0
  47. {params_proto-3.1.0 → params_proto-3.1.1}/docs/api/index.md +0 -0
  48. {params_proto-3.1.0 → params_proto-3.1.1}/docs/api/proto.md +0 -0
  49. {params_proto-3.1.0 → params_proto-3.1.1}/docs/conf.py +0 -0
  50. {params_proto-3.1.0 → params_proto-3.1.1}/docs/examples/basic_usage.md +0 -0
  51. {params_proto-3.1.0 → params_proto-3.1.1}/docs/examples/cli_applications.md +0 -0
  52. {params_proto-3.1.0 → params_proto-3.1.1}/docs/examples/ml_training.md +0 -0
  53. {params_proto-3.1.0 → params_proto-3.1.1}/docs/examples/rl_agent.md +0 -0
  54. {params_proto-3.1.0 → params_proto-3.1.1}/docs/index.md +0 -0
  55. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/advanced_patterns.md +0 -0
  56. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/ansi_formatting.md +0 -0
  57. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/cli-fundamentals.md +0 -0
  58. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/cli-patterns.md +0 -0
  59. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/configuration-patterns.md +0 -0
  60. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/core-concepts.md +0 -0
  61. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/environment_variables.md +0 -0
  62. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/help-generation.md +0 -0
  63. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/hyperparameter_sweeps.md +0 -0
  64. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/naming-conventions.md +0 -0
  65. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/parameter-iteration.md +0 -0
  66. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/parameter-overrides.md +0 -0
  67. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/type-system.md +0 -0
  68. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/union_types.md +0 -0
  69. {params_proto-3.1.0 → params_proto-3.1.1}/docs/key_concepts/welcome.md +0 -0
  70. {params_proto-3.1.0 → params_proto-3.1.1}/docs/migration.md +0 -0
  71. {params_proto-3.1.0 → params_proto-3.1.1}/docs/quick_start.md +0 -0
  72. {params_proto-3.1.0 → params_proto-3.1.1}/docs/requirements.txt +0 -0
  73. {params_proto-3.1.0 → params_proto-3.1.1}/examples/union_subcommands.py +0 -0
  74. {params_proto-3.1.0 → params_proto-3.1.1}/figures/man-page.png +0 -0
  75. {params_proto-3.1.0 → params_proto-3.1.1}/figures/params-proto-autocompletion.gif +0 -0
  76. {params_proto-3.1.0 → params_proto-3.1.1}/figures/spec_files.png +0 -0
  77. {params_proto-3.1.0 → params_proto-3.1.1}/scratch/demo_params_proto.py +0 -0
  78. {params_proto-3.1.0 → params_proto-3.1.1}/scratch/demo_v3.py +0 -0
  79. {params_proto-3.1.0 → params_proto-3.1.1}/scratch/proto_DAT_scratch.py +0 -0
  80. {params_proto-3.1.0 → params_proto-3.1.1}/scratch/proto_dependency_tree_pattern.py +0 -0
  81. {params_proto-3.1.0 → params_proto-3.1.1}/skills/params-proto/references/cli-and-types.md +0 -0
  82. {params_proto-3.1.0 → params_proto-3.1.1}/skills/params-proto/references/environment-vars.md +0 -0
  83. {params_proto-3.1.0 → params_proto-3.1.1}/skills/params-proto/references/patterns.md +0 -0
  84. {params_proto-3.1.0 → params_proto-3.1.1}/skills/params-proto/references/sweeps.md +0 -0
  85. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/__init__.py +0 -0
  86. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/app.py +0 -0
  87. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/cli/__init__.py +0 -0
  88. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/cli/ansi_help.py +0 -0
  89. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/cli/cli_parse.py +0 -0
  90. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/cli/help_gen.py +0 -0
  91. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/documentation.py +0 -0
  92. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/hyper/__init__.py +0 -0
  93. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/hyper/proxies.py +0 -0
  94. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/hyper/sweep.py +0 -0
  95. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/parse_env_template.py +0 -0
  96. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/type_utils.py +0 -0
  97. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v1/__init__.py +0 -0
  98. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v1/hyper.py +0 -0
  99. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v1/params_proto.py +0 -0
  100. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v2/__init__.py +0 -0
  101. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v2/hyper.py +0 -0
  102. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v2/partial.py +0 -0
  103. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v2/proto.py +0 -0
  104. {params_proto-3.1.0 → params_proto-3.1.1}/src/params_proto/v2/utils.py +0 -0
  105. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v1/__init__.py +0 -0
  106. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v1/test_hyper.py +0 -0
  107. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v1/test_params_proto.py +0 -0
  108. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v2/test_Eval.py +0 -0
  109. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v2/test_neo_hyper.py +0 -0
  110. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v2/test_neo_proto.py +0 -0
  111. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v2/test_neo_proto_cli.py +0 -0
  112. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v2/test_neo_proto_partial.py +0 -0
  113. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v2/test_utils.py +0 -0
  114. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/samples/train.py +0 -0
  115. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_advanced_types.py +0 -0
  116. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_class_level_methods.py +0 -0
  117. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_cli_parsing.py +0 -0
  118. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_help_strings.py +0 -0
  119. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_method_self_param.py +0 -0
  120. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_parse_env_template.py +0 -0
  121. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_piter.py +0 -0
  122. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_positional_example.sh +0 -0
  123. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_proto_comments.py +0 -0
  124. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_proto_core.py +0 -0
  125. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_proto_linebreaking.py +0 -0
  126. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_proto_partial.py +0 -0
  127. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_proto_required.py +0 -0
  128. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_strings.py +0 -0
  129. {params_proto-3.1.0 → params_proto-3.1.1}/tests/test_v3/test_sweep.py +0 -0
@@ -32,6 +32,7 @@
32
32
  </component>
33
33
  <component name="HighlightingSettingsPerFile">
34
34
  <setting file="file://$USER_HOME$/Library/Caches/JetBrains/PyCharm2025.2/python_stubs/-1979844046/_typing.py" root0="SKIP_INSPECTION" />
35
+ <setting file="file://$PROJECT_DIR$/pyproject.toml" root0="SKIP_INSPECTION" />
35
36
  </component>
36
37
  <component name="KubernetesApiPersistence">{}</component>
37
38
  <component name="KubernetesApiProvider">{
@@ -253,7 +254,7 @@
253
254
  <workItem from="1767857192117" duration="3689000" />
254
255
  <workItem from="1768126847510" duration="6729000" />
255
256
  <workItem from="1768382679094" duration="3000" />
256
- <workItem from="1769135684123" duration="659000" />
257
+ <workItem from="1769135684123" duration="4247000" />
257
258
  </task>
258
259
  <task id="LOCAL-00001" summary="add design specs">
259
260
  <option name="closed" value="true" />
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: params-proto
3
- Version: 3.1.0
3
+ Version: 3.1.1
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
@@ -2,6 +2,24 @@
2
2
 
3
3
  This page contains the release history and changelog for params-proto.
4
4
 
5
+ ## Version 3.1.1 (2025-01-25)
6
+
7
+ ### 🐛 Bug Fixes
8
+
9
+ - **EnvVar Descriptor Protocol**: EnvVar now works in plain classes without `@proto` decorator
10
+ - Previously `EnvVar @ "VAR" | default` returned `_EnvVar` object in plain classes
11
+ - Now auto-resolves via `__get__` descriptor when accessed as class attribute
12
+ - Fixes: `AttributeError: '_EnvVar' object has no attribute 'decode'`
13
+
14
+ ### ♻️ API Changes
15
+
16
+ - **Removed `.get()` method**: Use class attribute access instead
17
+ - Old: `EnvVar("PORT", dtype=int).get()`
18
+ - New: `class C: port = EnvVar("PORT", dtype=int)` then `C.port`
19
+ - Use `invalidate_cache()` to force re-read from environment
20
+
21
+ ---
22
+
5
23
  ## Version 3.1.0 (2025-01-23)
6
24
 
7
25
  ### ✨ Features
@@ -34,10 +52,9 @@ This page contains the release history and changelog for params-proto.
34
52
  - Function syntax: `EnvVar("PRIMARY", "FALLBACK", default="value")`
35
53
  - Returns first env var that is set, or default if none are set
36
54
 
37
- - **EnvVar Lazy Loading**: Environment variables are cached after first read
38
- - `ev.get()` caches result for subsequent calls
39
- - `ev.get(lazy=False)` forces re-read from environment
40
- - `ev.invalidate_cache()` clears cached value
55
+ - **EnvVar Lazy Loading**: Environment variables are cached after first access
56
+ - Values cached after first access via descriptor
57
+ - `invalidate_cache()` clears cached value for re-read
41
58
 
42
59
  ### 🐛 Bug Fixes
43
60
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "params-proto"
3
- version = "3.1.0"
3
+ version = "3.1.1"
4
4
  description = "Modern Hyper Parameter Management for Machine Learning"
5
5
  authors = [
6
6
  { name = "Ge Yang" }
@@ -10,14 +10,14 @@ description: |
10
10
  (6) Work with Union types for subcommand-like CLI patterns
11
11
  ---
12
12
 
13
- # params-proto v3.1.0
13
+ # params-proto v3.1.1
14
14
 
15
15
  Declarative hyperparameter management for ML experiments with automatic CLI generation.
16
16
 
17
17
  ## Installation
18
18
 
19
19
  ```bash
20
- pip install params-proto==3.1.0
20
+ pip install params-proto==3.1.1
21
21
  ```
22
22
 
23
23
  ## Three Decorators
@@ -153,24 +153,44 @@ class _EnvVar:
153
153
  return os.environ[name], True
154
154
  return None, False
155
155
 
156
- def get(self, *, lazy: bool = True) -> Any:
156
+ def invalidate_cache(self):
157
+ """Clear the cached value, forcing re-read from environment on next access."""
158
+ self._cached_value = None
159
+ self._is_cached = False
160
+
161
+ def __get__(self, obj, objtype=None):
157
162
  """
158
- Get the value from environment variable(s).
163
+ Descriptor protocol: auto-resolve EnvVar when accessed as a class attribute.
164
+
165
+ This enables EnvVar to work in plain classes (not decorated with @ParamsProto):
166
+
167
+ class MyConfig:
168
+ api_key: str = EnvVar @ "API_KEY" | "default"
169
+
170
+ # Accessing MyConfig.api_key returns the resolved value, not the _EnvVar object
171
+ print(MyConfig.api_key) # "default" or value of $API_KEY
159
172
 
160
- When multiple templates are specified (OR operation), tries each in order
161
- and returns the first one that is set in the environment.
173
+ Values are cached after first resolution. Use invalidate_cache() to force re-read:
174
+
175
+ envvar = MyConfig.__dict__['api_key'] # Get the _EnvVar object
176
+ envvar.invalidate_cache()
177
+ print(MyConfig.api_key) # Re-reads from environment
162
178
 
163
179
  Args:
164
- lazy: If True (default), cache the result for subsequent calls.
165
- Set to False to always re-read from environment.
180
+ obj: Instance (None if accessed on class)
181
+ objtype: The class being accessed
166
182
 
167
183
  Returns:
168
- Value from environment or default, converted to dtype if specified
184
+ The resolved environment variable value
169
185
  """
170
186
  from params_proto.type_utils import _convert_type
171
187
 
172
- # Return cached value if lazy loading is enabled and we have a cached value
173
- if lazy and self._is_cached:
188
+ # Don't resolve the singleton EnvVar instance itself (has no templates)
189
+ if not self.templates:
190
+ return self
191
+
192
+ # Return cached value if available
193
+ if self._is_cached:
174
194
  return self._cached_value
175
195
 
176
196
  # No templates means return default
@@ -192,18 +212,12 @@ class _EnvVar:
192
212
  if result is not None and self.dtype is not None:
193
213
  result = _convert_type(result, self.dtype)
194
214
 
195
- # Cache the result for lazy loading
196
- if lazy:
197
- self._cached_value = result
198
- self._is_cached = True
215
+ # Cache the result
216
+ self._cached_value = result
217
+ self._is_cached = True
199
218
 
200
219
  return result
201
220
 
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
206
-
207
221
  def __repr__(self):
208
222
  if self.templates:
209
223
  if len(self.templates) == 1:
@@ -177,7 +177,7 @@ class ProtoWrapper:
177
177
  if is_env_var:
178
178
  # Resolve env var at decoration time
179
179
  # NO auto-inference from parameter name for security reasons
180
- env_value = default.get()
180
+ env_value = default.__get__(None, None)
181
181
 
182
182
  # Apply type conversion based on dtype (if provided) or annotation
183
183
  if env_value is not None:
@@ -801,7 +801,17 @@ def proto(
801
801
 
802
802
  for name in annotations.keys():
803
803
  if hasattr(obj, name):
804
- value = getattr(obj, name)
804
+ # Look up value in class dict hierarchy to bypass descriptors like _EnvVar.__get__
805
+ value = None
806
+ for klass in obj.__mro__:
807
+ if klass is object:
808
+ continue
809
+ if name in vars(klass):
810
+ value = vars(klass)[name]
811
+ break
812
+ else:
813
+ value = getattr(obj, name)
814
+
805
815
  # Check for EnvVar first (before callable check, since _EnvVar has __call__)
806
816
  is_env_var = (
807
817
  hasattr(value, "__class__") and value.__class__.__name__ == "_EnvVar"
@@ -809,7 +819,7 @@ def proto(
809
819
 
810
820
  if is_env_var:
811
821
  # Resolve env var at decoration time
812
- env_value = value.get()
822
+ env_value = value.__get__(None, None)
813
823
  annotation = annotations.get(name, str)
814
824
 
815
825
  # Apply type conversion based on dtype (if provided) or annotation
@@ -848,6 +858,11 @@ def proto(
848
858
  except AttributeError:
849
859
  pass
850
860
 
861
+ # Replace _EnvVar objects with resolved values from defaults
862
+ # This ensures the descriptor doesn't interfere with class attribute access
863
+ for key, value in defaults.items():
864
+ namespace[key] = value
865
+
851
866
  # Create new class with metaclass
852
867
  new_cls = metaclass(
853
868
  obj.__name__,
@@ -445,11 +445,11 @@ def test_proto_envvar_class_with_type_conversion():
445
445
  del os.environ["RATIO"]
446
446
 
447
447
 
448
- def test_envvar_get_with_dtype():
449
- """Test EnvVar.get() applies dtype conversion correctly.
448
+ def test_envvar_dtype_conversion():
449
+ """Test EnvVar applies dtype conversion correctly via descriptor.
450
450
 
451
- This tests the direct .get() method, not via @proto decoration.
452
- Previously, dtype was stored but not applied in .get().
451
+ Tests that dtype parameter works for type conversion when accessed
452
+ as a class attribute (via __get__ descriptor).
453
453
  """
454
454
  import os
455
455
 
@@ -463,37 +463,51 @@ def test_envvar_get_with_dtype():
463
463
 
464
464
  try:
465
465
  # Test int conversion
466
- port = EnvVar("PORT", dtype=int, default=8012).get()
467
- assert port == 9000, f"Expected 9000, got {port}"
468
- assert isinstance(port, int), f"Expected int, got {type(port)}"
466
+ class IntConfig:
467
+ port = EnvVar("PORT", dtype=int, default=8012)
468
+
469
+ assert IntConfig.port == 9000, f"Expected 9000, got {IntConfig.port}"
470
+ assert isinstance(IntConfig.port, int), f"Expected int, got {type(IntConfig.port)}"
469
471
 
470
472
  # Test float conversion
471
- threshold = EnvVar("THRESHOLD", dtype=float, default=0.5).get()
472
- assert threshold == 0.75, f"Expected 0.75, got {threshold}"
473
- assert isinstance(threshold, float), f"Expected float, got {type(threshold)}"
473
+ class FloatConfig:
474
+ threshold = EnvVar("THRESHOLD", dtype=float, default=0.5)
475
+
476
+ assert FloatConfig.threshold == 0.75, f"Expected 0.75, got {FloatConfig.threshold}"
477
+ assert isinstance(FloatConfig.threshold, float), f"Expected float, got {type(FloatConfig.threshold)}"
474
478
 
475
479
  # Test bool conversion - "true"
476
- debug = EnvVar("DEBUG", dtype=bool, default=False).get()
477
- assert debug is True, f"Expected True, got {debug}"
478
- assert isinstance(debug, bool), f"Expected bool, got {type(debug)}"
480
+ class BoolConfig:
481
+ debug = EnvVar("DEBUG", dtype=bool, default=False)
482
+
483
+ assert BoolConfig.debug is True, f"Expected True, got {BoolConfig.debug}"
484
+ assert isinstance(BoolConfig.debug, bool), f"Expected bool, got {type(BoolConfig.debug)}"
479
485
 
480
486
  # Test bool conversion - "1"
481
- enabled = EnvVar("ENABLED", dtype=bool, default=False).get()
482
- assert enabled is True, f"Expected True, got {enabled}"
487
+ class EnabledConfig:
488
+ enabled = EnvVar("ENABLED", dtype=bool, default=False)
489
+
490
+ assert EnabledConfig.enabled is True, f"Expected True, got {EnabledConfig.enabled}"
483
491
 
484
492
  # Test bool conversion - "false"
485
- disabled = EnvVar("DISABLED", dtype=bool, default=True).get()
486
- assert disabled is False, f"Expected False, got {disabled}"
493
+ class DisabledConfig:
494
+ disabled = EnvVar("DISABLED", dtype=bool, default=True)
495
+
496
+ assert DisabledConfig.disabled is False, f"Expected False, got {DisabledConfig.disabled}"
487
497
 
488
498
  # Test default value when env var not set
489
- missing = EnvVar("MISSING_VAR", dtype=int, default=42).get()
490
- assert missing == 42, f"Expected 42 (default), got {missing}"
491
- assert isinstance(missing, int), f"Expected int, got {type(missing)}"
499
+ class MissingConfig:
500
+ missing = EnvVar("MISSING_VAR", dtype=int, default=42)
501
+
502
+ assert MissingConfig.missing == 42, f"Expected 42 (default), got {MissingConfig.missing}"
503
+ assert isinstance(MissingConfig.missing, int), f"Expected int, got {type(MissingConfig.missing)}"
492
504
 
493
505
  # Test without dtype - should return string
494
- port_str = EnvVar("PORT", default="8012").get()
495
- assert port_str == "9000", f"Expected '9000', got {port_str}"
496
- assert isinstance(port_str, str), f"Expected str, got {type(port_str)}"
506
+ class StrConfig:
507
+ port_str = EnvVar("PORT", default="8012")
508
+
509
+ assert StrConfig.port_str == "9000", f"Expected '9000', got {StrConfig.port_str}"
510
+ assert isinstance(StrConfig.port_str, str), f"Expected str, got {type(StrConfig.port_str)}"
497
511
 
498
512
  finally:
499
513
  del os.environ["PORT"]
@@ -503,8 +517,8 @@ def test_envvar_get_with_dtype():
503
517
  del os.environ["DISABLED"]
504
518
 
505
519
 
506
- def test_envvar_get_with_dtype_template():
507
- """Test EnvVar.get() applies dtype with template syntax."""
520
+ def test_envvar_dtype_with_template():
521
+ """Test EnvVar applies dtype with template syntax."""
508
522
  import os
509
523
 
510
524
  from params_proto import EnvVar
@@ -513,9 +527,11 @@ def test_envvar_get_with_dtype_template():
513
527
 
514
528
  try:
515
529
  # Test with $ prefix template
516
- count = EnvVar("$COUNT", dtype=int, default=0).get()
517
- assert count == 100, f"Expected 100, got {count}"
518
- assert isinstance(count, int), f"Expected int, got {type(count)}"
530
+ class TemplateConfig:
531
+ count = EnvVar("$COUNT", dtype=int, default=0)
532
+
533
+ assert TemplateConfig.count == 100, f"Expected 100, got {TemplateConfig.count}"
534
+ assert isinstance(TemplateConfig.count, int), f"Expected int, got {type(TemplateConfig.count)}"
519
535
 
520
536
  finally:
521
537
  del os.environ["COUNT"]
@@ -795,27 +811,117 @@ def test_envvar_lazy_loading():
795
811
  os.environ.pop("LAZY_TEST_VAR", None)
796
812
 
797
813
  try:
798
- # Create EnvVar without env var set
799
- ev = EnvVar @ "LAZY_TEST_VAR" | "default"
814
+ # Create class with EnvVar without env var set
815
+ class LazyConfig:
816
+ value = EnvVar @ "LAZY_TEST_VAR" | "default"
800
817
 
801
- # First call - should return default
802
- assert ev.get() == "default", f"Expected 'default', got {ev.get()}"
818
+ # First access - should return default
819
+ assert LazyConfig.value == "default", f"Expected 'default', got {LazyConfig.value}"
803
820
 
804
- # Set env var after EnvVar was created
821
+ # Set env var after class was defined
805
822
  os.environ["LAZY_TEST_VAR"] = "lazy_value"
806
823
 
807
824
  # Cached value should still be default (lazy loading)
808
- assert ev.get() == "default", f"Expected cached 'default', got {ev.get()}"
825
+ assert LazyConfig.value == "default", f"Expected cached 'default', got {LazyConfig.value}"
809
826
 
810
- # Invalidate cache
811
- ev.invalidate_cache()
827
+ # Invalidate cache via class __dict__
828
+ LazyConfig.__dict__["value"].invalidate_cache()
812
829
 
813
830
  # Now should read the new value
814
- assert ev.get() == "lazy_value", f"Expected 'lazy_value', got {ev.get()}"
831
+ assert LazyConfig.value == "lazy_value", f"Expected 'lazy_value', got {LazyConfig.value}"
815
832
 
816
- # Test non-lazy mode
833
+ # Test invalidate and re-read with updated value
817
834
  os.environ["LAZY_TEST_VAR"] = "updated_value"
818
- assert ev.get(lazy=False) == "updated_value", f"Expected 'updated_value', got {ev.get(lazy=False)}"
835
+ LazyConfig.__dict__["value"].invalidate_cache()
836
+ assert LazyConfig.value == "updated_value", f"Expected 'updated_value', got {LazyConfig.value}"
819
837
 
820
838
  finally:
821
839
  os.environ.pop("LAZY_TEST_VAR", None)
840
+
841
+
842
+ def test_envvar_descriptor_in_plain_class():
843
+ """Test EnvVar auto-resolves in plain classes (without @proto decorator).
844
+
845
+ This is a critical feature for users who want to use EnvVar in classes
846
+ that are not decorated with @proto, such as:
847
+
848
+ class VuerClient:
849
+ URI: str = EnvVar @ "VUER_CLIENT_URI" | "ws://localhost:8012"
850
+
851
+ Without the descriptor protocol, accessing VuerClient.URI would return
852
+ an _EnvVar object instead of the resolved value.
853
+ """
854
+ import os
855
+
856
+ from params_proto import EnvVar
857
+
858
+ # Clean up
859
+ os.environ.pop("PLAIN_CLASS_VAR", None)
860
+ os.environ.pop("PLAIN_CLASS_PORT", None)
861
+
862
+ try:
863
+ # Test 1: Plain class with EnvVar and default (env var NOT set)
864
+ class PlainConfig:
865
+ uri: str = EnvVar @ "PLAIN_CLASS_VAR" | "ws://localhost:8012"
866
+ port: int = EnvVar @ "PLAIN_CLASS_PORT" | 8080
867
+
868
+ # Accessing class attribute should return resolved value, not _EnvVar
869
+ assert PlainConfig.uri == "ws://localhost:8012", f"Expected default, got {PlainConfig.uri}"
870
+ assert isinstance(PlainConfig.uri, str), f"Expected str, got {type(PlainConfig.uri)}"
871
+
872
+ # Note: Without @proto, type conversion doesn't happen
873
+ # (the int annotation is not processed)
874
+ assert PlainConfig.port == 8080, f"Expected 8080, got {PlainConfig.port}"
875
+
876
+ # Test 2: With env var set
877
+ os.environ["PLAIN_CLASS_VAR"] = "ws://192.168.1.1:9000"
878
+
879
+ class PlainConfig2:
880
+ uri: str = EnvVar @ "PLAIN_CLASS_VAR" | "ws://localhost:8012"
881
+
882
+ assert PlainConfig2.uri == "ws://192.168.1.1:9000", f"Expected env value, got {PlainConfig2.uri}"
883
+
884
+ # Test 3: Instance access also works
885
+ c = PlainConfig2()
886
+ assert c.uri == "ws://192.168.1.1:9000", f"Expected env value on instance, got {c.uri}"
887
+
888
+ finally:
889
+ os.environ.pop("PLAIN_CLASS_VAR", None)
890
+ os.environ.pop("PLAIN_CLASS_PORT", None)
891
+
892
+
893
+ def test_envvar_descriptor_websocket_max_size():
894
+ """Test EnvVar works for websocket max size pattern (the bug report case).
895
+
896
+ This tests the exact pattern that caused the original bug:
897
+ WEBSOCKET_MAX_SIZE: int = EnvVar @ "WEBSOCKET_MAX_SIZE" | 2**28
898
+
899
+ The issue was that accessing WEBSOCKET_MAX_SIZE returned an _EnvVar object
900
+ instead of the int value, causing: AttributeError: '_EnvVar' object has no attribute 'decode'
901
+ """
902
+ import os
903
+
904
+ from params_proto import EnvVar
905
+
906
+ os.environ.pop("WEBSOCKET_MAX_SIZE", None)
907
+
908
+ try:
909
+ class VuerClient:
910
+ URI: str = EnvVar @ "VUER_CLIENT_URI" | "ws://localhost:8012"
911
+ WEBSOCKET_MAX_SIZE: int = EnvVar @ "WEBSOCKET_MAX_SIZE" | 2**28
912
+
913
+ # Both should resolve to values, not _EnvVar objects
914
+ assert VuerClient.URI == "ws://localhost:8012", f"Expected default URI, got {VuerClient.URI}"
915
+ assert VuerClient.WEBSOCKET_MAX_SIZE == 2**28, f"Expected 2**28, got {VuerClient.WEBSOCKET_MAX_SIZE}"
916
+
917
+ # Verify types
918
+ assert isinstance(VuerClient.URI, str), f"URI should be str, got {type(VuerClient.URI)}"
919
+ assert isinstance(VuerClient.WEBSOCKET_MAX_SIZE, int), f"WEBSOCKET_MAX_SIZE should be int, got {type(VuerClient.WEBSOCKET_MAX_SIZE)}"
920
+
921
+ # The value should be usable (the original bug was .decode() failing)
922
+ # This simulates websocket operations that need to check max size
923
+ assert VuerClient.WEBSOCKET_MAX_SIZE > 0
924
+
925
+ finally:
926
+ os.environ.pop("WEBSOCKET_MAX_SIZE", None)
927
+ os.environ.pop("VUER_CLIENT_URI", None)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes