Enable the PYI, RET and RSE ruff rule sets and fix the resulting
violations across the codebase. The pylint no-else-* checks
(RET505-508) are now disabled since ruff covers them.
The cleanups are mechanical:
- RSE102: drop empty parentheses from `raise Exception()` when no
arguments are passed.
- RET505-508: drop `else` branches that follow a `return`, `raise`,
`continue` or `break`, flattening control flow.
- RET502/504: add explicit return values and remove redundant
assign-then-return patterns.
- PYI030/032/041: tidy up type annotations (collapse literal unions,
use `object` for `__eq__`/`__ne__`, drop redundant numeric unions).
Turn on the PTH rule set in ruff and convert the remaining call sites
that triggered violations:
- `supervisor/utils/yaml.py` and `tests/homeassistant/test_module.py`
use `Path.open()` instead of the builtin `open()`.
- `tests/backups/test_backup.py` and
`tests/resolution/fixup/test_store_execute_reset.py` use
`Path.iterdir()` instead of `os.listdir()`.
- `tests/mounts/test_mount.py` uses `Path.unlink()` instead of
`os.remove()`.
- `tests/store/test_translation_load.py` uses `Path.mkdir(parents=True)`
instead of `os.makedirs()`.
- `tests/hardware/test_disk.py` captures the unpatched `Path.is_dir`,
`Path.is_symlink` and `Path.stat` before the `patch.object(Path, ...)`
block so the mocks can delegate to the real implementations without
reaching for `os.path.isdir`/`os.path.islink`/`os.stat`.
* tests: enable flake8-pytest-style (PT) ruff rules
Enable the `PT` ruff rule set and fix the resulting violations across the
test suite:
- PT006: pass parametrize argument names as tuples instead of a single
comma-separated string.
- PT022: switch fixtures that have no teardown from `yield` to `return`
so the lack of cleanup is obvious at a glance.
- PT011: add `match=` to broad `pytest.raises(ValueError)` blocks so the
expected error is anchored to a specific message.
- PT012: hoist setup (patches, branching) out of `pytest.raises()`
blocks so only the call that is expected to raise remains inside.
- PT013: replace `from pytest import X` with `import pytest` and access
attributes via the module.
- PT015: replace `try/except` + `assert False` patterns with
`pytest.raises(...)`.
- PT017: replace `assert` on exceptions inside `except` blocks with
`pytest.raises(...) as exc_info` and assert on `exc_info.value`.
No behavioral changes to the tests; the full suite still passes.
* tests: address review feedback on PT ruff rule enablement
- Fix fixture return-type annotations after switching `yield` to `return`
in tests/conftest.py: drop the `Generator[...]`/`AsyncGenerator[...]`
wrapper for `dns_manager_service`, `supervisor_internet`, `websession`,
and `mock_update_data` so the annotation matches what the fixture
actually returns.
- Correct the return-type annotation of `fixture_ip6config_service` from
`IP4ConfigService` to `IP6ConfigService`.
- Fix recurring "excepiton" typo in tests/utils/test_exception_helper.py.
* tests: verify backup cleanup on permission error
After `test_new_backup_permission_error` raises `BackupPermissionError`,
assert that no tarfile was left behind and `tmp_path` is empty. The
previous version only checked that the exception was raised, which
missed any regression where a partial tarfile would survive the failed
create.
* tests: rename DNS_GOOD_V6 to DNS_V6_UNSUPPORTED
The constant was named "good" but its tests assert that the URLs are
rejected by the DNS validator. The IPv6 URLs are well-formed but
currently rejected because IPv6 doesn't work with the Docker network
(see `dns_url` in supervisor/validate.py). Rename the constant and the
related test to make the intent obvious.
* Drop obsolete eager_start type ignore in CoreSys.create_task
The asyncio.AbstractEventLoop.create_task signature only gained the
eager_start keyword in the 3.14 typeshed. Now that Supervisor targets
3.14, the inline # type: ignore is redundant; mypy --warn-unused-ignores
flags it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop redundant int | None casts in OS slot status parsing
SlotStatusDataType already declares size, installed.count, and
activated.count as NotRequired[int], so .get(...) returns int | None
directly. The cast(int | None, ...) wrappers were no-ops; mypy
--warn-redundant-casts flags them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop unreachable scheduler else branch
_Task.interval is annotated float | time; the float|int isinstance
arm narrows to time on the else branch, making the prior elif and
the trailing _LOGGER.critical fallback dead code that mypy
--warn-unreachable flags. Collapse to a plain else for the time arm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Initialize credentials before pull_image try block
If _get_credentials raises before assigning, the except DockerError
handler at line 380-382 references an unbound credentials local. Mypy
--enable-error-code=possibly-undefined flags this path. Initialize to
an empty dict beforehand so the unauthorized branch reads as "no
credentials known", matching the auth=credentials or None semantics on
the success path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Use for/else to bind repository_file before .exists() check
If FILE_SUFFIX_CONFIGURATION is ever empty the loop body never runs
and repository_file is referenced unbound. Mypy
--enable-error-code=possibly-undefined flags it. The duplicate
.exists() check after the loop was also redundant: every loop body
either breaks on a hit or falls through to a final miss. Restructure
as for/else so the "no candidate matched" case is handled directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Make match statements exhaustive with explicit case _
mypy --enable-error-code=exhaustive-match flags match blocks that fall
through to a trailing return/raise without a wildcard arm. The
behaviour is unchanged; this just hoists the default into the match so
the compiler can verify all cases are handled and future enum
additions are caught.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tighten mypy: warn-unreachable, redundant casts, unused ignores
Add a [tool.mypy] section so the existing
mypy --ignore-missing-imports supervisor invocation in CI now also
enforces:
warn_unreachable — flags dead code, e.g. else branches the
type system has already eliminated.
warn_redundant_casts — flags cast() calls whose target type
already matches the source.
warn_unused_ignores — flags # type: ignore comments that no
longer suppress anything (typeshed
updates make these accumulate silently).
exhaustive-match — flags match statements without a default
arm, so future enum additions surface
as type errors instead of silent
fallthroughs.
possibly-undefined — flags references to names whose binding
depends on a path that may not run
(e.g. for-loop variables when the
iterable is empty).
ignore_missing_imports moves into the config so future invocations
don't need the CLI flag, but the workflow command is left unchanged
to keep the cache key stable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Disable import error codes instead of ignoring missing imports
Match the pattern used in Home Assistant Core's mypy config: use
disable_error_code = ["import-not-found", "import-untyped"] rather
than ignore_missing_imports. This drops the (now redundant)
--ignore-missing-imports flag from the CI invocation as well.
The semantics are equivalent for the five untyped third-party deps
(pyudev, log_rate_limit, pulsectl, cpe, atomicwrites), but expressed
as suppressing two specific error codes rather than disabling all
import resolution checks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Use match/case for scheduler interval dispatch
Express the float-vs-time dispatch as a match statement rather than
isinstance chain. Mypy treats case int() | float() and case time() as
exhaustive over the float | time annotation, so the new
exhaustive-match check is satisfied without a wildcard arm — and any
future broadening of the type will surface here as a static error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Hoist _get_credentials above pull_image try/finally
Per review feedback: _get_credentials only does dict reads against
internal config state and cannot raise DockerRegistryRateLimitExceeded
or aiodocker.DockerError, so it does not need to live inside the
try block. Lift it out, and place it before the bus listener
registration so a future raise site can't leak a stale listener
through the finally cycle.
This supersedes the earlier credentials: dict = {} initializer; the
local is now bound on every path that reaches the except handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Enumerate MountUsage members in container_where match
Replace the case _: catch-all with case MountUsage.BACKUP | None:.
The exhaustive-match check is designed to fire when the matched type
widens, so adding a new MountUsage member without deciding whether it
exposes a container path now produces a build error pointing at this
match. The wildcard form swallowed that question silently.
Behaviour unchanged: BACKUP and None still return None.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI and tox both passed ``--timeout=10`` explicitly, but a plain local
``pytest`` had no timeout — a hung asyncio task or stuck D-Bus signal
handler could stall a developer's run indefinitely while passing CI.
Move the timeout into ``[tool.pytest.ini_options]`` so it applies
everywhere (pytest auto-discovers ``pyproject.toml`` in the repo
root) and drop the now-redundant ``--timeout=10`` flags from
``ci.yaml`` and ``tox.ini``. The full suite already fits comfortably
under 10s per test, and ``@pytest.mark.timeout(N)`` remains
available for per-test overrides if a specific test ever needs more
headroom.
* Treat JobException as a client-side API error
Job condition guards (system not running, no free space, etc.) and
concurrency rejections (another job in flight) raised by the @Job
decorator are explicit precondition failures with descriptive messages,
not unexpected errors. JobException inheriting HassioError directly
meant api_process caught them in its HassioError branch — which since
#6739 logs them as unexpected and captures them to Sentry.
Inherit APIError instead so api_process surfaces these through its
APIError branch with the original message and skips the
unexpected-error path. Status stays at APIError's default 400, so the
API contract is unchanged.
Extended test_backup_immediate_errors to assert async_capture_exception
is not called for the freeze and free-space condition guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Silence too-many-ancestors on plugin job error mixins
The plugin-specific job error subclasses (CliJobError, ObserverJobError,
MulticastJobError, CoreDNSJobError, AudioJobError) cross pylint's
too-many-ancestors threshold once JobException inherits APIError. Add
the same `# pylint: disable=too-many-ancestors` already used on the
ResolutionNotFound subclasses with similar diamond inheritance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Disable too-many-ancestors globally instead of per class
The pylint config already disables every other too-many-* rule "for the
sake of readability", but kept too-many-ancestors and forced inline
disables on diamond-inherited exception classes (the ResolutionNotFound
subclasses, and now five plugin job error mixins after the JobException
APIError change).
Add too-many-ancestors to the global disable list and drop all eight
inline annotations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add versioned v2 API with apps terminology
Introduce a v2 API sub-app mounted at /v2 that uses 'apps' terminology
throughout, while keeping v1 fully backward-compatible.
Key changes:
- Add ATTR_ADDONS = 'addons' constant alongside ATTR_APPS = 'apps' so
backup file data (which must remain 'addons' for backward compat) and
v2 API responses can use distinct constants
- Add FeatureFlag.SUPERVISOR_V2_API to gate v2 route registration
- Mount aiohttp sub-app at /v2 in RestAPI.load() when flag is enabled
- Add _AppSecurityPatterns frozen dataclass and _V1_PATTERNS/_V2_PATTERNS
with strict per-version regex sets (no cross-version matching)
- Add _register_v2_apps, _register_v2_backups, _register_v2_store route
registration methods
- Add v1 thin wrapper methods (*_v1) for all affected endpoints so
business logic lives in the canonical v2 methods
- Extract _info_data() helper in APIApps so v1 closure can bypass
@api_process and still catch APIAppNotInstalled for store routing
- Add _rename_apps_to_addons_in_backups(), _process_location_in_body(),
_all_store_apps_info() shared helpers to eliminate duplication
- Add api_client_v2, api_client_with_prefix, app_api_client_with_root,
store_app_api_client_with_root parameterized test fixtures
- Add test_v2_api_disabled_without_feature_flag
- Parameterize backup, addons, and store tests to cover both v1 and v2
paths
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix pylint false positive for re.Pattern C extension methods
re.Pattern methods (match, search, etc.) are C extension methods.
Pylint cannot detect them via static analysis when re.Pattern is used
as a type annotation in a dataclass field, producing false E1101
no-member errors. Add generated-members to inform pylint these members
exist.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* pylint and feedback fixes
* Copilot suggested fixes
* Minor feedback fixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Since setuptools 70.1 the bdist_wheel command is implemented inside
setuptools itself; the standalone wheel package is no longer required
to build (editable) installs of Supervisor. With setuptools~=82.0.0
pinned, the wheel entry in build-system.requires is legacy.
Supervisor is not published as a wheel either — it's only installed
editably inside the container — so there is no reason to keep wheel as
a build dependency. Dropping it also avoids churn from dependabot bumps
against the home-assistant musllinux wheels index, which is only
updated on merges to main.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use Python 3.14(.3) in CI and base image
Update base image to the latest tag using Python 3.14.3 and update Python
version in CI workflows to 3.14.
With Python 3.14, backports.zstd is no longer necessary as it's now available
in the standard library.
* Update wheels ABI in the wheels builder to cp314
* Use explicit Python fix version in GH actions
Specify explicitly Python 3.14.3, as the setup-python action otherwise default
to 3.14.2 when 3.14.3, leading to different version in CI and in production.
* Update Python version references in pyproject.toml
* Fix all ruff quoted-annotation (UP037) errors
* Revert unquoting of DBus types in tests and ignore UP037 where needed
The UP038 rule was removed from ruff in version 0.13.0, causing a warning
when running ruff. Remove it from the ignore list to eliminate the warning.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
* Recreate aiohttp ClientSession after DNS plug-in load
Create a temporary ClientSession early in case we need to load version
information from the internet. This doesn't use the final DNS setup
and hence might fail to load in certain situations since we don't have
the fallback mechanims in place yet. But if the DNS container image
is present, we'll continue the setup and load the DNS plug-in. We then
can recreate the ClientSession such that it uses the DNS plug-in.
This works around an issue with aiodns, which today doesn't reload
`resolv.conf` automatically when it changes. This lead to Supervisor
using the initial `resolv.conf` as created by Docker. It meant that
we did not use the DNS plug-in (and its fallback capabilities) in
Supervisor. Also it meant that changes to the DNS setup at runtime
did not propagate to the aiohttp ClientSession (as observed in #5332).
* Mock aiohttp.ClientSession for all tests
Currently in several places pytest actually uses the aiohttp
ClientSession and reaches out to the internet. This is not ideal
for unit tests and should be avoided.
This creates several new fixtures to aid this effort: The `websession`
fixture simply returns a mocked aiohttp.ClientSession, which can be
used whenever a function is tested which needs the global websession.
A separate new fixture to mock the connectivity check named
`supervisor_internet` since this is often used through the Job
decorator which require INTERNET_SYSTEM.
And the `mock_update_data` uses the already existing update json
test data from the fixture directory instead of loading the data
from the internet.
* Log ClientSession nameserver information
When recreating the aiohttp ClientSession, log information what
nameservers exactly are going to be used.
* Refuse ClientSession initialization when API is available
Previous attempts to reinitialize the ClientSession have shown
use of the ClientSession after it was closed due to API requets
being handled in parallel to the reinitialization (see #5851).
Make sure this is not possible by refusing to reinitialize the
ClientSession when the API is available.
* Fix pytests
Also sure we don't create aiohttp ClientSession objects unnecessarily.
* Apply suggestions from code review
Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
---------
Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
* Bump Supervisor to Python 3.13
* Update ruff configuration to 0.9.1
Adjust pyproject.toml for ruff 0.9.1. Also make sure that latest version
of ruff is used in pre-commit.
* Set default configuration for pytest-asyncio
* Run ruff check
* Drop deprecated decorator no_type_check_decorator
The upstream PR (https://github.com/python/cpython/issues/106309) says
this never got really implemented by type checkers.
* Bump devcontainer to latest release
* Bump pylint from 3.2.7 to 3.3.0
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.7 to 3.3.0.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.7...v3.3.0)
---
updated-dependencies:
- dependency-name: pylint
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
* Set positional arguments limit to 10
This makes the current codebase pass with pylint 3.3.0 while still
warning in case many positional arguments are used.
* Move to design section
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
* Migrate to Ruff for lint and format
* Fix pylint issues
* DBus property sets into normal awaitable methods
* Fix tests relying on separate tasks in connect
* Fixes from feedback