mirror of
https://github.com/esphome/esphome.git
synced 2026-09-21 20:18:43 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
# Conflicts: # esphome/__main__.py # esphome/components/api/client.py # esphome/components/bl0942/bl0942.cpp # esphome/components/cover/__init__.py # esphome/components/cover/automation.h # esphome/components/esphome/ota/ota_esphome.cpp # esphome/components/ota/ota_backend_esp_idf.cpp # esphome/components/ota/ota_partitions_esp_idf.cpp # esphome/espota2.py # tests/components/template/common-base.yaml # tests/unit_tests/components/api/test_client.py # tests/unit_tests/test_main.py
This commit is contained in:
@@ -1065,7 +1065,40 @@ def convert_keys(converted, schema, path):
|
||||
else:
|
||||
converted["key_type"] = str(k)
|
||||
|
||||
if hasattr(k, "default") and str(k.default) != "...":
|
||||
# ``cv.OnlyWith`` / ``cv.OnlyWithout`` expose ``default`` as
|
||||
# a property that returns ``vol.UNDEFINED`` when the gating
|
||||
# component isn't loaded — and at schema-generation time
|
||||
# ``CORE.loaded_integrations`` is always empty, so the
|
||||
# property never resolves. The unconditional default lives
|
||||
# on ``_default``; expose it under a *new* per-class field
|
||||
# (``default_with`` for ``OnlyWith``, ``default_without`` for
|
||||
# ``OnlyWithout``) that bundles the value with the gating
|
||||
# component(s). Pure addition to the bundle — old consumers
|
||||
# that read only ``default`` see these fields as
|
||||
# default-less (same as today, no regression where they used
|
||||
# to fall back to a hard-coded UI default); new consumers
|
||||
# opt-in to the gated fields and apply the default
|
||||
# *conditionally* on which integrations the user has
|
||||
# loaded. Without the gate info, an ethernet-only config on
|
||||
# ``cv.OnlyWith(K, "wifi", default=True)`` would otherwise
|
||||
# render ``True`` even though ESPHome itself wouldn't apply
|
||||
# the default for that config.
|
||||
if isinstance(k, (cv.OnlyWith, cv.OnlyWithout)):
|
||||
default_value = k._default()
|
||||
if default_value is not None:
|
||||
components = (
|
||||
list(k._component)
|
||||
if isinstance(k._component, list)
|
||||
else [k._component]
|
||||
)
|
||||
gate_field = (
|
||||
"default_with" if isinstance(k, cv.OnlyWith) else "default_without"
|
||||
)
|
||||
result[gate_field] = {
|
||||
"value": str(default_value),
|
||||
"components": components,
|
||||
}
|
||||
elif hasattr(k, "default") and str(k.default) != "...":
|
||||
default_value = k.default()
|
||||
if default_value is not None:
|
||||
result["default"] = str(default_value)
|
||||
|
||||
+1
-1
@@ -250,7 +250,7 @@ def lint_ext_check(fname):
|
||||
]
|
||||
)
|
||||
def lint_executable_bit(fname: Path) -> str | None:
|
||||
ex = EXECUTABLE_BIT[str(fname)]
|
||||
ex = EXECUTABLE_BIT[fname.as_posix()]
|
||||
if ex != 100644:
|
||||
return (
|
||||
f"File has invalid executable bit {ex}. If running from a windows machine please "
|
||||
|
||||
@@ -10,6 +10,7 @@ what files have changed. It outputs JSON with the following structure:
|
||||
"clang_tidy": true/false,
|
||||
"clang_format": true/false,
|
||||
"python_linters": true/false,
|
||||
"device_builder": true/false,
|
||||
"changed_components": ["component1", "component2", ...],
|
||||
"component_test_count": 5,
|
||||
"memory_impact": {
|
||||
@@ -25,6 +26,7 @@ The CI workflow uses this information to:
|
||||
- Skip or run clang-tidy (and whether to do a full scan)
|
||||
- Skip or run clang-format
|
||||
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
|
||||
- Skip or run downstream esphome/device-builder tests against the PR's Python code
|
||||
- Determine which components to test individually
|
||||
- Decide how to split component tests (if there are many)
|
||||
- Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes
|
||||
@@ -440,6 +442,56 @@ def should_run_import_time(branch: str | None = None) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# Files outside esphome/**/*.py whose changes can affect the downstream
|
||||
# device-builder build. requirements.txt / pyproject.toml change the runtime
|
||||
# dependency graph that device-builder picks up when it installs esphome.
|
||||
DEVICE_BUILDER_TRIGGER_FILES = frozenset(
|
||||
{
|
||||
"requirements.txt",
|
||||
"pyproject.toml",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def should_run_device_builder(branch: str | None = None) -> bool:
|
||||
"""Determine if downstream esphome/device-builder tests should run.
|
||||
|
||||
device-builder imports esphome as a library, so whenever the importable
|
||||
Python surface, the runtime dependencies, or any non-C++ file packaged
|
||||
with esphome (pyproject.toml has ``include-package-data = true``, so
|
||||
things like esphome/idf_component.yml ship and can affect installs)
|
||||
changes we re-run its test suite against the PR's code to catch
|
||||
breakage we'd otherwise only see after a release.
|
||||
|
||||
Skipped on beta/release branches: those branches typically lag behind
|
||||
device-builder@main, so a new device-builder API dependency would
|
||||
falsely fail the run without reflecting any problem in the PR itself.
|
||||
|
||||
Args:
|
||||
branch: Branch to compare against. If None, uses default.
|
||||
|
||||
Returns:
|
||||
True if the device-builder downstream tests should run, False otherwise.
|
||||
"""
|
||||
target_branch = get_target_branch()
|
||||
if target_branch and (
|
||||
target_branch.startswith("release") or target_branch.startswith("beta")
|
||||
):
|
||||
return False
|
||||
|
||||
for file in changed_files(branch):
|
||||
if file in DEVICE_BUILDER_TRIGGER_FILES:
|
||||
return True
|
||||
# Anything under esphome/ that isn't C++ source can change the
|
||||
# importable / packaged surface device-builder consumes
|
||||
# (Python sources, packaged YAML/JSON like idf_component.yml,
|
||||
# etc.). C++ files only affect compiled firmware, not the
|
||||
# Python install device-builder pulls in.
|
||||
if file.startswith("esphome/") and not file.endswith(CPP_FILE_EXTENSIONS):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def determine_cpp_unit_tests(
|
||||
branch: str | None = None,
|
||||
) -> tuple[bool, list[str]]:
|
||||
@@ -874,6 +926,7 @@ def main() -> None:
|
||||
run_clang_format = should_run_clang_format(args.branch)
|
||||
run_python_linters = should_run_python_linters(args.branch)
|
||||
run_import_time = should_run_import_time(args.branch)
|
||||
run_device_builder = should_run_device_builder(args.branch)
|
||||
changed_cpp_file_count = count_changed_cpp_files(args.branch)
|
||||
|
||||
# Get changed components
|
||||
@@ -1007,6 +1060,7 @@ def main() -> None:
|
||||
"clang_format": run_clang_format,
|
||||
"python_linters": run_python_linters,
|
||||
"import_time": run_import_time,
|
||||
"device_builder": run_device_builder,
|
||||
"changed_components": changed_components,
|
||||
"changed_components_with_tests": changed_components_with_tests,
|
||||
"directly_changed_components_with_tests": list(directly_changed_with_tests),
|
||||
|
||||
@@ -44,7 +44,14 @@ def find_and_activate_virtualenv():
|
||||
def run_command():
|
||||
# Execute the remaining arguments in the new environment
|
||||
if len(sys.argv) > 1:
|
||||
result = subprocess.run(sys.argv[1:], check=False, close_fds=False)
|
||||
args = sys.argv[1:]
|
||||
# Windows CreateProcess doesn't follow shebangs, so prepend the
|
||||
# current interpreter when the entry is a .py script. Using
|
||||
# sys.executable also pins the nested call to the same Python that
|
||||
# ran us — no ambiguous PATH lookup for "python".
|
||||
if args[0].endswith(".py"):
|
||||
args = [sys.executable, *args]
|
||||
result = subprocess.run(args, check=False, close_fds=False)
|
||||
sys.exit(result.returncode)
|
||||
else:
|
||||
print(
|
||||
|
||||
Reference in New Issue
Block a user