Skip to content

feat: --prepare-pypi, license autodetect, CI robustness, tqdm AI safety - #2

Open
ahundt wants to merge 20 commits into
mainfrom
feature/prepare-pypi-license-autodetect
Open

ahundt wants to merge 20 commits into
mainfrom
feature/prepare-pypi-license-autodetect

Conversation

@ahundt

@ahundt ahundt commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • --prepare-pypi flag: new command that adds PyPI metadata to pyproject.toml (license, classifiers, keywords, URLs, description) and generates a SHA-pinned publish.yml OIDC workflow with a workflow_call trigger wired into the existing CI file
  • License autodetection: reads LICENSE / LICENSE.txt and maps to SPDX identifier (Apache-2.0, MIT, GPL-3.0-only, etc.) for license = {text = ...} and the matching PyPI classifier
  • CI workflow detection: content-based detection of CI vs publish workflows using _is_publish_workflow(); skips publish workflows when searching for the CI file to inject workflow_call into; uses SHA-pinned action refs in generated templates
  • Non-GitHub remote guard: _detect_github_owner_repo now returns (None, None) + WARN for GitLab/Bitbucket/Gitea instead of generating wrong github.com metadata URLs
  • tqdm AI safety: dynamic_ncols=True (adapts to terminal width on each refresh) + disable=not _is_tty where _is_tty = sys.stderr.isatty() and not NO_COLOR; zero tqdm output when stderr is piped to AI tools, CI log capture, or file redirects — prevents ANSI noise flooding AI context windows
  • uv consistency: uv run python -m pytest everywhere (README, run_all_tests.sh, error messages); uv add first in install guidance; module docstring updated from python pyuvstarter.py to pyuvstarter/uv run pyuvstarter
  • Shell test runner fix: tests/run_all_tests.sh was running test files in script mode (0 tests collected); now uses uv run python -m pytest 'tests/$test' -v
  • CI improvements: SHA-pinned actions/setup-python, Python 3.14 experimental matrix, timeout-minutes: 30, Windows $LASTEXITCODE checks, allow-prereleases: true
  • 171 tests in tests/test_pypi_readiness.py covering all new functionality (0 failures)

Files changed

File Change
pyuvstarter.py --prepare-pypi feature, license detection, tqdm fix, uv-first error messages, non-GitHub guard, classifier TODO comment
tests/test_pypi_readiness.py 171 tests (new from 0) covering --prepare-pypi, CI detection, publish detection, tqdm, GitHub URL parsing
.github/workflows/ci.yml SHA-pinned setup-python, Python 3.14 experimental, timeout, Windows exit code checks
.github/workflows/publish.yml New OIDC publish pipeline (SHA-pinned, uses ci.yml via workflow_call)
pyproject.toml Version 0.3.0, Apache-2.0 license, keywords, classifiers, project URLs
README.md --prepare-pypi documentation, uv run pytest, correct repo URLs
RELEASING.md Release checklist and PyPI OIDC setup guide
tests/run_all_tests.sh Fix script-mode runner → uv run python -m pytest

Test plan

  • CI passes on ubuntu-latest / macos-latest / windows-latest for Python 3.11–3.14
  • uv run python -m pytest tests/test_pypi_readiness.py -q → 171 passed
  • pyuvstarter --prepare-pypi generates valid pyproject.toml with license, classifiers, URLs
  • tqdm bar suppressed when stderr is piped (pyuvstarter 2>/tmp/out.txt; wc -c /tmp/out.txt → minimal bytes)
  • Non-GitHub remote (GitLab URL) → WARN logged, USERNAME placeholder in URLs, no wrong github.com metadata

ahundt added 20 commits March 8, 2026 23:20
Add --prepare-pypi flag to scaffold PyPI-ready projects:
- Patches pyproject.toml with license, classifiers, keywords,
  authors (placeholder), project URLs, and readme field
- Generates LICENSE file from canonical templates (MIT, Apache-2.0,
  GPL-3.0, BSD-3-Clause) with year/author substitution
- Creates minimal README.md template if missing
- Generates .github/workflows/publish.yml with Trusted Publisher
  OIDC (no API tokens), tag/version mismatch guard, separate
  build/publish jobs

