[core] Enable ruff PTH (flake8-use-pathlib) lint family (#16661)

This commit is contained in:
J. Nick Koston authored and GitHub committed 2026-05-26 05:14:42 +00:00
1 parent ae814cff5c
commit ae74920b81
54 files changed
+162 -155

No files matched your search

+2 -2
View File
@@ -794,7 +794,7 @@ def _check_and_emit_build_info() -> None:
# Read build_info from JSON # Read build_info from JSON
try: try:
with open(build_info_json_path, encoding="utf-8") as f: with build_info_json_path.open(encoding="utf-8") as f:
build_info = json.load(f) build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e: except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e) _LOGGER.debug("Failed to read build_info: %s", e)
@@ -1056,7 +1056,7 @@ def _wait_for_serial_port(
def _port_found() -> bool: def _port_found() -> bool:
if port is not None: if port is not None:
if os.name == "posix": if os.name == "posix":
return os.path.exists(port) return Path(port).exists()
return any(p.path == port for p in get_serial_ports()) return any(p.path == port for p in get_serial_ports())
ports = get_serial_ports() ports = get_serial_ports()
if known_ports is not None: if known_ports is not None:
+3 -3
View File
@@ -6,6 +6,7 @@ from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable
import heapq import heapq
from operator import itemgetter from operator import itemgetter
from pathlib import Path
import sys import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -699,7 +700,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
content = "\n".join(lines) content = "\n".join(lines)
if output_file: if output_file:
with open(output_file, "w", encoding="utf-8") as f: with Path(output_file).open("w", encoding="utf-8") as f:
f.write(content) f.write(content)
else: else:
print(content) print(content)
@@ -737,7 +738,6 @@ def main():
# Load build directory # Load build directory
import json import json
from pathlib import Path
from esphome.platformio.toolchain import IDEData from esphome.platformio.toolchain import IDEData
@@ -785,7 +785,7 @@ def main():
if not idedata_path.exists(): if not idedata_path.exists():
continue continue
try: try:
with open(idedata_path, encoding="utf-8") as f: with idedata_path.open(encoding="utf-8") as f:
raw_data = json.load(f) raw_data = json.load(f)
idedata = IDEData(raw_data) idedata = IDEData(raw_data)
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
+1 -2
View File
@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -37,7 +36,7 @@ def _find_in_platformio_packages(tool_name: str) -> str | None:
Full path to the tool or None if not found Full path to the tool or None if not found
""" """
# Get PlatformIO packages directory # Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages")) platformio_home = Path("~/.platformio/packages").expanduser()
if not platformio_home.exists(): if not platformio_home.exists():
return None return None
+1 -1
View File
@@ -24,7 +24,7 @@ def get_available_components() -> list[str] | None:
return None return None
try: try:
with open(project_desc, encoding="utf-8") as f: with project_desc.open(encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
component_info = data.get("build_component_info", {}) component_info = data.get("build_component_info", {})
+1 -1
View File
@@ -412,7 +412,7 @@ class ConfigBundleCreator:
@staticmethod @staticmethod
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None: def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
"""Add a BundleFile to the tar archive with deterministic metadata.""" """Add a BundleFile to the tar archive with deterministic metadata."""
with open(bf.source, "rb") as f: with bf.source.open("rb") as f:
_add_bytes_to_tar(tar, bf.path, f.read()) _add_bytes_to_tar(tar, bf.path, f.read())
+1 -1
View File
@@ -98,7 +98,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
else: else:
raise cv.Invalid("Unsupported file source") raise cv.Invalid("Unsupported file source")
with open(path, "rb") as f: with path.open("rb") as f:
data = f.read() data = f.read()
try: try:
+1 -1
View File
@@ -169,7 +169,7 @@ async def to_code_base(config):
path = _compute_local_file_path(_compute_url(config)) path = _compute_local_file_path(_compute_url(config))
try: try:
with open(path, encoding="utf-8") as f: with path.open(encoding="utf-8") as f:
bsec2_iaq_config = f.read() bsec2_iaq_config = f.read()
except Exception as e: except Exception as e:
raise core.EsphomeError( raise core.EsphomeError(
@@ -75,7 +75,7 @@ def _validate_firmware(config: dict[str, Any]) -> None:
return return
path = CORE.relative_config_path(config[CONF_PATH]) path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f: with path.open("rb") as f:
firmware_data = f.read() firmware_data = f.read()
calculated = hashlib.sha256(firmware_data).hexdigest() calculated = hashlib.sha256(firmware_data).hexdigest()
expected = config[CONF_SHA256].lower() expected = config[CONF_SHA256].lower()
@@ -93,7 +93,7 @@ async def to_code(config: dict[str, Any]) -> None:
if config[CONF_TYPE] == TYPE_EMBEDDED: if config[CONF_TYPE] == TYPE_EMBEDDED:
path = config[CONF_PATH] path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f: with CORE.relative_config_path(path).open("rb") as f:
firmware_data = f.read() firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data] rhs = [HexInt(x) for x in firmware_data]
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8) arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)
+3 -1
View File
@@ -1,3 +1,5 @@
from pathlib import Path
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32 from esphome.components import esp32
@@ -174,7 +176,7 @@ async def to_code(config):
if config.get(CONF_VERIFY_SSL): if config.get(CONF_VERIFY_SSL):
if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH): if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH):
with open(ca_cert_path, encoding="utf-8") as f: with Path(ca_cert_path).open(encoding="utf-8") as f:
ca_cert_content = f.read() ca_cert_content = f.read()
cg.add(var.set_ca_certificate(ca_cert_content)) cg.add(var.set_ca_certificate(ca_cert_content))
else: else:
+1 -1
View File
@@ -395,7 +395,7 @@ def download_image(value):
def is_svg_file(file): def is_svg_file(file):
if not file: if not file:
return False return False
with open(file, "rb") as f: with Path(file).open("rb") as f:
return "<svg" in str(f.read(1024)) return "<svg" in str(f.read(1024))
@@ -376,14 +376,14 @@ CONFIG_SCHEMA = cv.All(
def _load_model_data(manifest_path: Path): def _load_model_data(manifest_path: Path):
with open(manifest_path, encoding="utf-8") as f: with manifest_path.open(encoding="utf-8") as f:
manifest = json.load(f) manifest = json.load(f)
_validate_manifest_version(manifest) _validate_manifest_version(manifest)
model_path = manifest_path.parent / manifest[CONF_MODEL] model_path = manifest_path.parent / manifest[CONF_MODEL]
with open(model_path, "rb") as f: with model_path.open("rb") as f:
model = f.read() model = f.read()
if manifest.get(KEY_VERSION) == 1: if manifest.get(KEY_VERSION) == 1:
+1 -1
View File
@@ -139,7 +139,7 @@ async def _smpmgr_upload_connected(
already_uploaded = True already_uploaded = True
if not already_uploaded: if not already_uploaded:
with open(firmware, "rb") as file: with firmware.open("rb") as file:
image = file.read() image = file.read()
upload_size = len(image) upload_size = len(image)
progress = ProgressBar("Uploading") progress = ProgressBar("Uploading")
+2 -2
View File
@@ -67,7 +67,7 @@ def load_boards(arduino_pico_path: Path) -> tuple[dict, dict]:
for json_file in sorted(json_dir.glob("*.json")): for json_file in sorted(json_dir.glob("*.json")):
board_name = json_file.stem board_name = json_file.stem
with open(json_file, encoding="utf-8") as f: with json_file.open(encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
build = data.get("build", {}) build = data.get("build", {})
@@ -136,7 +136,7 @@ def _get_variant(json_file: Path) -> str | None:
"""Get variant name from a board JSON file.""" """Get variant name from a board JSON file."""
if not json_file.exists(): if not json_file.exists():
return None return None
with open(json_file, encoding="utf-8") as f: with json_file.open(encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
return data.get("build", {}).get("variant") return data.get("build", {}).get("variant")
+2 -2
View File
@@ -326,12 +326,12 @@ async def to_code(config):
if CONF_CSS_INCLUDE in config: if CONF_CSS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_CSS_INCLUDE") cg.add_define("USE_WEBSERVER_CSS_INCLUDE")
path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
with open(file=path, encoding="utf-8") as css_file: with path.open(encoding="utf-8") as css_file:
add_resource_as_progmem("CSS_INCLUDE", css_file.read()) add_resource_as_progmem("CSS_INCLUDE", css_file.read())
if CONF_JS_INCLUDE in config: if CONF_JS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_JS_INCLUDE") cg.add_define("USE_WEBSERVER_JS_INCLUDE")
path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) path = CORE.relative_config_path(config[CONF_JS_INCLUDE])
with open(file=path, encoding="utf-8") as js_file: with path.open(encoding="utf-8") as js_file:
add_resource_as_progmem("JS_INCLUDE", js_file.read()) add_resource_as_progmem("JS_INCLUDE", js_file.read())
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))
if CONF_LOCAL in config and config[CONF_LOCAL]: if CONF_LOCAL in config and config[CONF_LOCAL]:
+2 -3
View File
@@ -129,9 +129,8 @@ def final_validate_esp32(config: ConfigType) -> ConfigType:
if CONF_PARTITIONS in fv.full_config.get() and not isinstance( if CONF_PARTITIONS in fv.full_config.get() and not isinstance(
fv.full_config.get()[CONF_PARTITIONS], list fv.full_config.get()[CONF_PARTITIONS], list
): ):
with open( with CORE.relative_config_path(fv.full_config.get()[CONF_PARTITIONS]).open(
CORE.relative_config_path(fv.full_config.get()[CONF_PARTITIONS]), encoding="utf8"
encoding="utf8",
) as f: ) as f:
partitions_tab = f.read() partitions_tab = f.read()
for partition, types in [ for partition, types in [
+2 -1
View File
@@ -6,6 +6,7 @@ from concurrent.futures import ThreadPoolExecutor
import contextlib import contextlib
import logging import logging
import os import os
from pathlib import Path
import socket import socket
import threading import threading
from time import monotonic from time import monotonic
@@ -149,4 +150,4 @@ async def async_start(args) -> None:
await dashboard.async_run() await dashboard.async_run()
finally: finally:
if sock: if sock:
os.remove(sock) Path(sock).unlink()
+3 -3
View File
@@ -1040,7 +1040,7 @@ class DownloadListRequestHandler(BaseHandler):
class DownloadBinaryRequestHandler(BaseHandler): class DownloadBinaryRequestHandler(BaseHandler):
def _load_file(self, path: str, compressed: bool) -> bytes: def _load_file(self, path: str, compressed: bool) -> bytes:
"""Load a file from disk and compress it if requested.""" """Load a file from disk and compress it if requested."""
with open(path, "rb") as f: with Path(path).open("rb") as f:
data = f.read() data = f.read()
if compressed: if compressed:
return gzip.compress(data, 9) return gzip.compress(data, 9)
@@ -1292,7 +1292,7 @@ class EditRequestHandler(BaseHandler):
def _read_file(self, filename: str, configuration: str) -> bytes | None: def _read_file(self, filename: str, configuration: str) -> bytes | None:
"""Read a file and return the content as bytes.""" """Read a file and return the content as bytes."""
try: try:
with open(file=filename, encoding="utf-8") as f: with Path(filename).open(encoding="utf-8") as f:
return f.read() return f.read()
except FileNotFoundError: except FileNotFoundError:
if configuration in const.SECRETS_FILES: if configuration in const.SECRETS_FILES:
@@ -1493,7 +1493,7 @@ def get_base_frontend_path() -> Path:
static_path += "/" static_path += "/"
# This path can be relative, so resolve against the root or else templates don't work # This path can be relative, so resolve against the root or else templates don't work
path = Path(os.getcwd()) / static_path / "esphome_dashboard" path = Path.cwd() / static_path / "esphome_dashboard"
return path.resolve() return path.resolve()
+10 -8
View File
@@ -317,24 +317,26 @@ def _collect_filtered_files(src_dir: PathType, src_filters: list[str]) -> list[s
if pattern.endswith("/"): if pattern.endswith("/"):
pattern = pattern.rstrip("/") + "/**" pattern = pattern.rstrip("/") + "/**"
full_pattern = os.path.join(glob.escape(str(src_dir)), pattern) # glob.escape has no pathlib equivalent and the matcher works on raw
# path strings, so PTH118/PTH207 don't apply here.
full_pattern = os.path.join(glob.escape(str(src_dir)), pattern) # noqa: PTH118
matched = [] matched = []
for item in glob.glob(full_pattern, recursive=True): for item in glob.glob(full_pattern, recursive=True): # noqa: PTH207
if not os.path.isdir(item): if not Path(item).is_dir():
matched.append(item) matched.append(item)
else: else:
# PlatformIO quirk: a directory matched with "*" should include all its # PlatformIO quirk: a directory matched with "*" should include all its
# nested files and subdirectories, not just the directory itself. # nested files and subdirectories, not just the directory itself.
for root, _, files in os.walk(item): for root, _, files in os.walk(item):
matched.extend([os.path.join(root, f) for f in files]) matched.extend([str(Path(root) / f) for f in files])
if sign == "+": if sign == "+":
selected.update(matched) selected.update(matched)
elif sign == "-": elif sign == "-":
selected.difference_update(matched) selected.difference_update(matched)
return [r for r in selected if os.path.isfile(r)] return [r for r in selected if Path(r).is_file()]
def _convert_library_to_component(library: Library) -> IDFComponent: def _convert_library_to_component(library: Library) -> IDFComponent:
@@ -486,7 +488,7 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
# Only keep sources # Only keep sources
build_src_files = [os.path.relpath(p, component.path) for p in build_src_files] build_src_files = [os.path.relpath(p, component.path) for p in build_src_files]
build_src_files = [ build_src_files = [
f for f in build_src_files if os.path.splitext(f)[1] in SRC_FILE_EXTENSIONS f for f in build_src_files if Path(f).suffix in SRC_FILE_EXTENSIONS
] ]
# Handle build flags # Handle build flags
@@ -740,7 +742,7 @@ def _parse_library_json(library_json_path: PathType):
Returns: Returns:
dict: Parsed JSON content as a Python dictionary. dict: Parsed JSON content as a Python dictionary.
""" """
with open(library_json_path, encoding="utf8") as fp: with Path(library_json_path).open(encoding="utf8") as fp:
return json.load(fp) return json.load(fp)
@@ -754,7 +756,7 @@ def _parse_library_properties(library_properties_path: PathType):
Returns: Returns:
dict[str, str]: Mapping of parsed property keys to values. dict[str, str]: Mapping of parsed property keys to values.
""" """
with open(library_properties_path, encoding="utf8") as fp: with Path(library_properties_path).open(encoding="utf8") as fp:
data = {} data = {}
for line in fp.read().splitlines(): for line in fp.read().splitlines():
line = line.strip() line = line.strip()
+1 -1
View File
@@ -108,7 +108,7 @@ def run_extra_script(
""" """
env = _FakeSConsEnv(board_mcu=idf_target, pio_env=f"esphome_{idf_target}") env = _FakeSConsEnv(board_mcu=idf_target, pio_env=f"esphome_{idf_target}")
code = compile(script_path.read_text(), str(script_path), "exec") code = compile(script_path.read_text(), str(script_path), "exec")
old_cwd = os.getcwd() old_cwd = Path.cwd()
try: try:
os.chdir(library_dir) os.chdir(library_dir)
exec( # noqa: S102 pylint: disable=exec-used exec( # noqa: S102 pylint: disable=exec-used
+26 -18
View File
@@ -138,7 +138,7 @@ def rmdir(directory: PathType, msg: str | None = None):
Raises: Raises:
RuntimeError: If directory removal fails RuntimeError: If directory removal fails
""" """
if os.path.isdir(directory): if Path(directory).is_dir():
try: try:
if msg: if msg:
_LOGGER.debug(msg) _LOGGER.debug(msg)
@@ -192,7 +192,7 @@ def _check_stamp(file: PathType, data: dict[str, str]) -> bool:
return False return False
try: try:
with open(file, encoding="utf-8") as f: with Path(file).open(encoding="utf-8") as f:
return json.load(f) == data return json.load(f) == data
except (json.JSONDecodeError, OSError): except (json.JSONDecodeError, OSError):
return False return False
@@ -206,7 +206,7 @@ def _write_stamp(file: PathType, data: dict[str, str]):
file: Path to the stamp file to write file: Path to the stamp file to write
data: Dictionary containing data to write data: Dictionary containing data to write
""" """
with open(file, "w", encoding="utf8") as fp: with Path(file).open("w", encoding="utf8") as fp:
json.dump(data, fp) json.dump(data, fp)
@@ -471,8 +471,12 @@ def _tar_extract_all(
import stat import stat
import tarfile import tarfile
# Tar extraction safety: os.path.realpath / commonpath / normpath have no
# pathlib equivalents and Path.resolve() would follow symlinks unsafely.
# Use os.path for the security-sensitive parts; the simple checks move to
# Path.
extract_dir = os.fspath(extract_dir) extract_dir = os.fspath(extract_dir)
abs_dest = os.path.abspath(extract_dir) abs_dest = os.path.abspath(extract_dir) # noqa: PTH100
with tarfile.open(fileobj=data, mode="r") as tar_ref: with tarfile.open(fileobj=data, mode="r") as tar_ref:
all_members = tar_ref.getmembers() all_members = tar_ref.getmembers()
@@ -491,8 +495,8 @@ def _tar_extract_all(
name = name.lstrip("/" + os.sep) name = name.lstrip("/" + os.sep)
# 2. Reject absolute paths (incl. Windows drive) # 2. Reject absolute paths (incl. Windows drive)
if os.path.isabs(name) or ( if Path(name).is_absolute() or (
os.name == "nt" and ":" in name.split(os.sep)[0] os.name == "nt" and ":" in name.split(os.sep)[0] # noqa: PTH206
): ):
continue continue
@@ -506,7 +510,7 @@ def _tar_extract_all(
name = norm[len(strip_prefix) :] name = norm[len(strip_prefix) :]
# 4. Compute final path # 4. Compute final path
target_path = os.path.realpath(os.path.join(abs_dest, name)) target_path = os.path.realpath(os.path.join(abs_dest, name)) # noqa: PTH118
if os.path.commonpath([abs_dest, target_path]) != abs_dest: if os.path.commonpath([abs_dest, target_path]) != abs_dest:
continue continue
@@ -515,18 +519,20 @@ def _tar_extract_all(
linkname = member.linkname linkname = member.linkname
# Reject absolute link targets # Reject absolute link targets
if os.path.isabs(linkname): if Path(linkname).is_absolute():
continue continue
# Strip leading slashes # Strip leading slashes
linkname = os.path.normpath(linkname) linkname = os.path.normpath(linkname)
if member.issym(): if member.issym():
link_target = os.path.join( link_target = os.path.join( # noqa: PTH118
abs_dest, os.path.dirname(name), linkname abs_dest,
os.path.dirname(name), # noqa: PTH120
linkname,
) )
else: else:
link_target = os.path.join(abs_dest, linkname) link_target = os.path.join(abs_dest, linkname) # noqa: PTH118
link_target = os.path.realpath(link_target) link_target = os.path.realpath(link_target)
if os.path.commonpath([abs_dest, link_target]) != abs_dest: if os.path.commonpath([abs_dest, link_target]) != abs_dest:
@@ -598,7 +604,9 @@ def _zip_extract_all(
""" """
import zipfile import zipfile
extract_dir = os.path.abspath(extract_dir) # See note in archive_extract_all_tar: os.path is used intentionally for
# the security-sensitive abspath/commonpath checks below.
extract_dir = os.path.abspath(extract_dir) # noqa: PTH100
with zipfile.ZipFile(data, "r") as zip_ref: with zipfile.ZipFile(data, "r") as zip_ref:
all_members = zip_ref.infolist() all_members = zip_ref.infolist()
@@ -618,8 +626,8 @@ def _zip_extract_all(
name = member.filename.lstrip("/\\") name = member.filename.lstrip("/\\")
# 2. Reject absolute paths / Windows drives # 2. Reject absolute paths / Windows drives
if os.path.isabs(name) or ( if Path(name).is_absolute() or (
os.name == "nt" and ":" in name.split(os.sep)[0] os.name == "nt" and ":" in name.split(os.sep)[0] # noqa: PTH206
): ):
continue continue
@@ -633,7 +641,7 @@ def _zip_extract_all(
name = norm[len(strip_prefix) :] name = norm[len(strip_prefix) :]
# 4. Compute safe target path # 4. Compute safe target path
target_path = os.path.abspath(os.path.join(extract_dir, name)) target_path = os.path.abspath(os.path.join(extract_dir, name)) # noqa: PTH100, PTH118
if os.path.commonpath([extract_dir, target_path]) != extract_dir: if os.path.commonpath([extract_dir, target_path]) != extract_dir:
raise ValueError(f"Unsafe path detected: {member.filename}") raise ValueError(f"Unsafe path detected: {member.filename}")
@@ -680,7 +688,7 @@ def archive_extract_all(
with ExitStack() as stack: with ExitStack() as stack:
archive_ref: io.BufferedIOBase archive_ref: io.BufferedIOBase
if isinstance(archive, (str, os.PathLike)): if isinstance(archive, (str, os.PathLike)):
archive_ref = stack.enter_context(open(archive, "rb")) archive_ref = stack.enter_context(Path(archive).open("rb"))
elif isinstance(archive, (io.BufferedReader, io.BufferedRandom)): elif isinstance(archive, (io.BufferedReader, io.BufferedRandom)):
archive_ref = archive archive_ref = archive
elif isinstance(archive, io.RawIOBase): elif isinstance(archive, io.RawIOBase):
@@ -727,7 +735,7 @@ def download_from_mirrors(
# 1. Open target file for writing if path given # 1. Open target file for writing if path given
with ExitStack() as stack: with ExitStack() as stack:
if isinstance(target, (str, os.PathLike)): if isinstance(target, (str, os.PathLike)):
f = stack.enter_context(open(target, "wb")) f = stack.enter_context(Path(target).open("wb"))
elif isinstance(target, (io.RawIOBase, io.IOBase)): elif isinstance(target, (io.RawIOBase, io.IOBase)):
f = target f = target
else: else:
@@ -917,7 +925,7 @@ def _patch_tools_json_for_linux_arm64(framework_path: Path) -> None:
return return
try: try:
with open(tools_json, encoding="utf-8") as f: with tools_json.open(encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
except (json.JSONDecodeError, OSError) as e: except (json.JSONDecodeError, OSError) as e:
_LOGGER.warning( _LOGGER.warning(
+2 -1
View File
@@ -10,6 +10,7 @@ not installed.
import json import json
import os import os
from pathlib import Path
import sys import sys
from types import SimpleNamespace from types import SimpleNamespace
@@ -25,7 +26,7 @@ from idf_tools import (
g.idf_path = sys.argv[1] g.idf_path = sys.argv[1]
g.idf_tools_path = os.environ.get("IDF_TOOLS_PATH") g.idf_tools_path = os.environ.get("IDF_TOOLS_PATH")
g.tools_json = os.path.join(g.idf_path, TOOLS_FILE) g.tools_json = str(Path(g.idf_path) / TOOLS_FILE)
tools_info = filter_tools_info(IDFEnv.get_idf_env(), load_tools_info()) tools_info = filter_tools_info(IDFEnv.get_idf_env(), load_tools_info())
args = SimpleNamespace(prefer_system=False) args = SimpleNamespace(prefer_system=False)
+2 -1
View File
@@ -91,6 +91,7 @@ def main() -> int:
# ---- end sys.path fix-up ----------------------------------------------- # ---- end sys.path fix-up -----------------------------------------------
import os import os
from pathlib import Path
import re import re
import runpy import runpy
@@ -229,7 +230,7 @@ def main() -> int:
# runpy.run_path does not do this automatically, but idf.py relies # runpy.run_path does not do this automatically, but idf.py relies
# on it to import its sibling modules (python_version_checker, # on it to import its sibling modules (python_version_checker,
# idf_py_actions, ...). # idf_py_actions, ...).
script_dir = os.path.dirname(os.path.abspath(script_path)) script_dir = str(Path(script_path).resolve().parent)
if script_dir not in sys.path: if script_dir not in sys.path:
sys.path.insert(0, script_dir) sys.path.insert(0, script_dir)
+10 -9
View File
@@ -241,20 +241,21 @@ def has_outdated_files():
dependency_lock_path = CORE.relative_build_path("dependencies.lock") dependency_lock_path = CORE.relative_build_path("dependencies.lock")
build_ninja_path = CORE.relative_build_path("build/build.ninja") build_ninja_path = CORE.relative_build_path("build/build.ninja")
if not os.path.isdir(build_config_path) or not os.listdir(build_config_path): if not build_config_path.is_dir() or not any(build_config_path.iterdir()):
return True return True
if not os.path.isfile(cmakecache_txt_path): if not cmakecache_txt_path.is_file():
return True return True
if not os.path.isfile(build_ninja_path): if not build_ninja_path.is_file():
return True return True
if os.path.isfile(dependency_lock_path) and os.path.getmtime( if (
dependency_lock_path dependency_lock_path.is_file()
) > os.path.getmtime(build_ninja_path): and dependency_lock_path.stat().st_mtime > build_ninja_path.stat().st_mtime
):
return True return True
cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path) cmakecache_txt_mtime = cmakecache_txt_path.stat().st_mtime
return any( return any(
os.path.getmtime(f) > cmakecache_txt_mtime f.stat().st_mtime > cmakecache_txt_mtime
for f in [sdkconfig_internal_path, idf_component_yml_path] for f in [sdkconfig_internal_path, idf_component_yml_path]
if f.exists() if f.exists()
) )
@@ -452,7 +453,7 @@ def create_factory_bin() -> bool:
return False return False
try: try:
with open(flasher_args_path, encoding="utf-8") as f: with flasher_args_path.open(encoding="utf-8") as f:
flash_data = json.load(f) flash_data = json.load(f)
except (json.JSONDecodeError, OSError) as e: except (json.JSONDecodeError, OSError) as e:
_LOGGER.error("Failed to read flasher_args.json: %s", e) _LOGGER.error("Failed to read flasher_args.json: %s", e)
+1 -1
View File
@@ -517,7 +517,7 @@ def run_ota_impl_(
continue continue
_LOGGER.info("Connected to %s", sa[0]) _LOGGER.info("Connected to %s", sa[0])
with open(filename, "rb") as file_handle: with Path(filename).open("rb") as file_handle:
try: try:
perform_ota(sock, password, file_handle, filename, ota_type) perform_ota(sock, password, file_handle, filename, ota_type)
except OTAError as err: except OTAError as err:
+2 -2
View File
@@ -385,7 +385,7 @@ def rmtree(path: Path | str) -> None:
def _onerror(func, path, exc_info): def _onerror(func, path, exc_info):
if os.access(path, os.W_OK): if os.access(path, os.W_OK):
raise exc_info[1].with_traceback(exc_info[2]) raise exc_info[1].with_traceback(exc_info[2])
os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) Path(path).chmod(stat.S_IWUSR | stat.S_IRUSR)
func(path) func(path)
# ``onerror`` is deprecated in 3.12 in favour of ``onexc`` (different # ``onerror`` is deprecated in 3.12 in favour of ``onexc`` (different
@@ -512,7 +512,7 @@ def copy_file_if_changed(src: Path, dst: Path) -> bool:
# -> delete file (it would be overwritten anyway), and try again # -> delete file (it would be overwritten anyway), and try again
# if that fails, use normal error handler # if that fails, use normal error handler
with suppress(OSError): with suppress(OSError):
os.unlink(dst) Path(dst).unlink()
shutil.copyfile(src, dst) shutil.copyfile(src, dst)
return True return True
+3 -3
View File
@@ -2,7 +2,7 @@ import contextlib
from datetime import datetime from datetime import datetime
import json import json
import logging import logging
import os from pathlib import Path
import ssl import ssl
import tempfile import tempfile
import time import time
@@ -120,8 +120,8 @@ def prepare(
key_file.close() key_file.close()
context.load_cert_chain(cert_file.name, key_file.name) context.load_cert_chain(cert_file.name, key_file.name)
finally: finally:
os.unlink(cert_file.name) Path(cert_file.name).unlink()
os.unlink(key_file.name) Path(key_file.name).unlink()
client.tls_set_context(context) client.tls_set_context(context)
try: try:
+1 -1
View File
@@ -126,7 +126,7 @@ def _try_upload(
_LOGGER.info("Connecting to %s port %s...", ip, port) _LOGGER.info("Connecting to %s port %s...", ip, port)
try: try:
with open(filename, "rb") as fh: with filename.open("rb") as fh:
streamer = _MultipartStreamer(fh, file_size, filename.name) streamer = _MultipartStreamer(fh, file_size, filename.name)
try: try:
response = requests.post( response = requests.post(
+1
View File
@@ -127,6 +127,7 @@ select = [
"NPY", # numpy-specific rules "NPY", # numpy-specific rules
"PERF", # performance "PERF", # performance
"PL", # pylint "PL", # pylint
"PTH", # flake8-use-pathlib
"PYI", # flake8-pyi "PYI", # flake8-pyi
"Q", # flake8-quotes "Q", # flake8-quotes
"RSE", # flake8-raise "RSE", # flake8-raise
+6 -6
View File
@@ -3163,7 +3163,7 @@ def main() -> None:
defines_content += "\n" defines_content += "\n"
defines_content += "\nnamespace esphome::api {} // namespace esphome::api\n" defines_content += "\nnamespace esphome::api {} // namespace esphome::api\n"
with open(root / "api_pb2_defines.h", "w", encoding="utf-8") as f: with (root / "api_pb2_defines.h").open("w", encoding="utf-8") as f:
f.write(defines_content) f.write(defines_content)
content = FILE_HEADER content = FILE_HEADER
@@ -3448,13 +3448,13 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
#endif // HAS_PROTO_MESSAGE_DUMP #endif // HAS_PROTO_MESSAGE_DUMP
""" """
with open(root / "api_pb2.h", "w", encoding="utf-8") as f: with (root / "api_pb2.h").open("w", encoding="utf-8") as f:
f.write(content) f.write(content)
with open(root / "api_pb2.cpp", "w", encoding="utf-8") as f: with (root / "api_pb2.cpp").open("w", encoding="utf-8") as f:
f.write(cpp) f.write(cpp)
with open(root / "api_pb2_dump.cpp", "w", encoding="utf-8") as f: with (root / "api_pb2_dump.cpp").open("w", encoding="utf-8") as f:
f.write(dump_cpp) f.write(dump_cpp)
hpp = FILE_HEADER hpp = FILE_HEADER
@@ -3641,10 +3641,10 @@ static const char *const TAG = "api.service";
} // namespace esphome::api } // namespace esphome::api
""" """
with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f: with (root / "api_pb2_service.h").open("w", encoding="utf-8") as f:
f.write(hpp) f.write(hpp)
with open(root / "api_pb2_service.cpp", "w", encoding="utf-8") as f: with (root / "api_pb2_service.cpp").open("w", encoding="utf-8") as f:
f.write(cpp) f.write(cpp)
prot_file.unlink() prot_file.unlink()
+1 -1
View File
@@ -195,7 +195,7 @@ def load_component_yaml_configs(components: list[str], tests_dir: Path) -> dict:
yaml_path = tests_dir / component / BENCHMARK_YAML_FILENAME yaml_path = tests_dir / component / BENCHMARK_YAML_FILENAME
if not yaml_path.is_file(): if not yaml_path.is_file():
continue continue
with open(yaml_path) as f: with yaml_path.open() as f:
component_config = yaml.safe_load(f) component_config = yaml.safe_load(f)
if component_config and isinstance(component_config, dict): if component_config and isinstance(component_config, dict):
for key, value in component_config.items(): for key, value in component_config.items():
+3 -2
View File
@@ -2,6 +2,7 @@
import argparse import argparse
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
import re import re
import sys import sys
@@ -39,12 +40,12 @@ class Version:
def sub(path, pattern, repl, expected_count=1): def sub(path, pattern, repl, expected_count=1):
with open(path, encoding="utf-8") as fh: with Path(path).open(encoding="utf-8") as fh:
content = fh.read() content = fh.read()
content, count = re.subn(pattern, repl, content, flags=re.MULTILINE) content, count = re.subn(pattern, repl, content, flags=re.MULTILINE)
if expected_count is not None: if expected_count is not None:
assert count == expected_count, f"Pattern {pattern} replacement failed!" assert count == expected_count, f"Pattern {pattern} replacement failed!"
with open(path, "w", encoding="utf-8") as fh: with Path(path).open("w", encoding="utf-8") as fh:
fh.write(content) fh.write(content)
+1 -1
View File
@@ -14,7 +14,7 @@ import time
import colorama import colorama
from helpers import filter_changed, git_ls_files, print_error_for_file, styled from helpers import filter_changed, git_ls_files, print_error_for_file, styled
sys.path.append(os.path.dirname(__file__)) sys.path.append(str(Path(__file__).parent))
def find_all(a_str, sub): def find_all(a_str, sub):
+2 -2
View File
@@ -44,7 +44,7 @@ def main() -> int:
return 1 return 1
try: try:
with open(json_path, encoding="utf-8") as f: with Path(json_path).open(encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
except (json.JSONDecodeError, OSError) as e: except (json.JSONDecodeError, OSError) as e:
print(f"Error loading JSON: {e}", file=sys.stderr) print(f"Error loading JSON: {e}", file=sys.stderr)
@@ -74,7 +74,7 @@ def main() -> int:
# Write back # Write back
try: try:
with open(json_path, "w", encoding="utf-8") as f: with Path(json_path).open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
print(f"Added metadata to {args.json_file}", file=sys.stderr) print(f"Added metadata to {args.json_file}", file=sys.stderr)
except OSError as e: except OSError as e:
+2 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from pathlib import Path
def write_github_output(outputs: dict[str, str | int]) -> None: def write_github_output(outputs: dict[str, str | int]) -> None:
@@ -16,7 +17,7 @@ def write_github_output(outputs: dict[str, str | int]) -> None:
""" """
github_output = os.environ.get("GITHUB_OUTPUT") github_output = os.environ.get("GITHUB_OUTPUT")
if github_output: if github_output:
with open(github_output, "a", encoding="utf-8") as f: with Path(github_output).open("a", encoding="utf-8") as f:
f.writelines(f"{key}={value}\n" for key, value in outputs.items()) f.writelines(f"{key}={value}\n" for key, value in outputs.items())
else: else:
for key, value in outputs.items(): for key, value in outputs.items():
+1 -1
View File
@@ -91,7 +91,7 @@ def load_analysis_json(json_path: str) -> dict | None:
return None return None
try: try:
with open(json_file, encoding="utf-8") as f: with Path(json_file).open(encoding="utf-8") as f:
return json.load(f) return json.load(f)
except (json.JSONDecodeError, OSError) as e: except (json.JSONDecodeError, OSError) as e:
print(f"Failed to load analysis JSON: {e}", file=sys.stderr) print(f"Failed to load analysis JSON: {e}", file=sys.stderr)
+2 -2
View File
@@ -127,7 +127,7 @@ def run_detailed_analysis(build_dir: str) -> dict | None:
if not idedata_path.exists(): if not idedata_path.exists():
continue continue
try: try:
with open(idedata_path, encoding="utf-8") as f: with idedata_path.open(encoding="utf-8") as f:
raw_data = json.load(f) raw_data = json.load(f)
idedata = IDEData(raw_data) idedata = IDEData(raw_data)
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
@@ -264,7 +264,7 @@ def main() -> int:
output_path = Path(args.output_json) output_path = Path(args.output_json)
output_path.parent.mkdir(parents=True, exist_ok=True) output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f: with output_path.open("w", encoding="utf-8") as f:
json.dump(output_data, f, indent=2) json.dump(output_data, f, indent=2)
print(f"Saved analysis to {args.output_json}", file=sys.stderr) print(f"Saved analysis to {args.output_json}", file=sys.stderr)
+2 -1
View File
@@ -2,6 +2,7 @@
import argparse import argparse
import os import os
from pathlib import Path
import queue import queue
import re import re
import subprocess import subprocess
@@ -70,7 +71,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
cwd = os.getcwd() cwd = Path.cwd()
files = [ files = [
os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]) os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp", "*.h", "*.tcc"])
] ]
+5 -4
View File
@@ -2,6 +2,7 @@
import argparse import argparse
import os import os
from pathlib import Path
import queue import queue
import re import re
import shutil import shutil
@@ -32,7 +33,7 @@ def clang_options(idedata):
cmd = [] cmd = []
# extract target architecture from triplet in g++ filename # extract target architecture from triplet in g++ filename
triplet = os.path.basename(idedata["cxx_path"])[:-4] triplet = Path(idedata["cxx_path"]).name[:-4]
if triplet.startswith("xtensa-"): if triplet.startswith("xtensa-"):
# clang doesn't support Xtensa (yet?), so compile in 32-bit mode and pretend we're the Xtensa compiler # clang doesn't support Xtensa (yet?), so compile in 32-bit mode and pretend we're the Xtensa compiler
cmd.append("-m32") cmd.append("-m32")
@@ -153,8 +154,8 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files):
if sys.stdout.isatty(): if sys.stdout.isatty():
invocation.append("--use-color") invocation.append("--use-color")
invocation.append(f"--header-filter={os.path.abspath(basepath)}/.*") invocation.append(f"--header-filter={Path(basepath).resolve()}/.*")
invocation.append(os.path.abspath(path)) invocation.append(str(Path(path).resolve()))
invocation.append("--") invocation.append("--")
invocation.extend(options) invocation.extend(options)
@@ -229,7 +230,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
cwd = os.getcwd() cwd = Path.cwd()
files = [os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp"])] files = [os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp"])]
# Exclude benchmark files — they require google benchmark headers not # Exclude benchmark files — they require google benchmark headers not
# available in the ESP32 toolchain and use different naming conventions. # available in the ESP32 toolchain and use different naming conventions.
+3 -3
View File
@@ -16,7 +16,7 @@ sys.path.insert(0, str(script_dir))
def read_file_lines(path: Path) -> list[str]: def read_file_lines(path: Path) -> list[str]:
"""Read lines from a file.""" """Read lines from a file."""
with open(path) as f: with path.open() as f:
return f.readlines() return f.readlines()
@@ -65,7 +65,7 @@ def get_clang_tidy_version_from_requirements(repo_root: Path | None = None) -> s
def read_file_bytes(path: Path) -> bytes: def read_file_bytes(path: Path) -> bytes:
"""Read bytes from a file.""" """Read bytes from a file."""
with open(path, "rb") as f: with path.open("rb") as f:
return f.read() return f.read()
@@ -120,7 +120,7 @@ def read_stored_hash(repo_root: Path | None = None) -> str | None:
def write_file_content(path: Path, content: str) -> None: def write_file_content(path: Path, content: str) -> None:
"""Write content to a file.""" """Write content to a file."""
with open(path, "w") as f: with path.open("w") as f:
f.write(content) f.write(content)
+1 -1
View File
@@ -306,7 +306,7 @@ def _is_clang_tidy_full_scan() -> bool:
""" """
try: try:
result = subprocess.run( result = subprocess.run(
[os.path.join(root_path, "script", "clang_tidy_hash.py"), "--check"], [str(Path(root_path) / "script" / "clang_tidy_hash.py"), "--check"],
capture_output=True, capture_output=True,
check=False, check=False,
) )
+10 -9
View File
@@ -17,10 +17,10 @@ from typing import Any
import colorama import colorama
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) root_path = str(Path(__file__).resolve().parent.parent)
basepath = os.path.join(root_path, "esphome") basepath = str(Path(root_path) / "esphome")
temp_folder = os.path.join(root_path, ".temp") temp_folder = str(Path(root_path) / ".temp")
temp_header_file = os.path.join(temp_folder, "all-include.cpp") temp_header_file = str(Path(temp_folder) / "all-include.cpp")
# C++ file extensions used for clang-tidy and clang-format checks # C++ file extensions used for clang-tidy and clang-format checks
CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc") CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc")
@@ -339,8 +339,8 @@ def _get_github_event_data() -> dict | None:
Parsed event data dictionary, or None if not available Parsed event data dictionary, or None if not available
""" """
github_event_path = os.environ.get("GITHUB_EVENT_PATH") github_event_path = os.environ.get("GITHUB_EVENT_PATH")
if github_event_path and os.path.exists(github_event_path): if github_event_path and Path(github_event_path).exists():
with open(github_event_path) as f: with Path(github_event_path).open() as f:
return json.load(f) return json.load(f)
return None return None
@@ -464,7 +464,8 @@ def _get_changed_files_from_command(command: list[str]) -> list[str]:
raise Exception(f"Command failed: {' '.join(command)}\nstderr: {proc.stderr}") raise Exception(f"Command failed: {' '.join(command)}\nstderr: {proc.stderr}")
changed_files = splitlines_no_ends(proc.stdout) changed_files = splitlines_no_ends(proc.stdout)
changed_files = [os.path.relpath(f, os.getcwd()) for f in changed_files if f] cwd = Path.cwd()
changed_files = [os.path.relpath(f, cwd) for f in changed_files if f] # noqa: PTH109
changed_files.sort() changed_files.sort()
return changed_files return changed_files
@@ -499,7 +500,7 @@ def get_changed_components() -> list[str] | None:
return None return None
# Use list-components.py to get changed components # Use list-components.py to get changed components
script_path = os.path.join(root_path, "script", "list-components.py") script_path = str(Path(root_path) / "script" / "list-components.py")
cmd = [script_path, "--changed"] cmd = [script_path, "--changed"]
try: try:
@@ -619,7 +620,7 @@ def filter_changed(files: list[str]) -> list[str]:
def filter_grep(files: list[str], value: list[str]) -> list[str]: def filter_grep(files: list[str], value: list[str]) -> list[str]:
matched = [] matched = []
for file in files: for file in files:
with open(file, encoding="utf-8") as handle: with Path(file).open(encoding="utf-8") as handle:
contents = handle.read() contents = handle.read()
if any(v in contents for v in value): if any(v in contents for v in value):
matched.append(file) matched.append(file)
+4 -2
View File
@@ -2,6 +2,7 @@
import argparse import argparse
import os import os
from pathlib import Path
import re import re
import sys import sys
@@ -66,11 +67,12 @@ def main():
args = parser.parse_args() args = parser.parse_args()
files = [] files = []
cwd = Path.cwd()
for path in git_ls_files(): for path in git_ls_files():
filetypes = (".py",) filetypes = (".py",)
ext = os.path.splitext(path)[1] ext = Path(path).suffix
if ext in filetypes and path.startswith("esphome"): if ext in filetypes and path.startswith("esphome"):
path = os.path.relpath(path, os.getcwd()) path = os.path.relpath(path, cwd)
files.append(path) files.append(path)
# Match against re # Match against re
file_name_re = re.compile("|".join(args.files)) file_name_re = re.compile("|".join(args.files))
+3 -2
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from pathlib import Path
import re import re
# pylint: disable=import-error # pylint: disable=import-error
@@ -34,10 +35,10 @@ DOMAINS = {
def sub(path, pattern, repl): def sub(path, pattern, repl):
with open(path, encoding="utf-8") as handle: with Path(path).open(encoding="utf-8") as handle:
content = handle.read() content = handle.read()
content = re.sub(pattern, repl, content, flags=re.MULTILINE) content = re.sub(pattern, repl, content, flags=re.MULTILINE)
with open(path, "w", encoding="utf-8") as handle: with Path(path).open("w", encoding="utf-8") as handle:
handle.write(content) handle.write(content)
+1 -1
View File
@@ -297,7 +297,7 @@ def write_github_summary(
test_results: List of all test results test_results: List of all test results
""" """
summary_content = format_github_summary(test_results, toolchain) summary_content = format_github_summary(test_results, toolchain)
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as f: with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a", encoding="utf-8") as f:
f.write(summary_content) f.write(summary_content)
+3 -7
View File
@@ -34,9 +34,7 @@ def test_get_base_frontend_path_dev_mode() -> None:
# The function uses Path.resolve() which resolves symlinks # The function uses Path.resolve() which resolves symlinks
# The actual function adds "/" to the path, so we simulate that # The actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/" test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = ( expected = (Path.cwd() / test_path_with_slash / "esphome_dashboard").resolve()
Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard"
).resolve()
assert result == expected assert result == expected
@@ -62,9 +60,7 @@ def test_get_base_frontend_path_dev_mode_relative_path() -> None:
# The function uses Path.resolve() which resolves symlinks # The function uses Path.resolve() which resolves symlinks
# The actual function adds "/" to the path, so we simulate that # The actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/" test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = ( expected = (Path.cwd() / test_path_with_slash / "esphome_dashboard").resolve()
Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard"
).resolve()
assert result == expected assert result == expected
assert result.is_absolute() assert result.is_absolute()
@@ -157,7 +153,7 @@ def test_load_file_path(tmp_path: Path) -> None:
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
test_file.write_bytes(b"test content") test_file.write_bytes(b"test content")
with open(test_file, "rb") as f: with test_file.open("rb") as f:
content = f.read() content = f.read()
assert content == b"test content" assert content == b"test content"
+1 -1
View File
@@ -79,7 +79,7 @@ def shared_platformio_cache() -> Generator[Path]:
lock_file = Path.home() / ".esphome-integration-tests-init.lock" lock_file = Path.home() / ".esphome-integration-tests-init.lock"
# Always acquire the lock to ensure cache is ready before proceeding # Always acquire the lock to ensure cache is ready before proceeding
with open(lock_file, "w") as lock_fd: with lock_file.open("w") as lock_fd:
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX)
# Check if the native platform is installed (the actual indicator of a populated cache) # Check if the native platform is installed (the actual indicator of a populated cache)
+2 -5
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import importlib.util import importlib.util
import json import json
import os
from pathlib import Path from pathlib import Path
import sys import sys
from unittest.mock import patch from unittest.mock import patch
@@ -13,12 +12,10 @@ import pytest
# Load the script-under-test as `check_import_time` (it's a hyphenated path # Load the script-under-test as `check_import_time` (it's a hyphenated path
# inside `script/` that mirrors the existing `determine_jobs` pattern). # inside `script/` that mirrors the existing `determine_jobs` pattern).
script_dir = os.path.abspath( script_dir = str((Path(__file__).parent / ".." / ".." / "script").resolve())
os.path.join(os.path.dirname(__file__), "..", "..", "script")
)
sys.path.insert(0, script_dir) sys.path.insert(0, script_dir)
spec = importlib.util.spec_from_file_location( spec = importlib.util.spec_from_file_location(
"check_import_time", os.path.join(script_dir, "check_import_time.py") "check_import_time", str(Path(script_dir) / "check_import_time.py")
) )
check_import_time = importlib.util.module_from_spec(spec) check_import_time = importlib.util.module_from_spec(spec)
spec.loader.exec_module(check_import_time) spec.loader.exec_module(check_import_time)
+2 -5
View File
@@ -3,7 +3,6 @@
from collections.abc import Generator from collections.abc import Generator
import importlib.util import importlib.util
import json import json
import os
from pathlib import Path from pathlib import Path
import sys import sys
from unittest.mock import Mock, call, patch from unittest.mock import Mock, call, patch
@@ -11,9 +10,7 @@ from unittest.mock import Mock, call, patch
import pytest import pytest
# Add the script directory to Python path so we can import the module # Add the script directory to Python path so we can import the module
script_dir = os.path.abspath( script_dir = str((Path(__file__).parent / ".." / ".." / "script").resolve())
os.path.join(os.path.dirname(__file__), "..", "..", "script")
)
sys.path.insert(0, script_dir) sys.path.insert(0, script_dir)
# Import helpers module for patching # Import helpers module for patching
@@ -22,7 +19,7 @@ import helpers # noqa: E402
import script.helpers # noqa: E402 import script.helpers # noqa: E402
spec = importlib.util.spec_from_file_location( spec = importlib.util.spec_from_file_location(
"determine_jobs", os.path.join(script_dir, "determine-jobs.py") "determine_jobs", str(Path(script_dir) / "determine-jobs.py")
) )
determine_jobs = importlib.util.module_from_spec(spec) determine_jobs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(determine_jobs) spec.loader.exec_module(determine_jobs)
+1 -3
View File
@@ -12,9 +12,7 @@ import pytest
from pytest import MonkeyPatch from pytest import MonkeyPatch
# Add the script directory to Python path so we can import helpers # Add the script directory to Python path so we can import helpers
sys.path.insert( sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve()))
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "script"))
)
import helpers # noqa: E402 import helpers # noqa: E402
+1 -4
View File
@@ -1,6 +1,5 @@
"""Unit tests for script/build_helpers.py manifest override and build helpers.""" """Unit tests for script/build_helpers.py manifest override and build helpers."""
import os
from pathlib import Path from pathlib import Path
import sys import sys
import textwrap import textwrap
@@ -9,9 +8,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
# Add the script directory to Python path so we can import build_helpers # Add the script directory to Python path so we can import build_helpers
sys.path.insert( sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve()))
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "script"))
)
import build_helpers # noqa: E402 import build_helpers # noqa: E402
+10 -10
View File
@@ -486,7 +486,7 @@ def test_preload_core_config_basic(setup_core: Path) -> None:
assert CONF_BUILD_PATH in config[CONF_ESPHOME] assert CONF_BUILD_PATH in config[CONF_ESPHOME]
# Verify default build path is "build/<device_name>" # Verify default build path is "build/<device_name>"
build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] build_path = config[CONF_ESPHOME][CONF_BUILD_PATH]
assert build_path.endswith(os.path.join("build", "test_device")) assert build_path.endswith(str(Path("build") / "test_device"))
def test_preload_core_config_with_build_path(setup_core: Path) -> None: def test_preload_core_config_with_build_path(setup_core: Path) -> None:
@@ -523,7 +523,7 @@ def test_preload_core_config_env_build_path(setup_core: Path) -> None:
assert "test_device" in config[CONF_ESPHOME][CONF_BUILD_PATH] assert "test_device" in config[CONF_ESPHOME][CONF_BUILD_PATH]
# Verify it uses the env var path with device name appended # Verify it uses the env var path with device name appended
build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] build_path = config[CONF_ESPHOME][CONF_BUILD_PATH]
expected_path = os.path.join("/env/build", "test_device") expected_path = str(Path("/env/build") / "test_device")
assert build_path == expected_path or build_path == expected_path.replace( assert build_path == expected_path or build_path == expected_path.replace(
"/", os.sep "/", os.sep
) )
@@ -739,7 +739,7 @@ async def test_add_includes_with_single_file(
"""Test add_includes copies a single header file to build directory.""" """Test add_includes copies a single header file to build directory."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create include file # Create include file
include_file = tmp_path / "my_header.h" include_file = tmp_path / "my_header.h"
@@ -769,7 +769,7 @@ async def test_add_includes_with_directory_unix(
"""Test add_includes copies all files from a directory on Unix.""" """Test add_includes copies all files from a directory on Unix."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create include directory with files # Create include directory with files
include_dir = tmp_path / "includes" include_dir = tmp_path / "includes"
@@ -814,7 +814,7 @@ async def test_add_includes_with_directory_windows(
"""Test add_includes copies all files from a directory on Windows.""" """Test add_includes copies all files from a directory on Windows."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create include directory with files # Create include directory with files
include_dir = tmp_path / "includes" include_dir = tmp_path / "includes"
@@ -856,7 +856,7 @@ async def test_add_includes_with_multiple_sources(
"""Test add_includes with multiple files and directories.""" """Test add_includes with multiple files and directories."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create various include sources # Create various include sources
single_file = tmp_path / "single.h" single_file = tmp_path / "single.h"
@@ -884,7 +884,7 @@ async def test_add_includes_empty_directory(
"""Test add_includes with an empty directory doesn't fail.""" """Test add_includes with an empty directory doesn't fail."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create empty directory # Create empty directory
empty_dir = tmp_path / "empty" empty_dir = tmp_path / "empty"
@@ -906,7 +906,7 @@ async def test_add_includes_preserves_directory_structure_unix(
"""Test that add_includes preserves relative directory structure on Unix.""" """Test that add_includes preserves relative directory structure on Unix."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create nested directory structure # Create nested directory structure
lib_dir = tmp_path / "lib" lib_dir = tmp_path / "lib"
@@ -940,7 +940,7 @@ async def test_add_includes_preserves_directory_structure_windows(
"""Test that add_includes preserves relative directory structure on Windows.""" """Test that add_includes preserves relative directory structure on Windows."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create nested directory structure # Create nested directory structure
lib_dir = tmp_path / "lib" lib_dir = tmp_path / "lib"
@@ -973,7 +973,7 @@ async def test_add_includes_overwrites_existing_files(
"""Test that add_includes overwrites existing files in build directory.""" """Test that add_includes overwrites existing files in build directory."""
CORE.config_path = tmp_path / "config.yaml" CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build" CORE.build_path = tmp_path / "build"
os.makedirs(CORE.build_path, exist_ok=True) CORE.build_path.mkdir(parents=True, exist_ok=True)
# Create include file # Create include file
include_file = tmp_path / "header.h" include_file = tmp_path / "header.h"
+1 -1
View File
@@ -293,7 +293,7 @@ def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32") result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32")
assert result.libpath == [os.path.join("src", "esp32")] assert result.libpath == [str(Path("src") / "esp32")]
assert result.libs == ["algobsec"] assert result.libs == ["algobsec"]
assert ("BAR", "1") in result.cppdefines assert ("BAR", "1") in result.cppdefines
assert "FOO" in result.cppdefines assert "FOO" in result.cppdefines
+1 -2
View File
@@ -1,4 +1,3 @@
import glob
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -106,7 +105,7 @@ REMOTES = {
# Collect all input YAML files for test_substitutions_fixtures parametrized tests: # Collect all input YAML files for test_substitutions_fixtures parametrized tests:
HERE = Path(__file__).parent HERE = Path(__file__).parent
BASE_DIR = HERE / "fixtures" / "substitutions" BASE_DIR = HERE / "fixtures" / "substitutions"
SOURCES = sorted(glob.glob(str(BASE_DIR / "*.input.yaml"))) SOURCES = sorted(str(p) for p in BASE_DIR.glob("*.input.yaml"))
assert SOURCES, f"test_substitutions_fixtures: No input YAML files found in {BASE_DIR}" assert SOURCES, f"test_substitutions_fixtures: No input YAML files found in {BASE_DIR}"
+4 -4
View File
@@ -1358,7 +1358,7 @@ def test_clean_build_handles_readonly_files(
# Create a read-only file (simulating git pack files on Windows) # Create a read-only file (simulating git pack files on Windows)
readonly_file = git_dir / "pack-abc123.pack" readonly_file = git_dir / "pack-abc123.pack"
readonly_file.write_text("pack data") readonly_file.write_text("pack data")
os.chmod(readonly_file, stat.S_IRUSR) # Read-only readonly_file.chmod(stat.S_IRUSR) # Read-only
# Setup mocks # Setup mocks
mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_pioenvs_path.return_value = pioenvs_dir
@@ -1393,7 +1393,7 @@ def test_clean_all_handles_readonly_files(
subdir.mkdir() subdir.mkdir()
readonly_file = subdir / "readonly.txt" readonly_file = subdir / "readonly.txt"
readonly_file.write_text("content") readonly_file.write_text("content")
os.chmod(readonly_file, stat.S_IRUSR) # Read-only readonly_file.chmod(stat.S_IRUSR) # Read-only
# Verify file is read-only # Verify file is read-only
assert not os.access(readonly_file, os.W_OK) assert not os.access(readonly_file, os.W_OK)
@@ -1422,7 +1422,7 @@ def test_clean_build_reraises_for_other_errors(
test_file.write_text("content") test_file.write_text("content")
# Make subdir read-only so files inside can't be deleted # Make subdir read-only so files inside can't be deleted
os.chmod(subdir, stat.S_IRUSR | stat.S_IXUSR) subdir.chmod(stat.S_IRUSR | stat.S_IXUSR)
# Setup mocks # Setup mocks
mock_core.relative_pioenvs_path.return_value = pioenvs_dir mock_core.relative_pioenvs_path.return_value = pioenvs_dir
@@ -1440,7 +1440,7 @@ def test_clean_build_reraises_for_other_errors(
clean_build() clean_build()
finally: finally:
# Cleanup - restore write permission so tmp_path cleanup works # Cleanup - restore write permission so tmp_path cleanup works
os.chmod(subdir, stat.S_IRWXU) subdir.chmod(stat.S_IRWXU)
# Tests for get_build_info() # Tests for get_build_info()