UV#61
Conversation
zagy
left a comment
There was a problem hiding this comment.
The documentation is a lot and it is very redundant. It doesn't help this way.
Also I stopped review at "fix_whitespace.py". There is o much noise here that It's really hard to review.
The commits need to be squashed to a useful amount.
Functionality itself seems to be there and working (in my tests) alas the repo is almost unmaintainable currently.
| def main(): | ||
| # 1. Ensure correct Python version | ||
| ensure_best_python(base) | ||
|
|
||
| # 2. Clear PYTHONPATH for isolation | ||
| os.environ.pop("PYTHONPATH", None) | ||
|
|
||
| # 3. Dispatch to run or meta commands | ||
| appenv = AppEnv(base, original_cwd) | ||
| if application_name == "appenv": | ||
| appenv.meta(remaining) | ||
| else: | ||
| appenv.run(application_name, remaining) | ||
| ``` |
There was a problem hiding this comment.
I don't think this is very useful here. It will get out of date.
There was a problem hiding this comment.
I don't understand this file apart from being an AI code generation artefact.
9f683b0 to
9f744c2
Compare
8024e92 to
fed46a5
Compare
| ### PR Checklist | ||
|
|
||
| - [ ] Tests pass: `uv run pytest` | ||
| - [ ] Linting passes: `uv run ruff check .` | ||
| - [ ] Formatting correct: `uv run ruff format --check .` | ||
| - [ ] Type checking passes: `uv run ty check .` | ||
| - [ ] Documentation updated (if applicable) | ||
|
|
There was a problem hiding this comment.
This doesn't make sense here. If there was a checklist put it into the proper github template.
There was a problem hiding this comment.
Uhm, is this used anywhere? We usually use pre-commit to achieve this.
|
|
||
| # You can set these variables from the command line. | ||
| SPHINXOPTS ?= | ||
| SPHINXBUILD ?= sphinx-build |
There was a problem hiding this comment.
this would need to be installed somehow … don't we have UV now?
5d42002 to
3750be2
Compare
952acef to
571a893
Compare
e8e5ba2 to
71a2501
Compare
|
I have a system where a python 3.8 is lying around, and uv/appenv fails miserably. At least a useful error message would be nice. Also, are newer pythons but Python 3.8 seems to be preferred for some reason. |
Major migration from setuptools/tox to uv with comprehensive quality improvements: Core migration: - Replace setuptools + tox with uv + tox-uv for all package management - Add uv.lock, remove tox.ini in favor of pyproject [tool.tox] - Self-update delegates standalone script updates to uvx - init updates existing pyproject.toml on Python 3.11+ (tomllib) - Introduce SubprocessError for clean subprocess failure exit Test quality: - Achieve 100% test coverage with fail_under=100 - Add E2E tests for all CLI commands + platform targets (Alpine/musl, macOS ARM64, Debian Bookworm/Bullseye) - Add .pyi type stubs for all test modules - Integrate complexipy cognitive complexity checks Logging: - Enforce structured topic:key=value format via Logrambo audit - Add dual-channel error reporting, fill logging gaps Documentation: - Restructure docs into user/dev sections with Sphinx/Furo/MyST - Add SPDX headers, --version flag, improved --help texts Refactoring: - Extract methods to reduce cognitive complexity below 15 - Remove runtime type evaluation, update stubs to Python 3.14 - Hardened mocks (spec=), narrowed exception clauses
- tests/integration/ -> tests/e2e/ (renamed in recent commits) - slow marker no longer excluded by default; update Running Tests + Slow Marker sections - remove stale repo-tree ASCII art (replaced by git ls-files pointer) - compress discovery chain (6-step list -> concept + source pointer) - compress platform detection table -> description with example triples - compress quality-gates command table -> principle + pyproject pointer - fix orphaned definition-list term for command symlinks - add exit code numeric values to Error Handling Strategy - add hidden toctree for logging-conventions (fixes sphinx -W build)
- delete .agents/impl_specs/ (7 planning specs for completed work) - delete .agents/reports/quality-audit.md (stale one-time audit) - reframe tests/test_docs_spec.py -> tests/test_docs.py: drop spec framing + 4 one-time cleanup checks, keep 11 valuable doc regression tests with descriptive names - .agents/ now has no tracked files (tmp/ is gitignored)
The private-registry migration example named the wrong UV_INDEX_* env vars: it showed UV_INDEX_GITLAB_USERNAME/_PASSWORD, but the token uv expects is derived from the full registry hostname, not just the first label. The example now reads UV_INDEX_GITLAB_EXAMPLE_COM_USERNAME/ _PASSWORD, matching the _index_name_from_url derivation in src/appenv.py (gitlab.example.com -> gitlab-example-com -> token GITLAB_EXAMPLE_COM). - Replace UV_INDEX_GITLAB_USERNAME/_PASSWORD with UV_INDEX_GITLAB_EXAMPLE_COM_USERNAME/_PASSWORD to reflect hostname-based token derivation - Add a clarifying sentence explaining the <NAME> token derives from the registry hostname (gitlab.example.com -> GITLAB_EXAMPLE_COM)
…EADME Five user-facing bugs found during self-use testing are fixed: --python-version now validates its value, --description gained a non-interactive flag, --help passes through on run/uv/python, init and migrate write a consistent .gitignore, and the README init answer-hint order matches the wizard prompt order. F3 (python-version) was critical; F5/F6/F7/F8 were inconvenient. This commit also bundles two non-dogfooding changes that belong with this batch. (1) A hatchling sdist-exclude in pyproject.toml surfaced by the build gate running on this batch: `uv build` previously captured the runtime-bootstrapped .appenv/ venv (absolute-external symlinks) into the sdist and failed; it now succeeds. (2) Two previously-untracked spec files (fix-argparse-and-init-dispatch, fix-verbose-output) documenting work already shipped in src/appenv.py. All gates green: pre-commit (ruff check/format, ty, detect-private-key, check-logging, yaml/toml), pytest (346 passed incl. 32 e2e), uv build. - F3 (critical): add _validate_python_version type= validator on --python-version; rejects anything not matching ^\d+\.\d+$. argparse ArgumentTypeError routes through UsageArgumentParser.error -> exit 64 (EXIT_CODE_USAGE) before create_pyproject writes anything. Format only; no minimum enforced (the user's choice). - F7: add --description flag to the init subparser; _resolve_init_params_noninteractive uses args.description or "" instead of the hardcoded empty string. Interactive pre-fill is out of scope (the wizards do not yet receive the args Namespace). - F5: add add_help=False to the python/run/uv subparsers so --help falls through parse_known_args into `remaining` and reaches the wrapped tool (uv run / uv / python) instead of argparse's empty auto-help. init keeps its own --help. - F6: introduce module-level _GITIGNORE_ENTRIES = [".venv", ".appenv", ".batou-lock"] as the single source of truth shared by init and migrate, so `git add -A` after migrate never silently commits the .appenv/venv tree. - F8: swap README init walkthrough hints so "http as binary" precedes "httpie as dependency" in both blocks, matching the wizard prompt order. - docs: commands.md gains the --description row and the migrate .gitignore bullet now lists all three entries. - tests: add test_init_cli_flags.py (12 collected: 9 python-version parametrized + 3 description), test_help_passthrough.py (4), test_gitignore_consistency.py (5) + .pyi, test_readme_init_order.py (1) + .pyi; add description=None to the test_init.py Namespace. - build: pyproject.toml [tool.hatch.build.targets.sdist] exclude = [".appenv"] so the sdist no longer embeds the runtime .appenv/ venv; uv build now produces a valid sdist + wheel. - specs: add fix-init-cli-flags, fix-help-passthrough, fix-gitignore-consistency, fix-readme-init-order; backfill fix-argparse-and-init-dispatch and fix-verbose-output (implementation already in src/appenv.py; spec files were never committed).
… rollback `init` now refuses to re-run against an existing `[project]` section instead of silently dropping fields it cannot round-trip without a TOML writer, validates `--binary`/`--dep` before touching the filesystem, and rolls back pyproject.toml plus the command symlink when uv-lock fails mid-init. The CLI contract is tightened: run/uv/python forward unknown flags to the wrapped tool while every other subcommand exits 64 on unknown flags, and run/uv now require a pyproject.toml (exit 67) like prepare/python. Docs and the migrate-pip-options coverage gap are closed (full suite at 100% coverage). BREAKING: `init` no longer updates projects that already have a `[project]` section — it exits 65 (DATAERR) and points at `./appenv uv add` or direct editing. `--dep` is now always required (at least one); the optional-deps-when-updating path is gone. Behavior is identical on every supported Python version. - init: refuse re-init on existing [project] (F5); validate --binary against path traversal, absolute paths, and clobbering of user files or foreign symlinks (F1/F2/F3); dedup repeated --dep entries (F8) - init: wrap the create+symlink+lock sequence in try/except BaseException that restores pyproject bytes (or deletes a freshly-created file) and removes a symlink this run created, then re-raises so the original error and exit code propagate (F7) - contract: interactive init EOF surfaces as a clean exit 64 with a usage hint instead of a raw EOFError traceback (F4) - contract: run/uv gain an ensure_pyproject pre-flight (exit 67); run inherits it via run_script -> run_uv delegation (F9) - contract: argparse post-parse strictness — non-passthrough subcommands exit 64 on leftover flags; run/uv/python keep forwarding to the wrapped tool (F10) - safety: add "no-compatible-python" to the verbose-console hidden events so the internal debug line stops leaking to verbose stdout (F13) - docs: migrate after-step uses `appenv run <binary>` (F6); add a passthrough row to the exit-codes table (F11); correct the [app] default in the init interactive example (F12) - remove dead code orphaned by refuse-re-init: _get_tomllib, the module-level tomllib global, _replace_project_section, _find_project_section_bounds, the unreachable has_project_section branch in _generate_content_with_project, their tests, two no-op tomllib monkeypatches, and the stale "Conditional tomllib" dev-doc bullet - tests: cover migrate-pip-options edge branches (index-name collision suffix, dedup, value-consumption guards, inline `=` forms, unknown options) to close the pre-existing coverage gap and reach 100% - specs: add seven implementation specs under .agents/impl_specs/ documenting the decisions (fix-refuse-re-init, fix-init-safety-layer, fix-rollback-on-failure, fix-contract-treue, fix-dead-code-cleanup, fix-doc-consistency, fix-migrate-pip-options-coverage) Verification: 364 tests passed (incl. 32 e2e), ruff check + format clean, ty clean, vulture src/ clean, coverage 100% (1238 stmts, 346 branches).
…gaps The dogfooding bundle (8ec1123) pushed AppEnv.init cognitive complexity from 13 to 20, tripping the complexipy <=15 gate that the `cov` tox env enforces. init is now a ~24-line orchestrator delegating to three private helpers (_resolve_init_target, _resolve_init_params_with_eof_guard, _atomic_write_and_lock), all under the threshold. The move is behavior- preserving (the EOF-clean-exit and atomic-rollback SPEC markers travel into the helpers); the full suite stays at 364 passed / 100% coverage. Smaller post-dogfooding gaps close alongside it. - init: extract _resolve_init_target, _resolve_init_params_with_eof_guard (EOF guard), and _atomic_write_and_lock (snapshot + atomic write + rollback); init drops out of the complexipy top-5 (highest function now 15) - init: pass self.base instead of the out-of-scope `target` to create_pyproject — same value (_chdir_to_project sets self.base = target.resolve()), no observable change - appenv.pyi: declare the three new private helper signatures - commands.md: add exit-1 row to the BSD sysexits table for self-update --check version drift (already documented in the self-update subsection; the table gains only the pointer) - test_docs.py: test_init_prompt_matches_source now also pins the [app] default to appear in both src/appenv.py and commands.md, so future drift in either direction fails here - test_help_passthrough.py: relax "usage: appenv init" to "usage:" + "init" so the assertion holds on Python <3.14, where argparse leaks the <COMMAND> placeholder into the usage line (pre-existing failure, verified at 58548b4 on 3.10; init still owns its flags and is not delegated) - specs: delete obsolete migrate-pip-options-refactor.md (the _parse_requirement_line complexity-32 refactor it targeted has landed); update fix-migrate-pip-options-coverage.md to drop the dangling reference - add .agents/impl_specs/fix-post-dogfooding-cleanup.md documenting this work
GroupedHelpFormatter inherited from HelpFormatter, which collapses all whitespace in the parser description to single spaces. The intended blank line between the version banner and the description disappeared, merging 'appenv 2026.6.16b1' and 'appenv pins Python packages...' into one line. Inherit from RawDescriptionHelpFormatter instead — _format_action override for subcommand grouping is unaffected.
The 'New Project from Scratch' section created a project with only httpie, then showed 'ln -s appenv pytest && ./pytest -xvs' — pytest was never installed, so ./pytest failed with 'Binary not found'. Removed the block; the Development Workflow section right after already demonstrates 'uv run pytest -xvs' for dev dependencies.
Add tools/check_stub_sync.py + the check-stub-sync pre-commit hook (runs in CI via the pre-commit tox env) so drifted .pyi stubs can no longer be committed. Reconcile all 17 existing stubs (src/appenv.pyi + 16 tests) with their runtime modules, add a [tool.mypy] section (mypy_path + a scoped pytest_patterns.* override), and document the gate in docs/dev/index.md. src/appenv.py and all test .py files are untouched; reconciliation is one-directional (stub matches runtime, never vice versa). tools/stubtest-allowlist masks the single mypy-dataclass-plugin synthetic UvVersion._DT (not literal in any stub). Validation: tools/check_stub_sync.py green (17/17), ty check src/ clean, pytest 364 passed, ruff clean. Spec: .agents/impl_specs/stubs-sync.md
Add the 9 remaining .pyi stubs (tests/__init__.pyi, tests/e2e/__init__.pyi, tests/e2e/conftest.pyi, tests/test_migrate_pip_options.pyi, tests/test_init_cli_flags.pyi, tests/test_help_passthrough.pyi, tests/test_check_logging.pyi, tests/e2e/test_e2e_commands.pyi, tests/e2e/test_e2e_github_download.pyi) so every test module is now type-contracted. The stub-sync gate covers 26/26 modules. Fix tools/check_stub_sync.py _module_name() to map __init__.pyi to its package name (tests/__init__.pyi -> 'tests'), matching mypy's package-stub convention; otherwise mypy errors 'Source file found twice under different module names'. No .py runtime touched; reconciliation is one-directional (stub matches runtime, never vice versa). Validation: check_stub_sync 26/26, direct stubtest on tests + tests.e2e packages clean (gate fix not masking drift), ty check src/ clean, pytest 364 passed, ruff clean. Spec: .agents/impl_specs/stubs-complete.md
Add "--ty" to [tool.pytest.ini_options].addopts so pytest-ty (already in the test group but dormant until now) runs the ty type checker on every test run. This complements the mypy.stubtest stub-sync gate: stubtest enforces stub<->runtime consistency; pytest-ty enforces type-correctness of the test code itself. Cost is negligible (+0.5s / +2.7%); the suite is green (390 passed incl. the ty items); xdist-compatible. Activating a dormant tool rather than leaving it to rot. Validation: pytest 390 passed (0 fail/0 error), ty check src/ clean, stub-sync gate 26/26.
…l tests Merge 8 source-confirmed duplicate/subset test pairs across 5 files into pytest-patterns `full ==` structural assertions, replacing ad-hoc `assert "x" in stdout` fragments with ordered backbones + no_errors.refused guards. Net: 83 -> 75 test functions (-8), 390 -> 384 pytest cases (-6), zero coverage loss. Every merge verified against the identical src/appenv.py branch; fs/exit asserts preserved. Notable: - test_init: two single-branch rejections (path/absolute binary name, clobber user-file/foreign-symlink) parametrized; the py39 [project]-refusal variant removed (has_project_section is purely line-based, no tomllib path exists -- its documented purpose was impossible in the code). - test_update_lockfile: 4 ad-hoc negative fragments -> no_errors.refused. - First stderr-pattern tests in the repo (test_init merges assert full == captured.err; verified log.error does not reach captured.err under pytest). Test stubs (.pyi) co-updated to keep the stub-sync gate green (26/26). Validation: pytest 384 passed (0 fail/0 error), ty check src/ clean, stub-sync 26/26, ruff clean, src/ untouched.
Two small cleanups bundled:
- Delete test_self_update_standalone_script_same_version (byte-identical
duplicate of test_self_update_standalone_delegates_to_uvx: identical
same-version-standalone setup + identical run_calls assertion; only
comments/docstrings differed). Keep the better-named sibling.
- Fix merge-count typo in the consolidation spec ("Merge 7" / "## The 7
merges" -> 8; the doc lists MERGE 1-8).
Suite: 384 -> 383 (-1 case).
The deferred third cleanup (delete the zero-assertion
test_init_interactive_new_default_name) was NOT applied: doing so exposed
a pre-existing test-order-pollution bug -- src/appenv.py:1404
(_chdir_to_project) performs an un-monkeypatched os.chdir(), so init()-
calling tests leak the process cwd; test_init_skips_appenv_script_creation
depends on that leaked cwd for its expected SystemExit. Properly fixing
this needs a systemic test-hygiene refactor (autouse cwd-restore fixture
or per-test cwd management), out of scope here. Flagged for follow-up.
Validation: pytest 383 passed (0 fail/0 error), stable on re-run (randomized
+ deterministic), ty check src/ clean, stub-sync 26/26, ruff clean, src/
untouched.
…test appenv's _chdir_to_project (src/appenv.py:1404) performs an un-monkeypatched os.chdir() -- intended CLI behavior, but in tests it leaks the process cwd across tests. test_init_skips_appenv_script_creation depended on that leaked cwd for its expected SystemExit (the error only fires when the cwd's pyproject has a [project] section), making the suite fragile to collection- order changes (e.g. test deletions). Add _isolate_cwd autouse fixture to tests/conftest.py: snapshots Path.cwd() before each test, restores it after, OSError-safe on both legs with a stable _INVOCATION_CWD fallback (captured at collection). Complements the existing workdir fixture (finalizes after it, catching any leak). Now safe to remove the zero-assertion smoke test test_init_interactive_new_default_name. Suite 383 -> 382 (-1 case). Proof: with the test deleted but NO fixture, the canary fails with "DID NOT RAISE SystemExit"; with the fixture, it passes standalone and in-suite. src/appenv.py untouched (the chdir is intended CLI behavior). Validation: pytest 382 passed (0 fail/0 error), canary passes standalone, ty check src/ clean, stub-sync 26/26, ruff clean, src/ untouched.
…, ship pytest-randomly pytest-randomly (added in this commit) surfaced a second cross-test leak that the cwd-isolation fix (856c20e) does not cover: src/appenv.py:2115 and :1873 mutate os.environ['UV_PROJECT_ENVIRONMENT'] directly (intended CLI behavior -- transports venv path to the uv subprocess). test_stale_venv_broken_python called env.prepare() without the opt-in clean_uv_project_env fixture, leaking the var in-process; the e2e subprocess/pexpect boundaries had zero env sanitization, so real uv inherited the stale var and failed with EACCES on a non-executable python artifact. Reproduced 14/14 failures on seed 413120988. Two complementary test-layer fixes (src/appenv.py untouched -- the os.environ mutation is intended CLI behavior): FIX flyingcircusio#1 (stopgap): make clean_uv_project_env autouse in tests/conftest.py. Pops UV_PROJECT_ENVIRONMENT after every test, closing the leak at source. Tests asserting the var mid-test read it before teardown, so they stay green. FIX flyingcircusio#2 (durable): sanitize the e2e subprocess env boundary. _base_env() (relocated to tests/e2e/conftest.py) strips 6 appenv/uv leak keys (UV_PROJECT_ENVIRONMENT, VIRTUAL_ENV, APPENV_BASEDIR/VERBOSE/ EXTRAS/BEST_PYTHON) before applying overrides; keeps PATH/HOME/UV_INDEX_*. All previously-unsanitized boundaries (subprocess + pexpect across conftest, test_e2e_commands, test_cli, test_subprocess) now thread env=_base_env(). Proof: baseline polluter-then-e2e repro = 14 failed -> post-fix = 0 failures. Full suite 382 passed in 3 modes (randomized, deterministic, seed 413120988). Canary (test_init_skips_appenv_script_creation) green. Known architectural debt: src/appenv.py still mutates os.environ globally (investigator's FIX flyingcircusio#3). Tracked as a separate follow-up; the test layer now compensates so the suite is robust regardless. Validation: pytest 382 passed (3 modes), ty check src/ clean, stub-sync 26/26, ruff clean, src/ untouched, uv.lock stable.
…(TDD)
22 new tests across 6 classes specify the target behaviour for the
upcoming production refactor that replaces os.environ mutation with
explicit env dicts threaded through subprocess boundaries and execve.
All 22 are intentionally RED (production code unchanged in this commit).
Failure modes are spec-faithful, not setup bugs:
- 6x TestBuildEnv: AttributeError ('AppEnv' has no '_build_env')
- 2x TestCmdEnvThreading: cmd() does not yet accept env=
- 5x TestUvBinSubprocessEnv: subprocess.run/check_output env=None
- 2x TestModuleSubprocessEnv: find_available_pythons/_self_update_via_uvx env=None
- 3x TestExecveConversion: production uses os.execv, not os.execve
- 4x TestNoGlobalMutation: os.environ still mutated (UV_PROJECT_ENVIRONMENT, APPENV_BASEDIR, PYTHONPATH)
The new public surface specified:
- AppEnv._build_env(overlays) builds the env dict, drops PYTHONPATH,
preserves PATH/HOME, applies overlays without mutating os.environ
- cmd() accepts env= kwarg, threads to subprocess.check_output
- All 9 subprocess sites and 3 os.execv sites receive explicit env
pytest-ty kept green via '# ty: ignore[unknown-argument]' on the one
call that uses the not-yet-existing env= kwarg on cmd() -- this is the
TDD equivalent of an xfail marker for type-checks and gets removed when
FIX flyingcircusio#3 lands and the stub declares the kwarg.
Validation: 22 spec tests RED, 383 rest-of-suite + 2 ty meta tests GREEN
(no regression), ruff check + format clean (both .py and .pyi), ty clean,
stub-sync 27/27 (new file paired with .pyi stub).
Next commit (separate, by developer) implements FIX flyingcircusio#3 in src/appenv.py
to turn these 22 RED tests GREEN.
…utation
All subprocess boundaries now receive explicit env dicts built by the new
`_build_env()` helper instead of mutating `os.environ` before `os.execv()`.
`main()` no longer pops `PYTHONPATH` — the filter is embedded in
`_build_env()` so children get clean envs without side effects on the parent.
Test files are refactored from class-based to module-level functions, and a
new convention test (`test_no_test_classes`) prevents regression.
BREAKING: `UV_PROJECT_ENVIRONMENT`, `APPENV_BASEDIR`, and
`APPENV_BEST_PYTHON` are no longer set in the parent's `os.environ` after
`run()`/`run_uv()`/`prepare()`/`ensure_best_python()`. These vars are passed
exclusively via child process env dicts. Code reading them from `os.environ`
after these calls will see `None`.
- Replace `os.execv` with `os.execve` at all 3 call sites (AppEnv.run,
AppEnv.run_uv, ensure_best_python) and in _self_update_via_uvx/meta — env
dicts built with per-call overlays (APPENV_BASEDIR, UV_PROJECT_ENVIRONMENT,
APPENV_BEST_PYTHON)
- Add module-level `_build_env(overlays)` — copies os.environ, drops
PYTHONPATH, applies overlays; all subprocess boundaries consume it
- Add `AppEnv._build_env(overlays)` instance method delegating to the
module-level helper
- Remove `os.environ.pop("PYTHONPATH", None)` from `main()` — filter lives
in `_build_env()` instead
- Remove 4 `os.environ[...] = ...` mutations that leaked env vars globally
- Add `env=` parameter to `cmd()`, UvBin subprocess.run sites, `_uv_sync`,
`self_update_via_uvx`, and `find_available_pythons` — every subprocess
call receives an explicit env
- Add `tests/test_no_test_classes.py` — convention test banning `class
Test...` in `tests/**/test_*.py` (excludes conftest.py)
- Refactor `test_check_logging.py` and `test_env_threading.py` from
class-based to module-level functions (0 behavior change, purely
structural to satisfy the new convention)
- Refactor corresponding `.pyi` stubs to match flat function signatures
- Update `test_main.py::test_main_clears_pythonpath` to assert PYTHONPATH
is preserved in os.environ after main()
- Update `test_prepare.py::test_prepare_pyproject_sets_uv_project_environment`
to assert UV_PROJECT_ENVIRONMENT is absent from os.environ after prepare()
…est_prepare
The mypy dependency group is now included when running tests, enabling
stub-sync verification via mypy.stubtest in test tox environments. Three
stub files had drifted from their implementation: _uv_sync was missing
the env parameter, and two test functions had been renamed without
updating their .pyi counterparts. These are now all in sync.
- pyproject.toml: add { include-group = "mypy" } to test dependency group
- uv.lock: regenerate to include mypy and types-pexpect under test dep manifests
- src/appenv.pyi: add env: dict[str, str] | None = None to _uv_sync signature — implementation gained the parameter, stub was never updated
- tests/test_main.pyi: rename test_main_clears_pythonpath -> test_main_preserves_pythonpath — test name changed to reflect behavior
- tests/test_prepare.pyi: rename test_prepare_pyproject_sets_uv_project_environment -> test_prepare_pyproject_does_not_leak_uv_project_environment — test name changed to accurately describe what it verifies
…ix stale docstrings and SPEC references FIX flyingcircusio#3 (env dict threading) is now implemented — the global os.environ mutation is gone. The autouse clean_uv_project_env fixture and _LEAKED_ENV_KEYS e2e sanitization were compensating for the old global-mutation behaviour and are no longer needed. Stale RED-until docstrings and broken SPEC references that drifted during the refactor are corrected. No test behavior change — all removed compensations were rendered redundant by ff84ca9 (env dict threading). Test-only changes. - Remove clean_uv_project_env autouse fixture from conftest.py (was compensating for UV_PROJECT_ENVIRONMENT leaking via os.environ mutation, no longer needed since ff84ca9) - Remove _LEAKED_ENV_KEYS tuple and stripping loop from e2e/conftest.py (same compensation for e2e subprocess boundary, now redundant) - Update stale RED-until docstring in test_env_threading.py to GREEN-since - Fix broken SPEC reference test_main_does_not_pop_pythonpath -> test_no_global_mutation_main_does_not_pop_pythonpath - Fix broken SPEC reference test_prepare_leaves_uv_project_env_alone -> test_no_global_mutation_prepare_leaves_uv_project_env_alone - Remove clean_uv_project_env fixture argument from 5 test_prepare test functions (no longer needed as fixture is gone) - Sync conftest.pyi and test_prepare.pyi stubs with above deletions
Two coupled test-hygiene refactors. The internal exception class no longer shadows stdlib subprocess.SubprocessError, and the e2e uv-runnability guard now lives in one place and fails loudly on a broken helper instead of silently skipping the whole e2e suite. - Rename class SubprocessError→CommandError across src/appenv.py, src/appenv.pyi, tests/test_main.py, tests/test_prepare.py (12 edit points). Removes the namespace shadowing of stdlib subprocess.SubprocessError; no backward-compat alias (CODEX Art. 4). - Deduplicate _uv_is_runnable: single source of truth in tests/e2e/conftest.py; remove the copy from tests/e2e/test_e2e_commands.py and consolidate the import with the existing _base_env import. Drop the stale stub entry from tests/e2e/test_e2e_commands.pyi. - Narrow _uv_is_runnable exception handler from `except Exception` to `except (subprocess.SubprocessError, OSError)` so a broken helper crashes loudly instead of silently skipping e2e (CODEX Art. 5). - Ship the impl spec at .agents/impl_specs/refactor-subprocesserror-and-uv-is-runnable.md alongside the change (matches repo convention). - Verification: ruff + ty clean; mypy.stubtest clean for the renamed class (a pre-existing, unrelated UvVersion._DT stubtest error persists on HEAD); 406/407 tests pass — the single failure (test_pip_install_uv_from_which_pip) is pre-existing and environmental (no pip in PATH), confirmed failing identically on HEAD.
Two previously uncovered surfaces now have direct unit tests: the appenv_settings_from_env() env-parsing function (sole caller is main(), run on every invocation) and the LockFile class backing the update-lockfile workflow. A new `characterization` pytest marker documents four spots of current buggy behavior so the planned smell fixes make their behavior change visible instead of silent. Sibling .pyi stubs ship alongside so the project's check_stub_sync gate stays green. - Add 9 tests in tests/test_main.py for appenv_settings_from_env: APPENV_VERBOSE/APPENV_EXTRAS/APPENV_BASEDIR parsing, defaults, verbose truthy-on-any-value quirk, frozen-dataclass invariant, plus a _clear_appenv_settings_env helper for test isolation - Add tests/test_lockfile.py with 17 tests targeting LockFile directly: read_lockfile_lines set/filter semantics, diff_summary count/label behavior, mock-uv diff flow, pyproject copy into the temp dir, and original-lockfile immutability - Extend tests/test_main.pyi and add tests/test_lockfile.pyi so the stub set matches the new test surface (check_stub_sync: 29/29 ok) - Register `characterization` pytest marker in pyproject.toml - Mark 4 tests @pytest.mark.characterization that pin current behavior to flip on the planned fixes: APPENV_EXTRAS no-dedup, diff_summary empty->empty reports "Created", diff silent-Changed when uv produces no lockfile, and read_lockfile_lines keeping an inline `#` (regression guard against over-eager simplification) - Commit the two driving impl specs under .agents/impl_specs/
…g uv.lock Fixes three src/appenv.py bugs whose correct behavior was pinned by the characterization tests added in 5231579. APPENV_EXTRAS is now deduplicated (matching the --dep path); LockFile.diff_summary reports "No changes" for empty→empty instead of "✓ Created (+0 lines)"; LockFile.diff now fails loudly with CommandError when uv exits 0 without writing uv.lock, instead of silently rendering the entire old lockfile as removed. The three characterization tests are flipped to assert the fixed contracts and their markers dropped; only the inline-hash regression guard keeps its characterization marker. - appenv_settings_from_env now wraps APPENV_EXTRAS parsing in _dedup_preserve_order (line ~2516), aligning the env-var path with the --dep path (line 1503); "dev,dev,test" -> ["dev", "test"] - LockFile.diff_summary short-circuits empty→empty to "No changes" before the is_new computation, fixing the "✓ Created (+0 lines)" fallthrough - LockFile.diff raises CommandError(returncode=1) and logs "uv-lock-no-output: reason=no-lockfile-written" when uv exits 0 without writing uv.lock (CODEX Art. 5 "Fail loudly"); previously returned "Changed" and rendered the whole old lockfile as removed - test_diff_copies_pyproject_to_tmpdir mock now writes uv.lock so it passes the new no-output guard (collateral fix; original pyproject-copy verification intent preserved) - Three characterization tests renamed/flipped to pin the new contracts: test_settings_from_env_extras_deduplicated, test_diff_summary_empty_to_empty_reports_no_changes, test_diff_when_uv_produces_no_lockfile_raises; characterization markers removed - Sibling .pyi stubs updated to match renames; test_lockfile.pyi gains the LogCaptureFixture import for the caplog assertion - Exactly one characterization marker remains: test_read_lockfile_lines_inline_hash_kept — the inline-comment regression guard, intentionally kept - Track the governing spec .agents/impl_specs/fix-smells-extras-dedup-empty-empty-diff-silent.md alongside the implementation; the code's inline comments reference it by name - Verification: ruff + ty clean, tools/check_stub_sync.py 29/29 in sync, 443 pytest pass
The "What It Preserves" list for the `reset` command described `.appenv/current` as "Version tracking" — inaccurate, since it is a legacy symlink to the active venv, not a version tracker. Readers of commands.md now see the correct purpose of the preserved entry. - Rewrite line 297 from "Version tracking in `.appenv/current/`" to "Legacy symlink to current venv at `.appenv/current`" - Description verified against `_ensure_venv_symlinks` (src/appenv.py:2131-2135), which creates `.appenv/current` as a symlink to `venv` - Categorization under "What It Preserves" remains correct: the reset() keep-set (src/appenv.py:1941) retains "current"
APPENV_VERBOSE now activates verbose only when its value is non-empty
after stripping. Previously `os.environ.get(...) is not None` made ANY
assignment truthy — including `APPENV_VERBOSE=` (empty) and
`APPENV_VERBOSE= ` (whitespace) — which silently enabled verbose
output. Callers using the documented `APPENV_VERBOSE=1` convention are
unaffected.
BREAKING: Empty/whitespace-only APPENV_VERBOSE no longer activates
verbose (CODEX Art. 4 — BREAK EVERYTHING). Python string truthiness
still applies, so "0"/"false" remain truthy; the documented convention
is `APPENV_VERBOSE=1` or `=true`.
- Replace `is not None` with `bool(os.environ.get("APPENV_VERBOSE", "").strip())`
in appenv_settings_from_env (Smell flyingcircusio#2 of the settings/lockfile audit)
- Flip the characterization test into two contract tests: 7 non-empty
cases assert verbose=True, 4 empty/whitespace cases assert verbose=False
- Sync tests/test_main.pyi stub to the renamed/added test signatures
- Add governing spec .agents/impl_specs/fix-smell-appenv-verbose-truthy.md,
referenced by the inline SPEC comment and test docstrings
- All doc/e2e references already use the unaffected `=1` convention
(commands.md, workflows.md, logging-conventions.md, dev/index.md,
.github/workflows/e2e.yml) — no doc changes required
- Verified: ruff + ty + tools/check_stub_sync.py (29/29) clean; 448
pytest pass (was 443, +5 from splitting the 6-case test into 7+4);
characterization marker count unchanged at exactly 1
All 27 implementation-spec files under .agents/impl_specs/ have been implemented and committed across earlier sessions, so they are removed as bulk cleanup. The five inline references in src/appenv.py and three test files that pointed at the now-deleted specs are edited to drop the path while preserving the substantive comment/docstring text. No executable code or behavior changes — comment and docstring edits only. - Delete 27 spec files under .agents/impl_specs/ (2510 deletions); each was implemented in a prior commit (e.g. fix-smell-appenv-verbose-truthy -> 8feb18b, fix-smells-extras-dedup-empty-empty-diff-silent -> 78512e9, refactor-subprocesserror-and-uv-is-runnable -> 21ee7b9) - Drop the .agents/impl_specs/migrate-pip-options.md reference from the index/extra-index comment in src/appenv.py (~L130), keeping the explanation of index/extra-index URL handling - Drop the .agents/impl_specs/fix-init-cli-flags.md reference from the _validate_python_version docstring in src/appenv.py (~L348), keeping the note that only format, not a minimum, is enforced - Remove the "Encodes .agents/impl_specs/fix-init-cli-flags.md:" preamble line from the tests/test_init_cli_flags.py module docstring - Remove the spec-contract sentence from the tests/test_migrate_pip_options.py module docstring, preserving the rest - Collapse tests/test_readme_init_order.py module docstring to one line after dropping its Spec: reference - Verified zero remaining references: `rg "\.agents/impl_specs" src/ tests/` exits 1 (no matches); the .agents/impl_specs/ directory is gone - Verification: the git pre-commit hook suite passed on commit — detect-private-key, ruff check, ruff format, check logging conventions, ty type check, and stub<->runtime sync (mypy.stubtest) all Passed. (doit is installed system-wide but no dodo.py/precommit task is configured for this repo, so that task gate does not apply.)
No description provided.