Commit 33c6be5
authored
Fix callable save_to dispatch (UnboundLocalError) and stop leaking secrets in KeyError (#18)
* Fix save_to callable dispatch and stop leaking secrets in KeyError
Two same-file fixes in base.py.
1. `ask_user_for_key(save_to=<callable>)` raised UnboundLocalError.
`SaveTo = Optional[Union[MutableMapping, KTSaver]]` advertises callable
savers, and `user_gettable(save_to=...)` is re-exported from `__init__`,
but the save block bound `save_to_func` only inside
`if hasattr(save_to, '__setitem__')` with no `else` -- so a callable
saver blew up with UnboundLocalError, and inside `user_gettable` that
UnboundLocalError was swallowed into a KeyError by
FuncBasedGettableContainer, silently dropping the value.
Replaced with an explicit `_resolve_saver` dispatch (the single place
the SaveTo union is interpreted): `__setitem__` when present, else the
callable itself, else a TypeError naming the offending type. Resolution
is idempotent and happens up front, so an unusable save_to is reported
when it is specified rather than after the user has already typed a
value we then cannot save.
Behaviour is unchanged for existing callers: the mapping branch still
dispatches on `hasattr(.., '__setitem__')` rather than
`isinstance(.., MutableMapping)`, so write-only stores keep working.
2. FuncBasedGettableContainer.__getitem__ leaked credentials.
The KeyError message interpolated both the rejected value `v` and the
upstream exception text `e`. `v` is often a credential (that is why it
is being validated) and SDK exception text routinely echoes the
rejected token. These KeyErrors propagate through ChainMap lookups and
are typically logged, sending the secret to log aggregators.
Now `raise KeyError(k) from e` (and `raise KeyError(k)` for the invalid
value case): the message is just the key, `args[0]` stays the key per
the Mapping convention, and the upstream exception remains reachable
via `__cause__`.
Tests: new config2py/tests/test_base.py covers dict saver, __setitem__
only store, callable saver, the TypeError branch, save_condition, both
user_gettable paths, and asserts the secret appears in neither
`str()` nor `repr()` of the raised KeyError. Eight of them failed
before this change. Two doctests in the FuncBasedGettableContainer
docstring asserted the old leaky messages and were updated.
Closes #14
Claude-Session: https://claude.ai/code/session_01Kug7UUbVeCQgruvNXUq63c
* fix: make app-folder resolution and its tests cross-platform
Fixes the four Windows CI failures (all pre-existing: this PR had only
touched base.py and tests/test_base.py) and one real Windows bug found
while investigating.
Code fix -- Windows 'cache' collided with 'data'/'state':
APP_FOLDER_STANDARDS described the Windows cache root as
FolderSpec("LOCALAPPDATA", join(%LOCALAPPDATA%, "Temp")). Since the
fallback is only used when the env var is *absent*, and LOCALAPPDATA is
always set on Windows, the "Temp" default was dead code: cache resolved
to %LOCALAPPDATA%, exactly the data/state root. Clearing an app's cache
would have deleted the user's data. FolderSpec gains a `subpath` field
that expresses "a kind that lives inside another kind's root", so cache
is now the documented %LOCALAPPDATA%\Temp.
The table also snapshotted os.getenv() into its own fallbacks at import
time, which made those fallbacks untestable and unreachable. They are now
declarative literals (~\AppData\Roaming etc.), so a missing env var yields
a real absolute root instead of "" -- an empty root would have degraded
os.path.join(root, app_name) into a *relative* path.
The single os.name branch is now app_folder_standards(os_name), so either
platform's table can be resolved from any platform, and
system_default_for_app_data_folder takes it via an optional keyword. That
makes Windows behaviour testable on Linux/macOS -- see the new
tests/test_platform_standards.py, which loops over both platforms.
Test fixes -- assertions that were POSIX-shaped, not wrong behaviour:
- test_app_data.py steered app folders with XDG_CONFIG_HOME/XDG_DATA_HOME.
XDG is a POSIX standard and is correctly ignored on Windows, so the
redirection silently did nothing there: two tests failed on the location
assertion and five others passed while writing into the runner's real
user profile. They now use config2py's own CONFIG2PY_<KIND>_DIR override,
which is honoured on every platform, and compare resolved Path objects
rather than strings. Location is now asserted in 5 tests, up from 2.
- test_sync_store.py::test_store_repr asserted a separator literal
(`temp_file in repr(store)`); pathlib's repr always spells paths with
forward slashes, so the native Windows form never appears. It now
asserts the store's filepath identity plus the separator-free filename.
Doctest fix:
get_app_folder's doctest expected '.../.config/config2py'. Rather than
skipping it, it now asserts the properties that hold on every platform:
the result is absolute, is named after the app, and sits directly inside
the 'config' root. That is strictly more coverage than the ELLIPSIS form.
Also corrected get_app_rootdir's docstring, which advertised the override
variables under wrong names (CONFIG2PY_*_FOLDER instead of CONFIG2PY_*_DIR)
and implied XDG works everywhere.
pytest config: testpaths named "tests", which does not exist (tests live in
config2py/tests/), so pytest warned and fell back to recursive discovery from
the CWD. Set to ["config2py"]. doctest_optionflags now match what the wads
run-tests-uv action passes on the command line (ELLIPSIS,
IGNORE_EXCEPTION_DETAIL) -- it overrides the file, so NORMALIZE_WHITESPACE
here meant local runs and CI applied different rules.
Collected tests: 106 -> 127 (+20 platform tests, +1 doctest), no warnings.
Dependents gate (29 suites) unchanged before/after: 25 pass, 2 pre-existing
failures (smart-cv, yp), 1 no-tests, 1 tests-disabled.
Claude-Session: https://claude.ai/code/session_01Kug7UUbVeCQgruvNXUq63c
* test: pin the documented app-folder roots for both platforms
Restores coverage that the previous commit dropped. Making the
`get_app_folder` doctest platform-agnostic was right, but the form it
replaced ('.../.config/config2py') was the only assertion in the suite
that pinned a *literal* XDG root; the properties that replaced it
(absolute, basenamed after the app, dirname == get_app_rootdir('config'))
all hold by construction of `os.path.join(root, app_name)`, so they no
longer notice if the root itself moves.
The rest of test_platform_standards.py is structural -- non-empty, no
cache/data collision, right variable family -- and a silently retargeted
default satisfies all of it. DOCUMENTED_STANDARDS now spells out both
tables exactly as get_app_rootdir's docstring advertises them, so
relocating a user's config/data/cache/state/runtime folder has to be a
deliberate edit in two places rather than a side effect of an unrelated
change. Verified non-tautological: repointing the POSIX 'config' default
at '~/.conf' turns it red.
Collected tests: 127 -> 129.
Claude-Session: https://claude.ai/code/session_01Kug7UUbVeCQgruvNXUq63c1 parent 3bd4b26 commit 33c6be5
7 files changed
Lines changed: 601 additions & 56 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
249 | | - | |
| 249 | + | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
253 | 264 | | |
254 | 265 | | |
255 | 266 | | |
| |||
274 | 285 | | |
275 | 286 | | |
276 | 287 | | |
277 | | - | |
| 288 | + | |
278 | 289 | | |
279 | 290 | | |
280 | 291 | | |
| |||
296 | 307 | | |
297 | 308 | | |
298 | 309 | | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
303 | 317 | | |
304 | | - | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
305 | 321 | | |
306 | 322 | | |
307 | 323 | | |
| |||
358 | 374 | | |
359 | 375 | | |
360 | 376 | | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
361 | 437 | | |
362 | 438 | | |
363 | 439 | | |
| |||
374 | 450 | | |
375 | 451 | | |
376 | 452 | | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
377 | 493 | | |
378 | 494 | | |
379 | 495 | | |
380 | 496 | | |
381 | | - | |
| 497 | + | |
382 | 498 | | |
383 | 499 | | |
384 | 500 | | |
385 | 501 | | |
386 | 502 | | |
387 | 503 | | |
388 | 504 | | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
| 505 | + | |
| 506 | + | |
393 | 507 | | |
394 | 508 | | |
395 | 509 | | |
| |||
405 | 519 | | |
406 | 520 | | |
407 | 521 | | |
408 | | - | |
409 | | - | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
410 | 525 | | |
411 | 526 | | |
412 | 527 | | |
| |||
440 | 555 | | |
441 | 556 | | |
442 | 557 | | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
443 | 571 | | |
444 | 572 | | |
445 | 573 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
11 | 33 | | |
12 | 34 | | |
13 | 35 | | |
14 | 36 | | |
15 | 37 | | |
16 | 38 | | |
| 39 | + | |
17 | 40 | | |
18 | 41 | | |
19 | 42 | | |
20 | 43 | | |
21 | 44 | | |
| 45 | + | |
22 | 46 | | |
23 | 47 | | |
24 | 48 | | |
| |||
36 | 60 | | |
37 | 61 | | |
38 | 62 | | |
| 63 | + | |
39 | 64 | | |
40 | 65 | | |
41 | 66 | | |
| |||
110 | 135 | | |
111 | 136 | | |
112 | 137 | | |
| 138 | + | |
113 | 139 | | |
114 | 140 | | |
115 | 141 | | |
| |||
121 | 147 | | |
122 | 148 | | |
123 | 149 | | |
124 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
125 | 154 | | |
126 | 155 | | |
127 | 156 | | |
| |||
145 | 174 | | |
146 | 175 | | |
147 | 176 | | |
148 | | - | |
| 177 | + | |
149 | 178 | | |
150 | 179 | | |
151 | 180 | | |
152 | 181 | | |
| 182 | + | |
153 | 183 | | |
154 | 184 | | |
155 | | - | |
| 185 | + | |
156 | 186 | | |
157 | 187 | | |
158 | 188 | | |
159 | | - | |
| 189 | + | |
160 | 190 | | |
161 | 191 | | |
162 | | - | |
| 192 | + | |
163 | 193 | | |
164 | 194 | | |
165 | 195 | | |
| |||
168 | 198 | | |
169 | 199 | | |
170 | 200 | | |
171 | | - | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
172 | 204 | | |
173 | 205 | | |
174 | | - | |
| 206 | + | |
175 | 207 | | |
176 | 208 | | |
177 | 209 | | |
| |||
184 | 216 | | |
185 | 217 | | |
186 | 218 | | |
187 | | - | |
| 219 | + | |
188 | 220 | | |
189 | 221 | | |
190 | 222 | | |
| |||
194 | 226 | | |
195 | 227 | | |
196 | 228 | | |
| 229 | + | |
197 | 230 | | |
198 | 231 | | |
199 | | - | |
| 232 | + | |
200 | 233 | | |
201 | 234 | | |
202 | 235 | | |
203 | | - | |
| 236 | + | |
204 | 237 | | |
205 | 238 | | |
206 | | - | |
| 239 | + | |
207 | 240 | | |
208 | 241 | | |
209 | 242 | | |
| |||
0 commit comments