Add --license flag (works standalone or with --prepare-pypi):
- "auto" mode detects license from existing LICENSE/LICENCE files
  (case-insensitive search) using pattern matching against
  canonical license phrases
- "custom" mode preserves unrecognized license files as-is
- Standalone --license MIT creates just the LICENSE file

Also: fix uv venv --allow-existing flag, add 34 TDD tests in
test_pypi_readiness.py, add venv edge case tests in
test_error_handling.py, update README with new flag docs.
…ELEASING.md

Fix _create_publish_workflow() to include test: job referencing ci.yml
via uses: (was missing — build ran without tests). Add shell: bash to
version check step for Windows compatibility.

Add _add_workflow_call_trigger(): injects workflow_call: into existing
ci.yml so publish.yml can reuse it. Handles edge cases: missing ci.yml,
already present, compact on: format (skips with warning).

Fix _add_pypi_toml_metadata() Changelog URL: add Changelog to urls on
fresh projects, and add it individually when urls section already exists
but Changelog key is missing (was skipping entire section).

Add _create_releasing_doc(): generates RELEASING.md with version bump
checklist and PyPI publishing section (Trusted Publishers, TestPyPI
verification, manual fallback). Appends PyPI section to existing
RELEASING.md if it lacks one, rather than skipping.

Add 9 new tests (43 total in test_pypi_readiness.py).
Bug fixes:
- GitIgnore.__init__: call super().__init__([]) so pathspec internal
  state (_backend) is initialized; delete __dict__['patterns'] to
  preserve lazy @cached_property loading
- GitIgnore.is_ignored(): fix inverted match_file semantics — return
  True when match_file returns True (file IS ignored), not the inverse
- Typer Option: remove deprecated is_flag=True from all 5 bool flags

--prepare-pypi improvements:
- Early-exit in model_post_init: --prepare-pypi now skips
  gitignore/venv/dependency discovery since it only needs file I/O
- Auto-detect GitHub owner/repo from git remote URL (SSH + HTTPS)
- Auto-detect default branch from git symbolic-ref
- Auto-detect build backend (hatchling/poetry/setuptools/flit/uv)
- Auto-detect Python version from requires-python
- publish.yml: conditional test job (only when ci.yml exists)
- RELEASING.md: adaptive build/publish commands per detected backend
- All templates use detected owner instead of USERNAME placeholder

Tests: 63 PyPI readiness tests (up from 43), 140 total (up from 120)
Replace toml.dump() full-file rewrite with format-preserving string
insertion. The old approach read with tomllib and wrote back with
toml.dump(), which destroyed:
- Multi-line array formatting (collapsed to single lines)
- Comments (stripped entirely)
- Section ordering (rearranged [tool.*] sections)
- Inline table syntax ({text = "X"} → [project.license])

New approach: use tomllib only to detect which fields are missing,
then insert TOML snippets at the correct position in the raw text
via _find_toml_section_range() helper.

Add 3 tests: format preservation, missing field insertion, section
range detection. Total: 66 PyPI readiness tests, 143 overall.
…lity

- _detect_build_backend(): return uv build/uv publish for hatchling,
  setuptools, and flit (uv handles any PEP 517 backend; keeps poetry
  specific commands)
- _create_publish_workflow(): detect outdated build commands in existing
  publish.yml and regenerate; use 'uv run --no-project python' for
  version check (tomllib is stdlib, avoids dependency resolution)
- _create_releasing_doc(): detect and replace outdated build commands
  in existing RELEASING.md; normalize PyPI project name (PEP 503)
- Help messages: replace python3 with uv run python where applicable
- Tests: update expectations, add update/skip/dry-run/normalization tests
  (152 tests pass, 9 new)
- RELEASING.md template: use 'uv pip install' instead of bare 'pip install'
  for TestPyPI verification (consistent with uv-centric approach)
- Tests: add 8 edge case tests for PEP 503 normalization (dots, mixed case,
  consecutive separators), pdm backend detection, setuptools/flit/twine
  update detection, uv pip install verification (160 tests pass)
…ness tests

