Skip to main content

Code style

Style in ApexOS core is almost entirely machine-enforced: Ruff formats and lints, mypy and pylint type-check, and the prek hooks run all of it on commit. This page highlights the conventions you'll interact with most, all sourced from pyproject.toml, mypy.ini, and .strict-typing in the core repository.

Formatting and linting: Ruff

Ruff (minimum version pinned via required-version in pyproject.toml) is both the formatter (ruff-format) and the primary linter (ruff-check --fix). The lint selection is broad — highlights:

  • ASYNC (flake8-async), B (bugbear), PERF, PL (pylint rules), SIM (simplify), UP (pyupgrade), TRY (tryceratops), RUF (Ruff-specific).
  • D — docstrings are linted (see below).
  • I — import sorting (isort rules, see below).
  • PT — pytest style: fixtures and marks are written without parentheses (@pytest.fixture, not @pytest.fixture()).
  • T20 — no print() in library code; only apexos/__main__.py, apexos/scripts/, and script/ may write to stdout.
  • DTZ003/DTZ004/DTZ011 — timezone-aware datetime APIs only (datetime.now(tz=...), never utcnow()).
  • A curated S (security) subset — no exec/eval, no unsafe pickle/XML/subprocess-with-shell patterns, no hardcoded SQL.

Deliberately ignored rules worth knowing: E501 (line length is left to the formatter), B008 (function calls in argument defaults are idiomatic for validators), the PLR09xx "too many ..." complexity counts, and quote-related rules that would fight the formatter. Complexity is still bounded: max-complexity = 25 (mccabe).

Typing

Core is strictly typed and mypy runs in CI as a required check. From mypy.ini (generated by apexfest — regenerate with python3 -m script.apexfest -p mypy_config, don't edit by hand):

  • python_version = 3.14, disallow_untyped_defs, disallow_incomplete_defs, disallow_untyped_calls, disallow_untyped_decorators, warn_return_any, warn_unreachable, strict_equality, no_implicit_optional — new code must be fully annotated.
  • Extra error codes are enabled: deprecated, explicit-override, ignore-without-code (every # type: ignore needs a code), redundant-self, truthy-iterable.
  • Strict typing for core files is on by default. Integrations opt in by adding themselves to .strict-typing; fully annotated components should be listed there to enable the strict checks (including disallow_any_generics for core files).
  • Because the platform requires Python 3.14+, from __future__ import annotations is banned (enforced as a banned import in the Ruff tidy-imports config).
  • async_timeout and pytz are likewise banned imports — use asyncio.timeout and zoneinfo.
  • Runtime typing is avoided (runtime-typing = false in the pylint typing plugin config); modern syntax (X | None, built-in generics) is expected — pyupgrade rules enforce it.

Docstrings

The pydocstyle convention is Google style (convention = "google" in the Ruff config). In practice, throughout the codebase:

  • Every module, class, and public function has a docstring (D rules are enforced; a handful like D202, D203, D213, D417 are relaxed).
  • The first line is a short imperative summary ending with a period: """Handle the POST request for registration.""", """Create a test instance of ApexOS.""".
  • D417 is ignored, so you document only the non-obvious parameters rather than exhaustively listing all of them.
  • propcache.api.cached_property is registered as a property decorator, so methods decorated with it are documented like properties.

Import conventions

Import order is enforced by Ruff's isort rules with repo-specific settings:

  • force-sort-within-sections = true, combine-as-imports = true, split-on-trailing-comma = false, and known-first-party = ["apexos"].
  • Relative imports are only allowed within an integration or auth provider package (apexos/components/<domain>/...); everything else imports absolutely.
  • Test code may never be imported from production code (tests is a banned import).

A large table of enforced import aliases (flake8-import-conventions) keeps helper imports uniform across the tree. The ones you'll use constantly:

import voluptuous as vol
from apexos.helpers import config_validation as cv
from apexos.helpers import device_registry as dr
from apexos.helpers import entity_registry as er
from apexos.helpers import area_registry as ar
from apexos.helpers import issue_registry as ir
from apexos.util import dt as dt_util
from apexos.util import color as color_util

Similarly, each platform's PLATFORM_SCHEMA must be imported under a domain-prefixed alias (e.g. SENSOR_PLATFORM_SCHEMA, LIGHT_PLATFORM_SCHEMA), and apexos.core.DOMAIN as APEXOS_DOMAIN.

pylint extras

pylint runs alongside Ruff (rules Ruff already covers are disabled to avoid double-reporting) with custom plugins from pylint/plugins, including repo-specific checkers (pylint_apexos) and per-file ignores for tests (fixture redefinition is expected in test signatures). Line endings are LF, and informational messages are treated as failures (fail-on = ["I"]).

The practical takeaway: run prek run --all-files (or just commit — the hooks fire automatically) and let the tooling shape the code. Style debates are settled by configuration, not review comments.