diff --git a/esphome/__main__.py b/esphome/__main__.py index 01b33eb8acf..54d6384bfc0 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -766,24 +766,24 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: _LOGGER.info("Compiling app... Build path: %s", CORE.build_path) if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - rc = api.run_compile(config, CORE.verbose) + rc = toolchain.run_compile(config, CORE.verbose) if rc != 0: return rc # Create factory.bin, ota.bin, and firmware.elf copy - api.create_factory_bin() - api.create_ota_bin() - api.create_elf_copy() + toolchain.create_factory_bin() + toolchain.create_ota_bin() + toolchain.create_elf_copy() else: - from esphome import platformio_api + from esphome.platformio import toolchain - rc = platformio_api.run_compile(config, CORE.verbose) + rc = toolchain.run_compile(config, CORE.verbose) if rc != 0: return rc - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if idedata is None: return 1 @@ -879,13 +879,15 @@ def upload_using_esptool( if file is not None: flash_images = [FlashImage(path=file, offset="0x0")] elif CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - flash_images = [FlashImage(path=api.get_factory_firmware_path(), offset="0x0")] + flash_images = [ + FlashImage(path=toolchain.get_factory_firmware_path(), offset="0x0") + ] else: - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) firmware_offset = "0x10000" if CORE.is_esp32 else "0x0" flash_images = [ @@ -958,13 +960,13 @@ def upload_using_esptool( def upload_using_platformio(config: ConfigType, port: str) -> int: - from esphome import platformio_api + from esphome.platformio import toolchain # RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for # the upload target, but 'nobuild' skips the build phase that creates it. # Create it here so the upload doesn't fail. if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040: - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) build_dir = Path(idedata.firmware_elf_path).parent firmware_bin = build_dir / "firmware.bin" signed_bin = build_dir / "firmware.bin.signed" @@ -974,15 +976,15 @@ def upload_using_platformio(config: ConfigType, port: str) -> int: upload_args = ["-t", "upload", "-t", "nobuild"] if port is not None: upload_args += ["--upload-port", port] - return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) + return toolchain.run_platformio_cli_run(config, CORE.verbose, *upload_args) def _find_picotool() -> Path | None: """Find the picotool binary from PlatformIO packages.""" - from esphome import platformio_api + from esphome.platformio import toolchain try: - idedata = platformio_api.get_idedata(CORE.config) + idedata = toolchain.get_idedata(CORE.config) except Exception: # noqa: BLE001 # pylint: disable=broad-except return None return get_picotool_path(idedata.cc_path) @@ -995,9 +997,9 @@ def upload_using_picotool(config: ConfigType) -> int: the mass storage copy approach that causes "disk not ejected properly" warnings on macOS. """ - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) firmware_elf = Path(idedata.firmware_elf_path) if not firmware_elf.is_file(): @@ -1457,11 +1459,11 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: return exit_code if CORE.is_host: if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - program_path = str(api.get_elf_path()) + program_path = str(toolchain.get_elf_path()) else: - from esphome.platformio_api import get_idedata + from esphome.platformio.toolchain import get_idedata program_path = str(get_idedata(config).firmware_elf_path) _LOGGER.info("Successfully compiled program to path '%s'", program_path) @@ -1515,11 +1517,11 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: _LOGGER.info("Successfully compiled program.") if CORE.is_host: if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - program_path = str(api.get_elf_path()) + program_path = str(toolchain.get_elf_path()) else: - from esphome.platformio_api import get_idedata + from esphome.platformio.toolchain import get_idedata program_path = str(get_idedata(config).firmware_elf_path) _LOGGER.info("Running program from path '%s'", program_path) @@ -1719,12 +1721,12 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: ) return 1 - from esphome import platformio_api + from esphome.platformio import toolchain logging.disable(logging.INFO) logging.disable(logging.WARNING) - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if idedata is None: return 1 @@ -1753,16 +1755,16 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: # Get idedata for analysis idedata = None if CORE.using_toolchain_esp_idf: - from esphome.espidf import api + from esphome.espidf import toolchain - objdump_path = str(api.get_objdump_path()) - readelf_path = str(api.get_readelf_path()) + objdump_path = str(toolchain.get_objdump_path()) + readelf_path = str(toolchain.get_readelf_path()) - firmware_elf = api.get_elf_path() + firmware_elf = toolchain.get_elf_path() else: - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if idedata is None: _LOGGER.error("Failed to get IDE data for memory analysis") return 1 diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 33854ac2896..1198562218d 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -24,7 +24,7 @@ from .helpers import ( from .toolchain import find_tool, resolve_tool_path, run_tool if TYPE_CHECKING: - from esphome.platformio_api import IDEData + from esphome.platformio.toolchain import IDEData _LOGGER = logging.getLogger(__name__) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index b7561e8ffc0..8f1f39e1d65 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -739,7 +739,7 @@ def main(): import json from pathlib import Path - from esphome.platformio_api import IDEData + from esphome.platformio.toolchain import IDEData build_path = Path(build_dir) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ba32d13ab34..bb823937aa0 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2582,9 +2582,9 @@ def copy_files(): def _decode_pc(config, addr): - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if not idedata.addr2line_path or not idedata.firmware_elf_path: _LOGGER.debug("decode_pc no addr2line") return diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index b6383653f4e..38df282fb98 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -463,9 +463,9 @@ ESP8266_EXCEPTION_CODES = { def _decode_pc(config, addr): - from esphome import platformio_api + from esphome.platformio import toolchain - idedata = platformio_api.get_idedata(config) + idedata = toolchain.get_idedata(config) if not idedata.addr2line_path or not idedata.firmware_elf_path: _LOGGER.debug("decode_pc no addr2line") return diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index d2ed3b15e9c..38efccab11d 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -397,11 +397,11 @@ def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: def _upload_using_platformio( config: ConfigType, port: str, upload_args: list[str] ) -> int | str: - from esphome import platformio_api + from esphome.platformio import toolchain if port is not None: upload_args += ["--upload-port", port] - return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) + return toolchain.run_platformio_cli_run(config, CORE.verbose, *upload_args) def upload_program(config: ConfigType, args, host: str) -> bool: diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 7e450578cdd..81809c05519 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -510,7 +510,7 @@ def process_stacktrace(config, line: str, backtrace_state: bool) -> bool: if backtrace_state: if match := _CRASH_ADDR_RE.search(line): - from esphome.platformio_api import get_idedata + from esphome.platformio.toolchain import get_idedata idedata = get_idedata(config) if idedata.addr2line_path: diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index d67245967c5..916e937a532 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -40,8 +40,9 @@ import voluptuous as vol import yaml from yaml.nodes import Node -from esphome import const, platformio_api, yaml_util +from esphome import const, yaml_util from esphome.helpers import get_bool_env, mkdir_p, sort_ip_addresses +from esphome.platformio import toolchain from esphome.storage_json import ( StorageJSON, archive_storage_path, @@ -1090,7 +1091,7 @@ class DownloadBinaryRequestHandler(BaseHandler): self.send_error(404 if rc == 2 else 500) return - idedata = platformio_api.IDEData(json.loads(stdout)) + idedata = toolchain.IDEData(json.loads(stdout)) found = False for image in idedata.extra_flash_images: diff --git a/esphome/espidf/runner.py b/esphome/espidf/runner.py index e740ab72854..34e3e7694b5 100644 --- a/esphome/espidf/runner.py +++ b/esphome/espidf/runner.py @@ -190,7 +190,7 @@ def main() -> int: script_path = sys.argv[1] - # Mirror the platformio_runner behaviour: verbose mode disables the + # Mirror the platformio runner behaviour: verbose mode disables the # line filter so all output reaches the user. is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[2:]) filter_lines = None if is_verbose else FILTER_IDF_LINES or None diff --git a/esphome/espidf/api.py b/esphome/espidf/toolchain.py similarity index 99% rename from esphome/espidf/api.py rename to esphome/espidf/toolchain.py index 847de249a7f..da6d3a8a37f 100644 --- a/esphome/espidf/api.py +++ b/esphome/espidf/toolchain.py @@ -15,7 +15,7 @@ from esphome.espidf.framework import check_esp_idf_install, get_framework_env _LOGGER = logging.getLogger(__name__) -DOMAIN = "espidf_api" +DOMAIN = "espidf_toolchain" @dataclass diff --git a/esphome/platformio/__init__.py b/esphome/platformio/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/platformio_runner.py b/esphome/platformio/runner.py similarity index 99% rename from esphome/platformio_runner.py rename to esphome/platformio/runner.py index 5b14a725577..976979dc57b 100644 --- a/esphome/platformio_runner.py +++ b/esphome/platformio/runner.py @@ -1,6 +1,6 @@ """Subprocess entry point that applies ESPHome's PlatformIO patches. -Invoked via ``python -m esphome.platformio_runner`` instead of +Invoked via ``python -m esphome.platformio.runner`` instead of ``python -m platformio`` so that the patches (incremental rebuild preservation, download retries) apply inside the subprocess. Running PlatformIO in a subprocess keeps its ``sys.path`` mutations and other diff --git a/esphome/platformio_api.py b/esphome/platformio/toolchain.py similarity index 99% rename from esphome/platformio_api.py rename to esphome/platformio/toolchain.py index 81ff01306a6..073e134ac4b 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio/toolchain.py @@ -64,7 +64,7 @@ def run_platformio_cli(*args, **kwargs) -> str | int: # a user-provided value (or the unmodified path on platforms that # don't need the strip). os.environ["PYTHONEXEPATH"] = python_exe - cmd = [python_exe, "-m", "esphome.platformio_runner"] + list(args) + cmd = [python_exe, "-m", "esphome.platformio.runner"] + list(args) return run_external_process(*cmd, **kwargs) diff --git a/script/build_helpers.py b/script/build_helpers.py index 0e0e8170a0a..fa722aa0990 100644 --- a/script/build_helpers.py +++ b/script/build_helpers.py @@ -23,7 +23,7 @@ from esphome.config import validate_config from esphome.const import CONF_PLATFORM from esphome.core import CORE from esphome.loader import get_component, get_platform -from esphome.platformio_api import get_idedata +from esphome.platformio.toolchain import get_idedata from tests.testing_helpers import ComponentManifestOverride, set_testing_manifest # This must coincide with the version in /platformio.ini diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py index dd91fa861ce..2aa7394b112 100755 --- a/script/ci_memory_impact_extract.py +++ b/script/ci_memory_impact_extract.py @@ -26,7 +26,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) # pylint: disable=wrong-import-position from esphome.analyze_memory import MemoryAnalyzer -from esphome.platformio_api import IDEData +from esphome.platformio.toolchain import IDEData from script.ci_helpers import write_github_output # Regex patterns for extracting memory usage from PlatformIO output diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 1a62cfda904..626aea02162 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -128,8 +128,8 @@ def mock_storage_json() -> Generator[MagicMock]: @pytest.fixture def mock_idedata() -> Generator[MagicMock]: - """Fixture to mock platformio_api.IDEData.""" - with patch("esphome.dashboard.web_server.platformio_api.IDEData") as mock: + """Fixture to mock platformio toolchain.IDEData.""" + with patch("esphome.dashboard.web_server.toolchain.IDEData") as mock: yield mock diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f36543b7cd9..fb025ce427f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -23,7 +23,7 @@ import pytest_asyncio import esphome.config from esphome.core import CORE -from esphome.platformio_api import get_idedata +from esphome.platformio.toolchain import get_idedata from .const import ( API_CONNECTION_TIMEOUT, diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 626f4168a60..13450b10f08 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -64,15 +64,15 @@ def mock_copy_file_if_changed() -> Generator[Mock, None, None]: @pytest.fixture def mock_run_platformio_cli() -> Generator[Mock, None, None]: - """Mock run_platformio_cli for platformio_api.""" - with patch("esphome.platformio_api.run_platformio_cli") as mock: + """Mock run_platformio_cli for platformio toolchain.""" + with patch("esphome.platformio.toolchain.run_platformio_cli") as mock: yield mock @pytest.fixture def mock_run_platformio_cli_run() -> Generator[Mock, None, None]: - """Mock run_platformio_cli_run for platformio_api.""" - with patch("esphome.platformio_api.run_platformio_cli_run") as mock: + """Mock run_platformio_cli_run for platformio toolchain.""" + with patch("esphome.platformio.toolchain.run_platformio_cli_run") as mock: yield mock @@ -92,8 +92,8 @@ def mock_esp8266_decode_pc() -> Generator[Mock, None, None]: @pytest.fixture def mock_run_external_process() -> Generator[Mock, None, None]: - """Mock run_external_process for platformio_api.""" - with patch("esphome.platformio_api.run_external_process") as mock: + """Mock run_external_process for platformio toolchain.""" + with patch("esphome.platformio.toolchain.run_external_process") as mock: yield mock @@ -113,8 +113,8 @@ def mock_subprocess_run() -> Generator[Mock, None, None]: @pytest.fixture def mock_get_idedata() -> Generator[Mock, None, None]: - """Mock get_idedata for platformio_api.""" - with patch("esphome.platformio_api.get_idedata") as mock: + """Mock get_idedata for platformio toolchain.""" + with patch("esphome.platformio.toolchain.get_idedata") as mock: yield mock diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3eb50de76b4..3648de443dc 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -18,7 +18,6 @@ import pytest from pytest import CaptureFixture from zeroconf import ServiceStateChange -from esphome import platformio_api from esphome.__main__ import ( Purpose, _get_configured_xtal_freq, @@ -96,6 +95,7 @@ from esphome.espota2 import ( OTA_TYPE_UPDATE_BOOTLOADER, OTA_TYPE_UPDATE_PARTITION_TABLE, ) +from esphome.platformio import toolchain from esphome.util import BootselResult, FlashImage from esphome.zeroconf import _await_discovery, discover_mdns_devices @@ -287,7 +287,7 @@ def mock_run_external_process() -> Generator[Mock]: @pytest.fixture def mock_run_external_command_main() -> Generator[Mock]: - """Mock run_external_command in __main__ module (different from platformio_api).""" + """Mock run_external_command in __main__ module (different from platformio toolchain).""" with patch("esphome.__main__.run_external_command") as mock: mock.return_value = 0 # Default to success yield mock @@ -1199,7 +1199,7 @@ def test_upload_using_esptool_path_conversion( CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32} # Create mock IDEData with Path objects - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [ FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), @@ -1277,7 +1277,7 @@ def test_upload_using_esptool_skips_missing_extra_flash_images( missing_path = tmp_path / "variants" / "tasmota" / "tinyuf2.bin" - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [ FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"), @@ -1389,8 +1389,8 @@ def test_upload_using_platformio_creates_signed_bin_for_rp2040( mock_idedata.firmware_elf_path = str(firmware_elf) with ( - patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), - patch("esphome.platformio_api.run_platformio_cli_run", return_value=0), + patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata), + patch("esphome.platformio.toolchain.run_platformio_cli_run", return_value=0), ): result = upload_using_platformio({}, "/dev/ttyACM0") @@ -1406,7 +1406,7 @@ def test_upload_using_platformio_skips_signed_bin_for_non_rp2040( """Test that upload_using_platformio doesn't create signed bin for non-RP2040.""" setup_core(platform=PLATFORM_ESP32) - with patch("esphome.platformio_api.run_platformio_cli_run", return_value=0): + with patch("esphome.platformio.toolchain.run_platformio_cli_run", return_value=0): result = upload_using_platformio({}, "/dev/ttyUSB0") assert result == 0 @@ -1504,7 +1504,7 @@ def test_upload_using_picotool_success(tmp_path: Path) -> None: config = {} with ( - patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata), patch("subprocess.run", return_value=mock_result), ): exit_code = upload_using_picotool(config) @@ -1524,7 +1524,7 @@ def test_upload_using_picotool_no_elf(tmp_path: Path) -> None: mock_idedata.cc_path = "/fake/path/gcc" config = {} - with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + with patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata): exit_code = upload_using_picotool(config) assert exit_code == 1 @@ -1544,7 +1544,7 @@ def test_upload_using_picotool_not_found(tmp_path: Path) -> None: mock_idedata.cc_path = "/fake/path/gcc" config = {} - with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata): + with patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata): exit_code = upload_using_picotool(config) assert exit_code == 1 @@ -1578,7 +1578,7 @@ def test_upload_using_picotool_permission_error(tmp_path: Path) -> None: config = {} with ( - patch("esphome.platformio_api.get_idedata", return_value=mock_idedata), + patch("esphome.platformio.toolchain.get_idedata", return_value=mock_idedata), patch("subprocess.run", return_value=mock_result), ): exit_code = upload_using_picotool(config) @@ -4696,7 +4696,7 @@ def test_command_analyze_memory_success( firmware_elf.write_text("mock elf file") # Mock idedata - mock_idedata_obj = MagicMock(spec=platformio_api.IDEData) + mock_idedata_obj = MagicMock(spec=toolchain.IDEData) mock_idedata_obj.firmware_elf_path = str(firmware_elf) mock_idedata_obj.objdump_path = "/path/to/objdump" mock_idedata_obj.readelf_path = "/path/to/readelf" @@ -4768,7 +4768,7 @@ def test_command_analyze_memory_with_external_components( firmware_elf.write_text("mock elf file") # Mock idedata - mock_idedata_obj = MagicMock(spec=platformio_api.IDEData) + mock_idedata_obj = MagicMock(spec=toolchain.IDEData) mock_idedata_obj.firmware_elf_path = str(firmware_elf) mock_idedata_obj.objdump_path = "/path/to/objdump" mock_idedata_obj.readelf_path = "/path/to/readelf" @@ -4859,16 +4859,18 @@ def test_command_analyze_memory_no_idedata( @pytest.fixture def mock_compile_build_info_run_compile() -> Generator[Mock]: - """Mock platformio_api.run_compile for build_info tests.""" - with patch("esphome.platformio_api.run_compile", return_value=0) as mock: + """Mock toolchain.run_compile for build_info tests.""" + with patch("esphome.platformio.toolchain.run_compile", return_value=0) as mock: yield mock @pytest.fixture def mock_compile_build_info_get_idedata() -> Generator[Mock]: - """Mock platformio_api.get_idedata for build_info tests.""" + """Mock toolchain.get_idedata for build_info tests.""" mock_idedata = MagicMock() - with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata) as mock: + with patch( + "esphome.platformio.toolchain.get_idedata", return_value=mock_idedata + ) as mock: yield mock @@ -5778,7 +5780,7 @@ def test_upload_using_esptool_passes_crystal_callback( sdkconfig = build_dir / "sdkconfig.test" sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [] mock_get_idedata.return_value = mock_idedata @@ -5808,7 +5810,7 @@ def test_upload_using_esptool_subprocess_passes_crystal_callback( sdkconfig = build_dir / "sdkconfig.test" sdkconfig.write_text("CONFIG_XTAL_FREQ=40\n") - mock_idedata = MagicMock(spec=platformio_api.IDEData) + mock_idedata = MagicMock(spec=toolchain.IDEData) mock_idedata.firmware_bin_path = tmp_path / "firmware.bin" mock_idedata.extra_flash_images = [] mock_get_idedata.return_value = mock_idedata diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_toolchain.py similarity index 92% rename from tests/unit_tests/test_platformio_api.py rename to tests/unit_tests/test_platformio_toolchain.py index 7a88ec4d9e1..f771437dd47 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_toolchain.py @@ -1,4 +1,4 @@ -"""Tests for platformio_api.py path functions.""" +"""Tests for esphome.platformio.toolchain path functions.""" # pylint: disable=protected-access @@ -11,8 +11,8 @@ from unittest.mock import MagicMock, Mock, call, patch import pytest -from esphome import platformio_api, platformio_runner from esphome.core import CORE, EsphomeError +from esphome.platformio import runner, toolchain from esphome.util import FlashImage @@ -21,7 +21,7 @@ def test_idedata_firmware_elf_path(setup_core: Path) -> None: CORE.build_path = setup_core / "build" / "test" CORE.name = "test" raw_data = {"prog_path": "/path/to/firmware.elf"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) assert idedata.firmware_elf_path == Path("/path/to/firmware.elf") @@ -32,7 +32,7 @@ def test_idedata_firmware_bin_path(setup_core: Path) -> None: CORE.name = "test" prog_path = str(Path("/path/to/firmware.elf")) raw_data = {"prog_path": prog_path} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.firmware_bin_path assert isinstance(result, Path) @@ -47,7 +47,7 @@ def test_idedata_firmware_bin_path_preserves_directory(setup_core: Path) -> None CORE.name = "test" prog_path = str(Path("/complex/path/to/build/firmware.elf")) raw_data = {"prog_path": prog_path} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.firmware_bin_path expected = Path("/complex/path/to/build/firmware.bin") @@ -67,7 +67,7 @@ def test_idedata_extra_flash_images(setup_core: Path) -> None: ] }, } - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) images = idedata.extra_flash_images assert len(images) == 2 @@ -83,7 +83,7 @@ def test_idedata_extra_flash_images_empty(setup_core: Path) -> None: CORE.build_path = setup_core / "build" / "test" CORE.name = "test" raw_data = {"prog_path": "/path/to/firmware.elf", "extra": {"flash_images": []}} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) images = idedata.extra_flash_images assert images == [] @@ -97,7 +97,7 @@ def test_idedata_cc_path(setup_core: Path) -> None: "prog_path": "/path/to/firmware.elf", "cc_path": "/Users/test/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-gcc", } - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) assert ( idedata.cc_path @@ -132,7 +132,7 @@ def test_load_idedata_returns_dict( mock_run_platformio_cli_run.return_value = '{"prog_path": "/test/firmware.elf"}' config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) assert result is not None assert isinstance(result, dict) @@ -161,7 +161,7 @@ def test_load_idedata_uses_cache_when_valid( os.utime(idedata_path, (platformio_ini_mtime + 1, platformio_ini_mtime + 1)) config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) # Should not call _run_idedata since cache is valid mock_run_platformio_cli_run.assert_not_called() @@ -194,7 +194,7 @@ def test_load_idedata_regenerates_when_platformio_ini_newer( mock_run_platformio_cli_run.return_value = json.dumps(new_data) config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) # Should call _run_idedata since platformio.ini is newer mock_run_platformio_cli_run.assert_called_once() @@ -228,7 +228,7 @@ def test_load_idedata_regenerates_on_corrupted_cache( mock_run_platformio_cli_run.return_value = json.dumps(new_data) config = {"name": "test"} - result = platformio_api._load_idedata(config) + result = toolchain._load_idedata(config) # Should call _run_idedata since cache is corrupted mock_run_platformio_cli_run.assert_called_once() @@ -253,7 +253,7 @@ def test_run_idedata_parses_json_from_output( f"Some preamble\n{json.dumps(expected_data)}\nSome postamble" ) - result = platformio_api._run_idedata(config) + result = toolchain._run_idedata(config) assert result == expected_data @@ -267,7 +267,7 @@ def test_run_idedata_raises_on_no_json( mock_run_platformio_cli_run.return_value = "No JSON in this output" with pytest.raises(EsphomeError): - platformio_api._run_idedata(config) + toolchain._run_idedata(config) def test_run_idedata_raises_on_invalid_json( @@ -279,7 +279,7 @@ def test_run_idedata_raises_on_invalid_json( # The ValueError from json.loads is re-raised with pytest.raises(ValueError): - platformio_api._run_idedata(config) + toolchain._run_idedata(config) def test_run_platformio_cli_sets_environment_variables( @@ -290,7 +290,7 @@ def test_run_platformio_cli_sets_environment_variables( with patch.dict(os.environ, {}, clear=False): mock_run_external_process.return_value = 0 - platformio_api.run_platformio_cli("test", "arg") + toolchain.run_platformio_cli("test", "arg") # Check environment variables were set assert os.environ["PLATFORMIO_FORCE_COLOR"] == "true" @@ -303,11 +303,11 @@ def test_run_platformio_cli_sets_environment_variables( assert "PYTHONWARNINGS" in os.environ # Check command was called correctly — runs PlatformIO as a subprocess - # via the esphome.platformio_runner entry point. + # via the esphome.platformio.runner entry point. mock_run_external_process.assert_called_once() args = mock_run_external_process.call_args[0] assert "-m" in args - assert "esphome.platformio_runner" in args + assert "esphome.platformio.runner" in args assert "test" in args assert "arg" in args @@ -342,8 +342,8 @@ def test_strip_win_long_path_prefix( platform: str, input_path: str, expected: str ) -> None: r"""``\\?\`` and ``\\?\UNC\`` prefixes are stripped only on win32.""" - with patch("esphome.platformio_api.sys.platform", platform): - assert platformio_api._strip_win_long_path_prefix(input_path) == expected + with patch("esphome.platformio.toolchain.sys.platform", platform): + assert toolchain._strip_win_long_path_prefix(input_path) == expected def test_run_platformio_cli_strips_win_long_path_prefix( @@ -366,15 +366,15 @@ def test_run_platformio_cli_strips_win_long_path_prefix( with ( patch.dict(os.environ, {}, clear=False), - patch("esphome.platformio_api.sys.platform", "win32"), - patch("esphome.platformio_api.sys.executable", prefixed_exe), + patch("esphome.platformio.toolchain.sys.platform", "win32"), + patch("esphome.platformio.toolchain.sys.executable", prefixed_exe), ): # Pop any pre-existing PYTHONEXEPATH so the assertion below reflects # what run_platformio_cli set, not whatever the test runner's # environment happened to contain. os.environ.pop("PYTHONEXEPATH", None) mock_run_external_process.return_value = 0 - platformio_api.run_platformio_cli("test", "arg") + toolchain.run_platformio_cli("test", "arg") # The subprocess is invoked with the stripped executable path. mock_run_external_process.assert_called_once() @@ -398,12 +398,12 @@ def test_run_platformio_cli_does_not_set_pythonexepath_without_strip( with ( patch.dict(os.environ, {}, clear=False), - patch("esphome.platformio_api.sys.platform", "linux"), - patch("esphome.platformio_api.sys.executable", plain_exe), + patch("esphome.platformio.toolchain.sys.platform", "linux"), + patch("esphome.platformio.toolchain.sys.executable", plain_exe), ): os.environ.pop("PYTHONEXEPATH", None) mock_run_external_process.return_value = 0 - platformio_api.run_platformio_cli("test", "arg") + toolchain.run_platformio_cli("test", "arg") mock_run_external_process.assert_called_once() args = mock_run_external_process.call_args[0] @@ -419,7 +419,7 @@ def test_run_platformio_cli_run_builds_command( mock_run_platformio_cli.return_value = 0 config = {"name": "test"} - platformio_api.run_platformio_cli_run(config, True, "extra", "args") + toolchain.run_platformio_cli_run(config, True, "extra", "args") mock_run_platformio_cli.assert_called_once_with( "run", "-d", CORE.build_path, "-v", "extra", "args" @@ -434,7 +434,7 @@ def test_run_compile(setup_core: Path, mock_run_platformio_cli_run: Mock) -> Non config = {CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 4}} mock_run_platformio_cli_run.return_value = 0 - platformio_api.run_compile(config, verbose=True) + toolchain.run_compile(config, verbose=True) mock_run_platformio_cli_run.assert_called_once_with(config, True, "-j4") @@ -461,22 +461,22 @@ def test_get_idedata_caches_result( config = {"name": "test"} # First call should load and cache - result1 = platformio_api.get_idedata(config) + result1 = toolchain.get_idedata(config) mock_run_platformio_cli_run.assert_called_once() # Second call should use cache from CORE.data - result2 = platformio_api.get_idedata(config) + result2 = toolchain.get_idedata(config) mock_run_platformio_cli_run.assert_called_once() # Still only called once assert result1 is result2 - assert isinstance(result1, platformio_api.IDEData) + assert isinstance(result1, toolchain.IDEData) assert result1.firmware_elf_path == Path("/test/firmware.elf") def test_idedata_addr2line_path_windows(setup_core: Path) -> None: """Test IDEData.addr2line_path on Windows.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.addr2line_path assert result == "C:\\tools\\addr2line.exe" @@ -485,7 +485,7 @@ def test_idedata_addr2line_path_windows(setup_core: Path) -> None: def test_idedata_addr2line_path_unix(setup_core: Path) -> None: """Test IDEData.addr2line_path on Unix.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.addr2line_path assert result == "/usr/bin/addr2line" @@ -494,7 +494,7 @@ def test_idedata_addr2line_path_unix(setup_core: Path) -> None: def test_idedata_objdump_path_windows(setup_core: Path) -> None: """Test IDEData.objdump_path on Windows.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.objdump_path assert result == "C:\\tools\\objdump.exe" @@ -503,7 +503,7 @@ def test_idedata_objdump_path_windows(setup_core: Path) -> None: def test_idedata_objdump_path_unix(setup_core: Path) -> None: """Test IDEData.objdump_path on Unix.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.objdump_path assert result == "/usr/bin/objdump" @@ -512,7 +512,7 @@ def test_idedata_objdump_path_unix(setup_core: Path) -> None: def test_idedata_readelf_path_windows(setup_core: Path) -> None: """Test IDEData.readelf_path on Windows.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.readelf_path assert result == "C:\\tools\\readelf.exe" @@ -521,7 +521,7 @@ def test_idedata_readelf_path_windows(setup_core: Path) -> None: def test_idedata_readelf_path_unix(setup_core: Path) -> None: """Test IDEData.readelf_path on Unix.""" raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} - idedata = platformio_api.IDEData(raw_data) + idedata = toolchain.IDEData(raw_data) result = idedata.readelf_path assert result == "/usr/bin/readelf" @@ -547,7 +547,7 @@ def test_patch_structhash(setup_core: Path) -> None: }, ): # Call patch_structhash - platformio_runner.patch_structhash() + runner.patch_structhash() # Verify both modules had clean_build_dir patched # Check that clean_build_dir was set on both modules @@ -599,7 +599,7 @@ def test_patched_clean_build_dir_removes_outdated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_runner.patch_structhash() + runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -649,7 +649,7 @@ def test_patched_clean_build_dir_keeps_updated(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_runner.patch_structhash() + runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -697,7 +697,7 @@ def test_patched_clean_build_dir_creates_missing(setup_core: Path) -> None: }, ): # Call patch_structhash to install the patched function - platformio_runner.patch_structhash() + runner.patch_structhash() # Call the patched function mock_helpers.clean_build_dir(str(build_dir), []) @@ -727,7 +727,7 @@ def test_patch_file_downloader_succeeds_first_try() -> None: ), }, ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -766,7 +766,7 @@ def test_patch_file_downloader_retries_on_failure() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -807,7 +807,7 @@ def test_patch_file_downloader_raises_after_max_retries() -> None: ), patch("time.sleep") as mock_sleep, ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -855,7 +855,7 @@ def test_patch_file_downloader_closes_session_and_response_between_retries() -> ), patch("time.sleep"), ): - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -890,9 +890,9 @@ def test_patch_file_downloader_idempotent() -> None: }, ): # Patch multiple times - platformio_runner.patch_file_downloader() - platformio_runner.patch_file_downloader() - platformio_runner.patch_file_downloader() + runner.patch_file_downloader() + runner.patch_file_downloader() + runner.patch_file_downloader() from platformio.package.download import FileDownloader @@ -910,9 +910,7 @@ def _filter_through_redirect(line: str) -> str: from esphome.util import RedirectText captured = io.StringIO() - redirect = RedirectText( - captured, filter_lines=platformio_runner.FILTER_PLATFORMIO_LINES - ) + redirect = RedirectText(captured, filter_lines=runner.FILTER_PLATFORMIO_LINES) redirect.write(line + "\n") return captured.getvalue()