## pyuvstarter.py — _prepare_pypi subsystem

### _detect_ci_workflow_name() (new, pyuvstarter.py:5370)
Detects the project's CI workflow filename by trying 11 candidates in
priority order instead of hardcoding 'ci.yml':
  ci.yml → test.yml → tests.yml → build.yml → main.yml →
  workflow.yml → workflows.yml → python.yml →
  ci.yaml → test.yaml → workflow.yaml

- workflow.yml added: GitHub's web UI names new workflows this by default
- python.yml added: GitHub's Python starter workflow template uses this name
- .yaml variants included for projects not using .yml extension

### _create_publish_workflow() fixes (pyuvstarter.py:5382)
- Removed dead ci_path variable (set but never read after refactor)
- Uses detected CI filename in generated publish.yml test: job (not hardcoded ci.yml)
- Permissions block (checks: write, pull-requests: write, issues: read) on
  the test: job that calls ci.yml via workflow_call — prevents GitHub startup_failure
- Permissions detection uses regex r'\n  [a-z][\w-]*:' for job-boundary
  instead of fragile split("build:") which broke on build-docs: job names
- needs_update detection covers missing permissions block (was only checking
  outdated build commands)

### _add_workflow_call_trigger() fixes (pyuvstarter.py:~5530)
- SKIP log now uses f"{ci_filename} already has workflow_call trigger."
  instead of hardcoded "ci.yml already has workflow_call trigger."
- SUCCESS log now uses f"Added workflow_call trigger to {ci_filename}."
  instead of hardcoded "Added workflow_call trigger to ci.yml."

## tests/test_pypi_readiness.py — 94 tests (was 85)

New/updated tests:
- test_publish_yml_has_test_job_when_ci_exists: +3 assertions verifying
  permissions: block (checks: write, pull-requests: write) follows uses: line
- test_detect_ci_workflow_name_returns_ci_yml_first: priority ordering
- test_detect_ci_workflow_name_falls_back_to_test_yml: fallback behavior
- test_detect_ci_workflow_name_returns_none_when_absent: None case
- test_publish_yml_references_detected_ci_filename: test.yml → publish uses test.yml
- test_add_workflow_call_trigger_injects_into_test_yml: injection into test.yml
- test_publish_yml_permissions_detection_not_fooled_by_build_docs_job:
  regex guard against split("build:") false-matching build-docs: job names
- test_detect_ci_workflow_name_falls_back_to_workflow_yml: workflow.yml detection
- test_detect_ci_workflow_name_falls_back_to_python_yml: python.yml detection
- test_publish_yml_references_workflow_yml_when_detected: workflow.yml → publish uses it

## .github/workflows/publish.yml (new)
pyuvstarter's own 4-job OIDC pipeline: test (reuses ci.yml) → build → publish-testpypi → publish-pypi
- All action SHAs pinned (checkout v4, setup-uv v7.3.1, upload/download-artifact v4, gh-action-pypi-publish)
- test: job has permissions: block to prevent startup_failure
- Tag-version vs pyproject.toml version verification step

