Compare commits

..

1 Commits

Author SHA1 Message Date
Robert Resch 5f5d74cfbd Remove requirements_test_all file (#171530) 2026-05-20 23:54:31 +02:00
11 changed files with 9 additions and 3195 deletions
-1
View File
@@ -19,7 +19,6 @@ machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true
requirements_test_all.txt linguist-generated=true
requirements_test_pre_commit.txt linguist-generated=true
script/hassfest/docker/Dockerfile linguist-generated=true
.github/workflows/*.lock.yml linguist-generated=true
+3 -109
View File
@@ -161,10 +161,9 @@ Verify that the package's source repository is publicly reachable.
- Any other inconclusive result → ⚠️ with a one-line description.
If `repo_public` resolves to ❌ for a package, **also** mark that
package's `release_pipeline` and `async_blocking` cells/details as ``
(em dash) and explain `Skipped because the source repository is not
publicly accessible.` — neither check can be performed without a public
repo.
package's `release_pipeline` cell/detail as `` (em dash) and explain
`Skipped because the source repository is not publicly accessible.` —
because the release pipeline cannot be inspected without a public repo.
### Check kind: `pr_link`
@@ -240,111 +239,6 @@ host from `package.repo_url`, then apply the corresponding checklist.
3. If no CI config can be retrieved: ⚠️ `Release pipeline could not be
inspected; hosting provider is not GitHub or GitLab.`
### Check kind: `async_blocking`
Verify whether the dependency performs blocking I/O inside async code
paths. Home Assistant runs on a single asyncio event loop, so a library
that exposes an `async` surface must not call blocking APIs from inside
its `async def` functions — that stalls the whole loop. A purely sync
library is fine: Home Assistant integrations are expected to wrap such
calls in an executor.
**Two modes — pick by inspecting `package.old_version`:**
- `old_version` is `null` → **new package**: review the *entire current
source tree*. Nothing about this dependency has been vetted before.
- `old_version` is a string → **version bump**: review only the *diff
between `old_version` and `new_version`*. The previous version was
already accepted, so blocking calls that were present in
`old_version` are not regressions; report only what `new_version`
introduces.
#### Step 1 — Decide whether the library exposes an async surface
Use the `github` MCP tool (for `github.com` repos) or `web-fetch`
(other hosts) on `package.repo_url`. Always inspect the tag /
ref matching `new_version` (e.g. `v{new_version}` or `{new_version}`).
- Locate the top-level package directory (usually named after the
import name, often equal or close to `package.name`).
- Check `pyproject.toml` / `setup.py` / `setup.cfg` / `README*` for
async indicators (`Framework :: AsyncIO` trove classifier, `asyncio`
/ `aiohttp` / `httpx` / `anyio` in dependencies, an async usage
example in the README).
- Grep the package source for `async def`. A handful of `async def`
entries in the public modules is enough to treat the library as
having an async surface.
If the library is **sync-only** (no `async def` in its public modules
and no async framework dependency) → ✅
`Sync-only library; Home Assistant integrations must wrap calls in an
executor.` *This verdict is the same in both modes.*
#### Step 2a — Mode: new package (`old_version` is `null`)
Inspect **every `async def` in the public modules** for blocking
patterns. Walk transitively into helpers the async functions call.
#### Step 2b — Mode: version bump (`old_version` is a string)
Fetch the diff between the two tags and review **only changed lines**:
- GitHub: `GET /repos/{owner}/{repo}/compare/{old_tag}...{new_tag}` via
the `github` MCP tool, or
`https://github.com/{owner}/{repo}/compare/{old_tag}...{new_tag}.diff`
via `web-fetch`. Try the common tag formats in order until one
resolves: `v{version}`, `{version}`, `release-{version}`.
- GitLab: `https://gitlab.com/{namespace}/{project}/-/compare/{old_tag}...{new_tag}.diff`.
- Other hosts: use the project's equivalent compare URL via
`web-fetch`.
If neither tag format resolves on the host, fall back to a full review
(Step 2a) and mention in the detail that the diff was unavailable.
When reviewing the diff, only flag blocking patterns that appear in
**added lines** *inside or reachable from* an `async def`. A blocking
call that existed in `old_version` and is unchanged is not a regression
for this bump.
#### Step 3 — Blocking patterns to look for
In both modes, the patterns to flag inside `async def` bodies are:
- Sync HTTP: `requests.`, `urllib.request`, `urllib3.` direct use,
`http.client.`, sync `httpx.Client(` / `httpx.get(` (NOT the
`AsyncClient`), `pycurl`.
- `time.sleep(` (must be `await asyncio.sleep(`).
- Sync sockets: bare `socket.socket` reads/writes, `ssl.wrap_socket`,
blocking `select.select`.
- File I/O: `open(` / `pathlib.Path.read_*` / `.write_*` for
non-trivial sizes (small one-shot reads during import are
acceptable; reads/writes on the request path are not — prefer
`aiofiles` / executor).
- Sync DB drivers used directly: `sqlite3`, `psycopg2`, `pymysql`,
`pymongo` (sync client), `redis.Redis` (sync client).
- `subprocess.run` / `subprocess.call` / `os.system` (must be
`asyncio.create_subprocess_*`).
A call that is clearly dispatched to an executor
(`run_in_executor`, `asyncio.to_thread`, `anyio.to_thread.run_sync`)
does NOT count as blocking.
#### Step 4 — Verdict
- ✅ — no offending blocking pattern in the surface being reviewed
(whole tree for a new package, added lines for a bump). For a bump,
phrase the detail as `No new blocking calls introduced in
{old_version} → {new_version}.`.
- ⚠️ — blocking calls exist only in sync helpers that the async API
does not call, or only on a clearly non-hot path (e.g. one-shot
setup before the event loop is running). Cite at least one
`<file>:<line>` and explain why it is not on the hot path.
- ❌ — a blocking call is reachable from an `async def` that is part
of the public API on the request / polling path (for a bump: the
call was introduced or moved onto the hot path by this version).
Cite the offending `<file>:<line>` as a clickable link on the repo
host so the contributor can jump to it.
## Notes
- Be constructive and helpful. Reference the inspected workflow / CI
+3 -3
View File
@@ -132,7 +132,7 @@
"problemMatcher": []
},
{
"label": "Install all Requirements",
"label": "Install all production Requirements",
"type": "shell",
"command": "uv pip install -r requirements_all.txt",
"group": {
@@ -146,9 +146,9 @@
"problemMatcher": []
},
{
"label": "Install all Test Requirements",
"label": "Install all (test & production) Requirements",
"type": "shell",
"command": "uv pip install -r requirements.txt -r requirements_test_all.txt",
"command": "uv pip install -r requirements_all.txt -r requirements_test.txt",
"group": {
"kind": "build",
"isDefault": true
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -9,7 +9,8 @@ cd "$(realpath "$(dirname "$0")/..")"
echo "Installing development dependencies..."
uv pip install \
-e . \
-r requirements_test_all.txt \
-r requirements_all.txt \
-r requirements_test.txt \
colorlog \
--upgrade \
--config-settings editable_mode=compat
-1
View File
@@ -26,7 +26,6 @@ class CheckKind(StrEnum):
CI_UPLOAD = "ci_upload"
RELEASE_PIPELINE = "release_pipeline"
PR_LINK = "pr_link"
ASYNC_BLOCKING = "async_blocking"
@dataclass(slots=True)
-1
View File
@@ -20,7 +20,6 @@ _CHECK_DISPLAY: tuple[tuple[CheckKind, str], ...] = (
(CheckKind.CI_UPLOAD, "CI Upload"),
(CheckKind.RELEASE_PIPELINE, "Release Pipeline"),
(CheckKind.PR_LINK, "PR Link"),
(CheckKind.ASYNC_BLOCKING, "Async Safe"),
)
_ICONS: dict[CheckStatus, str] = {
-19
View File
@@ -10,9 +10,6 @@ What the runner defers to the LLM (NEEDS_AGENT):
- `pr_link`: presence of the right link in the PR description.
- `release_pipeline`: inspection of the publish workflow when the attestation
was missing or did not identify a recognised CI publisher.
- `async_blocking`: inspection of the dependency source for blocking I/O
inside `async def` functions. Always deferred when the source repo is
available — the deterministic stage cannot read the upstream source.
"""
from .diff import parse_diff
@@ -77,7 +74,6 @@ def run_checks(
)
pkg.checks[CheckKind.REPO_PUBLIC] = fail
pkg.checks[CheckKind.PR_LINK] = fail
pkg.checks[CheckKind.ASYNC_BLOCKING] = fail
elif pkg.repo_url:
pkg.checks[CheckKind.REPO_PUBLIC] = CheckResult(
CheckStatus.NEEDS_AGENT,
@@ -87,20 +83,6 @@ def run_checks(
CheckStatus.NEEDS_AGENT,
"Presence of the required link in the PR description must be verified by the agent.",
)
if pkg.old_version is None:
async_reason = (
"New dependency: agent must review the entire source tree "
"at the new version for blocking I/O inside async functions."
)
else:
async_reason = (
f"Version bump {pkg.old_version}{pkg.new_version}: "
"agent must review only the diff for newly introduced "
"blocking I/O inside async functions."
)
pkg.checks[CheckKind.ASYNC_BLOCKING] = CheckResult(
CheckStatus.NEEDS_AGENT, async_reason
)
else:
fail = CheckResult(
CheckStatus.FAIL,
@@ -108,7 +90,6 @@ def run_checks(
)
pkg.checks[CheckKind.REPO_PUBLIC] = fail
pkg.checks[CheckKind.PR_LINK] = fail
pkg.checks[CheckKind.ASYNC_BLOCKING] = fail
result = CheckRunResult(pr_number=pr_number, packages=packages)
result.rendered_comment = render_comment(result)
return result
-40
View File
@@ -262,19 +262,6 @@ IGNORE_PRE_COMMIT_HOOK_ID = (
PACKAGE_REGEX = re.compile(r"^(?:--.+\s)?([-_\.\w\d]+).*==.+$")
def has_tests(module: str) -> bool:
"""Test if a module has tests.
Module format: homeassistant.components.hue
Test if exists: tests/components/hue/__init__.py
"""
path = (
Path(module.replace(".", "/").replace("homeassistant", "tests", 1))
/ "__init__.py"
)
return path.exists()
def explore_module(package: str, explore_children: bool) -> list[str]:
"""Explore the modules."""
module = importlib.import_module(package)
@@ -511,31 +498,6 @@ def requirements_all_action_output(reqs: dict[str, list[str]], action: str) -> s
return "".join(output)
def requirements_test_all_output(reqs: dict[str, list[str]]) -> str:
"""Generate output for test_requirements."""
output = [
"# Home Assistant tests, full dependency set\n",
GENERATED_MESSAGE,
"-r requirements_test.txt\n",
]
filtered = {
requirement: modules
for requirement, modules in reqs.items()
if any(
# Always install requirements that are not part of integrations
not mdl.startswith("homeassistant.components.")
or
# Install tests for integrations that have tests
has_tests(mdl)
for mdl in modules
)
}
output.append(generate_requirements_list(filtered))
return "".join(output)
def requirements_pre_commit_output() -> str:
"""Generate output for pre-commit dependencies."""
source = ".pre-commit-config.yaml"
@@ -609,7 +571,6 @@ def main(validate: bool, ci: bool) -> int:
action: requirements_all_action_output(data, action)
for action in OVERRIDDEN_REQUIREMENTS_ACTIONS
}
reqs_test_all_file = requirements_test_all_output(data)
# Always calling requirements_pre_commit_output is intentional to ensure
# the code is called by the pre-commit hooks.
reqs_pre_commit_file = requirements_pre_commit_output()
@@ -619,7 +580,6 @@ def main(validate: bool, ci: bool) -> int:
("requirements.txt", reqs_file),
("requirements_all.txt", reqs_all_file),
("requirements_test_pre_commit.txt", reqs_pre_commit_file),
("requirements_test_all.txt", reqs_test_all_file),
("homeassistant/package_constraints.txt", constraints),
]
if ci:
@@ -26,7 +26,6 @@ def test_render_all_conclusive_collapses_details() -> None:
CheckKind.CI_UPLOAD: _pass("attestation found"),
CheckKind.RELEASE_PIPELINE: _pass("OIDC via attestation"),
CheckKind.PR_LINK: _pass("link found"),
CheckKind.ASYNC_BLOCKING: _pass("no blocking calls in async"),
},
)
result = CheckRunResult(pr_number=1, packages=[pkg])
@@ -50,7 +49,6 @@ def test_render_needs_agent_emits_generic_placeholders() -> None:
CheckKind.CI_UPLOAD: CheckResult(CheckStatus.WARN, "no attestation"),
CheckKind.RELEASE_PIPELINE: CheckResult(CheckStatus.NEEDS_AGENT, ""),
CheckKind.PR_LINK: CheckResult(CheckStatus.NEEDS_AGENT, ""),
CheckKind.ASYNC_BLOCKING: CheckResult(CheckStatus.NEEDS_AGENT, ""),
},
)
rendered = render_comment(CheckRunResult(pr_number=1, packages=[pkg]))
@@ -59,8 +57,6 @@ def test_render_needs_agent_emits_generic_placeholders() -> None:
assert "{{CHECK_CELL:pkg:release_pipeline}}" in rendered
assert "{{CHECK_DETAIL:pkg:release_pipeline}}" in rendered
assert "{{CHECK_CELL:pkg:pr_link}}" in rendered
assert "{{CHECK_CELL:pkg:async_blocking}}" in rendered
assert "{{CHECK_DETAIL:pkg:async_blocking}}" in rendered
assert "<details open>" in rendered
@@ -55,7 +55,6 @@ def test_runner_attestation_recognised(monkeypatch: pytest.MonkeyPatch) -> None:
assert pkg.checks[CheckKind.RELEASE_PIPELINE].status == CheckStatus.PASS
assert pkg.checks[CheckKind.REPO_PUBLIC].status == CheckStatus.NEEDS_AGENT
assert pkg.checks[CheckKind.PR_LINK].status == CheckStatus.NEEDS_AGENT
assert pkg.checks[CheckKind.ASYNC_BLOCKING].status == CheckStatus.NEEDS_AGENT
assert result.needs_agent is True
@@ -155,10 +154,9 @@ def test_runner_marks_missing_version_as_fail(
pkg = result.packages[0]
assert pkg.checks[CheckKind.CI_UPLOAD].status == CheckStatus.FAIL
assert pkg.checks[CheckKind.RELEASE_PIPELINE].status == CheckStatus.FAIL
# No repo URL → repo_public, pr_link and async_blocking short-circuit to FAIL
# No repo URL → repo_public and pr_link short-circuit to FAIL, not NEEDS_AGENT
assert pkg.checks[CheckKind.REPO_PUBLIC].status == CheckStatus.FAIL
assert pkg.checks[CheckKind.PR_LINK].status == CheckStatus.FAIL
assert pkg.checks[CheckKind.ASYNC_BLOCKING].status == CheckStatus.FAIL
assert result.needs_agent is False
@@ -193,81 +191,9 @@ def test_runner_pypi_found_but_no_repo_url_fails_repo_checks(
pkg = result.packages[0]
assert pkg.checks[CheckKind.REPO_PUBLIC].status == CheckStatus.FAIL
assert pkg.checks[CheckKind.PR_LINK].status == CheckStatus.FAIL
assert pkg.checks[CheckKind.ASYNC_BLOCKING].status == CheckStatus.FAIL
assert "does not advertise" in pkg.checks[CheckKind.REPO_PUBLIC].details
def test_runner_async_blocking_new_package_full_review(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A newly added package asks the agent for a full-tree async review."""
_patch_pypi(
monkeypatch,
PypiPackageInfo(
project_urls={"Source": "https://github.com/x/y"},
repo_url="https://github.com/x/y",
file_provenance_urls=["whatever"],
found=True,
),
ProvenanceResult(
has_attestation=True,
publisher_kind="GitHub",
recognized_publisher=True,
detail="ok",
),
)
diff = (
"diff --git a/requirements_all.txt b/requirements_all.txt\n"
"--- a/requirements_all.txt\n"
"+++ b/requirements_all.txt\n"
"@@ -0,0 +1 @@\n"
"+pkg==1.0.0\n"
)
result = run_checks(pr_number=1, diff_text=diff)
pkg = result.packages[0]
assert pkg.old_version is None
detail = pkg.checks[CheckKind.ASYNC_BLOCKING].details
assert pkg.checks[CheckKind.ASYNC_BLOCKING].status == CheckStatus.NEEDS_AGENT
assert "New dependency" in detail
assert "entire source tree" in detail
def test_runner_async_blocking_version_bump_diff_only(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A version bump asks the agent to review only the diff for new blocking calls."""
_patch_pypi(
monkeypatch,
PypiPackageInfo(
project_urls={"Source": "https://github.com/x/y"},
repo_url="https://github.com/x/y",
file_provenance_urls=["whatever"],
found=True,
),
ProvenanceResult(
has_attestation=True,
publisher_kind="GitHub",
recognized_publisher=True,
detail="ok",
),
)
diff = (
"diff --git a/requirements_all.txt b/requirements_all.txt\n"
"--- a/requirements_all.txt\n"
"+++ b/requirements_all.txt\n"
"@@ -1 +1 @@\n"
"-pkg==1.0.0\n"
"+pkg==1.1.0\n"
)
result = run_checks(pr_number=1, diff_text=diff)
pkg = result.packages[0]
assert pkg.old_version == "1.0.0"
detail = pkg.checks[CheckKind.ASYNC_BLOCKING].details
assert pkg.checks[CheckKind.ASYNC_BLOCKING].status == CheckStatus.NEEDS_AGENT
assert "1.0.0" in detail and "1.1.0" in detail
assert "diff" in detail
def test_runner_serialises_to_json(monkeypatch: pytest.MonkeyPatch) -> None:
"""The artifact contract: `to_dict()` is JSON-serialisable with expected keys."""
_patch_pypi(