Skip to content

refactor(config): typed Config model with consistent CLI/env/file/SSL handling#752

Open
TheKevJames wants to merge 10 commits into
masterfrom
kjames/config-cleanup
Open

refactor(config): typed Config model with consistent CLI/env/file/SSL handling#752
TheKevJames wants to merge 10 commits into
masterfrom
kjames/config-cleanup

Conversation

@TheKevJames

Copy link
Copy Markdown
Owner

Summary

Cleans up how coveralls-python handles configuration values (CLI flags, CI-system
environment detection, COVERALLS_* env vars, .coveralls.yml, and our own
settings such as disabling SSL verification). Introduces a single typed Config
model and a resolve() pipeline, replacing the untyped self.config dict that was
scattered across api.py.

Motivation

The old design merged three unrelated kinds of value into one untyped dict and then
dumped the whole dict into the submitted JSON. This leaked client-only settings
(base_dir, src_dir, rcfile, and the newly-added timeout family) into the
payload sent to coveralls.io. State was also spread across ad-hoc attributes
(_coveralls_host, _token_required), call-time os.environ reads (SSL/host), and
positional per-service tuples, and the CLI duplicated every option across two
commands.

What changed

New coveralls/configuration.py

  • Typed Config dataclass splitting API payload fields from client-behaviour
    settings
    , with to_payload() as the enforced boundary.
  • Normalized source functions merged by resolve() in the documented precedence
    order (CI env → COVERALLS_* → config file → explicit overrides). Positional
    per-service tuples and duplicated PR-parsing removed; a shared _pr_from_path
    helper handles URL-style PR numbers.
  • token_required is AND-ed across sources (any source may waive it, none may
    re-impose it). Unknown config-file keys are dropped with a warning.

Fixes

  • Client settings no longer leak into the submitted payload.
  • _coveralls_host / _token_required / _normalize_timeouts / _request_timeout
    and the call-time SSL/host env reads folded onto Config.
  • CLI: debug subcommand collapsed into a --debug flag; the copy-pasted option
    set (incl. --version and timeout flags) defined once.
  • api.py and cli.py are now typed (mypy passes).
  • Minor: service_name KeyError sharp edge, re.IGNORECASE on a digit regex, a
    docstring typo.

Behaviour changes (consistency-driven)

One naming convention for every setting: foo_bar--foo-bar
COVERALLS_FOO_BARfoo_bar (YAML).

  • New first-class flags: --host, --skip-ssl-verify, --parallel.
  • Renamed flags --service/--basedir/--srcdir
    --service-name/--base-dir/--src-dir; old spellings kept as hidden aliases
    that emit a deprecation warning
    .
  • YAML coveralls_hosthost, config_filercfile (unknown keys warn).
  • Coveralls(config_file=...) constructor kwarg → rcfile.

Migration notes are documented in docs/usage/configuration.rst; the
BREAKING CHANGE: footers will surface in the git-cliff changelog at release.

Testing

  • New tests/api/config_test.py (Config unit tests, incl. leak regression guards)
    and tests/api/resolve_test.py (per-source resolution + precedence).
  • configuration_test.py trimmed to Coveralls integration + ensure_token.
  • Full suite: 113 passed, 1 skipped. pre-commit run -a clean.

Known follow-ups (out of scope)

  • --help still renders COMMAND [ARGS]... (Typer callback-only app is a group);
    left as-is to avoid touching the main()/error-handling contract.
  • mypy --strict flip, exception __eq__ removal, the misleading resubmission
    test, and lockfile reconciliation remain separate concerns.

Introduce a typed home for coveralls-python configuration, splitting fields
into API payload fields (emitted via to_payload) and client-behaviour settings
that must never enter the submitted JSON. Absorbs the timeout validation and
(connect, read) derivation that previously lived as ad-hoc helpers on the
Coveralls class.

Not yet wired into Coveralls/CLI; subsequent commits move the resolution logic
onto this type.
Port the CI-service detection, COVERALLS_* env vars, and .coveralls.yml
loading into normalized source functions that each return a uniform partial
config, merged by resolve() in the documented precedence order into a typed
Config.

