diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index a49c145d4e..f1f730ce63 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -580,16 +580,12 @@ def _make_registry_client() -> Any: class _Registry(PackageManagerRegistryMixin): def __init__(self) -> None: - self._registry_client = None self.pkg_type = "library" - - def get_registry_client_instance(self) -> RegistryClient: - if self._registry_client is None: - self._registry_client = RegistryClient() - # Skip PlatformIO's account probe: it sleeps ~500 ms per lookup - # (see runner.patch_registry_private_packages) - self._registry_client.allowed_private_packages = lambda: False - return self._registry_client + self._registry_client = RegistryClient() + # PlatformIO's account probe sleeps ~500 ms per lookup (see + # runner.patch_registry_private_packages); override it on our own + # instance so the ESPHome process never patches PlatformIO's class + self._registry_client.allowed_private_packages = lambda: False @staticmethod def is_system_compatible(value: Any, custom_system: Any = None) -> bool: diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index 659dd6c0ff..da6426e4a3 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -925,6 +925,7 @@ def main(argv: list[str]) -> int: from esphome.platformio.runner import patch_registry_private_packages signal.signal(signal.SIGTERM, _sigterm) + patch_registry_private_packages() raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL") try: level = int(raw_level) if raw_level is not None else logging.INFO @@ -951,7 +952,6 @@ def main(argv: list[str]) -> int: _LOGGER.warning("prefetch usage: ") return 2 build_dir, env = argv - patch_registry_private_packages() try: _prefetch(Path(build_dir), env) except KeyboardInterrupt: diff --git a/esphome/platformio/runner.py b/esphome/platformio/runner.py index a5193bfa08..904bec2925 100644 --- a/esphome/platformio/runner.py +++ b/esphome/platformio/runner.py @@ -115,14 +115,7 @@ def patch_registry_private_packages() -> None: """ from platformio.registry.client import RegistryClient - if getattr(RegistryClient.allowed_private_packages, "_esphome_patched", False): - return - - def no_private_packages() -> bool: - return False - - no_private_packages._esphome_patched = True # type: ignore[attr-defined] # pylint: disable=protected-access - RegistryClient.allowed_private_packages = staticmethod(no_private_packages) # type: ignore[method-assign] + RegistryClient.allowed_private_packages = staticmethod(lambda: False) # type: ignore[method-assign] _IGNORE_LIB_WARNINGS = "(?:Hash|Update)" diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 5ebb0388f1..accff40128 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -7,6 +7,7 @@ exercised in their own test modules).""" import json import logging from pathlib import Path +from unittest.mock import Mock import pytest @@ -225,23 +226,20 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch): def test_make_registry_client_skips_private_package_probe(monkeypatch): - """Our client never calls PlatformIO's account probe, and only ours.""" + """Our client answers the probe locally without patching PlatformIO's class.""" from platformio.account.client import AccountClient from platformio.registry.client import RegistryClient pio_probe = RegistryClient.__dict__["allowed_private_packages"] + monkeypatch.setattr( + AccountClient, + "get_account_info", + Mock(side_effect=AssertionError("account probe must not run")), + ) - def fail(*_args, **_kwargs): - raise AssertionError("account probe must not run") - - monkeypatch.setattr(AccountClient, "get_account_info", fail) - - registry = lib._make_registry_client() - client = registry.get_registry_client_instance() + client = lib._make_registry_client().get_registry_client_instance() assert client.allowed_private_packages() is False - assert registry.get_registry_client_instance() is client - # Instance override only; the class keeps PlatformIO's probe in this process assert RegistryClient.__dict__["allowed_private_packages"] is pio_probe diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index e689f84925..0d9821ca19 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1151,17 +1151,18 @@ def test_main_runs_prefetch(tmp_path: Path) -> None: mock_prefetch.assert_called_once_with(tmp_path, "testenv") -def test_main_skips_private_package_probe(tmp_path: Path) -> None: +def test_main_skips_private_package_probe_before_prefetch(tmp_path: Path) -> None: """The registry probe patch is applied before any package manager runs.""" + order: list[str] = [] with ( - patch.object(pf, "_prefetch") as mock_prefetch, + patch.object(pf, "_prefetch", side_effect=lambda *_: order.append("prefetch")), patch( - "esphome.platformio.runner.patch_registry_private_packages" - ) as mock_patch, + "esphome.platformio.runner.patch_registry_private_packages", + side_effect=lambda: order.append("patch"), + ), ): assert pf.main([str(tmp_path), "testenv"]) == 0 - mock_patch.assert_called_once_with() - mock_prefetch.assert_called_once() + assert order == ["patch", "prefetch"] def test_main_bad_argv_is_a_distinct_exit( diff --git a/tests/unit_tests/test_platformio_runner.py b/tests/unit_tests/test_platformio_runner.py index a3076b1755..007455f45a 100644 --- a/tests/unit_tests/test_platformio_runner.py +++ b/tests/unit_tests/test_platformio_runner.py @@ -6,6 +6,7 @@ from collections.abc import Callable import io import sys from types import ModuleType +from unittest.mock import Mock from platformio.registry.client import RegistryClient import pytest @@ -113,40 +114,20 @@ def test_main_applies_registry_private_packages_patch( _PIO_PROBE = RegistryClient.__dict__["allowed_private_packages"] -@pytest.fixture -def _restore_registry_probe(): - """Start from and return to PlatformIO's own probe.""" - RegistryClient.allowed_private_packages = _PIO_PROBE # type: ignore[method-assign] - yield - RegistryClient.allowed_private_packages = _PIO_PROBE # type: ignore[method-assign] - - -@pytest.mark.usefixtures("_restore_registry_probe") def test_patch_registry_private_packages_skips_account_probe( monkeypatch: pytest.MonkeyPatch, ) -> None: """Answers False without touching the account client.""" from platformio.account.client import AccountClient - assert RegistryClient.__dict__["allowed_private_packages"] is _PIO_PROBE - - def fail(*_args, **_kwargs): - raise AssertionError("account probe must not run") - - monkeypatch.setattr(AccountClient, "get_account_info", fail) + monkeypatch.setattr(RegistryClient, "allowed_private_packages", _PIO_PROBE) + monkeypatch.setattr( + AccountClient, + "get_account_info", + Mock(side_effect=AssertionError("account probe must not run")), + ) runner.patch_registry_private_packages() assert RegistryClient.allowed_private_packages() is False assert RegistryClient().allowed_private_packages() is False - - -@pytest.mark.usefixtures("_restore_registry_probe") -def test_patch_registry_private_packages_is_idempotent() -> None: - assert RegistryClient.__dict__["allowed_private_packages"] is _PIO_PROBE - - runner.patch_registry_private_packages() - patched = RegistryClient.allowed_private_packages - runner.patch_registry_private_packages() - - assert RegistryClient.allowed_private_packages is patched