diff --git a/esphome/__main__.py b/esphome/__main__.py index 6393e4a70e..c4f76fcc99 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -865,11 +865,8 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: RuntimeError, ValueError, ) as err: - # The firmware already built; idedata is a bonus artifact here. - # Broad on purpose: a vanished compiler (OSError), a failed - # include probe (RuntimeError), or a truncated compile DB - # (ValueError/LookupError) must not fail a successful build - # either. + # Broad on purpose: the firmware already built; an idedata + # failure must not fail a successful build. _LOGGER.warning("Could not generate idedata: %s", err) else: from esphome.platformio import toolchain @@ -2737,12 +2734,8 @@ def run_esphome(argv): cache_write_eligible = ( args.command in ("upload", "logs") and not command_line_substitutions ) - # An explicit CLI toolchain must run the per-platform validators; the - # cache was validated under whatever the last compile used. Only the - # read is gated: the refresh below still saves the freshly validated - # config. The sidecar is only written when none exists; a - # compile-written one keeps the compile's toolchain (the firmware on - # disk was built by it), which upload/logs then restore. + # An explicit --toolchain must re-run the per-platform validators, so + # gate only the cache read; the refresh below still saves the result. cache_read_eligible = cache_write_eligible and args.toolchain is None if cache_read_eligible: from esphome.compiled_config import load_compiled_config @@ -2768,11 +2761,8 @@ def run_esphome(argv): return 2 CORE.config = config - # Every platform resolves the toolchain during validation now, but the - # compiled-config cache fast path skips validation entirely and a - # sidecar written before the toolchain field existed restores nothing; - # this fallback covers that path. Must run before the cache refresh - # below so its sidecar records the same toolchain a compile would. + # The cache fast path skips validation, and legacy sidecars lack the + # toolchain field. Must run before the cache refresh below. if CORE.toolchain is None: CORE.toolchain = Toolchain.PLATFORMIO diff --git a/esphome/arduino/library.py b/esphome/arduino/library.py index 5d7712e4ad..80e7aa0043 100644 --- a/esphome/arduino/library.py +++ b/esphome/arduino/library.py @@ -1,25 +1,12 @@ """Arduino-core backend for the shared PlatformIO library converter. -Turns the libraries registered via ``cg.add_library()`` into build inputs for -a native Arduino build. Bare names that exist under the framework's bundled -``libraries/`` directory (ESP8266WiFi, Wire, SPI, ...) are read straight from -the framework tree; everything else goes through the shared -resolution/download pipeline in ``esphome.platformio.library``. Nothing here -is core-specific: the caller names the PlatformIO platform, MCU, and cache -key of the Arduino core it builds. +Bundled names build straight from the framework tree; everything else goes +through ``esphome.platformio.library``. Mirrors ``lib_ldf_mode=off``: each +library builds its own archive; all include dirs join one global path. -Known deviations: flat-layout (``library.properties``, no ``src/``) -libraries get the recursive default source filter rather than PlatformIO's -root-only Arduino-1.0 filter (no bundled library is affected), and the -Arduino ``dot_a_linkage`` property is honored even though PlatformIO -ignores it. Bundled libraries never run a manifest ``extraScript`` (a -warning names the library if one declares it). Manifest ``-I`` build -flags join the global include path rather than staying private to the -library's own sources as under PlatformIO. - -Mirrors PlatformIO's ``lib_ldf_mode=off`` behavior: each library builds into -its own static archive and every library's include dir joins one global -include path. +Deviations from PlatformIO: flat-layout libraries get the recursive default +source filter; ``dot_a_linkage`` is honored; bundled libraries never run a +manifest ``extraScript``; manifest ``-I`` flags join the global include path. """ from __future__ import annotations @@ -101,12 +88,8 @@ def _warn_properties_depends(name: str, data: object) -> None: def _manifest_build(name: str, data: object) -> dict: - """The manifest's ``build`` section, validated by name. - - A bare json.load imposes no shape; a malformed manifest must name the - library instead of an AttributeError deep in a traceback (and must do so - before apply_extra_script dereferences the same section). - """ + """The manifest's ``build`` section; a malformed manifest must fail + naming the library, not with an AttributeError.""" build = data.get("build", {}) if isinstance(data, dict) else None if not isinstance(build, dict): raise EsphomeError(f"Library {name} has a malformed manifest") @@ -119,9 +102,8 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary: # PIO's source-dir resolution: manifest srcDir, else src/Src, else the root if "srcDir" in build: - # An explicitly declared srcDir (falsy included) that does not - # resolve is unambiguously a manifest/tree error; a silently empty - # source set would surface as link errors far from the cause + # A declared srcDir (falsy included) that does not resolve is a + # manifest error src_dir = build["srcDir"] if not ( isinstance(src_dir, str) and src_dir and (read_path / src_dir).is_dir() @@ -138,11 +120,8 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary: # PlatformIO shell-lexes each build.flags entry flag_tokens = lex_build_flags(build.get("flags", []), f"library {name}") - # build.libArchive is PIO behavior; dot_a_linkage is honored as a - # deliberate extra (Arduino IDE's property, which PIO ignores) so - # properties-only libraries can opt out of archiving too. Both parse - # through the same strict table: bool("false") is True, and a typo'd - # value must not silently change link semantics. + # dot_a_linkage (Arduino IDE's property, ignored by PIO) is a deliberate + # extra. Strict parse: bool("false") is True. def _parse_archive(key: str, raw: object) -> bool: if isinstance(raw, bool): return raw @@ -233,9 +212,7 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary: manifest = lib_dir / "library.properties" data = parse_library_properties(manifest) if manifest.is_file() else {} if isinstance(data, dict): - # The dependency walk never runs for bundled libraries (a no-op for - # the ESP8266 core, whose bundled manifests declare none); on a core - # where one does, the skip must be visible before link errors + # Bundled manifest deps are never walked; make the skip visible if data.get("dependencies"): _LOGGER.warning( "Bundled library %s declares dependencies, which are not " @@ -296,10 +273,8 @@ def resolve_libraries( # PlatformIO's lib_ignore covers framework-bundled libraries too; the # shared converter only filters the registry/git ones. lib_ignore = lib_ignore_set() - # One memoized answer to "does the framework bundle this name?" for the - # classification loop, the provides hook, and the dependency walk: the - # safety guard and the dir probe must stay fused (path traversal), and - # common names (Wire, SPI) are asked repeatedly + # Memoized "does the framework bundle this name?"; the safety guard and + # dir probe must stay fused (path traversal) _provided = functools.cache( lambda name: ( _is_safe_library_name(name) @@ -309,16 +284,11 @@ def resolve_libraries( for library in CORE.platformio_libraries.values(): if is_lib_ignored(library.name, lib_ignore): continue - # Only a bare name with a matching framework directory is bundled: a - # version pin means a registry package ("pngle@1.1.0"), and a bare - # name without the directory resolves from the registry at the - # latest version, matching PlatformIO (a typo fails loudly as a - # registry lookup error). + # Bundled only for a bare name with a matching framework dir; pinned + # or unmatched names resolve from the registry, as under PlatformIO. if not library.repository and not library.version and _provided(library.name): - # A bundled library's own manifest dependencies are not walked. - # PlatformIO would walk them even under lib_ldf_mode=off, but no - # library bundled with the ESP8266 core declares any, so the walk - # is a no-op there; core add_library() calls list what they need. + # Bundled libraries' own manifest deps are not walked (none of + # the ESP8266 core's declare any; _bundled_library warns if one does) bundled.append(_bundled_library(framework_path, library.name)) else: external.append(library) @@ -328,9 +298,8 @@ def resolve_libraries( converted_manifest_names: set[str] = set() # Ordered set of bundled dependency names to add once conversion is done pending_bundled: dict[str, None] = {} - # Short names of the separately-requested externals: a manifest - # dependency matching one is already in the build, not a bundled name to - # add (a duplicate archive shows up as duplicate-symbol link errors) + # Deps matching a separately-requested external are already in the build + # (a duplicate archive means duplicate-symbol link errors) external_short_names = { _external_short_name(lib.name) for lib in external if lib.name } @@ -360,18 +329,14 @@ def resolve_libraries( ): continue if dep.get("owner") or not _provided(name): - # The converter resolves owner-qualified and non-bundled - # names from the registry; an owner-less name that exists in - # the framework tree prefers the bundled copy ({"Wire": "*"} - # normalizes to version="*"), matching PlatformIO's - # process_dependencies. The shared walk's post-emit - # reconciliation reports any real drops. + # Owner-less names in the framework tree prefer the bundled + # copy (PIO's process_dependencies); everything else resolves + # via the converter, and the walk reports any real drops continue if not dependency_is_usable(dep, pio_platform, "arduino", component.name): continue - # Deferred: a later-emitted converted library may satisfy this - # name (its manifest name is only known at its own emit), and - # adding the bundled copy too would double the archive + # Deferred: a later-emitted library's manifest name may satisfy + # this; adding now could double the archive pending_bundled.setdefault(name) def _emit(component: ConvertedLibrary) -> None: @@ -388,9 +353,6 @@ def resolve_libraries( _add_bundled_dependencies(component) if external: - # Every converter drop path raises (an incompatible top-level is a - # RuntimeError, resolution and download failures raise), so the - # return needs no re-verification here. convert_libraries( external, LibraryBackend( @@ -398,10 +360,8 @@ def resolve_libraries( framework="arduino", emit=_emit, cache_key=cache_key, - # The graph walk must not resolve a bundled name from the - # registry ({"Wire": "*"} in a manifest); the bundled copy - # is added by _add_bundled_dependencies after emit. Unsafe - # names are simply not provided. + # The walk must not resolve bundled names from the registry; + # _add_bundled_dependencies adds them after emit provides=_provided, ), ) diff --git a/esphome/arduino8266/framework.py b/esphome/arduino8266/framework.py index 5ca305e0ef..f680815c31 100644 --- a/esphome/arduino8266/framework.py +++ b/esphome/arduino8266/framework.py @@ -6,14 +6,9 @@ ESP-IDF install in ``esphome.espidf.framework``): /arduino8266/frameworks// framework-arduinoespressif8266 /arduino8266/toolchains// toolchain-xtensa (gcc 10.3) -ninja itself comes from PATH or the ninja PyPI wheel (a requirements.txt -dependency), so only the two packages above are downloaded, via the shared -PlatformIO-registry installer in ``esphome.platformio.registry``. - -Sources default to the PlatformIO registry (the exact packages the PlatformIO -toolchain has always used, so the bits are identical); the -``ESPHOME_ARDUINO8266_*_MIRRORS`` environment variables override the URLs with -``{VERSION}`` / ``{SYSTEM}`` substitution. +Packages come from the PlatformIO registry (identical bits to the PlatformIO +backend); ``ESPHOME_ARDUINO8266_*_MIRRORS`` overrides the URLs. ninja comes +from PATH or the ninja PyPI wheel. """ from __future__ import annotations @@ -31,9 +26,8 @@ from esphome.platformio.registry import install_package FRAMEWORK_PACKAGE = "framework-arduinoespressif8266" TOOLCHAIN_PACKAGE = "toolchain-xtensa" -# gcc 10.3, the toolchain Arduino core 3.x builds with. The compile flags in -# the build generator are tuned to it; treat version changes as a full -# reinstall (the install dir is keyed on the version). +# gcc 10.3, the toolchain Arduino core 3.x builds with; the build +# generator's compile flags are tuned to it. TOOLCHAIN_VERSION = "2.100300.220621" ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS = str_to_lst_of_str( @@ -56,31 +50,20 @@ MIN_FRAMEWORK_VERSION = Version(3, 1, 1) def framework_package_version(ver: Version) -> str: - """Map an Arduino core version (e.g. 3.1.2) to its package version. + """Map an Arduino core version to its registry package version (3.1.2 -> + 3.30102.0; the leading 3 is the package major). - The PlatformIO registry's encoding for cores newer than 2.6.2 (3.1.2 -> - 3.30102.0, and 2.7.4 -> 3.20704.0: the leading 3 is the package major, - not the core major). Exact registry names only from 3.0.2 up: 2.6.3, - 3.0.0 and 3.0.1 ship as 3.20603.200130 / 3.30000.210519 / - 3.30001.210627, which this formula cannot produce. Safe for the - PlatformIO caller (a ~ range) and for check_and_install (floored at - MIN_FRAMEWORK_VERSION); an exact lookup below that floor must not use - this helper. A future core 4.x needs its own encoding and toolchain pin - rather than a registry lookup for a package that cannot exist. + Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor + at MIN_FRAMEWORK_VERSION. """ if ver.major > 3: - # Backend-neutral: this also fires on the PlatformIO validation path - # (via _format_framework_arduino_version), where switching toolchains - # would not help raise EsphomeError( f"Arduino core {ver} is not supported yet; " "the newest known core series is 3.x" ) if ver <= Version(2, 6, 2): - # Same boundary as _format_framework_arduino_version's era guard (a - # 2.6.2 pre-release sorts above 2.6.2 and belongs to this encoding). - # Older cores use the 1.x/2.x package-major encodings; never encode - # them wrongly for a caller that skipped that guard + # Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same + # boundary as _format_framework_arduino_version's era guard) raise EsphomeError( f"Arduino core {ver} uses an older package encoding than this " "helper implements (newer than 2.6.2)" @@ -141,10 +124,7 @@ def check_and_install(framework_version: Version) -> InstalledPaths: ) -# Sentinel: "resolve for me" (None is a real value meaning disabled). -# The native run_compile (a later PR in the chain) will resolve once and -# thread the result so one build never pays the PATH scan and runnability -# probe three times. +# Sentinel: "resolve for me"; None is a real value meaning disabled. CCACHE_UNRESOLVED: Any = object() @@ -176,8 +156,7 @@ def get_build_env( def ccache_path() -> str | None: """The ccache binary to prefix compiles with, or None when disabled. - Deliberately uncached (matching espidf): the decision reads - ESPHOME_CCACHE_ENABLE and PATH, which can change between builds in a + Deliberately uncached: env/PATH can change between builds in a long-lived host process. """ return resolve_ccache_path() @@ -186,10 +165,7 @@ def ccache_path() -> str | None: def ccache_env(ccache: str | None = CCACHE_UNRESOLVED) -> dict[str, str]: """Return ccache settings for the build subprocess (not os.environ). - Mirrors ``espidf.framework._ccache_env``: cache under the machine-global - tools dir, depend mode (gcc emits depfiles via -MMD), and CCACHE_BASEDIR - scoped to the build dir so devices share framework cache entries. Values - the user already set in the environment are respected. + Values the user already set in the environment are respected. """ if ccache is CCACHE_UNRESOLVED: ccache = ccache_path() diff --git a/esphome/build_gen/build_tool.py b/esphome/build_gen/build_tool.py index 4cdbb88c28..6395f84dd0 100644 --- a/esphome/build_gen/build_tool.py +++ b/esphome/build_gen/build_tool.py @@ -21,14 +21,9 @@ def main() -> int: # Remove first: ``ar rc`` replaces members but never drops ones whose # source was removed from the build, which would leak stale objects. Path(archive).unlink(missing_ok=True) - # Expand the response file here instead of passing @rspfile: GNU ar - # treats backslashes in response files as escapes, corrupting Windows - # paths ("sub\a.o" -> "suba.o"). - # One path per line (rspfile_content = $in_newline). ninja shell-quotes - # a path containing specials, so undo a simple surrounding quote per - # line. Expanding into argv trades away the OS command-line length - # limit rspfiles dodge; the relative object paths here stay far - # below it. + # GNU ar treats backslashes in response files as escapes (corrupts + # Windows paths), so expand the rspfile into argv, stripping the + # simple surrounding quote ninja adds to special paths. objects = [ line[1:-1] if len(line) >= 2 and line[0] == line[-1] and line[0] in "'\"" diff --git a/esphome/build_helpers/ccache.py b/esphome/build_helpers/ccache.py index 81bafe874e..8804e5028d 100644 --- a/esphome/build_helpers/ccache.py +++ b/esphome/build_helpers/ccache.py @@ -1,13 +1,5 @@ -"""Shared ccache policy for build backends. - -``ccache_defaults_env`` serves the backends that export ``CCACHE_*`` into a -build subprocess (native ESP-IDF and Arduino); ``resolve_ccache_path`` -carries the probe and enable rules (PlatformIO and the native Arduino -build). The ESP-IDF backend keeps ``IDF_CCACHE_ENABLE`` as a -higher-precedence override and falls back to the shared resolver (probe -included) when it is unset; PlatformIO feeds its SCons wrapper script -through env channels instead of ``CCACHE_*`` defaults. -""" +"""Shared ccache policy for build backends: env-knob parsing, binary +resolution, and default ``CCACHE_*`` values.""" from __future__ import annotations @@ -50,12 +42,8 @@ def parse_enable_env(name: str) -> bool | None: def resolve_ccache_path() -> str | None: """The ccache binary to wrap compiles with, or None when disabled. - Shared policy for every backend: on by default when a runnable ccache is - on PATH, ``ESPHOME_CCACHE_ENABLE=0`` opts out, and an explicit ``=1`` - warns when no binary is found and skips the runnability probe; - any other value warns and is treated as unset. The - Windows extended-length prefix is stripped before probing so the probe - validates the exact string the build will execute (#18399). + An explicit ``ESPHOME_CCACHE_ENABLE=1`` skips the runnability probe; the + Windows extended-length prefix is stripped before probing (#18399). """ import shutil @@ -85,9 +73,8 @@ def ccache_defaults_env(cache_dir: Path) -> dict[str, str]: """ from esphome.core import CORE - # build_path is set during preload for every config-loading command; unset - # means the caller built the environment too early. Fail loudly rather - # than silently drop CCACHE_BASEDIR (losing cross-device cache hits). + # An unset build_path means the env was built before preload; fail loudly + # rather than silently drop CCACHE_BASEDIR. if CORE.build_path is None: raise ValueError( "CORE.build_path must be set before constructing the build environment" diff --git a/esphome/build_helpers/idedata.py b/esphome/build_helpers/idedata.py index 31b62df528..00a890e087 100644 --- a/esphome/build_helpers/idedata.py +++ b/esphome/build_helpers/idedata.py @@ -123,10 +123,8 @@ def _pick_entry(entries: list[dict]) -> dict: raise ValueError("no C++ translation unit found in compile_commands.json") -# Compiler launchers that may prefix a compile command. A closed denylist is -# sturdier than trying to enumerate compiler names: launchers are few and -# stable, while compilers (cross prefixes, versioned names, icx, armcc, ...) -# are an open set. +# Compiler launchers that may prefix a compile command; a closed launcher +# denylist beats enumerating compiler names, an open set. _LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"}) @@ -300,17 +298,14 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d representative = _pick_entry(entries) cxx_path, defines, rep_includes, cxx_flags = parse_entry(representative, launcher) if _is_launcher(cxx_path): - # Checked before the toolchain probe (which would fail opaquely on - # a launcher) so the unusable compile DB is named, and never - # cached or conflated with "nothing built yet" + # Reject before the toolchain probe, which would fail opaquely on + # a launcher; never cache the unusable compile DB raise EsphomeError( f"compile_commands.json names the launcher {cxx_path} as the " "compiler; the compile database is unusable" ) # Seed with the representative's includes so it is not parsed twice - # (per-file -c/-o arguments make every command distinct, so memoizing - # whole commands would never hit) build_includes: dict[str, None] = dict.fromkeys( rep_includes if _is_esphome_src(representative["file"]) else () ) diff --git a/esphome/build_helpers/ninja.py b/esphome/build_helpers/ninja.py index 9850dfd828..8c25bc9513 100644 --- a/esphome/build_helpers/ninja.py +++ b/esphome/build_helpers/ninja.py @@ -25,11 +25,7 @@ def _ninja_runs(binary: str) -> bool: def find_ninja() -> Path: """Locate the ninja binary: a runnable PATH hit first, else the ninja - PyPI wheel. - - The wheel is a requirements.txt dependency, so pip has already - integrity-checked it; no download logic is needed here. - """ + PyPI wheel.""" if binary := shutil.which("ninja"): binary = strip_win_long_path_prefix(binary) if _ninja_runs(binary): @@ -58,13 +54,9 @@ def escape(value: Path | str) -> str: def quote_arg(tok: str) -> str: - """Wrap a token in double quotes with the Windows argv rule. - - Same escaping rule as ``subprocess.list2cmdline``: a backslash run - doubles only immediately before a quote (or the closing quote), and the - quote itself is escaped. CreateProcess-only; POSIX sh collapses - backslash runs inside double quotes, so shell_token single-quotes - there instead. ``$`` must already be doubled for ninja. + """Quote with the CreateProcess argv rule (as ``subprocess.list2cmdline``): + backslash runs double only before a quote. Windows-only; ``$`` must + already be doubled for ninja. """ quoted = re.sub(r'(\\*)"', lambda m: m.group(1) * 2 + '\\"', tok) quoted = re.sub(r"(\\+)\Z", lambda m: m.group(1) * 2, quoted) @@ -78,16 +70,11 @@ _NEEDS_QUOTE = re.compile(r"[^\w@%+=:,./-]") def shell_token(tok: str, force: bool = False) -> str: - """Quote a lexed token only when needed; ``force`` always quotes. + """Re-quote a lexed token for the platform shell; ``force`` always quotes. - Lexing strips the quoting a user wrote (``-DX="a b"`` becomes the single - token ``-DX=a b``); re-quote on the way out so the compiler receives the - same argv element SCons would pass under PlatformIO. Ninja hands POSIX - commands to ``/bin/sh -c`` and Windows commands to CreateProcess, so the - quoting style is chosen per platform: single quotes on POSIX (sh expands - nothing inside them, matching SCons's no-shell spawn) and the argv rule - on Windows. ``$`` is doubled first in either case because ninja expands - ``$`` before the command reaches the shell. + Single quotes on POSIX (/bin/sh), the argv rule on Windows + (CreateProcess). ``$`` is doubled first because ninja expands it before + the command reaches the shell. """ tok = tok.replace("$", "$$") # ninja would expand a bare $ to nothing if not (force or not tok or _NEEDS_QUOTE.search(tok)): diff --git a/esphome/build_helpers/size_summary.py b/esphome/build_helpers/size_summary.py index 72c429d095..b888111044 100644 --- a/esphome/build_helpers/size_summary.py +++ b/esphome/build_helpers/size_summary.py @@ -4,11 +4,7 @@ from __future__ import annotations def format_bar(used: int, total: int) -> str: - """Match PlatformIO's ``_format_availale_bytes`` (pioupload.py) exactly. - - The upstream helper's name really is spelled that way; keep the citation - verbatim so it stays greppable in the PlatformIO source. - """ + """Match PlatformIO's ``_format_availale_bytes`` (sic, pioupload.py) exactly.""" pct_raw = used / total if total else 0 blocks = 10 filled = min(int(round(blocks * pct_raw)), blocks) diff --git a/esphome/compiled_config.py b/esphome/compiled_config.py index 066209b184..0d855d71db 100644 --- a/esphome/compiled_config.py +++ b/esphome/compiled_config.py @@ -105,10 +105,9 @@ def _refresh_sidecar() -> bool: and CORE.toolchain is not None and old.toolchain != CORE.toolchain.value ): - # The config was validated under a different toolchain than - # the compile's, and platforms normalize toolchain-sensitive - # keys (e.g. the esp32 board name) differently; caching it - # would disagree with the sidecar until the next compile + # Platforms normalize toolchain-sensitive keys differently; + # never cache a config validated under a different toolchain + # than the compile's _LOGGER.debug( "Not caching: config validated with toolchain %r but the " "last compile used %r", diff --git a/esphome/components/esp8266/boards.py b/esphome/components/esp8266/boards.py index 5be8011ba4..d458442dbd 100644 --- a/esphome/components/esp8266/boards.py +++ b/esphome/components/esp8266/boards.py @@ -362,14 +362,9 @@ BOARDS = { } -# Per-board Arduino core build metadata for the native (PlatformIO-free) -# toolchain: the variant directory (supplies pins_arduino.h) and the -# board-identity defines the PlatformIO builder passes via build.extra_flags. -# Valid for platform 4.x only (older tags differ, e.g. esp8285's variant); -# the native toolchain's validator enforces that pairing by requiring core -# >= 3.1.1 and rejecting a custom platform_version. -# -DESP8266 and -DARDUINO_ARCH_ESP8266 are shared by every board and added by -# the generator; only the per-board defines are listed here. +# Per-board variant dir + identity defines from platform-espressif8266 4.x +# build.extra_flags; the shared -DESP8266/-DARDUINO_ARCH_ESP8266 are added +# by the generator. # # Regenerate ESP8266_BOARD_BUILD with (v4.2.1 is the platform version the # native toolchain mirrors; regenerate against the tag when bumping it): diff --git a/esphome/components/esp8266/build_surgery.py b/esphome/components/esp8266/build_surgery.py index 97ce750dd5..2df1d5dbb8 100644 --- a/esphome/components/esp8266/build_surgery.py +++ b/esphome/components/esp8266/build_surgery.py @@ -84,8 +84,6 @@ def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str """ for segment in _TESTING_SEGMENT_SIZES: if segment not in segments and _segment_line_re(segment).search(content): - # A known segment left unpatched would keep its real memory limit - # and silently under-provision the testing build raise RuntimeError( f"Testing-mode segment {segment} is present in the linker " "script but was not selected for patching" @@ -111,12 +109,8 @@ def segment_length(content: str, segment_name: str) -> int | None: def surgery_fingerprint() -> str: - """Fingerprint of this module's source, covering every behavioral input. - - Linker-script caches include it so an edit here invalidates them; hashing - the source over-invalidates on comment edits, which is the safe direction. - Native-toolchain-only, like ``segment_length``; no script twin. - """ + """Hash of this module's source; linker-script caches include it so an + edit here invalidates them.""" import inspect import sys diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 102e78b3d9..98001d5d5b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2540,12 +2540,8 @@ def platformio_version_constraint(value): def _check_supported_toolchain( platform_name: str, supported: tuple[Toolchain, ...] ) -> None: - """Raise when the resolved ``CORE.toolchain`` is not in ``supported``. - - One message shape for every platform, so a ``--toolchain`` a platform - cannot serve always fails by name instead of silently building with a - different backend. - """ + """Raise when the resolved ``CORE.toolchain`` is not in ``supported`` + (one message shape for every platform).""" toolchain = CORE.toolchain if toolchain is None: # A caller ran the check before resolving; an ordering bug, not a @@ -2591,14 +2587,8 @@ def resolve_toolchain( def require_platformio_toolchain( platform_name: str, ) -> Callable[[ConfigType], ConfigType]: - """Reject a CLI-selected toolchain other than PlatformIO. - - For platforms with only the PlatformIO backend. Without this a - ``--toolchain`` they cannot serve would either build with PlatformIO - while claiming another backend, or (for a toolchain another platform - owns, like ``esp-idf``) dispatch to a native backend that cannot - build this platform at all. - """ + """Reject a CLI-selected toolchain other than PlatformIO, for platforms + with only the PlatformIO backend.""" return resolve_toolchain( platform_name, (Toolchain.PLATFORMIO,), Toolchain.PLATFORMIO ) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index a20a60e322..a4fd2ced30 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -985,12 +985,8 @@ class EsphomeCore: @property def using_toolchain_arduino(self): - """The native (PlatformIO-free) ESP8266 Arduino build backend. - - Unlike ``using_arduino`` (the target *framework*, true for any - platform compiling Arduino code), this is a build *toolchain* - choice, like its ``using_toolchain_*`` siblings. - """ + """The native ESP8266 Arduino build toolchain (unlike + ``using_arduino``, which is the target framework).""" return self.toolchain == Toolchain.ARDUINO @property diff --git a/esphome/core/config.py b/esphome/core/config.py index cbd36e8924..bade3ba9c4 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -566,11 +566,7 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No if CORE.using_native_toolchain: # The native builds don't read platformio.ini; honor the options # with a native equivalent and warn about the rest, which would - # otherwise be silently ignored. Every dispatch site that tests a - # specific using_toolchain_* as a stand-in for "native" (project - # writing, compile, upload, firmware paths) must agree with this - # gate: a toolchain treated as native here must never fall through - # to a PlatformIO code path there. + # otherwise be silently ignored. for key, val in pio_options.items(): vals = [val] if isinstance(val, str) else val if key == CONF_BUILD_FLAGS: @@ -600,12 +596,8 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No # discovered dependencies cg.add_platformio_option(key, vals) elif key in NATIVE_ARDUINO_PIO_OPTIONS and CORE.using_toolchain_arduino: - # Real-world knobs many published ESP8266 configs rely on: - # f_cpu 160000000L for timing-sensitive integrations, and a - # custom ldscript to reserve a filesystem region or correct - # a board's flash size. The esp8266 native generator reads - # both; other native toolchains have no equivalent and fall - # through to the warning. + # The esp8266 native generator reads these; other native + # toolchains have no equivalent and fall through to the warning. cg.add_platformio_option(key, val) elif key != "upload_speed": # upload_speed needs no handling: it is read from the raw diff --git a/esphome/espidf/component.py b/esphome/espidf/component.py index d71607714c..4eeaa30e7f 100644 --- a/esphome/espidf/component.py +++ b/esphome/espidf/component.py @@ -88,10 +88,8 @@ def generate_cmakelists_txt(component: IDFComponent) -> str: build_src_filter = ensure_list( component.data.get("build", {}).get("srcFilter", DEFAULT_BUILD_SRC_FILTER) ) - # PlatformIO shell-lexes each build.flags entry, so one entry can carry a - # flag and its argument (e.g. "-include cp_custom_alloc.h"); bare - # -I/-L/-l/-D tokens re-glue to their argument ("-I foo" -> "-Ifoo") so - # prefix classifiers below still route them. + # PlatformIO shell-lexes each build.flags entry; bare -I/-L/-l/-D tokens + # re-glue to their argument so the prefix classifiers below route them. build_flags = lex_build_flags( component.data.get("build", {}).get("flags", DEFAULT_BUILD_FLAGS), f"library {component.name}", diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 3977c6560c..c6d9ef13f1 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -1155,11 +1155,8 @@ def _ccache_env() -> dict[str, str]: Only values the user has not already set in the environment are returned, so a custom ``CCACHE_DIR`` / ``CCACHE_MAXSIZE`` / etc. is respected. """ - # Honor an explicit choice already in the environment (opt-out or opt-in). - # IDF_CCACHE_ENABLE (this backend's native knob) wins over the shared - # ESPHOME_CCACHE_ENABLE, which resolve_ccache_path parses; without it a - # user disabling ccache to debug a miscompile would silently keep it - # enabled here. + # IDF_CCACHE_ENABLE (the backend-native knob) wins over the shared + # ESPHOME_CCACHE_ENABLE. idf_knob = parse_enable_env("IDF_CCACHE_ENABLE") if idf_knob is False: return {} @@ -1167,8 +1164,6 @@ def _ccache_env() -> dict[str, str]: # ESP-IDF silently skips ccache without the binary; don't enable it. return {} - # ccache is enabled past here; the shared helper carries the CCACHE_* - # policy (and the fail-loud build_path guard). env = ccache_defaults_env(get_idf_tools_path() / "ccache") if idf_knob is None: # An unparsable IDF_CCACHE_ENABLE must not leak to idf.py as truthy diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 3da8f403ee..d98d9e4982 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -199,10 +199,8 @@ def run_command( def tool_version_runs(binary: str, warning: str) -> bool: """Probe ``binary --version``; on failure warn with ``warning`` % binary. - ``shutil.which`` proves existence, not runnability: on Windows it also - matches ``.bat``/``.cmd`` wrappers and stale package-manager shims whose - target is gone. Callers probe once and fall back instead of failing - every build step with an opaque OS error. + ``shutil.which`` proves existence, not runnability (Windows .bat/.cmd + shims, stale package-manager shims). """ try: subprocess.run( diff --git a/esphome/platformio/extra_script.py b/esphome/platformio/extra_script.py index 4a0a0dc7c5..e311e8729b 100644 --- a/esphome/platformio/extra_script.py +++ b/esphome/platformio/extra_script.py @@ -141,9 +141,7 @@ class _FakeSConsEnv: def Append(self, **kwargs) -> None: # noqa: N802 (SCons API name) for key, value in kwargs.items(): if key not in _CAPTURED_KEYS: - # Diagnosable from the build log when a script configures - # something this shim does not translate; once per key so a - # loop of Appends cannot spam + # Warn once per key so a loop of Appends cannot spam if key not in self._warned_keys: self._warned_keys.add(key) _LOGGER.warning( @@ -239,9 +237,8 @@ def run_extra_script( ) return ExtraScriptResult() except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught - # Discard any partial capture: folding half a script's flags into the - # build could produce wrong-output firmware that links cleanly. The - # warning plus the resulting loud link error point back here. + # Discard any partial capture: half-applied flags could build wrong + # firmware that links cleanly. _LOGGER.warning( "PIO extra-script %s (in %s) raised %r; ignoring its output", script_path, diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 110ab1483e..a37c7fe5bf 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -47,11 +47,8 @@ DEFAULT_BUILD_SRC_FILTER = ( DEFAULT_BUILD_SRC_DIRS = "src" DEFAULT_BUILD_INCLUDE_DIR = "include" DEFAULT_BUILD_FLAGS = [] -# Source suffix -> compiler kind, PlatformIO's CSUFFIXES/CXXSUFFIXES/ASSUFFIXES -# split. Native build generators map the kind to their compile rules. "asm" -# deliberately merges SCons's AS (.s/.asm) and ASPP (.S/.spp/.sx) sets: the -# ninja rules compile all of them as assembler-with-cpp, whose asm-mode -# preprocessor passes non-directive text through unchanged. +# Suffix -> compiler kind (PlatformIO's CSUFFIXES/CXXSUFFIXES/ASSUFFIXES). +# "asm" merges SCons's AS and ASPP sets: all compile as assembler-with-cpp. SOURCE_KIND_FOR_SUFFIX: dict[str, str] = { ".c": "c", ".cpp": "cxx", @@ -301,10 +298,9 @@ class LibraryBackend: framework: str emit: Callable[["ConvertedLibrary"], None] cache_key: str - # When set, an owner-less manifest dependency this returns True for is - # skipped by the graph walk: the backend provides it outside the - # registry (e.g. a library bundled with the Arduino core), mirroring - # PlatformIO's process_dependencies preference for bundled builders. + # Owner-less dependency names this returns True for are skipped by the + # walk; the backend supplies them outside the registry (e.g. core-bundled + # libraries). provides: Callable[[str], bool] | None = None @@ -633,13 +629,8 @@ def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]: def dependency_is_usable( dep: dict, platform: str | None, framework: str, requester: str ) -> bool: - """Whether a manifest dependency passes the compatibility filter. - - The routine cross-platform skip logs at debug; any other - ``InvalidLibrary`` cause is a dropped dependency and warns naming the - requester (unreachable from ``check_library_data`` today, which raises - only for the platform filter). - """ + """Compatibility filter for a manifest dependency: platform mismatches + skip at debug, any other ``InvalidLibrary`` warns naming the requester.""" try: check_library_data(dep, platform, framework) except IncompatiblePlatform as e: @@ -699,9 +690,7 @@ def normalize_dependencies( continue normalized.append(entry) elif isinstance(entry, str) and entry: - # PIO also accepts a bare list of names ("dependencies": - # ["Wire"]); dropping them here would hide a real dependency - # from every caller's visibility warning + # PIO also accepts a bare list of names ("dependencies": ["Wire"]) normalized.append({"name": entry}) else: _LOGGER.warning( @@ -1025,10 +1014,8 @@ def convert_libraries( component.data.get("dependencies"), component.name ): if "name" not in dependency or "version" not in dependency: - # Version-less deps cannot resolve from the registry. - # Deferred: only the final resolution set can tell a real - # drop from a name another manifest resolves later, so the - # reconciliation after emit owns the warning + # Version-less deps cannot resolve from the registry; the + # post-emit reconciliation owns the drop warning _LOGGER.debug( "Skip version-less dependency %r of %s", dependency.get("name"), @@ -1056,9 +1043,8 @@ def convert_libraries( # The backend adds it from its own tree; resolving it here # would fetch a same-named registry package instead if (pin := dependency.get("version")) and pin != "*": - # The declared constraint is discarded for the bundled - # copy; a too-old bundled library must not surface as - # link errors with no stated cause + # The version pin is discarded for the bundled copy; make + # the substitution visible _LOGGER.warning( "Dependency %s pins version %s; using the library " "bundled with the framework instead", @@ -1126,11 +1112,9 @@ def convert_libraries( for component in components.values(): backend.emit(component) - # A version-less dependency is satisfied when its request key resolved, - # a resolved component's manifest name matches, or the backend provides - # it from its own tree (e.g. the arduino bundled libraries, added by the - # backend after emit). Anything else is a real drop that would otherwise - # surface as link errors far from the cause. + # Warn for version-less deps nothing satisfied (request key, manifest + # name, or backend provides()); a silent drop surfaces as link errors + # far from the cause. resolved_manifest_names = {c.data.get("name") for c in components.values()} warned: set[str] = set() for dep_name, dep_owner, requester in skipped_versionless: diff --git a/esphome/platformio/registry.py b/esphome/platformio/registry.py index 09a66e0cf9..99ccbe2334 100644 --- a/esphome/platformio/registry.py +++ b/esphome/platformio/registry.py @@ -1,9 +1,5 @@ -"""Install packages from the PlatformIO registry without PlatformIO. - -Native toolchains install the exact registry packages the PlatformIO backend -uses, so the bits are identical, but resolve and verify them with esphome's -own download machinery instead of importing the platformio package. -""" +"""Install packages from the PlatformIO registry without importing the +platformio package (identical bits, esphome's own download machinery).""" from __future__ import annotations @@ -33,11 +29,9 @@ _REGISTRY_URL = ( def get_systype() -> str: """The registry system tag for the current host. - A transliteration of ``platformio.util.get_systype()``, honoring the same - ``PLATFORMIO_SYSTEM_TYPE`` override, so this module never imports the - platformio package. One deviation: windows-arm64 maps straight to - ``windows_amd64``: the registry ships no arm64 toolchains and those hosts - run x86 binaries via emulation, which upstream leaves to the override. + Transliterates ``platformio.util.get_systype()`` (same + ``PLATFORMIO_SYSTEM_TYPE`` override). Deviation: windows-arm64 maps to + ``windows_amd64`` (no arm64 toolchains; x86 emulation). """ if systype := os.environ.get("PLATFORMIO_SYSTEM_TYPE"): return systype @@ -100,10 +94,8 @@ def registry_download(package: str, version: str) -> tuple[str, str, int | None] f"Unexpected package registry response for {package}: " f"{str(ver)[:200]}" ) - # Only a MISSING key means "any system"; an explicitly empty - # list must not match (a wrong-architecture download would be - # cached as a good install). A bare string would make ``in`` a - # substring test. + # Only a missing key means "any system"; an empty list must not + # match, and a bare string would make ``in`` a substring test. systems = file.get("system") if systems is None: systems = ["*"] @@ -138,12 +130,8 @@ def registry_download(package: str, version: str) -> tuple[str, str, int | None] def _check_layout(name: str, dest: Path, expect: Collection[str]) -> None: - """Raise when an install tree is missing an expected directory. - - Runs on fresh extracts and on marker hits: a marked tree that later - lost files (manual deletion, antivirus quarantine) must fail by name - instead of surfacing as an opaque toolchain error. - """ + """Raise when an install tree is missing an expected directory (runs on + fresh extracts and on marker hits).""" for rel in expect: if not (dest / rel).is_dir(): raise EsphomeError( @@ -177,21 +165,16 @@ def install_package( return from filelock import FileLock - # The cache is machine-global; serialize concurrent cold builds so one - # process cannot wipe the directory another is extracting into (same - # filelock pattern as platformio/toolchain.py and git.py). + # Serialize concurrent cold builds (same filelock pattern as git.py). dest.parent.mkdir(parents=True, exist_ok=True) - # fallback_to_soft would silently degrade to an existence lock on a - # flock-less filesystem; a hard-killed run would then hang every later - # build forever (same hazard git.py documents). + # A soft-lock fallback would turn a hard-killed run into a permanent + # hang (see git.py). with FileLock(f"{dest}.lock", fallback_to_soft=False): if marker.is_file(): # Another process finished the install while we waited return rmdir(dest, msg=f"Clean up incomplete {name} install") - # A persistent download location (not a temp dir) so an interrupted - # download resumes across esphome runs via download_with_resume's - # .part file, mirroring the espidf dist/ convention. + # Persistent location so an interrupted download resumes across runs. downloads_dir.mkdir(parents=True, exist_ok=True) archive = downloads_dir / f"{name}-{version}" _LOGGER.info("Downloading %s %s ...", name, version) diff --git a/tests/unit_tests/build_gen/test_build_tool.py b/tests/unit_tests/build_gen/test_build_tool.py index 875ffcb773..7f56abe9cb 100644 --- a/tests/unit_tests/build_gen/test_build_tool.py +++ b/tests/unit_tests/build_gen/test_build_tool.py @@ -3,6 +3,8 @@ from __future__ import annotations from pathlib import Path +import subprocess +import sys from unittest.mock import MagicMock, patch import pytest @@ -50,8 +52,6 @@ def test_unknown_mode(capsys: pytest.CaptureFixture[str]) -> None: def test_runs_as_script(tmp_path: Path) -> None: """The ninja rules invoke the file as a plain script.""" - import subprocess - import sys src = tmp_path / "a.bin" src.write_text("x") diff --git a/tests/unit_tests/build_helpers/test_idedata.py b/tests/unit_tests/build_helpers/test_idedata.py index 7297912955..52ffc88224 100644 --- a/tests/unit_tests/build_helpers/test_idedata.py +++ b/tests/unit_tests/build_helpers/test_idedata.py @@ -429,8 +429,7 @@ def test_load_or_build_idedata_corrupted_cache_is_logged( def test_load_or_build_idedata_never_caches_a_launcher(tmp_path: Path) -> None: - """A compile DB naming a launcher as the compiler is rejected by name, - before the toolchain probe could fail opaquely, and never cached.""" + """A compile DB naming a launcher as the compiler is rejected, never cached.""" compile_commands = tmp_path / "compile_commands.json" compile_commands.write_text( json.dumps( diff --git a/tests/unit_tests/build_helpers/test_ninja.py b/tests/unit_tests/build_helpers/test_ninja.py index b1a43bd4af..6f0bbda0b9 100644 --- a/tests/unit_tests/build_helpers/test_ninja.py +++ b/tests/unit_tests/build_helpers/test_ninja.py @@ -4,6 +4,7 @@ from __future__ import annotations import os from pathlib import Path +import subprocess import sys from unittest.mock import MagicMock, patch @@ -84,7 +85,6 @@ def test_shell_token_quotes_shell_metacharacters() -> None: def test_shell_token_posix_roundtrips_through_sh() -> None: """Backslash runs, $, backticks, and quotes must reach the compiler exactly as lexed once ninja un-doubles $$ and /bin/sh strips quotes.""" - import subprocess if sys.platform == "win32": pytest.skip("POSIX sh quoting") diff --git a/tests/unit_tests/components/esp8266/test_build_surgery.py b/tests/unit_tests/components/esp8266/test_build_surgery.py index b1c3fd7be8..411a35eb96 100644 --- a/tests/unit_tests/components/esp8266/test_build_surgery.py +++ b/tests/unit_tests/components/esp8266/test_build_surgery.py @@ -2,8 +2,13 @@ from __future__ import annotations +import importlib.util +from pathlib import Path +import sys + import pytest +from esphome.components.esp8266 import build_surgery from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD from esphome.components.esp8266.build_surgery import ( RATETABLE_RULE, @@ -110,11 +115,6 @@ def test_board_build_covers_every_board() -> None: def test_surgery_fingerprint_is_stable_and_sensitive(tmp_path) -> None: """The properties the linker-script cache depends on: the fingerprint is stable across calls and changes when the module's source changes.""" - import importlib.util - from pathlib import Path as _Path - import sys - - from esphome.components.esp8266 import build_surgery first = build_surgery.surgery_fingerprint() assert first == build_surgery.surgery_fingerprint() @@ -124,7 +124,7 @@ def test_surgery_fingerprint_is_stable_and_sensitive(tmp_path) -> None: # A modified copy of the module must fingerprint differently copy = tmp_path / "build_surgery_variant.py" copy.write_text( - _Path(build_surgery.__file__).read_text(encoding="utf-8") + Path(build_surgery.__file__).read_text(encoding="utf-8") + "\nEXTRA_BEHAVIORAL_INPUT = 1\n", encoding="utf-8", ) diff --git a/tests/unit_tests/test_arduino8266_framework.py b/tests/unit_tests/test_arduino8266_framework.py index c173fb27f2..2837be1905 100644 --- a/tests/unit_tests/test_arduino8266_framework.py +++ b/tests/unit_tests/test_arduino8266_framework.py @@ -26,9 +26,8 @@ def test_framework_package_version() -> None: # A future major bump needs its own encoding, not a doomed registry lookup with pytest.raises(EsphomeError, match="not supported yet"): framework.framework_package_version(cv.Version(4, 0, 0)) - # Cores up to 2.6.2 use other encodings; the helper is total, not wrong, - # and its boundary matches the PlatformIO era guard: a 2.6.2 pre-release - # sorts above 2.6.2 and keeps the package-major-3 encoding + # The boundary matches the PlatformIO era guard; a 2.6.2 pre-release + # keeps this encoding with pytest.raises(EsphomeError, match="older package encoding"): framework.framework_package_version(cv.Version(2, 6, 2)) assert framework.framework_package_version(cv.Version(2, 6, 2, "b1")) == "3.20602.0" @@ -102,9 +101,8 @@ def test_get_build_env_prepends_toolchain_bin(tmp_path: Path) -> None: def test_ccache_path_delegates_uncached( monkeypatch: pytest.MonkeyPatch, ) -> None: - """The wrapper delegates to the shared policy (covered in - build_helpers/test_ccache.py) on every call: the env/PATH decision - must not freeze for the process lifetime in a long-lived host.""" + """Delegates on every call; the env/PATH decision must not freeze for + the process lifetime.""" monkeypatch.delenv("ESPHOME_CCACHE_ENABLE", raising=False) with patch.object( framework, "resolve_ccache_path", return_value="/usr/bin/ccache" diff --git a/tests/unit_tests/test_arduino_library.py b/tests/unit_tests/test_arduino_library.py index 42e1bfc1c9..c310c964d9 100644 --- a/tests/unit_tests/test_arduino_library.py +++ b/tests/unit_tests/test_arduino_library.py @@ -12,7 +12,13 @@ import pytest from esphome.arduino import library as component from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM, PLATFORM_ESP8266 from esphome.core import CORE, EsphomeError, Library -from esphome.platformio.library import ConvertedLibrary, LibraryBackend +import esphome.platformio.library as pio_library +from esphome.platformio.library import ( + ConvertedLibrary, + IncompatiblePlatform, + InvalidLibrary, + LibraryBackend, +) @pytest.fixture(autouse=True) @@ -212,9 +218,7 @@ def test_resolve_libraries_bundled(tmp_path: Path) -> None: def test_resolve_libraries_registry_name_is_external( tmp_path: Path, version: str | None ) -> None: - """A name that is not bundled reaches the converter: bare resolves from - the registry at the latest version (matching PlatformIO and the - documented libraries: key) and a version pin is a registry package.""" + """A name that is not bundled reaches the converter, bare or pinned.""" framework = _make_framework(tmp_path) _add_library("pngle", version) with patch.object(component, "convert_libraries", return_value=[]) as mock_convert: @@ -410,12 +414,8 @@ def test_bundled_dependency_nonplatform_rejection_warns( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: """An InvalidLibrary whose cause is not the platform filter is visible.""" - from esphome.platformio.library import InvalidLibrary - framework = _make_framework(tmp_path) converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]}) - import esphome.platformio.library as pio_library - with ( _emitting_converter(converted), patch.object( @@ -466,9 +466,7 @@ def test_library_info_lib_archive_parse( def test_bundled_dependency_dict_shorthand_prefers_bundled(tmp_path: Path) -> None: - """The {"Wire": "*"} dict shorthand (version="*", no owner) must resolve - to the bundled library, matching PIO's process_dependencies, instead of - being routed to the registry.""" + """The {"Wire": "*"} dict shorthand resolves to the bundled library.""" framework = _make_framework(tmp_path) converted = _webserver(tmp_path, {"build": {}, "dependencies": {"Wire": "*"}}) with _emitting_converter(converted): @@ -481,12 +479,8 @@ def test_bundled_dependency_platform_rejection_is_debug( ) -> None: """The typed IncompatiblePlatform (the routine cross-platform skip) stays at debug regardless of message wording.""" - from esphome.platformio.library import IncompatiblePlatform - framework = _make_framework(tmp_path) converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]}) - import esphome.platformio.library as pio_library - with ( _emitting_converter(converted), patch.object( @@ -626,11 +620,8 @@ def test_bundled_library_non_dict_manifest_skips_probes_and_raises( def test_dict_shorthand_dependency_skips_registry_through_real_converter( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """{"Wire": "*"} in a real manifest must never reach the registry: the - graph walk skips backend-provided names and the bundled copy is added - after emit (no converter mock; a registry touch fails the test).""" - import esphome.platformio.library as pio_library - + """{"Wire": "*"} resolves to the bundled copy without touching the + registry (real converter).""" framework = _make_framework(tmp_path) _local_lib(tmp_path, {"Wire": "*"}) # Pin the component cache to tmp_path (data_dir honors an ambient @@ -702,8 +693,6 @@ def test_pinned_bundled_dependency_substitution_warns( ) -> None: """A non-* version pin on a backend-provided dependency is discarded for the bundled copy; the substitution must be visible.""" - import esphome.platformio.library as pio_library - framework = _make_framework(tmp_path) _local_lib(tmp_path, {"Wire": "^2.0.0"}) monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) @@ -720,9 +709,7 @@ def test_pinned_bundled_dependency_substitution_warns( def test_transitively_resolved_dependency_does_not_warn( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: - """A version-less dependency the walk resolved as another library's - registry dependency is present in the build; the skipping warning must - stay quiet for it.""" + """A dependency the walk already resolved does not warn.""" framework = _make_framework(tmp_path) _add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") ws, tcp = _ws_tcp_pair(tmp_path) @@ -754,9 +741,8 @@ def test_external_short_name(spec: str, expected: str) -> None: def test_converted_manifest_name_suppresses_bundled_dependency( tmp_path: Path, ) -> None: - """A dependency name a converted library's manifest provides is not - also added from the framework tree (a duplicate archive would surface - as duplicate-symbol link errors), even when the provider emits later.""" + """A name a converted library's manifest provides is not also added + from the framework tree, even when the provider emits later.""" framework = _make_framework(tmp_path) _add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") # Requested under a different short name; only the manifest says "Wire" @@ -814,8 +800,6 @@ def test_versionless_dependency_with_provider_stays_quiet( ) -> None: """With a provides backend the version-less skip is routine (debug) and the bundled copy is picked up after emit.""" - import esphome.platformio.library as pio_library - framework = _make_framework(tmp_path) _local_lib(tmp_path, [{"name": "Wire"}]) monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) diff --git a/tests/unit_tests/test_compiled_config.py b/tests/unit_tests/test_compiled_config.py index 62d043f480..4333420a9e 100644 --- a/tests/unit_tests/test_compiled_config.py +++ b/tests/unit_tests/test_compiled_config.py @@ -643,8 +643,7 @@ def test_save_compiled_config_and_sidecar_toolchain_mismatch( tmp_path: Path, sidecar_toolchain: str | None, saved: bool ) -> None: """A config validated under a different toolchain than the compile's - must not overwrite the cache: platforms normalize toolchain-sensitive - keys differently and the sidecar keeps the compile's toolchain.""" + must not overwrite the cache.""" yaml_path = _bare_yaml(tmp_path) _prime_core(tmp_path) CORE.config = {CONF_ESPHOME: {CONF_NAME: "lite_test"}} diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 9f8633667e..0f927a6513 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1,3 +1,4 @@ +import importlib import json import logging from pathlib import Path @@ -48,6 +49,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, Framework, + Toolchain, ) from esphome.core import ( CORE, @@ -3169,9 +3171,6 @@ def test_file__remapped_path_is_directory_raises(setup_core: Path) -> None: def test_require_platformio_toolchain() -> None: """Platforms with only the PlatformIO backend reject other toolchains.""" - from esphome.const import Toolchain - from esphome.core import CORE - validator = cv.require_platformio_toolchain("RP2") CORE.toolchain = None config: dict = {} @@ -3186,9 +3185,6 @@ def test_require_platformio_toolchain() -> None: def test_check_supported_toolchain_unresolved_is_an_ordering_bug() -> None: """Calling the check before resolution fails naming the ordering bug, not a user-facing unsupported-toolchain error.""" - from esphome.const import Toolchain - from esphome.core import CORE - CORE.toolchain = None with pytest.raises(Invalid, match="not resolved before RP2 validation"): cv._check_supported_toolchain("RP2", (Toolchain.PLATFORMIO,)) @@ -3209,14 +3205,7 @@ def test_check_supported_toolchain_unresolved_is_an_ordering_bug() -> None: def test_every_platformio_only_platform_rejects_arduino_toolchain( platform: str, minimal_config: dict ) -> None: - """The invariant every native-toolchain gate relies on: a platform that - cannot serve a CLI toolchain rejects it at validation (esp32, esp8266, - and nrf52 pin this in their own suites).""" - import importlib - - from esphome.const import Toolchain - from esphome.core import CORE - + """A platform that cannot serve a CLI toolchain rejects it at validation.""" module = importlib.import_module(f"esphome.components.{platform}") CORE.toolchain = Toolchain.ARDUINO with pytest.raises(Invalid, match="Unsupported toolchain 'arduino'"): diff --git a/tests/unit_tests/test_espidf_component.py b/tests/unit_tests/test_espidf_component.py index 1baa22777c..e2884454e5 100644 --- a/tests/unit_tests/test_espidf_component.py +++ b/tests/unit_tests/test_espidf_component.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock import pytest +from esphome.components import esp32 as esp32_module from esphome.const import ( KEY_CORE, KEY_TARGET_FRAMEWORK, @@ -15,6 +16,7 @@ from esphome.const import ( ) from esphome.core import CORE, Library from esphome.espidf.component import ( + _emit_idf_component, generate_cmakelists_txt, generate_idf_component_yml, generate_idf_components, @@ -1072,8 +1074,6 @@ def test_idf_component_download_passes_salt() -> None: def test_emit_idf_component_wires_esp32_target(tmp_path, monkeypatch): """Emitting a component resolves the esp32 variant into the shared extraScript helper.""" - from esphome.components import esp32 as esp32_module - from esphome.espidf.component import _emit_idf_component monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32") (tmp_path / "src").mkdir() diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index 32e2d7a3fb..d66f1fc7db 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -1395,8 +1395,6 @@ def test_get_framework_env_without_python_env_uses_os_path(tmp_path: Path) -> No def _ccache_patches(tmp_path: Path, which: str | None, build_path: Path | None): return ( - # The gate defers to the shared resolver (which carries the PATH - # lookup, ESPHOME_CCACHE_ENABLE parse, and runnability probe) patch("esphome.espidf.framework.resolve_ccache_path", return_value=which), patch( "esphome.espidf.framework.get_idf_tools_path", diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index b991c9900e..b820f6b551 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -101,6 +101,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WIFI, KEY_CORE, + KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -7139,7 +7140,7 @@ def test_warn_source_tree_mismatch_falls_back_when_stat_fails( RuntimeError("Could not query builtin include dirs"), ValueError("no C++ translation unit found"), KeyError("command"), - None, # replaced with EsphomeError inside (import is function-local) + None, # replaced with EsphomeError inside ], ) def test_compile_program_espidf_idedata_failure_does_not_fail_build( @@ -7147,14 +7148,6 @@ def test_compile_program_espidf_idedata_failure_does_not_fail_build( caplog: pytest.LogCaptureFixture, ) -> None: """A post-compile idedata error is a warning: the firmware already built.""" - from esphome.const import ( - KEY_CORE, - KEY_TARGET_FRAMEWORK, - KEY_TARGET_PLATFORM, - Toolchain, - ) - from esphome.core import CORE, EsphomeError - if error is None: error = EsphomeError("compile database is unusable") CORE.toolchain = Toolchain.ESP_IDF @@ -7178,14 +7171,6 @@ def test_compile_program_espidf_idedata_success_is_silent( caplog: pytest.LogCaptureFixture, ) -> None: """The healthy path: idedata generated, nothing to warn about.""" - from esphome.const import ( - KEY_CORE, - KEY_TARGET_FRAMEWORK, - KEY_TARGET_PLATFORM, - Toolchain, - ) - from esphome.core import CORE - CORE.toolchain = Toolchain.ESP_IDF CORE.data[KEY_CORE] = { KEY_TARGET_PLATFORM: "esp32", @@ -7207,14 +7192,6 @@ def test_compile_program_espidf_idedata_none_warns( caplog: pytest.LogCaptureFixture, ) -> None: """A silent None from the post-compile idedata refresh is made visible.""" - from esphome.const import ( - KEY_CORE, - KEY_TARGET_FRAMEWORK, - KEY_TARGET_PLATFORM, - Toolchain, - ) - from esphome.core import CORE - CORE.toolchain = Toolchain.ESP_IDF CORE.data[KEY_CORE] = { KEY_TARGET_PLATFORM: "esp32", @@ -7235,8 +7212,6 @@ def test_compile_program_espidf_idedata_none_warns( def test_cli_toolchain_skips_the_validated_config_cache(tmp_path: Path) -> None: """An explicit --toolchain must run the per-platform validators, so the upload/logs fast path becomes a cache miss.""" - from esphome.__main__ import run_esphome - conf = tmp_path / "device.yaml" conf.write_text("esphome:\n name: t\n") argv = ["esphome", "--toolchain", "arduino", "logs", str(conf)] @@ -7255,8 +7230,6 @@ def test_cli_toolchain_still_refreshes_the_validated_config_cache( """An explicit --toolchain gates only the cache read; the freshly validated config is still saved so a later plain run keeps the fast path (an existing compile-written sidecar keeps its toolchain).""" - from esphome.__main__ import run_esphome - conf = tmp_path / "device.yaml" conf.write_text("esphome:\n name: t\n") argv = ["esphome", "--toolchain", "platformio", "logs", str(conf)] diff --git a/tests/unit_tests/test_nrf52_framework.py b/tests/unit_tests/test_nrf52_framework.py index b5f6b5794f..65b73e37fe 100644 --- a/tests/unit_tests/test_nrf52_framework.py +++ b/tests/unit_tests/test_nrf52_framework.py @@ -7,8 +7,10 @@ import sys from types import SimpleNamespace from unittest.mock import patch +import platformdirs import pytest +from esphome.components.nrf52 import _resolve_toolchain from esphome.components.nrf52.framework import ( _PLATFORMIO_PENV_REQUIREMENTS, _REQUIREMENTS, @@ -22,8 +24,9 @@ from esphome.components.nrf52.framework import ( get_sdk_nrf_tools_path, setup_platformio_python_env, ) +import esphome.config_validation as cv from esphome.config_validation import Version -from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION +from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain from esphome.core import CORE, EsphomeError from esphome.framework_helpers import get_python_env_executable_path @@ -558,7 +561,6 @@ def testget_tools_path_blank_env_falls_back_to_default( Path("") would resolve to the working directory, which clean-all could then delete by accident. """ - import platformdirs monkeypatch.setenv("ESPHOME_SDK_NRF_PREFIX", value) expected = ( @@ -570,7 +572,6 @@ def testget_tools_path_blank_env_falls_back_to_default( def testget_tools_path_default_is_global_cache( monkeypatch: pytest.MonkeyPatch, ) -> None: - import platformdirs monkeypatch.delenv("ESPHOME_SDK_NRF_PREFIX", raising=False) expected = ( @@ -623,9 +624,6 @@ def test_needs_venv_rebuild_on_dangling_interpreter_symlink(tmp_path: Path) -> N def test_resolve_toolchain_rejects_unsupported() -> None: """A --toolchain nRF52 cannot serve fails instead of degrading silently.""" - from esphome.components.nrf52 import _resolve_toolchain - import esphome.config_validation as cv - from esphome.const import Toolchain CORE.toolchain = Toolchain.ARDUINO with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"): diff --git a/tests/unit_tests/test_platformio_extra_script.py b/tests/unit_tests/test_platformio_extra_script.py index 7e904183f1..d09f02d95f 100644 --- a/tests/unit_tests/test_platformio_extra_script.py +++ b/tests/unit_tests/test_platformio_extra_script.py @@ -2,19 +2,25 @@ from __future__ import annotations +import logging import os from pathlib import Path +from unittest.mock import patch import pytest +from esphome.core import EsphomeError +from esphome.platformio.extra_script import ( + ExtraScriptResult, + _FakeSConsEnv, + apply_extra_script, + captured_as_build_flags, + run_extra_script, +) from esphome.platformio.library import ConvertedLibrary as IDFComponent, URLSource def test_extra_script_captures_libpath_libs_and_defines(tmp_path): - from esphome.platformio.extra_script import ( - captured_as_build_flags, - run_extra_script, - ) (tmp_path / "src" / "esp32").mkdir(parents=True) script = tmp_path / "extra_script.py" @@ -58,10 +64,6 @@ def test_extra_script_libpath_relative_resolves_against_library_dir( """Relative LIBPATH entries must resolve against ``library_dir``, not the caller's CWD (the shim restores CWD before ``captured_as_build_flags`` runs).""" - from esphome.platformio.extra_script import ( - ExtraScriptResult, - captured_as_build_flags, - ) (tmp_path / "lib" / "esp32").mkdir(parents=True) elsewhere = tmp_path.parent / "not_the_library_dir" @@ -76,10 +78,6 @@ def test_extra_script_libpath_relative_resolves_against_library_dir( def test_extra_script_libpath_absolute_outside_library_dir(tmp_path): - from esphome.platformio.extra_script import ( - ExtraScriptResult, - captured_as_build_flags, - ) outside = tmp_path.parent / "system_lib" outside.mkdir(exist_ok=True) @@ -90,7 +88,6 @@ def test_extra_script_libpath_absolute_outside_library_dir(tmp_path): def test_extra_script_failure_returns_empty_result(tmp_path, caplog): - from esphome.platformio.extra_script import run_extra_script script = tmp_path / "broken.py" script.write_text("raise RuntimeError('boom')\n") @@ -106,7 +103,6 @@ def test_extra_script_failure_returns_empty_result(tmp_path, caplog): def test_apply_extra_script_path_traversal_is_rejected(tmp_path): - from esphome.platformio.extra_script import apply_extra_script library_dir = tmp_path / "lib" library_dir.mkdir() @@ -117,8 +113,6 @@ def test_apply_extra_script_path_traversal_is_rejected(tmp_path): c.path = library_dir c.data = {"build": {"extraScript": "../evil.py"}} - from esphome.core import EsphomeError - with pytest.raises(EsphomeError, match="escapes the library directory"): apply_extra_script(c, board_mcu=lambda: "esp32", pio_platform="espressif32") # Nothing was folded into flags: the traversal was rejected before @@ -127,7 +121,6 @@ def test_apply_extra_script_path_traversal_is_rejected(tmp_path): def test_apply_extra_script_merges_into_existing_flags(tmp_path): - from esphome.platformio.extra_script import apply_extra_script (tmp_path / "src").mkdir() script = tmp_path / "extra.py" @@ -146,8 +139,6 @@ def test_apply_extra_script_merges_into_existing_flags(tmp_path): def test_apply_extra_script_malformed_flags_raises(tmp_path) -> None: """A null/dict build.flags fails naming the library instead of injecting a non-string into the compiler command line.""" - from esphome.core import EsphomeError - from esphome.platformio.extra_script import apply_extra_script (tmp_path / "src").mkdir() script = tmp_path / "extra.py" @@ -164,7 +155,6 @@ def test_apply_extra_script_malformed_flags_raises(tmp_path) -> None: def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None: """The shared helper resolves the board_mcu callable lazily and normalizes a string ``build.flags`` value into a list before extending it.""" - from esphome.platformio.extra_script import apply_extra_script (tmp_path / "src").mkdir() script = tmp_path / "extra.py" @@ -180,7 +170,6 @@ def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None: def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None: - from esphome.platformio.extra_script import apply_extra_script # No extraScript declared: nothing happens, the target is never resolved c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) @@ -203,9 +192,6 @@ def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None: def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path, caplog) -> None: """Un-captured env vars and unsupported env methods are skipped but diagnosable from the build log.""" - import logging - - from esphome.platformio.extra_script import apply_extra_script caplog.set_level(logging.DEBUG) script = tmp_path / "extra.py" @@ -223,7 +209,6 @@ def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path, caplog) -> No def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None: """A raising extra-script is best-effort: logged and skipped.""" - from esphome.platformio.extra_script import apply_extra_script script = tmp_path / "extra.py" script.write_text("raise RuntimeError('boom')\n") @@ -237,7 +222,6 @@ def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None: def test_apply_extra_script_pio_platform(tmp_path) -> None: """The backend's platform token is exposed to the script as PIOPLATFORM.""" - from esphome.platformio.extra_script import apply_extra_script script = tmp_path / "extra.py" script.write_text("env.Append(LIBS=[env.get('PIOPLATFORM')])\n") @@ -251,8 +235,6 @@ def test_apply_extra_script_pio_platform(tmp_path) -> None: def test_apply_extra_script_missing_script_raises(tmp_path) -> None: """A declared but absent extraScript is a broken package and fails by name, as it would under PlatformIO.""" - from esphome.core import EsphomeError - from esphome.platformio.extra_script import apply_extra_script c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) c.path = tmp_path @@ -264,7 +246,6 @@ def test_apply_extra_script_missing_script_raises(tmp_path) -> None: def test_run_extra_script_failure_discards_partial_capture(tmp_path, caplog) -> None: """A crashed script yields an empty result: half-applied flags could build wrong-output firmware that links cleanly.""" - from esphome.platformio.extra_script import run_extra_script script = tmp_path / "extra.py" script.write_text("env.Append(LIBS=['algobsec'])\nraise RuntimeError('boom')\n") @@ -278,7 +259,6 @@ def test_run_extra_script_failure_discards_partial_capture(tmp_path, caplog) -> def test_run_extra_script_syntax_error_is_best_effort(tmp_path, caplog) -> None: """A vendored script that does not even compile warns and skips instead of aborting the build.""" - from esphome.platformio.extra_script import run_extra_script script = tmp_path / "extra.py" script.write_text("def broken(:\n") @@ -291,7 +271,6 @@ def test_run_extra_script_syntax_error_is_best_effort(tmp_path, caplog) -> None: def test_unsupported_env_method_warns_once(caplog) -> None: """Repeated calls to the same unsupported method warn only once.""" - from esphome.platformio.extra_script import _FakeSConsEnv env = _FakeSConsEnv( board_mcu="esp8266", pio_env="esphome_esp8266", pio_platform="espressif8266" @@ -304,7 +283,6 @@ def test_unsupported_env_method_warns_once(caplog) -> None: def test_run_extra_script_sys_exit_is_best_effort(tmp_path, caplog) -> None: """A nonzero sys.exit() in a vendored script must not kill the esphome run, and its output is discarded.""" - from esphome.platformio.extra_script import run_extra_script script = tmp_path / "extra.py" script.write_text("import sys\nenv.Append(LIBS=['x'])\nsys.exit(3)\n") @@ -317,7 +295,6 @@ def test_run_extra_script_sys_exit_is_best_effort(tmp_path, caplog) -> None: def test_run_extra_script_sys_exit_zero_is_success(tmp_path, caplog) -> None: """sys.exit(0) is a normal PlatformIO script ending: the capture is kept.""" - from esphome.platformio.extra_script import run_extra_script script = tmp_path / "extra.py" script.write_text("import sys\nenv.Append(LIBS=['algobsec'])\nsys.exit(0)\n") @@ -330,10 +307,6 @@ def test_run_extra_script_sys_exit_zero_is_success(tmp_path, caplog) -> None: def test_run_extra_script_unreadable_raises(tmp_path) -> None: """An unreadable declared script is a broken package, like a missing one.""" - from unittest.mock import patch - - from esphome.core import EsphomeError - from esphome.platformio.extra_script import run_extra_script script = tmp_path / "extra.py" script.write_text("") @@ -348,7 +321,6 @@ def test_run_extra_script_unreadable_raises(tmp_path) -> None: def test_run_extra_script_bad_encoding_is_best_effort(tmp_path, caplog) -> None: """Undecodable content warns and skips, like a SyntaxError.""" - from esphome.platformio.extra_script import run_extra_script script = tmp_path / "extra.py" script.write_bytes(b"\xff\xfe\x00bad") @@ -361,7 +333,6 @@ def test_run_extra_script_bad_encoding_is_best_effort(tmp_path, caplog) -> None: def test_uncaptured_append_key_warns_once(caplog) -> None: """A loop of Appends to the same uncaptured key warns once.""" - from esphome.platformio.extra_script import _FakeSConsEnv env = _FakeSConsEnv( board_mcu="esp8266", pio_env="esphome_esp8266", pio_platform="espressif8266" diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 67acebe505..febaf830a5 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -13,6 +13,7 @@ import pytest from esphome.core import EsphomeError, Library import esphome.platformio.library as lib from esphome.platformio.library import ( + SOURCE_KIND_FOR_SUFFIX, ConvertedLibrary, GitSource, InvalidLibrary, @@ -23,6 +24,8 @@ from esphome.platformio.library import ( _resolve_registry_version, check_library_data, convert_libraries, + join_flag_args, + split_flag_entry, ) @@ -539,7 +542,6 @@ def test_convert_libraries_skips_incompatible_dependency(tmp_path, monkeypatch): def test_split_flag_entry_unbalanced_quote_is_clean() -> None: """A malformed flags entry raises EsphomeError, not a raw ValueError.""" - from esphome.platformio.library import split_flag_entry assert split_flag_entry('-DX="a b"', "library x") == ["-DX=a b"] with pytest.raises(EsphomeError, match=r"Malformed build flag.*library x"): @@ -548,7 +550,6 @@ def test_split_flag_entry_unbalanced_quote_is_clean() -> None: def test_join_flag_args_reglues_spaced_define() -> None: """A spaced -D re-glues to its argument, as ParseFlags does.""" - from esphome.platformio.library import join_flag_args assert join_flag_args(["-D", "FOO=1", "-Os"], "x") == ["-DFOO=1", "-Os"] @@ -556,7 +557,6 @@ def test_join_flag_args_reglues_spaced_define() -> None: def test_join_flag_args_trailing_bare_flag_warns( caplog: pytest.LogCaptureFixture, ) -> None: - from esphome.platformio.library import join_flag_args assert join_flag_args(["-Os", "-l"], "library x") == ["-Os"] assert "Ignoring trailing '-l'" in caplog.text @@ -576,7 +576,6 @@ def test_lex_build_flags_dangling_flag_does_not_cross_entries( def test_split_flag_entry_non_string_is_clean() -> None: """A dict or number from a third-party manifest fails naming the entry, not with an opaque shlex traceback.""" - from esphome.platformio.library import split_flag_entry with pytest.raises(EsphomeError, match="Malformed build flag"): split_flag_entry({"esp32": ["-DX"]}, "lib x") @@ -587,7 +586,6 @@ def test_split_flag_entry_non_string_is_clean() -> None: def test_source_kind_map_shape() -> None: """The kind values the native compile rules key on, and the deliberate AS/ASPP merge (.s and .S both map to asm).""" - from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX assert set(SOURCE_KIND_FOR_SUFFIX.values()) == {"c", "cxx", "asm"} assert SOURCE_KIND_FOR_SUFFIX[".s"] == "asm" @@ -681,9 +679,8 @@ def test_walk_warns_for_nonplatform_invalid_library( def test_versionless_owner_qualified_dependency_warns_despite_provides( tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture ) -> None: - """The backend's provides() only covers owner-less names (the walk's - backend-provided skip has the same guard), so an owner-qualified - version-less dependency that nobody adds must still warn.""" + """An owner-qualified version-less dependency is not satisfied by + provides(); it must still warn.""" _patch_download_with_manifests( monkeypatch, tmp_path, @@ -758,9 +755,8 @@ def test_versionless_url_ish_dependency_name_warns_cleanly( def test_versionless_dependency_matching_resolved_manifest_name_stays_quiet( tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture ) -> None: - """A bare dependency name satisfied by a component requested under an - owner-qualified spec (manifest names match) is not a drop; a nameless - entry is skipped without a reconciliation warning.""" + """A bare name satisfied by an owner-qualified component's manifest + name is not a drop.""" _patch_download_with_manifests( monkeypatch, tmp_path, diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 6c05fc330b..9ccfc4f4ab 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1051,8 +1051,6 @@ def test_clean_all_removes_global_arduino8266_install( config_dir = tmp_path / "config" config_dir.mkdir() - from esphome.writer import clean_all - with caplog.at_level("INFO"): clean_all([str(config_dir)])