- Replace the per-service positional (name, job, number, pr) tuples with
  partial dicts and a shared _pr_from_path helper for URL-style PR parsing.
- token_required is AND-ed across sources: any source may waive it (TravisCI,
  debug/output) but none may re-impose it.
- Unknown keys from the config file are dropped with a warning, giving a clean
  migration path for the upcoming coveralls_host/config_file YAML renames.

Not yet wired into Coveralls; that swap lands next.
…o payload

Coveralls now resolves configuration into a typed Config via resolve() instead
of building an untyped dict, and create_data() emits only Config.to_payload()
(the API payload fields). This fixes base_dir/src_dir/rcfile and the timeout
family leaking into the submitted JSON.

- Delete the load_config_from_* loaders, _normalize_timeouts, _request_timeout,
  _coveralls_host and _token_required; all now live on Config/resolve.
- ensure_token, submit_report, parallel_finish and get_coverage read typed
  fields; the 422 branch uses config.service_name (never None post-resolve),
  removing the KeyError sharp edge.
- Rename the coverage rcfile constructor kwarg config_file -> rcfile.
- Trim configuration_test.py to Coveralls integration + ensure_token; the
  per-CI/per-timeout resolution is covered by resolve_test.py/config_test.py.
The call-time os.environ reads for these were removed when Coveralls moved onto
Config; add an override-path test to lock in that host and skip_ssl_verify are
settable through any channel, not only COVERALLS_HOST/COVERALLS_SKIP_SSL_VERIFY.
…d aliases

Collapse the duplicated coveralls()/debug() commands into one command with a
--debug flag, removing the copy-pasted option set (incl. the duplicated
--version callback and timeout flags).

- Add first-class --host, --skip-ssl-verify and --parallel flags.
- Rename --service/--basedir/--srcdir to --service-name/--base-dir/--src-dir,
  following the field/env/flag naming convention; keep the old spellings as
  hidden aliases that log a deprecation warning.
- Fully type cli.py; build Config overrides via a helper that only forwards
  explicitly-set boolean flags.
Update the configuration and troubleshooting docs for the single-command CLI,
the new --host/--skip-ssl-verify/--parallel flags, and the renamed
--service-name/--base-dir/--src-dir flags (with deprecation notes). Document
the .coveralls.yml key renames and clean the leaked config_file/base_dir keys
out of the example report fixture.

BREAKING CHANGE: configuration now uses one consistent name per setting across
CLI flags, COVERALLS_* env vars and .coveralls.yml. The 'coveralls_host' and
'config_file' config-file keys are renamed to 'host' and 'rcfile'; the
--service/--basedir/--srcdir flags are renamed to --service-name/--base-dir/
--src-dir (old spellings still work but are deprecated); the 'debug' subcommand
becomes a --debug flag; and the Coveralls(config_file=...) constructor kwarg is
renamed to rcfile. Client settings (base_dir, src_dir, rcfile, host,
skip_ssl_verify, timeouts) are no longer leaked into the submitted payload.
…a warning

Mirror the deprecated CLI flag aliases for config keys: translate the renamed
coveralls_host -> host and config_file -> rcfile keys to their canonical names
(warning on use) instead of dropping them. The same rule applies to both the
config file and Coveralls() keyword arguments, since both are dicts of
user-supplied config keys.

This restores backwards compatibility for existing .coveralls.yml files and for
Coveralls(config_file=...) callers, removing those breaking changes. An
explicit canonical key in the same source still takes precedence.
@TheKevJames

Copy link
Copy Markdown
Owner Author

Update: backwards-compat for renamed config keys

Applied the same deprecated-alias-with-warning treatment (used for CLI flags) to
the renamed config keys, via a single general rule in resolve() that
translates deprecated keys to canonical ones for both the config file and
Coveralls() kwargs:

  • .coveralls.yml: coveralls_host/config_file now work again (warn) instead
    of being dropped.
  • Coveralls(config_file=...) / Coveralls(coveralls_host=...) now work again
    (warn) instead of raising TypeError.
  • An explicit canonical key in the same source still wins.

Re: "does this make it non-breaking?" — verified, partially

Key aliasing removes the common user-facing breaks. Remaining breaks after
this change:

Break Status
YAML coveralls_host/config_file dropped ✅ fixed (deprecated + warn)
Coveralls(config_file=...) TypeError ✅ fixed (deprecated + warn)
CLI --service/--basedir/--srcdir ✅ already aliased + warn
coveralls debug subcommand → --debug flag ⚠️ still breaking
cover.config is now a Config object, not a dict (.config['x'].config.x) ⚠️ still breaking (inherent to the typing goal)
Removed public load_config_from_* methods ⚠️ still breaking (unusual to call directly)

So it is not strictly non-breaking, but the realistically-impactful surfaces
(existing .coveralls.yml files and basic Coveralls() usage) are now
compatible.

Open question: the debug subcommand

Making coveralls debug non-breaking would mean re-adding it as a hidden
deprecated subcommand — which reintroduces the full option duplication this PR
deliberately removed (#4). I left it as a --debug flag. Happy to add a thin
deprecated debug subcommand if we'd rather keep that fully compatible; let me
know your preference.

Bring back 'coveralls debug' as a subcommand (the documented troubleshooting
entry point) instead of a --debug flag, while avoiding the copy-pasted
typer.Option blocks that CODE_REVIEW #4 called out.

Each option spec is defined once as a shared Annotated type alias and
referenced by both the default command and the debug subcommand, which are thin
wrappers over a common _dispatch(). Changing a flag name or help string now
updates both entry points automatically.
@TheKevJames

Copy link
Copy Markdown
Owner Author

Update: coveralls debug restored as a subcommand

Reverted the --debug flag back to the coveralls debug subcommand (the
documented troubleshooting entry point), without re-introducing the
duplicated typer.Option(...) blocks that #4 flagged.

Each option spec is now defined once as a shared Annotated type alias; the
default command and the debug subcommand are thin wrappers over a common
_dispatch(). Adding/renaming a flag or editing its help text updates both
entry points automatically, so they can't drift.

coveralls debug [OPTIONS] accepts the full option set (verified by a new
test). This also removes the debug subcommand from the earlier breaking-change
list.

service_name is now a plain str defaulting to 'coveralls-python' rather than
str | None resolved to that value later, so submit_report()'s
service_name.startswith('github') check can never hit None. The default literal
lives on the Config field (DEFAULT_SERVICE_NAME); _detect_ci() returns None
when no CI is detected and lets that default apply.
The CLI always forwarded rcfile='.coveragerc' as a highest-precedence override,
so an rcfile/config_file set in .coveralls.yml was silently ignored (while its
deprecation warning still fired, implying it took effect).

Default --rcfile to None and let resolve() drop it, matching how the other
string flags already work, so the config-file value survives. coverage.py
treats config_file='.coveragerc' and the auto-discovery default identically, so
the no-config behaviour is unchanged.
@TheKevJames

Copy link
Copy Markdown
Owner Author

Good catch — reproduced and fixed.

Root cause was exactly as described: _build_overrides always forwarded
rcfile='.coveragerc' (a non-None default), which is highest precedence in
resolve() and clobbered any rcfile/config_file from .coveralls.yml while
the deprecation warning still fired.

Fix: --rcfile now defaults to None and resolve() drops it (the same
None-forwarding the other string flags already rely on), so the config-file
value survives.

I went with the "only forward when passed" option rather than just documenting,
after verifying it's behaviour-preserving for the no-config case: coverage.py
treats config_file='.coveragerc' and its auto-discovery default (True)
identically — even a missing .coveragerc falls through to discovering
setup.cfg/tox.ini/pyproject.toml. So dropping the hardcoded .coveragerc
default changes nothing when no config is set.

Regression coverage added:

  • resolve() with a rcfile=None override no longer clobbers a
    config_file:/rcfile: set in the file.
  • CLI tests assert rcfile=None is forwarded when --rcfile is omitted.

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