refactor(config): typed Config model with consistent CLI/env/file/SSL handling#752
refactor(config): typed Config model with consistent CLI/env/file/SSL handling#752TheKevJames wants to merge 13 commits into
Conversation
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.
Update: backwards-compat for renamed config keysApplied the same deprecated-alias-with-warning treatment (used for CLI flags) to
Re: "does this make it non-breaking?" — verified, partiallyKey aliasing removes the common user-facing breaks. Remaining breaks after
So it is not strictly non-breaking, but the realistically-impactful surfaces Open question: the
|
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.
Update:
|
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.
|
Good catch — reproduced and fixed. Root cause was exactly as described: Fix: I went with the "only forward when passed" option rather than just documenting, Regression coverage added:
|
_from_file() relies on 'yaml.safe_load(content) or {}' to treat an empty or
comment-only .coveralls.yml (which safe_load()s to None) as no config; the old
code crashed with a TypeError on None. Lock this in for empty, blank-line, and
comment-only files.
|
Added |
…field service_name was special-cased as an explicit __init__ param and re-injected into overrides, while every other Config field flows through **kwargs. It is always passed by keyword, so drop the special case and let it flow through kwargs; resolve()'s None-drop replaces the old truthy guard. token_required stays explicit: it is the documented positional argument the CLI passes as Coveralls(token_required, **overrides), so it genuinely cannot collapse into **kwargs.
|
Collapsed — Kept |
Generic CI parsed CI_PULL_REQUEST with NUMBER_REGEX (trailing integer) while
circle/jenkins used a separate split('/')[-1] helper with no numeric
validation -- two semantics for the same value (both read CI_PULL_REQUEST).
Replace _pr_from_path with a single _parse_pr_number using the Coveralls-
documented trailing-integer semantic, shared by the generic, circle and jenkins
loaders. For the real-world URL/int inputs this is identical; it additionally
rejects non-numeric trailing segments rather than forwarding them verbatim.
|
Unified rather than commented, since consolidating PR-parsing is a stated goal of this PR. Both callers of the old For real-world inputs (bare int / |
Summary
Cleans up how coveralls-python handles configuration values (CLI flags, CI-system
environment detection,
COVERALLS_*env vars,.coveralls.yml, and our ownsettings such as disabling SSL verification). Introduces a single typed
Configmodel and a
resolve()pipeline, replacing the untypedself.configdict that wasscattered 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 thepayload sent to coveralls.io. State was also spread across ad-hoc attributes
(
_coveralls_host,_token_required), call-timeos.environreads (SSL/host), andpositional per-service tuples, and the CLI duplicated every option across two
commands.
What changed
New
coveralls/configuration.pyConfigdataclass splitting API payload fields from client-behavioursettings, with
to_payload()as the enforced boundary.resolve()in the documented precedenceorder (CI env →
COVERALLS_*→ config file → explicit overrides). Positionalper-service tuples removed; a single
_parse_pr_numberhelper (trailing-integer)now serves the generic, circle and jenkins loaders.
token_requiredis AND-ed across sources (any source may waive it, none mayre-impose it). Unknown config-file keys are dropped with a warning.
Fixes / internal
_coveralls_host/_token_required/_normalize_timeouts/_request_timeoutand the call-time SSL/host env reads folded onto
Config.Annotatedoption aliases;coveralls(default) andcoveralls debugare thin wrappers over a shared_dispatch(), removing thecopy-pasted option blocks while keeping the
debugsubcommand.api.pyandcli.pyare now typed (mypy passes).service_nameKeyError sharp edge,re.IGNORECASEon a digit regex, adocstring typo.
Behaviour changes (for the changelog)
Naming convention introduced: every setting
foo_baris spelled--foo-bar(CLI),
COVERALLS_FOO_BAR(env), andfoo_bar(YAML).Breaking
Coveralls.configis now a typedConfigobject rather than a dict:cover.config['x']→cover.config.x.load_config/load_config_from_*methods were removed (logicmoved to
configuration.resolve).Coveralls.__init__signature is now(token_required=True, **kwargs);service_nameis no longer a dedicated positional/keyword param (still acceptedvia
**kwargs), soCoveralls(True, 'my-service')positionally no longer works.(
base_dir,src_dir,rcfile/config_file,host,skip_ssl_verify, thetimeout family) or unrecognised
.coveralls.ymlkeys, all of which werepreviously merged into it.
Deprecated (still work, now emit a warning)
--service→--service-name,--basedir→--base-dir,--srcdir→--src-dir..coveralls.ymlandCoveralls()kwargs)coveralls_host→host,config_file→rcfile.New
--host,--skip-ssl-verify,--parallel.hostandskip_ssl_verifyare now first-class settings configurable throughevery channel (previously
skip_ssl_verifywas env-only and read at call time;hostwas env + an undocumented kwarg).Other observable changes
--rcfileno longer defaults to.coveragerc; when unset, coverage.py'sstandard auto-discovery is used. (coverage.py treats an explicit
.coveragercand auto-discovery identically — including discovering
pyproject.tomletc. —so this is functionally equivalent, but the value we pass / advertised default
changed.) This also fixed a bug where the CLI's
.coveragercdefault clobberedan
rcfile/config_fileset in.coveralls.yml..coveralls.ymlis now treated as no config instead ofraising
TypeError.generic CI; a non-numeric trailing value yields no PR number instead of being
forwarded verbatim.
coverallslogger.Testing
tests/api/config_test.py(Config unit tests, incl. payload-leak regressionguards) and
tests/api/resolve_test.py(per-source resolution + precedence, PRparsing, deprecated keys, empty-file handling).
configuration_test.pytrimmed to Coveralls integration +ensure_token.pre-commit run -aclean.Open question
To get the breaking changes above to render as
[**breaking**]in the git-cliffchangelog, the relevant commit subjects need a
!(e.g.refactor(api)!: ...).That means amending already-pushed commits and force-pushing this branch — happy
to do so if you're OK with a force-push here, or to squash-merge with a curated
message instead. Let me know which you'd prefer.
Known follow-ups (out of scope)
--helpstill rendersCOMMAND [ARGS]...(Typer group); left as-is.--strictflip, exception__eq__removal, the misleading resubmissiontest, and lockfile reconciliation remain separate concerns.