## .github/workflows/ci.yml changes
- workflow_call: trigger added (line 8) so publish.yml can reuse ci.yml
- SHA-pinned actions/checkout (34e114876b0b11c390a56381ad16ebd13914f8d5 # v4)
- NO_COLOR=1 env var suppresses Rich ANSI codes for stable test assertions
- dorny/test-reporter (d61b558 # v1) replaces EnricoMi/publish-unit-test-result-action
- Windows test step: adds uv run python -m pytest tests/ --junitxml=test_results_pytest_windows.xml
  so dorny/test-reporter gets XML output on Windows (was Linux-only before)

## RELEASING.md (new)
Release checklist: version bump → test → commit → reinstall → verify → tag → push
PyPI Trusted Publisher setup instructions, TestPyPI verification, troubleshooting.

All 171 tests pass: uv run python -m pytest tests/ -q
YAML validated: python3 -c "import yaml; yaml.safe_load(open('...'));" for both workflows
…(C4-C7)

## pyuvstarter.py

### C4: Content-based publish workflow exclusion (_is_publish_workflow)
- Add _CI_WORKFLOW_CANDIDATES tuple (13 candidates: ci.yml, test.yml, tests.yml,
  build.yml, main.yml, workflow.yml, workflows.yml, python.yml, python-app.yml,
  python-package.yml, ci.yaml, test.yaml, workflow.yaml)
- Add _CI_CANDIDATES_STR module-level constant (pre-joined for WARN messages)
- Add _is_publish_workflow(content: str) -> bool with 3-tier detection:
  Tier 1: pypa/gh-action-pypi-publish substring (definitive)
  Tier 2a: environment: pypi/testpypi regex — allows inline comment (pypi-staging safe)
  Tier 2b: name: pypi/testpypi dict-form regex with EOL anchor
  Tier 3: run: step with twine upload or uv publish — regex handles both
    "- run: cmd" (YAML list item) and "    run: cmd" (multi-key step) forms
    via ^\s*(?:-\s+)?run:\s pattern
- Update _detect_ci_workflow_name() to use _CI_WORKFLOW_CANDIDATES constant
  and call _is_publish_workflow() on each candidate; skips publish workflows
  (e.g. build.yml containing pypa/gh-action-pypi-publish) so existing release
  automation is never corrupted by workflow_call injection

### C5: Fix DRY RUN log hardcoded "ci.yml"
- _add_workflow_call_trigger: DRY RUN log now uses f"{ci_filename}" instead of
  hardcoded "ci.yml" — reflects actual detected filename (test.yml, build.yml, etc.)

### C6: Fix WARN candidate lists
- _create_publish_workflow WARN: uses _CI_CANDIDATES_STR (was 5 names, now all 13)
- _add_workflow_call_trigger INFO: same fix

### C7: SHA-pin all action refs in generated publish.yml template
- actions/checkout@34e1148 # v4
- astral-sh/setup-uv@5a095e7 # v7
- actions/upload-artifact@ea165f8 # v4
- actions/download-artifact@d3f86a1 # v4
- pypa/gh-action-pypi-publish@ed0c539 # release/v1
  (matches SHAs already used in pyuvstarter's own .github/workflows/publish.yml)

## tests/test_pypi_readiness.py (+19 tests, 113 total in file)

### _is_publish_workflow tests (15 new)
- test_is_publish_workflow_detects_pypa_action (Tier 1)
- test_is_publish_workflow_detects_pypa_action_sha_pinned (Tier 1 SHA form)
- test_is_publish_workflow_detects_pypi_environment_single_value (Tier 2a)
- test_is_publish_workflow_detects_pypi_environment_with_inline_comment (Tier 2a + comment)
- test_is_publish_workflow_does_not_match_pypi_staging (Tier 2a negative)
- test_is_publish_workflow_detects_dict_form_environment (Tier 2b)
- test_is_publish_workflow_dict_form_does_not_match_job_display_name (Tier 2b negative)
- test_is_publish_workflow_detects_twine_upload_in_run_step (Tier 3)
- test_is_publish_workflow_detects_python_m_twine_upload (Tier 3 python -m form)
- test_is_publish_workflow_twine_in_comment_does_not_match (Tier 3 comment negative)
- test_is_publish_workflow_detects_uv_publish_in_run_step (Tier 3 uv publish)
- test_is_publish_workflow_uv_publish_in_comment_does_not_match (Tier 3 comment negative)
- test_is_publish_workflow_uv_build_does_not_match (uv build ≠ uv publish)
- test_is_publish_workflow_returns_false_for_ci_content (CI content negative)
- test_is_publish_workflow_empty_content_returns_false (empty file negative)

### Content-based CI exclusion tests (2 new)
- test_detect_ci_workflow_name_skips_publish_workflow_named_build
  (build.yml with pypa action → returns test.yml, not build.yml)
- test_detect_ci_workflow_name_skips_workflow_with_uv_publish
  (workflow.yml with uv publish → returns ci.yml)

### DRY RUN log tests (2 new, use mock.patch since _log_action uses Rich Console)
- test_add_workflow_call_trigger_dry_run_does_not_modify_file
- test_add_workflow_call_trigger_dry_run_log_uses_ci_filename

190 total tests pass (was 171).
…y (+32)

## _is_publish_workflow: additional Tier 3 variants and full-content tests

- test_is_publish_workflow_detects_python3_m_twine_upload
  (python3 -m twine — whole-word regex matches regardless of python version)
- test_is_publish_workflow_detects_uv_run_twine_upload
  (uv run twine upload — whole-word match)
- test_is_publish_workflow_detects_twine_upload_with_flags
  (--skip-existing, --repository flags still detected)
- test_is_publish_workflow_twine_check_does_not_match
  (twine check has no 'upload' word — correct negative)
- test_is_publish_workflow_detects_compound_uv_build_and_publish
  (uv build && uv publish on one line — detected)
- test_is_publish_workflow_multiline_run_block_not_caught_by_tier3
  (documents design limitation: multi-line run: | block not caught by Tier 3)
- test_is_publish_workflow_multiline_run_caught_by_tier1
  (same workflow caught when Tier 1 tell also present — safety net confirmed)
- test_is_publish_workflow_uv_publish_with_env_vars
  (UV_PUBLISH_TOKEN=${{ secrets.PYPI_TOKEN }} uv publish — detected)
- test_is_publish_workflow_complete_realistic_publish_yaml
  (full 14-line publish workflow YAML — all 3 tiers match)
- test_is_publish_workflow_complete_realistic_ci_yaml
  (full 14-line CI workflow YAML — correctly returns False)
- test_is_publish_workflow_tier2a_crlf_line_endings (Windows CRLF handled)
- test_is_publish_workflow_tier2b_crlf_line_endings (dict-form CRLF handled)
- test_is_publish_workflow_pypi_in_workflow_name_does_not_match
  ("name: Run pypi tests", "name: pypi-tests" — EOL anchor protects both)

## _detect_ci_workflow_name: remaining candidates and edge cases

- test_detect_ci_workflow_name_falls_back_to_tests_yml (tests.yml plural)
- test_detect_ci_workflow_name_falls_back_to_python_app_yml (python-app.yml)
- test_detect_ci_workflow_name_falls_back_to_python_package_yml (python-package.yml)
- test_detect_ci_workflow_name_falls_back_to_ci_yaml (.yaml extension)
- test_detect_ci_workflow_name_yml_takes_priority_over_yaml (ci.yml > ci.yaml)
- test_detect_ci_workflow_name_all_candidates_are_publish_workflows → None
- test_detect_ci_workflow_name_ci_yml_is_publish_falls_back_to_test_yml
- test_detect_ci_workflow_name_nonexistent_directory_returns_none
- test_detect_ci_workflow_name_skips_unreadable_file
  (OSError on read skipped via mock.patch; next candidate returned)

## _add_workflow_call_trigger: all remaining paths

- test_add_workflow_call_trigger_skips_when_already_present (idempotent)
- test_add_workflow_call_trigger_warns_when_no_on_block (WARN + no-op)
- test_add_workflow_call_trigger_warns_on_compact_on_format (WARN + no-op)
- test_add_workflow_call_trigger_handles_quoted_on_block ("on": form injected)
- test_add_workflow_call_trigger_returns_true_when_no_ci_found (graceful no-op)

## _create_publish_workflow: SHA pins, structural validity, params

- test_publish_yml_contains_sha_pinned_actions (all 5 SHA refs verified)
- test_publish_yml_structural_validity_with_ci (all job sections present)
- test_publish_yml_structural_validity_without_ci (no uses: ref when no CI)
- test_publish_yml_valid_yaml_with_pyyaml (skips if pyyaml not installed)
- test_publish_yml_uses_custom_python_version (python_version param)
- test_publish_yml_uses_custom_build_cmd (build_cmd param)

Total: 145 tests pass, 1 skipped (pyyaml optional) in test_pypi_readiness.py
Full suite: 222 passed, 1 skipped.
## pyproject.toml

Add [dependency-groups] (PEP 735) with dev dependencies:
  pytest>=7.0  — test runner (was manually installed, not declared anywhere)
  pyyaml>=6.0  — YAML validation in tests (was missing, causing 1 skipped test)

Neither is a runtime dependency of pyuvstarter itself:
- pyuvstarter reads YAML files as plain text via regex (no import yaml at runtime)
- pytest is only needed to run the test suite

## uv.lock

Updated by `uv sync` to include pytest (9.0.2) and pyyaml (6.0.3) in the
dev dependency group. Both are now resolved and locked.

## .github/workflows/ci.yml (Linux "Run unit tests" step)

The previous implementation ran tests via `uv run "$test_file"` (executing each
test_*.py as a plain Python script). Since test_pypi_readiness.py has no
`if __name__ == "__main__"` block, ZERO tests were actually executed on Linux CI
— all runs silently passed with no assertions checked.

Replace the per-file loop with:
  uv run python -m pytest tests/ --junitxml=test_results_pytest.xml --tb=short

This runs all 223 tests properly via pytest and produces JUnit XML for
dorny/test-reporter. Windows CI already used this form (line 302).

`uv sync` (run before tests at line 65) now installs the dev group automatically
(uv includes all dependency-groups in sync by default), so pytest and pyyaml
are available in CI without any additional install step.

Result: test_publish_yml_valid_yaml_with_pyyaml now passes (was 1 skipped),
full suite is 223 passed, 0 skipped.
pyuvstarter.py — 11 targeted fixes:
- C1: _prepare_pypi_metadata post-check calls _detect_ci_workflow_name() instead
  of hardcoding ci.yml path, eliminating false WARN when test.yml is the CI workflow
- C3: _create_publish_workflow detects stale CI references when ci_filename is None
  (regex r'uses:.+/.github/workflows/\S+\.ya?ml' triggers needs_update=True)
- H3: _is_publish_workflow Tier 2b regex requires >=2 spaces indentation
  ('^  +name:\s*(pypi|testpypi)') preventing false positive on top-level 'name: pypi'
- H4: _CI_WORKFLOW_CANDIDATES tuple expanded with 7 missing .yaml variants:
  tests.yaml, build.yaml, main.yaml, workflows.yaml, python.yaml,
  python-app.yaml, python-package.yaml
- H5: _create_releasing_doc gains ci_filename parameter; caller _prepare_pypi_metadata
  passes detected_ci or 'ci.yml' so generated RELEASING.md names real CI workflow
- H6: _add_pypi_toml_metadata generates classifiers for all Python versions
  from requires-python minimum up through 3.14 (loop over _KNOWN_PYTHON_VERSIONS)
- M2: _add_pypi_toml_metadata checks 'license-files' not in project (PEP 639)
  before inserting license field, preventing duplicate license keys
- M3: _add_pypi_toml_metadata detects [tool.poetry] and logs WARN + returns True
  instead of silently failing with 'Could not find [project] section' error
- M4: keywords generated as empty list with TODO comment instead of splitting
  project name into low-quality word fragments
- M5: _create_publish_workflow backs up publish.yml.bak_TIMESTAMP before
  regenerating, preserving any custom jobs the user added
- L2: _create_releasing_doc TestPyPI install command uses {pypi_name} not {name}
  (prevents install failure when project name contains uppercase or underscores)

pyproject.toml — add missing PyPI publish metadata:
- license = {text = "Apache-2.0"} (matches LICENSE file content)
- keywords = ["uv", "python", "project-setup", "scaffolding", "pyproject", ...]
- classifiers with Development Status, License, OS, Python 3.11-3.13, Topic entries
- [project.urls] with Homepage, Repository, Issues, Changelog pointing to athundt/
- Removed template comments from version and requires-python lines

README.md — fix username and add feature docs:
- Replace all ahundt/pyuvstarter -> athundt/pyuvstarter (badges, clone/install URLs)
- Add 'Publishing to PyPI (--prepare-pypi)' section documenting what the flag
  generates (LICENSE, README, pyproject metadata, publish.yml, RELEASING.md),
  usage examples, and PyPI Trusted Publisher setup reference

.github/workflows/ci.yml — security and reliability:
- SHA-pin actions/setup-python@v5 -> @a26af69be951a213d495a4c3e4e4022e16d87065
- Add allow-prereleases: true to setup-python step
- Add Python 3.14 experimental: true matrix variable + continue-on-error
- Add timeout-minutes: 30 to test job to prevent 6-hour runner hangs
- Fix run_all_tests.sh: chmod before -x check (was silently skip if not executable)

tests/test_pypi_readiness.py — 13 new tests, 1 updated:
- test_prepare_pypi_no_spurious_ci_warning_with_test_yml (M6)
- test_is_publish_workflow_top_level_workflow_name_pypi_does_not_match (M7)
- test_is_publish_workflow_indented_environment_name_pypi_matches (M7)
- test_is_publish_workflow_two_space_indented_name_pypi_matches (M7)
- test_add_pypi_toml_metadata_skips_license_when_license_files_present (M8)
- test_add_pypi_toml_metadata_handles_poetry_format_gracefully (M9)
- test_detect_ci_workflow_name_falls_back_to_{tests,build,main,workflows,python,
  python_app,python_package}_yaml x7 (M10)
- test_prepare_pypi_keywords_placeholder: updated to expect empty list + TODO comment
Total: 236 tests pass (was 223)
Add tests for all _CI_WORKFLOW_CANDIDATES that lacked coverage:
- test_detect_ci_workflow_name_falls_back_to_main_yml
- test_detect_ci_workflow_name_falls_back_to_workflows_yml
- test_detect_ci_workflow_name_falls_back_to_test_yaml
- test_detect_ci_workflow_name_falls_back_to_workflow_yaml

All 4 use the _make_ci_wf_dir() helper defined in the same file.

Total: 163 tests in test_pypi_readiness.py (was 159).
All pass in 0.55s.
Progress bar (P1):
- pyuvstarter.py:1425-1440: replace ncols=80 with dynamic_ncols=True
  (tqdm re-queries shutil.get_terminal_size() on each refresh; adapts to
  terminal width and resize)
- Add disable=not _is_tty where _is_tty = sys.stderr.isatty() and not NO_COLOR;
  zero tqdm output when stderr is piped to AI tools, file redirects, or CI
  without PTY — prevents ANSI noise flooding AI context windows

uv consistency (P3, P4, P8):
- README.md:418-425: bare python -m pytest -> uv run python -m pytest
- pyuvstarter.py:420-437: uv add first in typer/pydantic install error messages
  (uv pip install as fallback, bare pip removed)
- pyuvstarter.py:66-87: module docstring python pyuvstarter.py -> pyuvstarter /
  uv run pyuvstarter
- pyuvstarter.py:287-320: version error messages use uv run pyuvstarter instead
  of uv run python pyuvstarter.py

Shell test runner (P2):
- tests/run_all_tests.sh:285: uv run 'tests/$test' (script mode, 0 tests
  collected) -> uv run python -m pytest 'tests/$test' -v (pytest mode)

Non-GitHub remote guard (P5):
- pyuvstarter.py:4960-4970: _detect_github_owner_repo now checks
  "github.com" not in url early; returns (None, None) + WARN for GitLab,
  Bitbucket, Gitea — prevents wrong github.com URLs in generated [project.urls]

Windows CI exit codes (P6):
- .github/workflows/ci.yml:284-291: add $LASTEXITCODE checks after
  pyuvstarter --help and uv tool run pyuvstarter . in Windows step

Generated classifier TODO (P7):
- pyuvstarter.py:5228-5235: prepend "# TODO: Update Development Status..."
  comment above classifiers block in generated pyproject.toml

New tests (8 tests in tests/test_pypi_readiness.py):
- test_tqdm_disabled_when_stderr_not_tty: AST check for isatty + disable=not
- test_tqdm_has_dynamic_ncols_not_hardcoded_ncols: AST check for dynamic_ncols
- test_tqdm_disabled_when_no_color_set: AST check for NO_COLOR guard
- test_detect_github_owner_repo_github_ssh_url: git@github.com SSH parse
- test_detect_github_owner_repo_github_https_url: https://github.com parse
- test_detect_github_owner_repo_non_github_returns_none: GitLab -> (None,None)
- test_detect_github_owner_repo_bitbucket_returns_none: Bitbucket -> (None,None)
- test_generated_classifiers_include_development_status_todo_comment: TODO comment present

All 171 tests pass (tests/test_pypi_readiness.py).
…pytest

test_extraction_fix.py is a legacy script-mode test (has main() and
__main__ block, zero pytest-style test functions). The previous commit
changed the runner to use uv run python -m pytest for all PYTHON_TESTS,
which caused pytest to exit with code 5 (no tests collected) for this
file, making run_all_tests.sh report it as FAILED.

Fix: split into two lists in run_all_tests.sh:
- PYTHON_TESTS: pytest-based files -> uv run python -m pytest 'tests/$test'
- SCRIPT_TESTS: script-mode files  -> uv run python 'tests/$test'

test_extraction_fix.py moved to SCRIPT_TESTS.

This restores CI green on macos-latest py3.13 and ubuntu-latest py3.14,
where run_all_tests.sh was failing with COMPREHENSIVE_EXIT_CODE=1.
These files have a main()/__main__ block but zero pytest-style def test_*
functions, so uv run python -m pytest exits with code 5 (no tests collected)
causing run_all_tests.sh to report them as FAILED:
- test_import_fixing.py
- test_jupyter_pipeline.py
- test_mixed_package_availability.py
- test_wheel_unavailability.py
- test_utils.py

Moved to SCRIPT_TESTS (run via uv run python tests/$test).
Remaining in PYTHON_TESTS (have def test_* functions, run via pytest):
- test_dependency_migration.py (1 pytest test)
- test_project_structure.py (1 pytest test)
- test_configuration.py (9 pytest tests)
- test_cross_platform.py (8 pytest tests)
- test_error_handling.py (12 pytest tests)
Two failures in dorny/test-reporter Publish Test Results step:

1. Windows: step runs before Run unit tests (Windows) in step order,
   so test_results_pytest_windows.xml does not exist yet. dorny default
   fail-on-empty: true causes the step to fail.
   Fix: add fail-on-empty: false.

2. macOS: test_results.xml generated by create_demo2.sh contains
   unquoted attribute values (invalid XML). dorny fails with
   "Error: Unquoted attribute value" even with fail-on-error: false
   because that option only covers test assertion failures, not XML
   parse errors.
   Fix: narrow path glob from test_results*.xml to
   test_results_pytest*.xml so only valid pytest-generated JUnit XML
   (test_results_pytest.xml, test_results_pytest_windows.xml) is
   published. Excludes test_results.xml from create_demo2.sh.
Three AST-based tqdm tests use open("pyuvstarter.py").read() which
on Windows defaults to cp1252 encoding. pyuvstarter.py contains UTF-8
emoji characters (0x81, etc.) that cp1252 cannot decode, causing:

  UnicodeDecodeError: 'charmap' codec can't decode byte 0x81

Failing tests on Windows:
- test_tqdm_disabled_when_stderr_not_tty
- test_tqdm_has_dynamic_ncols_not_hardcoded_ncols
- test_tqdm_disabled_when_no_color_set

Fix: open("pyuvstarter.py", encoding="utf-8") in all three tests.
Two pre-existing Windows CI failures fixed:

1. test_utils.py:370 subprocess.run(..., text=True) uses Windows default
   encoding cp1252 to decode pyuvstarter stdout/stderr. pyuvstarter emits
   emoji output (🚀, ✅, ❌, 📁, etc.) which cp1252 cannot decode,
   causing UnicodeDecodeError → CompletedProcess.stdout=None →
   AttributeError: 'NoneType' object has no attribute 'lower' in 12 tests:
   test_configuration.py (7), test_dependency_migration.py (1),
   test_jupyter_pipeline.py (3), test_project_structure.py (1).
   Fix: add encoding="utf-8" to subprocess.run().

2. test_cross_platform.py:457 test_special_characters_in_filenames creates
   file"with"double"quotes.py which is illegal on Windows NTFS (double-quote
   forbidden in filenames), causing OSError: [Errno 22] Invalid argument.
   Fix: remove the double-quote filename from the list; add Windows runtime
   filter for any remaining NTFS-forbidden characters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant