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

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

View File

@@ -794,7 +794,7 @@ def _check_and_emit_build_info() -> None:
# Read build_info from JSON
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)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
@@ -1056,7 +1056,7 @@ def _wait_for_serial_port(
def _port_found() -> bool:
if port is not None:
if os.name == "posix":
return os.path.exists(port)
return Path(port).exists()
return any(p.path == port for p in get_serial_ports())
ports = get_serial_ports()
if known_ports is not None:

View File

@@ -6,6 +6,7 @@ from collections import defaultdict
from collections.abc import Callable
import heapq
from operator import itemgetter
from pathlib import Path
import sys
from typing import TYPE_CHECKING
@@ -699,7 +700,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
content = "\n".join(lines)
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)
else:
print(content)
@@ -737,7 +738,6 @@ def main():
# Load build directory
import json
from pathlib import Path
from esphome.platformio.toolchain import IDEData
@@ -785,7 +785,7 @@ def main():
if not idedata_path.exists():
continue
try:
with open(idedata_path, encoding="utf-8") as f:
with idedata_path.open(encoding="utf-8") as f:
raw_data = json.load(f)
idedata = IDEData(raw_data)
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
import subprocess
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
"""
# Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
platformio_home = Path("~/.platformio/packages").expanduser()
if not platformio_home.exists():
return None

View File

@@ -24,7 +24,7 @@ def get_available_components() -> list[str] | None:
return None
try:
with open(project_desc, encoding="utf-8") as f:
with project_desc.open(encoding="utf-8") as f:
data = json.load(f)
component_info = data.get("build_component_info", {})

View File

@@ -412,7 +412,7 @@ class ConfigBundleCreator:
@staticmethod
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
"""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())

View File

@@ -98,7 +98,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
else:
raise cv.Invalid("Unsupported file source")
with open(path, "rb") as f:
with path.open("rb") as f:
data = f.read()
try:

View File

@@ -169,7 +169,7 @@ async def to_code_base(config):
path = _compute_local_file_path(_compute_url(config))
try:
with open(path, encoding="utf-8") as f:
with path.open(encoding="utf-8") as f:
bsec2_iaq_config = f.read()
except Exception as e:
raise core.EsphomeError(

View File

@@ -75,7 +75,7 @@ def _validate_firmware(config: dict[str, Any]) -> None:
return
path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f:
with path.open("rb") as f:
firmware_data = f.read()
calculated = hashlib.sha256(firmware_data).hexdigest()
expected = config[CONF_SHA256].lower()
@@ -93,7 +93,7 @@ async def to_code(config: dict[str, Any]) -> None:
if config[CONF_TYPE] == TYPE_EMBEDDED:
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()
rhs = [HexInt(x) for x in firmware_data]
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)

View File

@@ -1,3 +1,5 @@
from pathlib import Path
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32
@@ -174,7 +176,7 @@ async def to_code(config):
if config.get(CONF_VERIFY_SSL):
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()
cg.add(var.set_ca_certificate(ca_cert_content))
else:

View File

@@ -395,7 +395,7 @@ def download_image(value):
def is_svg_file(file):
if not file:
return False
with open(file, "rb") as f:
with Path(file).open("rb") as f:
return "<svg" in str(f.read(1024))

View File

@@ -376,14 +376,14 @@ CONFIG_SCHEMA = cv.All(
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)
_validate_manifest_version(manifest)
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()
if manifest.get(KEY_VERSION) == 1:

View File

@@ -139,7 +139,7 @@ async def _smpmgr_upload_connected(
already_uploaded = True
if not already_uploaded:
with open(firmware, "rb") as file:
with firmware.open("rb") as file:
image = file.read()
upload_size = len(image)
progress = ProgressBar("Uploading")

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")):
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)
build = data.get("build", {})
@@ -136,7 +136,7 @@ def _get_variant(json_file: Path) -> str | None:
"""Get variant name from a board JSON file."""
if not json_file.exists():
return None
with open(json_file, encoding="utf-8") as f:
with json_file.open(encoding="utf-8") as f:
data = json.load(f)
return data.get("build", {}).get("variant")

View File

@@ -326,12 +326,12 @@ async def to_code(config):
if CONF_CSS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_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())
if CONF_JS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_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())
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))
if CONF_LOCAL in config and config[CONF_LOCAL]:

View File

@@ -129,9 +129,8 @@ def final_validate_esp32(config: ConfigType) -> ConfigType:
if CONF_PARTITIONS in fv.full_config.get() and not isinstance(
fv.full_config.get()[CONF_PARTITIONS], list
):
with open(
CORE.relative_config_path(fv.full_config.get()[CONF_PARTITIONS]),
encoding="utf8",
with CORE.relative_config_path(fv.full_config.get()[CONF_PARTITIONS]).open(
encoding="utf8"
) as f:
partitions_tab = f.read()
for partition, types in [

View File

@@ -6,6 +6,7 @@ from concurrent.futures import ThreadPoolExecutor
import contextlib
import logging
import os
from pathlib import Path
import socket
import threading
from time import monotonic
@@ -149,4 +150,4 @@ async def async_start(args) -> None:
await dashboard.async_run()
finally:
if sock:
os.remove(sock)
Path(sock).unlink()

View File

@@ -1040,7 +1040,7 @@ class DownloadListRequestHandler(BaseHandler):
class DownloadBinaryRequestHandler(BaseHandler):
def _load_file(self, path: str, compressed: bool) -> bytes:
"""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()
if compressed:
return gzip.compress(data, 9)
@@ -1292,7 +1292,7 @@ class EditRequestHandler(BaseHandler):
def _read_file(self, filename: str, configuration: str) -> bytes | None:
"""Read a file and return the content as bytes."""
try:
with open(file=filename, encoding="utf-8") as f:
with Path(filename).open(encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
if configuration in const.SECRETS_FILES:
@@ -1493,7 +1493,7 @@ def get_base_frontend_path() -> Path:
static_path += "/"
# 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()

View File

@@ -317,24 +317,26 @@ def _collect_filtered_files(src_dir: PathType, src_filters: list[str]) -> list[s
if pattern.endswith("/"):
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 = []
for item in glob.glob(full_pattern, recursive=True):
if not os.path.isdir(item):
for item in glob.glob(full_pattern, recursive=True): # noqa: PTH207
if not Path(item).is_dir():
matched.append(item)
else:
# PlatformIO quirk: a directory matched with "*" should include all its
# nested files and subdirectories, not just the directory itself.
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 == "+":
selected.update(matched)
elif sign == "-":
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:
@@ -486,7 +488,7 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
# Only keep sources
build_src_files = [os.path.relpath(p, component.path) for p in 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
@@ -740,7 +742,7 @@ def _parse_library_json(library_json_path: PathType):
Returns:
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)
@@ -754,7 +756,7 @@ def _parse_library_properties(library_properties_path: PathType):
Returns:
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 = {}
for line in fp.read().splitlines():
line = line.strip()

View File

@@ -108,7 +108,7 @@ def run_extra_script(
"""
env = _FakeSConsEnv(board_mcu=idf_target, pio_env=f"esphome_{idf_target}")
code = compile(script_path.read_text(), str(script_path), "exec")
old_cwd = os.getcwd()
old_cwd = Path.cwd()
try:
os.chdir(library_dir)
exec( # noqa: S102 pylint: disable=exec-used

View File

@@ -138,7 +138,7 @@ def rmdir(directory: PathType, msg: str | None = None):
Raises:
RuntimeError: If directory removal fails
"""
if os.path.isdir(directory):
if Path(directory).is_dir():
try:
if msg:
_LOGGER.debug(msg)
@@ -192,7 +192,7 @@ def _check_stamp(file: PathType, data: dict[str, str]) -> bool:
return False
try:
with open(file, encoding="utf-8") as f:
with Path(file).open(encoding="utf-8") as f:
return json.load(f) == data
except (json.JSONDecodeError, OSError):
return False
@@ -206,7 +206,7 @@ def _write_stamp(file: PathType, data: dict[str, str]):
file: Path to the stamp file 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)
@@ -471,8 +471,12 @@ def _tar_extract_all(
import stat
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)
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:
all_members = tar_ref.getmembers()
@@ -491,8 +495,8 @@ def _tar_extract_all(
name = name.lstrip("/" + os.sep)
# 2. Reject absolute paths (incl. Windows drive)
if os.path.isabs(name) or (
os.name == "nt" and ":" in name.split(os.sep)[0]
if Path(name).is_absolute() or (
os.name == "nt" and ":" in name.split(os.sep)[0] # noqa: PTH206
):
continue
@@ -506,7 +510,7 @@ def _tar_extract_all(
name = norm[len(strip_prefix) :]
# 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:
continue
@@ -515,18 +519,20 @@ def _tar_extract_all(
linkname = member.linkname
# Reject absolute link targets
if os.path.isabs(linkname):
if Path(linkname).is_absolute():
continue
# Strip leading slashes
linkname = os.path.normpath(linkname)
if member.issym():
link_target = os.path.join(
abs_dest, os.path.dirname(name), linkname
link_target = os.path.join( # noqa: PTH118
abs_dest,
os.path.dirname(name), # noqa: PTH120
linkname,
)
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)
if os.path.commonpath([abs_dest, link_target]) != abs_dest:
@@ -598,7 +604,9 @@ def _zip_extract_all(
"""
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:
all_members = zip_ref.infolist()
@@ -618,8 +626,8 @@ def _zip_extract_all(
name = member.filename.lstrip("/\\")
# 2. Reject absolute paths / Windows drives
if os.path.isabs(name) or (
os.name == "nt" and ":" in name.split(os.sep)[0]
if Path(name).is_absolute() or (
os.name == "nt" and ":" in name.split(os.sep)[0] # noqa: PTH206
):
continue
@@ -633,7 +641,7 @@ def _zip_extract_all(
name = norm[len(strip_prefix) :]
# 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:
raise ValueError(f"Unsafe path detected: {member.filename}")
@@ -680,7 +688,7 @@ def archive_extract_all(
with ExitStack() as stack:
archive_ref: io.BufferedIOBase
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)):
archive_ref = archive
elif isinstance(archive, io.RawIOBase):
@@ -727,7 +735,7 @@ def download_from_mirrors(
# 1. Open target file for writing if path given
with ExitStack() as stack:
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)):
f = target
else:
@@ -917,7 +925,7 @@ def _patch_tools_json_for_linux_arm64(framework_path: Path) -> None:
return
try:
with open(tools_json, encoding="utf-8") as f:
with tools_json.open(encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError) as e:
_LOGGER.warning(

View File

@@ -10,6 +10,7 @@ not installed.
import json
import os
from pathlib import Path
import sys
from types import SimpleNamespace
@@ -25,7 +26,7 @@ from idf_tools import (
g.idf_path = sys.argv[1]
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())
args = SimpleNamespace(prefer_system=False)

View File

@@ -91,6 +91,7 @@ def main() -> int:
# ---- end sys.path fix-up -----------------------------------------------
import os
from pathlib import Path
import re
import runpy
@@ -229,7 +230,7 @@ def main() -> int:
# runpy.run_path does not do this automatically, but idf.py relies
# on it to import its sibling modules (python_version_checker,
# 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:
sys.path.insert(0, script_dir)

View File

@@ -241,20 +241,21 @@ def has_outdated_files():
dependency_lock_path = CORE.relative_build_path("dependencies.lock")
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
if not os.path.isfile(cmakecache_txt_path):
if not cmakecache_txt_path.is_file():
return True
if not os.path.isfile(build_ninja_path):
if not build_ninja_path.is_file():
return True
if os.path.isfile(dependency_lock_path) and os.path.getmtime(
dependency_lock_path
) > os.path.getmtime(build_ninja_path):
if (
dependency_lock_path.is_file()
and dependency_lock_path.stat().st_mtime > build_ninja_path.stat().st_mtime
):
return True
cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path)
cmakecache_txt_mtime = cmakecache_txt_path.stat().st_mtime
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]
if f.exists()
)
@@ -452,7 +453,7 @@ def create_factory_bin() -> bool:
return False
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)
except (json.JSONDecodeError, OSError) as e:
_LOGGER.error("Failed to read flasher_args.json: %s", e)

View File

@@ -517,7 +517,7 @@ def run_ota_impl_(
continue
_LOGGER.info("Connected to %s", sa[0])
with open(filename, "rb") as file_handle:
with Path(filename).open("rb") as file_handle:
try:
perform_ota(sock, password, file_handle, filename, ota_type)
except OTAError as err:

View File

@@ -385,7 +385,7 @@ def rmtree(path: Path | str) -> None:
def _onerror(func, path, exc_info):
if os.access(path, os.W_OK):
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)
# ``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
# if that fails, use normal error handler
with suppress(OSError):
os.unlink(dst)
Path(dst).unlink()
shutil.copyfile(src, dst)
return True

View File

@@ -2,7 +2,7 @@ import contextlib
from datetime import datetime
import json
import logging
import os
from pathlib import Path
import ssl
import tempfile
import time
@@ -120,8 +120,8 @@ def prepare(
key_file.close()
context.load_cert_chain(cert_file.name, key_file.name)
finally:
os.unlink(cert_file.name)
os.unlink(key_file.name)
Path(cert_file.name).unlink()
Path(key_file.name).unlink()
client.tls_set_context(context)
try:

View File

@@ -126,7 +126,7 @@ def _try_upload(
_LOGGER.info("Connecting to %s port %s...", ip, port)
try:
with open(filename, "rb") as fh:
with filename.open("rb") as fh:
streamer = _MultipartStreamer(fh, file_size, filename.name)
try:
response = requests.post(

View File

@@ -127,6 +127,7 @@ select = [
"NPY", # numpy-specific rules
"PERF", # performance
"PL", # pylint
"PTH", # flake8-use-pathlib
"PYI", # flake8-pyi
"Q", # flake8-quotes
"RSE", # flake8-raise

View File

@@ -3163,7 +3163,7 @@ def main() -> None:
defines_content += "\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)
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
"""
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)
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)
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)
hpp = FILE_HEADER
@@ -3641,10 +3641,10 @@ static const char *const TAG = "api.service";
} // 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)
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)
prot_file.unlink()

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
if not yaml_path.is_file():
continue
with open(yaml_path) as f:
with yaml_path.open() as f:
component_config = yaml.safe_load(f)
if component_config and isinstance(component_config, dict):
for key, value in component_config.items():

View File

@@ -2,6 +2,7 @@
import argparse
from dataclasses import dataclass
from pathlib import Path
import re
import sys
@@ -39,12 +40,12 @@ class Version:
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, count = re.subn(pattern, repl, content, flags=re.MULTILINE)
if expected_count is not None:
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)

View File

@@ -14,7 +14,7 @@ import time
import colorama
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):

View File

@@ -44,7 +44,7 @@ def main() -> int:
return 1
try:
with open(json_path, encoding="utf-8") as f:
with Path(json_path).open(encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"Error loading JSON: {e}", file=sys.stderr)
@@ -74,7 +74,7 @@ def main() -> int:
# Write back
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)
print(f"Added metadata to {args.json_file}", file=sys.stderr)
except OSError as e:

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import os
from pathlib import Path
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")
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())
else:
for key, value in outputs.items():

View File

@@ -91,7 +91,7 @@ def load_analysis_json(json_path: str) -> dict | None:
return None
try:
with open(json_file, encoding="utf-8") as f:
with Path(json_file).open(encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
print(f"Failed to load analysis JSON: {e}", file=sys.stderr)

View File

@@ -127,7 +127,7 @@ def run_detailed_analysis(build_dir: str) -> dict | None:
if not idedata_path.exists():
continue
try:
with open(idedata_path, encoding="utf-8") as f:
with idedata_path.open(encoding="utf-8") as f:
raw_data = json.load(f)
idedata = IDEData(raw_data)
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.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)
print(f"Saved analysis to {args.output_json}", file=sys.stderr)

View File

@@ -2,6 +2,7 @@
import argparse
import os
from pathlib import Path
import queue
import re
import subprocess
@@ -70,7 +71,7 @@ def main():
)
args = parser.parse_args()
cwd = os.getcwd()
cwd = Path.cwd()
files = [
os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp", "*.h", "*.tcc"])
]

View File

@@ -2,6 +2,7 @@
import argparse
import os
from pathlib import Path
import queue
import re
import shutil
@@ -32,7 +33,7 @@ def clang_options(idedata):
cmd = []
# 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-"):
# clang doesn't support Xtensa (yet?), so compile in 32-bit mode and pretend we're the Xtensa compiler
cmd.append("-m32")
@@ -153,8 +154,8 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files):
if sys.stdout.isatty():
invocation.append("--use-color")
invocation.append(f"--header-filter={os.path.abspath(basepath)}/.*")
invocation.append(os.path.abspath(path))
invocation.append(f"--header-filter={Path(basepath).resolve()}/.*")
invocation.append(str(Path(path).resolve()))
invocation.append("--")
invocation.extend(options)
@@ -229,7 +230,7 @@ def main():
)
args = parser.parse_args()
cwd = os.getcwd()
cwd = Path.cwd()
files = [os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp"])]
# Exclude benchmark files — they require google benchmark headers not
# available in the ESP32 toolchain and use different naming conventions.

View File

@@ -16,7 +16,7 @@ sys.path.insert(0, str(script_dir))
def read_file_lines(path: Path) -> list[str]:
"""Read lines from a file."""
with open(path) as f:
with path.open() as f:
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:
"""Read bytes from a file."""
with open(path, "rb") as f:
with path.open("rb") as f:
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:
"""Write content to a file."""
with open(path, "w") as f:
with path.open("w") as f:
f.write(content)

View File

@@ -306,7 +306,7 @@ def _is_clang_tidy_full_scan() -> bool:
"""
try:
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,
check=False,
)

View File

@@ -17,10 +17,10 @@ from typing import Any
import colorama
root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", "..")))
basepath = os.path.join(root_path, "esphome")
temp_folder = os.path.join(root_path, ".temp")
temp_header_file = os.path.join(temp_folder, "all-include.cpp")
root_path = str(Path(__file__).resolve().parent.parent)
basepath = str(Path(root_path) / "esphome")
temp_folder = str(Path(root_path) / ".temp")
temp_header_file = str(Path(temp_folder) / "all-include.cpp")
# C++ file extensions used for clang-tidy and clang-format checks
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
"""
github_event_path = os.environ.get("GITHUB_EVENT_PATH")
if github_event_path and os.path.exists(github_event_path):
with open(github_event_path) as f:
if github_event_path and Path(github_event_path).exists():
with Path(github_event_path).open() as f:
return json.load(f)
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}")
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()
return changed_files
@@ -499,7 +500,7 @@ def get_changed_components() -> list[str] | None:
return None
# 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"]
try:
@@ -619,7 +620,7 @@ def filter_changed(files: list[str]) -> list[str]:
def filter_grep(files: list[str], value: list[str]) -> list[str]:
matched = []
for file in files:
with open(file, encoding="utf-8") as handle:
with Path(file).open(encoding="utf-8") as handle:
contents = handle.read()
if any(v in contents for v in value):
matched.append(file)

View File

@@ -2,6 +2,7 @@
import argparse
import os
from pathlib import Path
import re
import sys
@@ -66,11 +67,12 @@ def main():
args = parser.parse_args()
files = []
cwd = Path.cwd()
for path in git_ls_files():
filetypes = (".py",)
ext = os.path.splitext(path)[1]
ext = Path(path).suffix
if ext in filetypes and path.startswith("esphome"):
path = os.path.relpath(path, os.getcwd())
path = os.path.relpath(path, cwd)
files.append(path)
# Match against re
file_name_re = re.compile("|".join(args.files))

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
from pathlib import Path
import re
# pylint: disable=import-error
@@ -34,10 +35,10 @@ DOMAINS = {
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 = 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)

View File

@@ -297,7 +297,7 @@ def write_github_summary(
test_results: List of all test results
"""
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)

View File

@@ -34,9 +34,7 @@ def test_get_base_frontend_path_dev_mode() -> None:
# The function uses Path.resolve() which resolves symlinks
# The actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = (
Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard"
).resolve()
expected = (Path.cwd() / test_path_with_slash / "esphome_dashboard").resolve()
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 actual function adds "/" to the path, so we simulate that
test_path_with_slash = test_path if test_path.endswith("/") else test_path + "/"
expected = (
Path(os.getcwd()) / test_path_with_slash / "esphome_dashboard"
).resolve()
expected = (Path.cwd() / test_path_with_slash / "esphome_dashboard").resolve()
assert result == expected
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.write_bytes(b"test content")
with open(test_file, "rb") as f:
with test_file.open("rb") as f:
content = f.read()
assert content == b"test content"

View File

@@ -79,7 +79,7 @@ def shared_platformio_cache() -> Generator[Path]:
lock_file = Path.home() / ".esphome-integration-tests-init.lock"
# 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)
# Check if the native platform is installed (the actual indicator of a populated cache)

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import importlib.util
import json
import os
from pathlib import Path
import sys
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
# inside `script/` that mirrors the existing `determine_jobs` pattern).
script_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "script")
)
script_dir = str((Path(__file__).parent / ".." / ".." / "script").resolve())
sys.path.insert(0, script_dir)
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)
spec.loader.exec_module(check_import_time)

View File

@@ -3,7 +3,6 @@
from collections.abc import Generator
import importlib.util
import json
import os
from pathlib import Path
import sys
from unittest.mock import Mock, call, patch
@@ -11,9 +10,7 @@ from unittest.mock import Mock, call, patch
import pytest
# Add the script directory to Python path so we can import the module
script_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "script")
)
script_dir = str((Path(__file__).parent / ".." / ".." / "script").resolve())
sys.path.insert(0, script_dir)
# Import helpers module for patching
@@ -22,7 +19,7 @@ import helpers # noqa: E402
import script.helpers # noqa: E402
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)
spec.loader.exec_module(determine_jobs)

View File

@@ -12,9 +12,7 @@ import pytest
from pytest import MonkeyPatch
# Add the script directory to Python path so we can import helpers
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "script"))
)
sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve()))
import helpers # noqa: E402

View File

@@ -1,6 +1,5 @@
"""Unit tests for script/build_helpers.py manifest override and build helpers."""
import os
from pathlib import Path
import sys
import textwrap
@@ -9,9 +8,7 @@ from unittest.mock import MagicMock, patch
import pytest
# Add the script directory to Python path so we can import build_helpers
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "script"))
)
sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve()))
import build_helpers # noqa: E402

View File

@@ -486,7 +486,7 @@ def test_preload_core_config_basic(setup_core: Path) -> None:
assert CONF_BUILD_PATH in config[CONF_ESPHOME]
# Verify default build path is "build/<device_name>"
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:
@@ -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]
# Verify it uses the env var path with device name appended
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(
"/", 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."""
CORE.config_path = tmp_path / "config.yaml"
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
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."""
CORE.config_path = tmp_path / "config.yaml"
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
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."""
CORE.config_path = tmp_path / "config.yaml"
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
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."""
CORE.config_path = tmp_path / "config.yaml"
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
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."""
CORE.config_path = tmp_path / "config.yaml"
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
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."""
CORE.config_path = tmp_path / "config.yaml"
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
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."""
CORE.config_path = tmp_path / "config.yaml"
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
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."""
CORE.config_path = tmp_path / "config.yaml"
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
include_file = tmp_path / "header.h"

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")
assert result.libpath == [os.path.join("src", "esp32")]
assert result.libpath == [str(Path("src") / "esp32")]
assert result.libs == ["algobsec"]
assert ("BAR", "1") in result.cppdefines
assert "FOO" in result.cppdefines

View File

@@ -1,4 +1,3 @@
import glob
import logging
from pathlib import Path
from typing import Any
@@ -106,7 +105,7 @@ REMOTES = {
# Collect all input YAML files for test_substitutions_fixtures parametrized tests:
HERE = Path(__file__).parent
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}"

View File

@@ -1358,7 +1358,7 @@ def test_clean_build_handles_readonly_files(
# Create a read-only file (simulating git pack files on Windows)
readonly_file = git_dir / "pack-abc123.pack"
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
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
@@ -1393,7 +1393,7 @@ def test_clean_all_handles_readonly_files(
subdir.mkdir()
readonly_file = subdir / "readonly.txt"
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
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")
# 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
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
@@ -1440,7 +1440,7 @@ def test_clean_build_reraises_for_other_errors(
clean_build()
finally:
# 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()