This is an automated email from the ASF dual-hosted git repository.

kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 12127b07978 Fix check-conn-id-templated crashing on a vanishing 
__pycache__ (#74016)
12127b07978 is described below

commit 12127b079788b5f294ababc508ece35e037dd6da
Author: Kaxil Naik <[email protected]>
AuthorDate: Thu Oct 1 10:53:37 2026 +0100

    Fix check-conn-id-templated crashing on a vanishing __pycache__ (#74016)
    
    The hook walked the providers tree with Path.rglob, which on Python 3.10
    raises FileNotFoundError when a directory is removed after rglob lists it 
but
    before it reads it. Other processes create and remove __pycache__ 
directories
    under providers/ while static checks run, so the hook failed intermittently.
    Walk with os.walk, which skips a directory that has gone, and prune
    __pycache__ and tests directories instead of filtering paths afterwards.
---
 scripts/ci/prek/check_conn_id_templated.py         | 18 ++++++++++-----
 .../tests/ci/prek/test_check_conn_id_templated.py  | 26 +++++++++++++++++++++-
 2 files changed, 38 insertions(+), 6 deletions(-)

diff --git a/scripts/ci/prek/check_conn_id_templated.py 
b/scripts/ci/prek/check_conn_id_templated.py
index 9e242dc3954..b011f0c9c26 100755
--- a/scripts/ci/prek/check_conn_id_templated.py
+++ b/scripts/ci/prek/check_conn_id_templated.py
@@ -39,6 +39,7 @@ or it is never stored under ``self.<argument>`` (there is 
nothing for the render
 from __future__ import annotations
 
 import ast
+import os
 import sys
 from dataclasses import dataclass, field
 from pathlib import Path
@@ -89,12 +90,19 @@ class ClassInfo:
 
 
 def iter_source_files(roots: list[Path]):
-    """Yield source (non-test) Python files under the roots."""
+    """
+    Yield source (non-test) Python files under the roots.
+
+    ``os.walk`` rather than ``Path.rglob``: other processes create and remove 
``__pycache__``
+    directories under the providers tree while this walks it, and ``rglob`` 
raises
+    ``FileNotFoundError`` on a directory that disappears mid-walk, where 
``os.walk`` skips it.
+    """
     for root in roots:
-        for path in root.rglob("*.py"):
-            parts = path.relative_to(root).parts
-            if "tests" not in parts:
-                yield path
+        for dirpath, dirnames, filenames in os.walk(root):
+            dirnames[:] = [name for name in dirnames if name not in 
("__pycache__", "tests")]
+            for filename in filenames:
+                if filename.endswith(".py"):
+                    yield Path(dirpath, filename)
 
 
 def parse_classes(path: Path) -> list[ClassInfo]:
diff --git a/scripts/tests/ci/prek/test_check_conn_id_templated.py 
b/scripts/tests/ci/prek/test_check_conn_id_templated.py
index 3272b5d5e08..1a0bf767271 100644
--- a/scripts/tests/ci/prek/test_check_conn_id_templated.py
+++ b/scripts/tests/ci/prek/test_check_conn_id_templated.py
@@ -16,10 +16,11 @@
 # under the License.
 from __future__ import annotations
 
+import shutil
 import textwrap
 
 import pytest
-from check_conn_id_templated import build_index, check_file, main
+from check_conn_id_templated import build_index, check_file, 
iter_source_files, main
 
 BASE = """
     class BaseOperator:
@@ -223,6 +224,29 @@ def test_missing_conn_ids_are_reported(check, code, 
expected):
     assert errors[0].endswith(f"{expected} missing from template_fields")
 
 
+def test_iter_source_files_skips_tests_and_bytecode_directories(tmp_path):
+    for relative in ("pkg/source.py", "pkg/tests/test_source.py", 
"pkg/__pycache__/stray.py"):
+        (tmp_path / relative).parent.mkdir(parents=True, exist_ok=True)
+        (tmp_path / relative).write_text("")
+
+    assert list(iter_source_files([tmp_path])) == [tmp_path / "pkg" / 
"source.py"]
+
+
+def test_iter_source_files_survives_a_directory_removed_mid_walk(tmp_path):
+    """Another process can remove a __pycache__ directory after the walk has 
listed it but before it reads it."""
+    for name in ("a", "b"):
+        (tmp_path / name).mkdir()
+        (tmp_path / name / f"{name}.py").write_text("")
+
+    found = []
+    for path in iter_source_files([tmp_path]):
+        found.append(path)
+        # Both siblings have been listed by now; remove the one not read yet.
+        shutil.rmtree(tmp_path / ("b" if path.parent.name == "a" else "a"), 
ignore_errors=True)
+
+    assert len(found) == 1
+
+
 def test_main_skips_files_without_conn_id(tmp_path):
     path = tmp_path / "code.py"
     path.write_text("class MyOperator:\n    pass\n")

Reply via email to