This is an automated email from the ASF dual-hosted git repository. ephraimanierobi pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 449e634fd6b6d8e637ce194c014bb2e5c643813f Author: Wei Lee <[email protected]> AuthorDate: Thu Dec 11 22:54:38 2025 +0800 [v3-1-test] fix airflowignore negation does not work in subfolders (#58740) (#59305) Co-authored-by: Henry Chen <[email protected]> --- airflow-core/src/airflow/utils/file.py | 6 +- .../tests/unit/plugins/test_plugin_ignore.py | 11 ++-- airflow-core/tests/unit/utils/test_file.py | 65 ++++++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/utils/file.py b/airflow-core/src/airflow/utils/file.py index d546c0ac352..76cdf846fe0 100644 --- a/airflow-core/src/airflow/utils/file.py +++ b/airflow-core/src/airflow/utils/file.py @@ -111,7 +111,11 @@ class _GlobIgnoreRule(NamedTuple): for rule in rules: if not isinstance(rule, _GlobIgnoreRule): raise ValueError(f"_GlobIgnoreRule cannot match rules of type: {type(rule)}") - rel_path = str(path.relative_to(rule.relative_to) if rule.relative_to else path.name) + rel_obj = path.relative_to(rule.relative_to) if rule.relative_to else Path(path.name) + if path.is_dir(): + rel_path = f"{rel_obj.as_posix()}/" + else: + rel_path = rel_obj.as_posix() if ( rule.wild_match_pattern.include is not None and rule.wild_match_pattern.match_file(rel_path) is not None diff --git a/airflow-core/tests/unit/plugins/test_plugin_ignore.py b/airflow-core/tests/unit/plugins/test_plugin_ignore.py index 1eeb9a559bb..24d6f3b44e9 100644 --- a/airflow-core/tests/unit/plugins/test_plugin_ignore.py +++ b/airflow-core/tests/unit/plugins/test_plugin_ignore.py @@ -94,20 +94,21 @@ class TestIgnorePluginFile: should_ignore_files = { "test_notload.py", "test_notload_sub.py", - "test_noneload_sub1.py", + "subdir1/test_noneload_sub1.py", + "subdir2/test_shouldignore.py", + "subdir3/test_notload_sub3.py", } should_not_ignore_files = { "test_load.py", - "test_load_sub1.py", - "test_shouldignore.py", # moved to here because it should not ignore, as we do not ignore all - # things from subdir 2 + "subdir1/test_load_sub1.py", } ignore_list_file = ".airflowignore_glob" print("-" * 20) for file_path in find_path_from_directory(plugin_folder_path, ignore_list_file, "glob"): file_path = Path(file_path) if file_path.is_file() and file_path.suffix == ".py": - detected_files.add(file_path.name) + rel_path = file_path.relative_to(plugin_folder_path).as_posix() + detected_files.add(rel_path) print(file_path) print("-" * 20) diff --git a/airflow-core/tests/unit/utils/test_file.py b/airflow-core/tests/unit/utils/test_file.py index ca6c7e534b9..4eac48cb3d3 100644 --- a/airflow-core/tests/unit/utils/test_file.py +++ b/airflow-core/tests/unit/utils/test_file.py @@ -189,6 +189,71 @@ class TestListPyFilesPath: assert file_utils.might_contain_dag(file_path=file_path_with_dag, safe_mode=True) + def test_airflowignore_negation_unignore_subfolder_file_glob(self, tmp_path): + """Ensure negation rules can unignore a subfolder and a file inside it when using glob syntax. + + Patterns: + * -> ignore everything + !subfolder/ -> unignore the subfolder (must match directory rule) + !subfolder/keep.py -> unignore a specific file inside the subfolder + """ + dags_root = tmp_path / "dags" + (dags_root / "subfolder").mkdir(parents=True) + # files + (dags_root / "drop.py").write_text("raise Exception('ignored')\n") + (dags_root / "subfolder" / "keep.py").write_text("# should be discovered\n") + (dags_root / "subfolder" / "drop.py").write_text("raise Exception('ignored')\n") + + (dags_root / ".airflowignore").write_text( + "\n".join( + [ + "*", + "!subfolder/", + "!subfolder/keep.py", + ] + ) + ) + + detected = set() + for raw in find_path_from_directory(dags_root, ".airflowignore", "glob"): + p = Path(raw) + if p.is_file() and p.suffix == ".py": + detected.add(p.relative_to(dags_root).as_posix()) + + assert detected == {"subfolder/keep.py"} + + def test_airflowignore_negation_nested_with_globstar(self, tmp_path): + """Negation with ** should work for nested subfolders.""" + dags_root = tmp_path / "dags" + nested = dags_root / "a" / "b" / "subfolder" + nested.mkdir(parents=True) + + # files + (dags_root / "ignore_top.py").write_text("raise Exception('ignored')\n") + (nested / "keep.py").write_text("# should be discovered\n") + (nested / "drop.py").write_text("raise Exception('ignored')\n") + + (dags_root / ".airflowignore").write_text( + "\n".join( + [ + "*", + "!a/", + "!a/b/", + "!**/subfolder/", + "!**/subfolder/keep.py", + "drop.py", + ] + ) + ) + + detected = set() + for raw in find_path_from_directory(dags_root, ".airflowignore", "glob"): + p = Path(raw) + if p.is_file() and p.suffix == ".py": + detected.add(p.relative_to(dags_root).as_posix()) + + assert detected == {"a/b/subfolder/keep.py"} + @conf_vars({("core", "might_contain_dag_callable"): "unit.utils.test_file.might_contain_dag"}) def test_might_contain_dag(self): """Test might_contain_dag_callable"""
