Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 23 additions & 42 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,42 @@
# config2py

Tools to read and write configurations from various sources and formats: layered
lookup (env vars, files, user prompt), a `ConfigStore`/`ConfigReader` data-object
layer over `configparser`, extension-based codecs, and synchronized key-value
stores with automatic persistence.
Tools to read and write configurations from various sources and formats: layered lookup (env vars, files, user prompt), a `ConfigStore`/`ConfigReader` data-object layer over `configparser`, extension-based codecs, per-app XDG/Windows folders, and synchronized key-value stores with automatic persistence.

## Module map (`config2py/`)

- `base.py` — `get_config`, `user_gettable`, `sources_chainmap`: the layered
lookup chain a config value is resolved through.
- `tools.py` — `config_getter`/`simple_config_getter` (the headline "ask user for
missing key -> save to disk" flow), `get_configs_local_store`, `local_configs`,
`configs`, `Configs`, `extract_exports`.
- `util.py` — `envvar` (like `os.environ` but hides secrets on display),
`ask_user_for_input`, `get_app_config_folder`/`get_app_data_folder`/`get_app_folder`.
- `s_configparser.py` — `ConfigStore`/`ConfigReader`: a data-object layer over
stdlib `configparser`.
- `codecs.py` — extension-based codec registry (bytes <-> JSON-friendly Python
types), keyed by file extension.
- `sync_store.py` — `MutableMapping`s that auto-sync changes to a backing store.
- `errors.py` — `Config2PyError` and subclasses.
- `base.py`: `get_config`, `user_gettable`, `sources_chainmap`. This is the layered lookup chain a config value is resolved through.
- `tools.py`: `config_getter`/`simple_config_getter` (the headline "ask user for missing key, then save to disk" flow), `get_configs_local_store`, `local_configs`, `configs`, `Configs`, `extract_exports`. `config_getter` and `local_configs` are built at import time.
- `util.py`: `envvar` (like `os.environ` but hides values from `repr`), `ask_user_for_input` and `looks_like_secret` (masking), the app folders (`app_folder_standards`, `get_app_folder`, `get_app_config_folder`, `get_app_data_folder`, `AppData`, `ensure_seeded`), and `secure_open`/`secure_makedirs`.
- `s_configparser.py`: `ConfigStore`/`ConfigReader`, a data-object layer over stdlib `configparser`.
- `codecs.py`: an extension-based codec registry (bytes to and from JSON-friendly Python types).
- `sync_store.py`: `MutableMapping`s that auto-sync changes to a backing store. It deliberately has no intra-package imports.
- `errors.py`: `Config2PyError` and `ConfigNotFound`.
- `data/skills/config2py-quickstart/`: the consumer skill. It ships in the wheel.

## Agent layer

- Skills: the consumer skill is `config2py/data/skills/config2py-quickstart/SKILL.md` (pip-shipped). The dev skill is `skills/config2py-dev/SKILL.md` (repo only). `.claude/skills/<name>` are relative symlinks to them.
- Because of those symlinks, `pyproject.toml` has `[tool.hatch.build.targets.sdist] exclude = [".claude/skills"]` with `skip-excluded-dirs = true`. Without it, hatchling drops the real skill folders from the sdist. `config2py/tests/test_packaging.py` checks this when hatchling is installed.
- `config2py/tests/test_docs_examples.py` runs every ```` ```python ```` block and every `>>>` example of `README.md` and of the consumer skill, in a subprocess with a sandboxed HOME and closed stdin. Mark a block that must prompt with `<!-- no-test -->` on the line before its fence.
- The README section between the `epythet:agentic-readme` markers is generated by `epythet ai-readme-check . --write`. Don't hand-edit inside the markers.

## Tests & lint (verified)

```bash
uv venv .venv && uv pip install -e . pytest ruff
.venv/bin/pytest config2py --doctest-modules \
-o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL' -q # 132 passed, 4 skipped
-o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL' -q # 155 passed, 5 skipped
.venv/bin/ruff check .
```
Or `wads ci-local`. **Gotchas already documented in `pyproject.toml` comments:**
`testpaths = ["config2py"]`, not `"tests"` — there is no top-level `tests/` dir,
only `config2py/tests/`. `doctest_optionflags` here is kept in sync with what CI's
`run-tests-uv` action passes on the command line (which *overrides* this
setting) — notably CI does **not** set `NORMALIZE_WHITESPACE`, so a doctest that
passes locally with the bare `pytest` config can still fail in CI; use the
command above (matching CI) to catch that before pushing.

Or `wads ci-local`. **Gotchas already documented in `pyproject.toml` comments:** `testpaths = ["config2py"]`, not `"tests"`. There is no top-level `tests/` dir, only `config2py/tests/`. `doctest_optionflags` there is kept in sync with what CI's `run-tests-uv` action passes on the command line, which *overrides* the setting. Notably, CI does **not** set `NORMALIZE_WHITESPACE`, so a doctest that passes locally with the bare `pytest` config can still fail in CI. Use the command above, which matches CI, to catch that before pushing.

## Invariants / known gaps (see open issues before "fixing" these)

- **`DFLT_MASKING_INPUT = False`** in `util.py` — `simple_config_getter`'s
prompt-for-missing-key flow echoes typed input (including secrets) to the
terminal by default; masking (`getpass`) is wired in but off by default.
Flipping the default is blocked on 3 fleet dependents not present on every
box (see [i2mint/config2py#13](https://github.com/i2mint/config2py/issues/13)) —
don't change it without re-running the dependents check.
- [i2mint/config2py#16](https://github.com/i2mint/config2py/issues/16) — an
omnibus of smaller audit findings (docstring overclaims, a broad fallback,
the pickle codec, import-time side effects) — read before touching those areas.
- [i2mint/config2py#22](https://github.com/i2mint/config2py/pull/22) (open) —
removes the vestigial `setup.cfg`, adds `[tool.wads.ci]`, migrates CI to the
reusable-workflow stub; currently blocked on a hosted-CI `action_required`
anomaly (see [#23](https://github.com/i2mint/config2py/issues/23)). Until it
lands, CI here is the inline uv workflow using discrete `i2mint/wads` actions.
- **`DFLT_MASKING_INPUT = looks_like_secret`** in `util.py` (since [#13](https://github.com/i2mint/config2py/issues/13)). `ask_user_for_input` masks prompts that mention something secret-looking (`API_KEY`, `TOKEN`, `PASSWORD`, ...) and echoes the others. `mask_input` accepts a bool or a `prompt -> bool` predicate. When masking was inferred by the predicate, stdin is not a terminal, and `getpass.getpass` is the stdlib one, the read falls back to `input`, as before the default existed (stdlib `getpass` reads `/dev/tty`, not stdin). An explicit `mask_input=True` always uses `getpass.getpass`. A frontend's replacement `getpass` (Jupyter) is always used. The tests are in `config2py/tests/test_masking.py`. Don't change the default without re-running the dependents check.
- The #16 audit was split into follow-ups, each with a plan. Read the matching one before touching an area: [#25](https://github.com/i2mint/config2py/issues/25) (broad `(Exception,)` fallback), [#26](https://github.com/i2mint/config2py/issues/26) (import-time folder creation), [#27](https://github.com/i2mint/config2py/issues/27) (typo'd paths give silent empty configs), [#28](https://github.com/i2mint/config2py/issues/28) (`os.path.sep` path sniffing), [#29](https://github.com/i2mint/config2py/issues/29) (pickle decoder opt-in). Also [#30](https://github.com/i2mint/config2py/issues/30) (the masking toggle drops `egress`, which interacts with `oa`) and [#33](https://github.com/i2mint/config2py/issues/33) (configs-store value files are written `0o644`).
- [#22](https://github.com/i2mint/config2py/pull/22) (open) removes the vestigial `setup.cfg`, adds `[tool.wads.ci]`, and migrates CI to the reusable-workflow stub. It is blocked on a hosted-CI `action_required` anomaly (see [#23](https://github.com/i2mint/config2py/issues/23)). Until it lands, CI here is the inline uv workflow using discrete `i2mint/wads` actions.

## Dependents

`accompy`, `aix`, `arioso`, `aw`, `brand`, `mood`, `py2store`, `tonal`, and 25
others (see `fleet_dependents.json`) import this package — check their tests
before changing `get_config`, `simple_config_getter`, or any default.
`accompy`, `aix`, `arioso`, `aw`, `brand`, `mood`, `py2store`, `tonal`, and about 25 others import this package. The full list is the maintainer's local `fleet_dependents.json`, which is not in this repo. Check their tests before changing `get_config`, `simple_config_getter`, or any default. The publicly clonable ones checked in cloud sessions are `i2mint/py2store`, `i2mint/xdol` and `thorwhalen/oa`. At baseline, `xdol` has 2 doctest-format failures and `oa` has 3 tests that need a real OpenAI key.
1 change: 1 addition & 0 deletions .claude/skills/config2py-dev
1 change: 1 addition & 0 deletions .claude/skills/config2py-quickstart
Loading
Loading