jason810496 commented on code in PR #72369:
URL: https://github.com/apache/airflow/pull/72369#discussion_r3987113769


##########
task-sdk/src/airflow/sdk/importers/base.py:
##########
@@ -0,0 +1,477 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Abstract base class for DAG importers."""
+
+from __future__ import annotations
+
+import contextlib
+import functools
+import logging
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk._shared.module_loading.file_discovery import 
find_path_from_directory
+from airflow.sdk.configuration import conf
+from airflow.sdk.exceptions import AirflowConfigException
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterable, Iterator
+
+    from typing_extensions import Self
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+    from airflow.sdk import DAG
+
+log = logging.getLogger(__name__)
+
+
+class DagDefinition(ABC):
+    """Abstract base class for a DAG source definition."""
+
+    @property
+    @abstractmethod
+    def freshness_token(self) -> str:
+        """Opaque, generalized token representing the current state of the 
source."""
+
+    @abstractmethod
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        """Get relative location of the definition to a root directory."""
+
+    @abstractmethod
+    def read_bytes(self) -> bytes:
+        """Read and return the content of the resource as bytes."""
+
+    def read_text(self, encoding: str = "utf-8") -> str:
+        """Read and return the content of the resource as a string."""
+        return self.read_bytes().decode(encoding)
+
+    @abstractmethod
+    def as_file(self) -> contextlib.AbstractContextManager[Path]:
+        """
+        Return a context manager yielding a Path pointing to a local file.
+
+        For file-backed resources, this is the actual file path.
+        For others, a temp file is created and cleaned up.
+        """
+
+    @abstractmethod
+    def __repr__(self) -> str:
+        """Return string representation used by import error and warning 
objects."""
+
+
+@dataclass
+class FileDagDefinition(DagDefinition):
+    """A DAG definition backed by a file on the local filesystem."""
+
+    path: Path
+
+    @property
+    def freshness_token(self) -> str:
+        try:
+            stat = self.path.stat()
+            return f"{stat.st_mtime_ns}-{stat.st_size}"
+        except OSError:
+            return ""
+
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        if root is None:
+            return str(self.path)
+        try:
+            return str(self.path.relative_to(root))
+        except ValueError:
+            return str(self.path)
+
+    def read_bytes(self) -> bytes:
+        return self.path.read_bytes()
+
+    @contextlib.contextmanager
+    def as_file(self) -> Generator[Path, None, None]:
+        yield self.path
+
+    def __repr__(self) -> str:
+        return str(self.path)
+
+
+@dataclass
+class DagImportError:
+    """Structured error information for DAG import failures."""
+
+    source_reference: str
+    message: str
+    error_type: str = "import"
+    line_number: int | None = None
+    column_number: int | None = None
+    context: str | None = None
+    suggestion: str | None = None
+    stacktrace: str | None = None
+
+    def format_message(self) -> str:
+        """Format the error as a human-readable string."""
+        parts = [f"Error in {self.source_reference}"]
+        if self.line_number is not None:
+            loc = f"line {self.line_number}"
+            if self.column_number is not None:
+                loc += f", column {self.column_number}"
+            parts.append(f"Location: {loc}")
+        parts.append(f"Error ({self.error_type}): {self.message}")
+        if self.context:
+            parts.append(f"Context:\n{self.context}")
+        if self.suggestion:
+            parts.append(f"Suggestion: {self.suggestion}")
+        return "\n".join(parts)

Review Comment:
   It seems we should format the error message in to same line instead of 
multiple line (e.g for the better log tracing purpose).



##########
shared/module_loading/src/airflow_shared/module_loading/dag_file.py:
##########
@@ -19,5 +19,78 @@
 
 from __future__ import annotations
 
+import hashlib
+import re
+import zipfile
+from collections.abc import Callable
+from pathlib import Path
+
 UNUSUAL_MODULE_PREFIX = "unusual_prefix_"
 MODIFIED_DAG_MODULE_NAME = 
f"{UNUSUAL_MODULE_PREFIX}{{path_hash}}_{{module_name}}"
+
+
+def get_unique_dag_module_name(file_path: str) -> str:
+    """Return a unique module name in the format unusual_prefix_{sha1 of 
module's file path}_{original module name}."""
+    if isinstance(file_path, str):
+        path_hash = hashlib.sha1(file_path.encode("utf-8"), 
usedforsecurity=False).hexdigest()
+        org_mod_name = re.sub(r"[.-]", "_", Path(file_path).stem)
+        return MODIFIED_DAG_MODULE_NAME.format(path_hash=path_hash, 
module_name=org_mod_name)
+    raise ValueError("file_path should be a string to generate unique module 
name")
+
+
+def might_contain_dag_via_default_heuristic(file_path: str, zip_file: 
zipfile.ZipFile | None = None) -> bool:
+    """
+    Heuristic that guesses whether a Python file contains an Airflow DAG 
definition.
+
+    :param file_path: Path to the file to be checked.
+    :param zip_file: if passed, checks the archive. Otherwise, check local 
filesystem.
+    :return: True, if file might contain DAGs.
+    """
+    if zip_file:
+        with zip_file.open(file_path) as current_file:
+            content = current_file.read()
+    else:
+        if zipfile.is_zipfile(file_path):
+            return True
+        with open(file_path, "rb") as dag_file:
+            content = dag_file.read()
+    content = content.lower()
+    if b"airflow" not in content:
+        return False
+    return any(s in content for s in (b"dag", b"asset"))
+
+
+def might_contain_dag(file_path: str, safe_mode: bool, zip_file: 
zipfile.ZipFile | None = None) -> bool:
+    """
+    Check whether a Python file contains Airflow DAGs.
+
+    When safe_mode is off (with False value), this function always returns 
True.
+
+    If might_contain_dag_callable isn't specified, it uses airflow default 
heuristic.
+    """
+    if not safe_mode:
+        return True
+
+    might_contain_dag_callable: Callable[[str, zipfile.ZipFile | None], bool] 
| None = None
+    try:
+        # Use importlib to avoid hard import at module level
+        import importlib
+
+        config_module = importlib.import_module("airflow.configuration")
+        conf = config_module.conf

Review Comment:
   I think we should make the dynamic import part module aware by the caller. 
e.g. if the caller is core, then the current implementation is correct. 
However, if the caller is task-sdk, we should use `airflow.sdk.configuration` 
instead.



##########
task-sdk/src/airflow/sdk/importers/base.py:
##########
@@ -0,0 +1,477 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Abstract base class for DAG importers."""
+
+from __future__ import annotations
+
+import contextlib
+import functools
+import logging
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk._shared.module_loading.file_discovery import 
find_path_from_directory
+from airflow.sdk.configuration import conf
+from airflow.sdk.exceptions import AirflowConfigException
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterable, Iterator
+
+    from typing_extensions import Self
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+    from airflow.sdk import DAG
+
+log = logging.getLogger(__name__)
+
+
+class DagDefinition(ABC):
+    """Abstract base class for a DAG source definition."""
+
+    @property
+    @abstractmethod
+    def freshness_token(self) -> str:
+        """Opaque, generalized token representing the current state of the 
source."""
+
+    @abstractmethod
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        """Get relative location of the definition to a root directory."""
+
+    @abstractmethod
+    def read_bytes(self) -> bytes:
+        """Read and return the content of the resource as bytes."""
+
+    def read_text(self, encoding: str = "utf-8") -> str:
+        """Read and return the content of the resource as a string."""
+        return self.read_bytes().decode(encoding)
+
+    @abstractmethod
+    def as_file(self) -> contextlib.AbstractContextManager[Path]:
+        """
+        Return a context manager yielding a Path pointing to a local file.
+
+        For file-backed resources, this is the actual file path.
+        For others, a temp file is created and cleaned up.
+        """
+
+    @abstractmethod
+    def __repr__(self) -> str:
+        """Return string representation used by import error and warning 
objects."""
+
+
+@dataclass
+class FileDagDefinition(DagDefinition):
+    """A DAG definition backed by a file on the local filesystem."""
+
+    path: Path
+
+    @property
+    def freshness_token(self) -> str:
+        try:
+            stat = self.path.stat()
+            return f"{stat.st_mtime_ns}-{stat.st_size}"
+        except OSError:
+            return ""
+
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        if root is None:
+            return str(self.path)
+        try:
+            return str(self.path.relative_to(root))
+        except ValueError:
+            return str(self.path)
+
+    def read_bytes(self) -> bytes:
+        return self.path.read_bytes()
+
+    @contextlib.contextmanager
+    def as_file(self) -> Generator[Path, None, None]:
+        yield self.path
+
+    def __repr__(self) -> str:
+        return str(self.path)
+
+
+@dataclass
+class DagImportError:
+    """Structured error information for DAG import failures."""
+
+    source_reference: str
+    message: str
+    error_type: str = "import"
+    line_number: int | None = None
+    column_number: int | None = None
+    context: str | None = None
+    suggestion: str | None = None
+    stacktrace: str | None = None
+
+    def format_message(self) -> str:
+        """Format the error as a human-readable string."""
+        parts = [f"Error in {self.source_reference}"]
+        if self.line_number is not None:
+            loc = f"line {self.line_number}"
+            if self.column_number is not None:
+                loc += f", column {self.column_number}"
+            parts.append(f"Location: {loc}")
+        parts.append(f"Error ({self.error_type}): {self.message}")
+        if self.context:
+            parts.append(f"Context:\n{self.context}")
+        if self.suggestion:
+            parts.append(f"Suggestion: {self.suggestion}")
+        return "\n".join(parts)
+
+
+@dataclass
+class DagImportWarning:
+    """Warning information for non-fatal issues during DAG import."""
+
+    source_reference: str
+    message: str
+    warning_type: str = "import"
+    line_number: int | None = None
+    context: dict[str, Any] | None = None
+
+
+@dataclass
+class DagImportResult:
+    """Result of importing DAGs from a definition."""
+
+    definition: DagDefinition | None = None
+    dags: list[DAG] = field(default_factory=list)
+    errors: list[DagImportError] = field(default_factory=list)
+    skipped_definitions: list[DagDefinition] = field(default_factory=list)
+    warnings: list[DagImportWarning] = field(default_factory=list)
+    dependencies: list[DagDefinition] = field(default_factory=list)
+
+    @property
+    def success(self) -> bool:
+        """Return True if no fatal errors occurred."""
+        return not self.errors
+
+
+@dataclass
+class DagSourceCode:
+    """Raw source code and its language identifier for a DAG definition."""
+
+    source_code: str
+    language: str
+
+
+def _normalize_extensions(extensions: Iterable[str]) -> list[str]:
+    """Normalize file extensions to lowercase with leading dot."""
+    return [ext.lower() if ext.startswith(".") else f".{ext.lower()}" for ext 
in extensions]
+
+
+def _get_importer_extensions(importer: Any) -> list[str]:

Review Comment:
   nit:
   
   ```suggestion
   def _get_importer_extensions(importer: AbstractDagImporter) -> list[str]:
   ```



##########
task-sdk/src/airflow/sdk/importers/base.py:
##########
@@ -0,0 +1,477 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Abstract base class for DAG importers."""
+
+from __future__ import annotations
+
+import contextlib
+import functools
+import logging
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk._shared.module_loading.file_discovery import 
find_path_from_directory
+from airflow.sdk.configuration import conf
+from airflow.sdk.exceptions import AirflowConfigException
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterable, Iterator
+
+    from typing_extensions import Self
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+    from airflow.sdk import DAG
+
+log = logging.getLogger(__name__)
+
+
+class DagDefinition(ABC):
+    """Abstract base class for a DAG source definition."""
+
+    @property
+    @abstractmethod
+    def freshness_token(self) -> str:
+        """Opaque, generalized token representing the current state of the 
source."""
+
+    @abstractmethod
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        """Get relative location of the definition to a root directory."""
+
+    @abstractmethod
+    def read_bytes(self) -> bytes:
+        """Read and return the content of the resource as bytes."""
+
+    def read_text(self, encoding: str = "utf-8") -> str:
+        """Read and return the content of the resource as a string."""
+        return self.read_bytes().decode(encoding)
+
+    @abstractmethod
+    def as_file(self) -> contextlib.AbstractContextManager[Path]:
+        """
+        Return a context manager yielding a Path pointing to a local file.
+
+        For file-backed resources, this is the actual file path.
+        For others, a temp file is created and cleaned up.
+        """
+
+    @abstractmethod
+    def __repr__(self) -> str:
+        """Return string representation used by import error and warning 
objects."""
+
+
+@dataclass
+class FileDagDefinition(DagDefinition):
+    """A DAG definition backed by a file on the local filesystem."""
+
+    path: Path
+
+    @property
+    def freshness_token(self) -> str:
+        try:
+            stat = self.path.stat()
+            return f"{stat.st_mtime_ns}-{stat.st_size}"
+        except OSError:
+            return ""
+
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        if root is None:
+            return str(self.path)
+        try:
+            return str(self.path.relative_to(root))
+        except ValueError:
+            return str(self.path)
+
+    def read_bytes(self) -> bytes:
+        return self.path.read_bytes()
+
+    @contextlib.contextmanager
+    def as_file(self) -> Generator[Path, None, None]:
+        yield self.path
+
+    def __repr__(self) -> str:
+        return str(self.path)
+
+
+@dataclass
+class DagImportError:
+    """Structured error information for DAG import failures."""
+
+    source_reference: str
+    message: str
+    error_type: str = "import"
+    line_number: int | None = None
+    column_number: int | None = None
+    context: str | None = None
+    suggestion: str | None = None
+    stacktrace: str | None = None
+
+    def format_message(self) -> str:
+        """Format the error as a human-readable string."""
+        parts = [f"Error in {self.source_reference}"]
+        if self.line_number is not None:
+            loc = f"line {self.line_number}"
+            if self.column_number is not None:
+                loc += f", column {self.column_number}"
+            parts.append(f"Location: {loc}")
+        parts.append(f"Error ({self.error_type}): {self.message}")
+        if self.context:
+            parts.append(f"Context:\n{self.context}")
+        if self.suggestion:
+            parts.append(f"Suggestion: {self.suggestion}")
+        return "\n".join(parts)
+
+
+@dataclass
+class DagImportWarning:
+    """Warning information for non-fatal issues during DAG import."""
+
+    source_reference: str
+    message: str
+    warning_type: str = "import"
+    line_number: int | None = None
+    context: dict[str, Any] | None = None
+
+
+@dataclass
+class DagImportResult:
+    """Result of importing DAGs from a definition."""
+
+    definition: DagDefinition | None = None
+    dags: list[DAG] = field(default_factory=list)
+    errors: list[DagImportError] = field(default_factory=list)
+    skipped_definitions: list[DagDefinition] = field(default_factory=list)
+    warnings: list[DagImportWarning] = field(default_factory=list)
+    dependencies: list[DagDefinition] = field(default_factory=list)
+
+    @property
+    def success(self) -> bool:
+        """Return True if no fatal errors occurred."""
+        return not self.errors
+
+
+@dataclass
+class DagSourceCode:
+    """Raw source code and its language identifier for a DAG definition."""
+
+    source_code: str
+    language: str
+
+
+def _normalize_extensions(extensions: Iterable[str]) -> list[str]:
+    """Normalize file extensions to lowercase with leading dot."""
+    return [ext.lower() if ext.startswith(".") else f".{ext.lower()}" for ext 
in extensions]
+
+
+def _get_importer_extensions(importer: Any) -> list[str]:
+    """Extract supported extensions from an importer via duck typing."""
+    exts = getattr(importer, "supported_extensions", None)
+    if callable(exts):
+        return _normalize_extensions(exts())
+    if exts is not None:
+        return _normalize_extensions(exts)
+    return []
+
+
+class AbstractDagImporter(ABC):
+    """Abstract base class for DAG importers."""
+
+    @abstractmethod
+    def can_handle(self, definition: DagDefinition | str | Path) -> bool:
+        """Check if this importer can handle the given definition."""
+
+    @abstractmethod
+    def list_dag_definitions(
+        self,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> Iterator[DagDefinition]:
+        """List DAG definitions in a bundle that this importer can handle."""
+
+    @abstractmethod
+    def import_definition(
+        self,
+        definition: DagDefinition,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> DagImportResult:
+        """Import DAGs from a DAG definition."""
+
+    @abstractmethod
+    def get_source_code(self, definition: DagDefinition) -> DagSourceCode:
+        """Retrieve the raw source code and its language identifier for the 
specified DAG definition."""
+
+
+def get_file_suffix(definition: DagDefinition | str | Path) -> str | None:
+    """Extract lowercase file suffix from a definition, path, or filename."""
+    path = (
+        definition
+        if isinstance(definition, (str, Path))
+        else getattr(definition, "path", getattr(definition, "file_path", 
None))
+    )
+    return Path(path).suffix.lower() if path else None
+
+
+def find_file_dag_definitions(
+    bundle_path: Path,
+    supported_extensions: Iterable[str],
+) -> Iterator[DagDefinition]:
+    """Find file DAG definitions in a bundle matching given extensions and 
respecting .airflowignore."""
+    ignore_file_syntax = conf.get_mandatory_value("core", 
"DAG_IGNORE_FILE_SYNTAX", fallback="glob")
+    supported_exts = _normalize_extensions(supported_extensions)
+
+    for file_path in find_path_from_directory(bundle_path, ".airflowignore", 
ignore_file_syntax):
+        path = Path(file_path)
+
+        if not path.is_file():
+            continue
+
+        if path.suffix.lower() not in supported_exts:
+            continue
+
+        yield FileDagDefinition(path=path)
+
+
+@dataclass(frozen=True)
+class _ImporterSpec:
+    """Declarative specification for a DAG importer."""
+
+    classpath: str
+    kwargs: dict[str, Any] = field(default_factory=dict)
+    extensions: list[str] | None = None
+    context: str = "importer configuration"
+
+
+def _parse_importer_specs(configs: Any, context: str) -> list[_ImporterSpec]:
+    if not isinstance(configs, list):
+        raise AirflowConfigException(
+            f"Invalid importer configuration for {context}: expected a list of 
dictionaries."
+        )
+    specs: list[_ImporterSpec] = []
+    for item in configs:
+        if not isinstance(item, dict):
+            raise AirflowConfigException(
+                f"Invalid importer configuration for {context}: each entry 
must be a dictionary."
+            )
+        classpath = item.get("classpath")
+        if not classpath:
+            raise AirflowConfigException(
+                f"Missing required 'classpath' in importer configuration for 
{context}."
+            )
+        kwargs = item.get("kwargs", {})
+        if not isinstance(kwargs, dict):
+            raise AirflowConfigException(
+                f"Field 'kwargs' must be a dictionary in importer 
configuration for {context}."
+            )
+        extensions = item.get("extensions")
+        if extensions is not None:
+            if not isinstance(extensions, list) or any(not isinstance(ext, 
str) for ext in extensions):
+                raise AirflowConfigException(
+                    f"Field 'extensions' must be a list of strings in importer 
configuration for {context}."
+                )
+            extensions = _normalize_extensions(extensions)
+        specs.append(
+            _ImporterSpec(
+                classpath=classpath,
+                kwargs=kwargs,
+                extensions=extensions,
+                context=context,
+            )
+        )
+    return specs
+
+
+class DagImporterRegistry:

Review Comment:
   Additionally, should we replace the `DagImporterRegistry` introduced in 
https://github.com/apache/airflow/commit/ec441299d3da962f48e66a74678c7a1455bb6baf
 with the current one?



##########
task-sdk/src/airflow/sdk/importers/base.py:
##########
@@ -0,0 +1,477 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Abstract base class for DAG importers."""
+
+from __future__ import annotations
+
+import contextlib
+import functools
+import logging
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk._shared.module_loading.file_discovery import 
find_path_from_directory
+from airflow.sdk.configuration import conf
+from airflow.sdk.exceptions import AirflowConfigException
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterable, Iterator
+
+    from typing_extensions import Self
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+    from airflow.sdk import DAG
+
+log = logging.getLogger(__name__)
+
+
+class DagDefinition(ABC):
+    """Abstract base class for a DAG source definition."""
+
+    @property
+    @abstractmethod
+    def freshness_token(self) -> str:
+        """Opaque, generalized token representing the current state of the 
source."""
+
+    @abstractmethod
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        """Get relative location of the definition to a root directory."""
+
+    @abstractmethod
+    def read_bytes(self) -> bytes:
+        """Read and return the content of the resource as bytes."""
+
+    def read_text(self, encoding: str = "utf-8") -> str:
+        """Read and return the content of the resource as a string."""
+        return self.read_bytes().decode(encoding)
+
+    @abstractmethod
+    def as_file(self) -> contextlib.AbstractContextManager[Path]:
+        """
+        Return a context manager yielding a Path pointing to a local file.
+
+        For file-backed resources, this is the actual file path.
+        For others, a temp file is created and cleaned up.
+        """
+
+    @abstractmethod
+    def __repr__(self) -> str:
+        """Return string representation used by import error and warning 
objects."""
+
+
+@dataclass
+class FileDagDefinition(DagDefinition):
+    """A DAG definition backed by a file on the local filesystem."""
+
+    path: Path
+
+    @property
+    def freshness_token(self) -> str:
+        try:
+            stat = self.path.stat()
+            return f"{stat.st_mtime_ns}-{stat.st_size}"
+        except OSError:
+            return ""
+
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        if root is None:
+            return str(self.path)
+        try:
+            return str(self.path.relative_to(root))
+        except ValueError:
+            return str(self.path)
+
+    def read_bytes(self) -> bytes:
+        return self.path.read_bytes()
+
+    @contextlib.contextmanager
+    def as_file(self) -> Generator[Path, None, None]:
+        yield self.path
+
+    def __repr__(self) -> str:
+        return str(self.path)
+
+
+@dataclass
+class DagImportError:
+    """Structured error information for DAG import failures."""
+
+    source_reference: str
+    message: str
+    error_type: str = "import"
+    line_number: int | None = None
+    column_number: int | None = None
+    context: str | None = None
+    suggestion: str | None = None
+    stacktrace: str | None = None
+
+    def format_message(self) -> str:
+        """Format the error as a human-readable string."""
+        parts = [f"Error in {self.source_reference}"]
+        if self.line_number is not None:
+            loc = f"line {self.line_number}"
+            if self.column_number is not None:
+                loc += f", column {self.column_number}"
+            parts.append(f"Location: {loc}")
+        parts.append(f"Error ({self.error_type}): {self.message}")
+        if self.context:
+            parts.append(f"Context:\n{self.context}")
+        if self.suggestion:
+            parts.append(f"Suggestion: {self.suggestion}")
+        return "\n".join(parts)
+
+
+@dataclass
+class DagImportWarning:
+    """Warning information for non-fatal issues during DAG import."""
+
+    source_reference: str
+    message: str
+    warning_type: str = "import"
+    line_number: int | None = None
+    context: dict[str, Any] | None = None
+
+
+@dataclass
+class DagImportResult:
+    """Result of importing DAGs from a definition."""
+
+    definition: DagDefinition | None = None
+    dags: list[DAG] = field(default_factory=list)
+    errors: list[DagImportError] = field(default_factory=list)
+    skipped_definitions: list[DagDefinition] = field(default_factory=list)
+    warnings: list[DagImportWarning] = field(default_factory=list)
+    dependencies: list[DagDefinition] = field(default_factory=list)
+
+    @property
+    def success(self) -> bool:
+        """Return True if no fatal errors occurred."""
+        return not self.errors
+
+
+@dataclass
+class DagSourceCode:
+    """Raw source code and its language identifier for a DAG definition."""
+
+    source_code: str
+    language: str
+
+
+def _normalize_extensions(extensions: Iterable[str]) -> list[str]:
+    """Normalize file extensions to lowercase with leading dot."""
+    return [ext.lower() if ext.startswith(".") else f".{ext.lower()}" for ext 
in extensions]
+
+
+def _get_importer_extensions(importer: Any) -> list[str]:
+    """Extract supported extensions from an importer via duck typing."""
+    exts = getattr(importer, "supported_extensions", None)
+    if callable(exts):
+        return _normalize_extensions(exts())
+    if exts is not None:
+        return _normalize_extensions(exts)
+    return []
+
+
+class AbstractDagImporter(ABC):
+    """Abstract base class for DAG importers."""
+
+    @abstractmethod
+    def can_handle(self, definition: DagDefinition | str | Path) -> bool:
+        """Check if this importer can handle the given definition."""
+
+    @abstractmethod
+    def list_dag_definitions(
+        self,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> Iterator[DagDefinition]:
+        """List DAG definitions in a bundle that this importer can handle."""
+
+    @abstractmethod
+    def import_definition(
+        self,
+        definition: DagDefinition,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> DagImportResult:
+        """Import DAGs from a DAG definition."""
+
+    @abstractmethod
+    def get_source_code(self, definition: DagDefinition) -> DagSourceCode:
+        """Retrieve the raw source code and its language identifier for the 
specified DAG definition."""
+
+
+def get_file_suffix(definition: DagDefinition | str | Path) -> str | None:
+    """Extract lowercase file suffix from a definition, path, or filename."""
+    path = (
+        definition
+        if isinstance(definition, (str, Path))
+        else getattr(definition, "path", getattr(definition, "file_path", 
None))
+    )
+    return Path(path).suffix.lower() if path else None
+
+
+def find_file_dag_definitions(
+    bundle_path: Path,
+    supported_extensions: Iterable[str],
+) -> Iterator[DagDefinition]:
+    """Find file DAG definitions in a bundle matching given extensions and 
respecting .airflowignore."""
+    ignore_file_syntax = conf.get_mandatory_value("core", 
"DAG_IGNORE_FILE_SYNTAX", fallback="glob")
+    supported_exts = _normalize_extensions(supported_extensions)
+
+    for file_path in find_path_from_directory(bundle_path, ".airflowignore", 
ignore_file_syntax):
+        path = Path(file_path)
+
+        if not path.is_file():
+            continue
+
+        if path.suffix.lower() not in supported_exts:
+            continue
+
+        yield FileDagDefinition(path=path)
+
+
+@dataclass(frozen=True)
+class _ImporterSpec:
+    """Declarative specification for a DAG importer."""
+
+    classpath: str
+    kwargs: dict[str, Any] = field(default_factory=dict)
+    extensions: list[str] | None = None
+    context: str = "importer configuration"
+
+
+def _parse_importer_specs(configs: Any, context: str) -> list[_ImporterSpec]:
+    if not isinstance(configs, list):
+        raise AirflowConfigException(
+            f"Invalid importer configuration for {context}: expected a list of 
dictionaries."
+        )
+    specs: list[_ImporterSpec] = []
+    for item in configs:
+        if not isinstance(item, dict):
+            raise AirflowConfigException(
+                f"Invalid importer configuration for {context}: each entry 
must be a dictionary."
+            )
+        classpath = item.get("classpath")
+        if not classpath:
+            raise AirflowConfigException(
+                f"Missing required 'classpath' in importer configuration for 
{context}."
+            )
+        kwargs = item.get("kwargs", {})
+        if not isinstance(kwargs, dict):
+            raise AirflowConfigException(
+                f"Field 'kwargs' must be a dictionary in importer 
configuration for {context}."
+            )
+        extensions = item.get("extensions")
+        if extensions is not None:
+            if not isinstance(extensions, list) or any(not isinstance(ext, 
str) for ext in extensions):
+                raise AirflowConfigException(
+                    f"Field 'extensions' must be a list of strings in importer 
configuration for {context}."
+                )
+            extensions = _normalize_extensions(extensions)
+        specs.append(
+            _ImporterSpec(
+                classpath=classpath,
+                kwargs=kwargs,
+                extensions=extensions,
+                context=context,
+            )
+        )
+    return specs
+
+
+class DagImporterRegistry:
+    """
+    Registry for DAG importers. Manages importers by file extension and 
generic definition.
+
+    Each file extension can only be handled by one importer at a time. If 
multiple
+    importers claim the same extension, the last registered one wins and a 
warning
+    is logged. The built-in PythonDagImporter handles .py and ZipImporter 
handles .zip files.
+    """
+
+    _extension_importers: dict[str, AbstractDagImporter]
+    _extension_specs: dict[str, _ImporterSpec]
+    _ordered_importers: list[AbstractDagImporter]
+
+    def __init__(self, register_defaults: bool = True) -> None:
+        self._extension_importers = {}
+        self._extension_specs = {}
+        self._ordered_importers = []
+        if register_defaults:
+            self._register_default_importers()
+
+    @classmethod
+    def from_config(cls, bundle_name: str | None = None) -> Self:
+        """Create and configure a DagImporterRegistry with 3-tier 
precedence."""
+        registry = cls(register_defaults=True)
+
+        global_importers = conf.getjson("dag_processor", 
"dag_importer_configs", fallback=None)
+        if global_importers:
+            if not isinstance(global_importers, list):
+                raise AirflowConfigException(
+                    "Section `dag_processor` key `dag_importer_configs` must 
be a list "
+                    f"but got {global_importers.__class__.__name__}"
+                )
+            registry.register_specs(global_importers, context="global 
configuration")
+
+        if bundle_name:
+            bundle_importers = cls._get_bundle_importers_config(bundle_name)
+            if bundle_importers:
+                registry.register_specs(bundle_importers, context=f"bundle 
'{bundle_name}'")
+
+        return registry
+
+    def register(self, importer: AbstractDagImporter, extensions: list[str] | 
None = None) -> None:
+        """
+        Register an importer.
+
+        Each extension can only have one importer. If an extension is already 
registered,
+        the new importer will override it and a warning will be logged.
+        """
+        if importer not in self._ordered_importers:
+            self._ordered_importers.append(importer)
+
+        if extensions is None:
+            extensions = _get_importer_extensions(importer)
+
+        if extensions:
+            normalized_extensions = _normalize_extensions(extensions)
+            if hasattr(importer, "supported_extensions"):
+                with contextlib.suppress(AttributeError, TypeError):
+                    importer.supported_extensions = normalized_extensions

Review Comment:
   This is the ambiguous part that I want double check.
   
   IIUC, if I define a `MyDagImporter.normalized_extensions = [.my]` and call 
with `DagImporterRegistry.register(MyDagImporter(), [.they]`, it will be 
registered as `.they -> MyDagImporter` mapping. I wonder should we reserve the 
DagImporter's extension, which means`.my -> MyDagImporter + .they -> 
MyDagImporter` mapping in the example I gave.
   
   Though I'm not sure will the case I mentioned above happen and which 
registration semantic is correct?



##########
shared/module_loading/src/airflow_shared/module_loading/dag_file.py:
##########
@@ -19,5 +19,78 @@
 
 from __future__ import annotations
 
+import hashlib
+import re
+import zipfile
+from collections.abc import Callable
+from pathlib import Path
+
 UNUSUAL_MODULE_PREFIX = "unusual_prefix_"
 MODIFIED_DAG_MODULE_NAME = 
f"{UNUSUAL_MODULE_PREFIX}{{path_hash}}_{{module_name}}"
+
+
+def get_unique_dag_module_name(file_path: str) -> str:
+    """Return a unique module name in the format unusual_prefix_{sha1 of 
module's file path}_{original module name}."""
+    if isinstance(file_path, str):
+        path_hash = hashlib.sha1(file_path.encode("utf-8"), 
usedforsecurity=False).hexdigest()
+        org_mod_name = re.sub(r"[.-]", "_", Path(file_path).stem)
+        return MODIFIED_DAG_MODULE_NAME.format(path_hash=path_hash, 
module_name=org_mod_name)
+    raise ValueError("file_path should be a string to generate unique module 
name")
+
+
+def might_contain_dag_via_default_heuristic(file_path: str, zip_file: 
zipfile.ZipFile | None = None) -> bool:
+    """
+    Heuristic that guesses whether a Python file contains an Airflow DAG 
definition.
+
+    :param file_path: Path to the file to be checked.
+    :param zip_file: if passed, checks the archive. Otherwise, check local 
filesystem.
+    :return: True, if file might contain DAGs.
+    """
+    if zip_file:
+        with zip_file.open(file_path) as current_file:
+            content = current_file.read()
+    else:
+        if zipfile.is_zipfile(file_path):
+            return True
+        with open(file_path, "rb") as dag_file:
+            content = dag_file.read()
+    content = content.lower()
+    if b"airflow" not in content:
+        return False
+    return any(s in content for s in (b"dag", b"asset"))
+
+
+def might_contain_dag(file_path: str, safe_mode: bool, zip_file: 
zipfile.ZipFile | None = None) -> bool:
+    """
+    Check whether a Python file contains Airflow DAGs.
+
+    When safe_mode is off (with False value), this function always returns 
True.
+
+    If might_contain_dag_callable isn't specified, it uses airflow default 
heuristic.
+    """
+    if not safe_mode:
+        return True
+
+    might_contain_dag_callable: Callable[[str, zipfile.ZipFile | None], bool] 
| None = None
+    try:
+        # Use importlib to avoid hard import at module level
+        import importlib
+
+        config_module = importlib.import_module("airflow.configuration")
+        conf = config_module.conf
+
+        might_contain_dag_callable = conf.getimport(
+            "core",
+            "might_contain_dag_callable",
+            fallback=None,
+        )
+    except ImportError:
+        # airflow package not available in this context
+        pass
+    except Exception:
+        pass

Review Comment:
   Claude code comment that makes sense to me.
   
   `except Exception: pass` silently swallows config errors here (e.g. a broken 
`might_contain_dag_callable` classpath), where the pre-refactor inline code let 
them propagate. At minimum log it so misconfiguration isn't silently masked:
   
   ```suggestion
       except Exception as e:
           import logging
   
           logging.getLogger(__name__).warning(
               "Failed to load might_contain_dag_callable from config, falling 
back to default heuristic: %s", e
           )
   ```



##########
task-sdk/src/airflow/sdk/importers/python_importer.py:
##########
@@ -0,0 +1,270 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Python DAG importer - imports DAGs from Python files."""
+
+from __future__ import annotations
+
+import functools
+import importlib.machinery
+import importlib.util
+import logging
+import os
+import sys
+import traceback
+import warnings
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk._shared.module_loading.dag_file import 
get_unique_dag_module_name, might_contain_dag
+from airflow.sdk.configuration import conf
+from airflow.sdk.definitions._internal.contextmanager import DagContext
+from airflow.sdk.definitions.dag import DAG
+from airflow.sdk.exceptions import AirflowConfigException
+from airflow.sdk.execution_time.timeout import timeout
+from airflow.sdk.importers.base import (
+    AbstractDagImporter,
+    DagDefinition,
+    DagImportError,
+    DagImportResult,
+    DagImportWarning,
+    DagSourceCode,
+    _normalize_extensions,
+    find_file_dag_definitions,
+    get_file_suffix,
+)
+
+if TYPE_CHECKING:
+    from collections.abc import Iterator
+    from types import ModuleType
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+
+log = logging.getLogger(__name__)
+
+
+class PythonDagImporter(AbstractDagImporter):
+    """
+    Importer for Python DAG files.
+
+    This is the default importer registered with the DagImporterRegistry. It 
handles
+    .py files containing Python DAGs.
+    """
+
+    supported_extensions = [".py"]

Review Comment:
   Claude code comment that makes sense to me.
   
   The old zip importer (`dag_processing/importers/python_importer.py:290`) 
explicitly accepted `.py` and `.pyc` members. `ZipImporter`'s default internal 
importer is `PythonDagImporter()`, whose extensions are `.py`-only, so 
`.pyc`-only DAG bundles are now silently skipped (`_get_internal_importer` 
returns `None`, no error/warning).
   
   ```suggestion
       supported_extensions = [".py", ".pyc"]
   ```
   
   Note this alone isn't sufficient — `_load_modules_from_file`'s `parse()` 
still hardcodes `importlib.machinery.SourceFileLoader`, which can't exec a 
bytecode-only file; it needs to branch to 
`importlib.machinery.SourcelessFileLoader` when `Path(filepath).suffix == 
".pyc"`.



##########
task-sdk/src/airflow/sdk/importers/python_importer.py:
##########
@@ -0,0 +1,270 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Python DAG importer - imports DAGs from Python files."""
+
+from __future__ import annotations
+
+import functools
+import importlib.machinery
+import importlib.util
+import logging
+import os
+import sys
+import traceback
+import warnings
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk._shared.module_loading.dag_file import 
get_unique_dag_module_name, might_contain_dag
+from airflow.sdk.configuration import conf
+from airflow.sdk.definitions._internal.contextmanager import DagContext
+from airflow.sdk.definitions.dag import DAG
+from airflow.sdk.exceptions import AirflowConfigException
+from airflow.sdk.execution_time.timeout import timeout
+from airflow.sdk.importers.base import (
+    AbstractDagImporter,
+    DagDefinition,
+    DagImportError,
+    DagImportResult,
+    DagImportWarning,
+    DagSourceCode,
+    _normalize_extensions,
+    find_file_dag_definitions,
+    get_file_suffix,
+)
+
+if TYPE_CHECKING:
+    from collections.abc import Iterator
+    from types import ModuleType
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+
+log = logging.getLogger(__name__)
+
+
+class PythonDagImporter(AbstractDagImporter):
+    """
+    Importer for Python DAG files.
+
+    This is the default importer registered with the DagImporterRegistry. It 
handles
+    .py files containing Python DAGs.
+    """
+
+    supported_extensions = [".py"]
+
+    def __init__(self, extensions: list[str] | None = None) -> None:
+        if extensions is not None:
+            self.supported_extensions = _normalize_extensions(extensions)
+
+    def can_handle(self, definition: DagDefinition | str | Path) -> bool:
+        """Check if this importer can handle the given definition based on 
file extension."""
+        suffix = get_file_suffix(definition)
+        return suffix in self.supported_extensions if suffix else False
+
+    def list_dag_definitions(
+        self,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> Iterator[DagDefinition]:
+        """List Python DAG definitions in a bundle matching supported 
extensions."""
+        yield from find_file_dag_definitions(bundle.path, 
self.supported_extensions)
+
+    def import_definition(
+        self,
+        definition: DagDefinition,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> DagImportResult:
+        """
+        Import DAGs from a Python DAG definition.
+
+        :param definition: The definition to import from.
+        :param bundle: The DAG bundle containing the definition.
+        :param safe_mode: If True, skip files that don't appear to contain 
DAGs.
+        :return: DagImportResult with imported DAGs and any errors.
+        """
+        result = DagImportResult(definition=definition)
+        DagContext.autoregistered_dags.clear()
+        captured_warnings: list[warnings.WarningMessage] = []
+
+        try:
+            with warnings.catch_warnings(record=True) as captured_warnings:
+                with definition.as_file() as local_path:
+                    filepath = os.fspath(local_path)
+                    modules = self._load_modules_from_file(
+                        filepath,
+                        safe_mode,
+                        result,
+                        bundle=bundle,
+                    )
+        except AirflowConfigException:
+            # Configuration errors (e.g., invalid timeout type) should 
propagate
+            raise
+        except Exception as e:
+            result.errors.append(
+                DagImportError(
+                    source_reference=repr(definition),
+                    message=str(e),
+                    error_type="import",
+                    stacktrace=traceback.format_exc(),
+                )
+            )
+            return result
+
+        for warn_msg in captured_warnings:
+            category = warn_msg.category.__name__
+            if (module := warn_msg.category.__module__) != "builtins":
+                category = f"{module}.{category}"
+            result.warnings.append(
+                DagImportWarning(
+                    source_reference=repr(definition),
+                    message=str(warn_msg.message),
+                    warning_type=category,
+                    line_number=warn_msg.lineno,
+                )
+            )
+
+        self._process_modules(
+            modules,
+            result,
+            bundle=bundle,
+        )
+
+        return result
+
+    def get_source_code(self, definition: DagDefinition) -> DagSourceCode:
+        """Retrieve the raw source code for the Python definition."""
+        return DagSourceCode(
+            source_code=definition.read_text(encoding="utf-8"),
+            language="python",
+        )
+
+    def might_contain_dag(self, file_path: str | Path, safe_mode: bool = True) 
-> bool:
+        """Check whether a file might contain Airflow DAGs according to safe 
mode heuristics."""
+        if not safe_mode:
+            return True
+        return might_contain_dag(str(file_path), safe_mode)
+
+    def _load_modules_from_file(
+        self,
+        filepath: str,
+        safe_mode: bool,
+        result: DagImportResult,
+        bundle: BaseDagBundle,
+    ) -> list[ModuleType]:
+        definition = result.definition

Review Comment:
   Claude code comment that makes sense to me.
   
   The pre-existing `airflow-core` importer 
(`dag_processing/importers/python_importer.py:203-217`) installs a SIGSEGV 
handler before `exec_module` so a segfaulting DAG file records a structured 
`DagImportError` instead of silently killing the process. This new importer 
drops that entirely. Restoring it:
   
   ```suggestion
           definition = result.definition
   
           import signal
   
           def sigsegv_handler(signum, frame):
               msg = f"Received SIGSEGV signal while processing {filepath}."
               log.error(msg)
               result.errors.append(
                   DagImportError(
                       source_reference=repr(definition),
                       message=msg,
                       error_type="segfault",
                   )
               )
   
           try:
               signal.signal(signal.SIGSEGV, sigsegv_handler)
           except ValueError:
               log.warning("SIGSEGV signal handler registration failed. Not in 
the main thread")
   ```



##########
task-sdk/src/airflow/sdk/importers/zip_importer.py:
##########
@@ -0,0 +1,293 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Zip archive DAG importer."""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+import sys
+import tempfile
+import threading
+import zipfile
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk.exceptions import AirflowConfigException
+from airflow.sdk.importers.base import (
+    AbstractDagImporter,
+    DagDefinition,
+    DagImporterRegistry,
+    DagImportError,
+    DagImportResult,
+    DagSourceCode,
+    _get_importer_extensions,
+    _normalize_extensions,
+    _parse_importer_specs,
+    find_file_dag_definitions,
+    get_file_suffix,
+)
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterator
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+
+log = logging.getLogger(__name__)
+
+_sys_path_lock = threading.Lock()
+
+
[email protected]
+def _temporary_sys_path(path: str) -> Generator[None, None, None]:
+    """Safely prepend a path to sys.path with synchronization and 
restoration."""
+    with _sys_path_lock:
+        already_present = path in sys.path
+        if not already_present:
+            sys.path.insert(0, path)
+        try:
+            yield
+        finally:
+            if not already_present:
+                with contextlib.suppress(ValueError):
+                    sys.path.remove(path)
+
+
+@dataclass
+class ZipFileDagDefinition(DagDefinition):
+    """A DAG definition backed by a file inside a ZIP archive."""
+
+    zip_path: Path
+    file_path: str
+    _content: bytes | None = field(default=None, repr=False, compare=False)
+
+    @property
+    def freshness_token(self) -> str:
+        try:
+            stat = self.zip_path.stat()
+        except OSError:
+            return ""
+        return f"{stat.st_mtime_ns}-{stat.st_size}-{self.file_path}"
+
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        if root is not None:
+            with contextlib.suppress(ValueError):
+                return f"{self.zip_path.relative_to(root)}:{self.file_path}"
+        return f"{self.zip_path}:{self.file_path}"
+
+    def read_bytes(self) -> bytes:
+        if self._content is None:
+            with zipfile.ZipFile(self.zip_path) as z:
+                self._content = z.read(self.file_path)
+        return self._content
+
+    @contextlib.contextmanager
+    def as_file(self) -> Generator[Path, None, None]:
+        suffix = Path(self.file_path).suffix
+        with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
+            f.write(self.read_bytes())
+            temp_path = Path(f.name)
+        try:
+            yield temp_path
+        finally:
+            with contextlib.suppress(OSError):
+                temp_path.unlink()
+
+    def __repr__(self) -> str:
+        return f"{self.zip_path}:{self.file_path}"
+
+
+class ZipImporter(AbstractDagImporter):
+    """Composite importer responsible for routing archive members to internal 
importers."""
+
+    supported_extensions = [".zip"]
+
+    def __init__(
+        self,
+        internal_importers: dict[str, AbstractDagImporter | dict[str, Any]]
+        | list[dict[str, Any]]
+        | None = None,
+        extensions: list[str] | None = None,
+    ) -> None:
+        if extensions is not None:
+            self.supported_extensions = _normalize_extensions(extensions)
+        self._internal_extension_importers: dict[str, AbstractDagImporter] = {}
+        self._ordered_internal_importers: list[AbstractDagImporter] = []
+
+        if internal_importers is None:
+            from airflow.sdk.importers.python_importer import PythonDagImporter
+
+            self._register_internal(PythonDagImporter())
+        elif isinstance(internal_importers, list):
+            specs = _parse_importer_specs(internal_importers, 
context="internal_importers of ZipImporter")
+            for spec in specs:
+                importer = DagImporterRegistry._instantiate_spec(spec)
+                self._register_internal(importer, extensions=spec.extensions)
+        elif isinstance(internal_importers, dict):
+            for ext, cfg in internal_importers.items():
+                if isinstance(cfg, AbstractDagImporter):
+                    self._register_internal(cfg, extensions=[ext])
+                elif isinstance(cfg, dict):
+                    specs = _parse_importer_specs(
+                        [cfg], context=f"internal_importers configuration for 
extension '{ext}'"
+                    )
+                    importer = DagImporterRegistry._instantiate_spec(specs[0])
+                    self._register_internal(importer, 
extensions=specs[0].extensions or [ext])
+                else:
+                    raise AirflowConfigException(
+                        f"Invalid internal importer configuration for 
extension '{ext}': "
+                        f"expected AbstractDagImporter or dictionary, got 
{type(cfg).__name__}."
+                    )
+        else:
+            raise AirflowConfigException(
+                f"Field 'internal_importers' must be a list or dictionary, got 
{type(internal_importers).__name__}."
+            )
+
+    def can_handle(self, definition: DagDefinition | str | Path) -> bool:
+        """Check if this importer can handle the given definition based on 
file extension."""
+        suffix = get_file_suffix(definition)
+        return suffix in self.supported_extensions if suffix else False
+
+    def list_dag_definitions(
+        self,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> Iterator[DagDefinition]:
+        """List zip archive DAG definitions in a bundle matching supported 
extensions."""
+        yield from find_file_dag_definitions(bundle.path, 
self.supported_extensions)
+
+    def import_definition(
+        self,
+        definition: DagDefinition,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> DagImportResult:
+        """
+        Import DAGs from a ZIP archive by routing its members to internal 
importers.
+
+        The archive itself is placed on ``sys.path`` so Python imports between
+        members resolve via ``zipimport``. A real file is materialized on 
demand
+        with :meth:`.as_file()` for internal importers.
+        """
+        result = DagImportResult(definition=definition)
+
+        with definition.as_file() as local_zip_path:
+            try:
+                with zipfile.ZipFile(local_zip_path) as z:
+                    member_names = z.namelist()
+            except Exception as e:
+                result.errors.append(
+                    DagImportError(
+                        
source_reference=definition.get_relative_loc(bundle.path),
+                        message=f"Failed to read ZIP archive: {e}",
+                        error_type="zip_read_error",
+                    )
+                )
+                return result
+
+            with _temporary_sys_path(str(local_zip_path)):
+                for member_name in member_names:
+                    if member_name.endswith("/") or 
member_name.startswith("__MACOSX/"):
+                        continue
+                    # ZipSlip defence: reject traversal or absolute member 
names.
+                    member_path = Path(member_name)
+                    if member_path.is_absolute() or ".." in member_path.parts:
+                        log.warning(
+                            "Skipping zip member %r in %s: directory traversal 
patterns detected",
+                            member_name,
+                            definition,
+                        )
+                        continue
+
+                    importer = self._get_internal_importer(member_name)
+                    if importer is None:
+                        continue
+
+                    nested_def = ZipFileDagDefinition(zip_path=local_zip_path, 
file_path=member_name)
+                    if not importer.can_handle(nested_def):
+                        continue
+
+                    member_result = importer.import_definition(nested_def, 
bundle, safe_mode=safe_mode)
+                    result.dags.extend(member_result.dags)
+                    result.errors.extend(member_result.errors)
+                    result.warnings.extend(member_result.warnings)
+                    
result.skipped_definitions.extend(member_result.skipped_definitions)
+                    result.dependencies.extend(member_result.dependencies)
+
+        return result
+
+    def get_source_code(self, definition: DagDefinition) -> DagSourceCode:
+        if isinstance(definition, ZipFileDagDefinition):
+            importer = self._get_internal_importer(definition.file_path)
+            if importer is not None:
+                return importer.get_source_code(definition)
+            raise ValueError(f"No internal importer registered for zip member 
{definition.file_path}")

Review Comment:
   
   Claude code comment that makes sense to me.
   
   Unlike `import_definition` (which wraps the equivalent zip-read failure in a 
try/except and returns a structured `DagImportError`), this branch has no 
exception handling — a member renamed/removed from the archive after this 
`ZipFileDagDefinition` was captured raises `KeyError`/`BadZipFile` uncaught.
   
   ```suggestion
       def get_source_code(self, definition: DagDefinition) -> DagSourceCode:
           if isinstance(definition, ZipFileDagDefinition):
               importer = self._get_internal_importer(definition.file_path)
               if importer is not None:
                   try:
                       return importer.get_source_code(definition)
                   except (KeyError, zipfile.BadZipFile) as e:
                       raise ValueError(
                           f"Failed to read {definition.file_path} from 
{definition.zip_path}: {e}"
                       ) from e
               raise ValueError(f"No internal importer registered for zip 
member {definition.file_path}")
   ```



##########
task-sdk/src/airflow/sdk/importers/zip_importer.py:
##########
@@ -0,0 +1,293 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Zip archive DAG importer."""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+import sys
+import tempfile
+import threading
+import zipfile
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk.exceptions import AirflowConfigException
+from airflow.sdk.importers.base import (
+    AbstractDagImporter,
+    DagDefinition,
+    DagImporterRegistry,
+    DagImportError,
+    DagImportResult,
+    DagSourceCode,
+    _get_importer_extensions,
+    _normalize_extensions,
+    _parse_importer_specs,
+    find_file_dag_definitions,
+    get_file_suffix,
+)
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterator
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+
+log = logging.getLogger(__name__)
+
+_sys_path_lock = threading.Lock()
+
+
[email protected]
+def _temporary_sys_path(path: str) -> Generator[None, None, None]:
+    """Safely prepend a path to sys.path with synchronization and 
restoration."""
+    with _sys_path_lock:
+        already_present = path in sys.path
+        if not already_present:
+            sys.path.insert(0, path)
+        try:
+            yield
+        finally:
+            if not already_present:
+                with contextlib.suppress(ValueError):
+                    sys.path.remove(path)
+
+
+@dataclass
+class ZipFileDagDefinition(DagDefinition):
+    """A DAG definition backed by a file inside a ZIP archive."""
+
+    zip_path: Path
+    file_path: str
+    _content: bytes | None = field(default=None, repr=False, compare=False)
+
+    @property
+    def freshness_token(self) -> str:
+        try:
+            stat = self.zip_path.stat()
+        except OSError:
+            return ""
+        return f"{stat.st_mtime_ns}-{stat.st_size}-{self.file_path}"
+
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        if root is not None:
+            with contextlib.suppress(ValueError):
+                return f"{self.zip_path.relative_to(root)}:{self.file_path}"
+        return f"{self.zip_path}:{self.file_path}"
+
+    def read_bytes(self) -> bytes:
+        if self._content is None:
+            with zipfile.ZipFile(self.zip_path) as z:
+                self._content = z.read(self.file_path)
+        return self._content
+
+    @contextlib.contextmanager
+    def as_file(self) -> Generator[Path, None, None]:
+        suffix = Path(self.file_path).suffix
+        with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
+            f.write(self.read_bytes())
+            temp_path = Path(f.name)
+        try:
+            yield temp_path
+        finally:
+            with contextlib.suppress(OSError):
+                temp_path.unlink()
+
+    def __repr__(self) -> str:
+        return f"{self.zip_path}:{self.file_path}"
+
+
+class ZipImporter(AbstractDagImporter):
+    """Composite importer responsible for routing archive members to internal 
importers."""
+
+    supported_extensions = [".zip"]
+
+    def __init__(
+        self,
+        internal_importers: dict[str, AbstractDagImporter | dict[str, Any]]
+        | list[dict[str, Any]]
+        | None = None,
+        extensions: list[str] | None = None,
+    ) -> None:
+        if extensions is not None:
+            self.supported_extensions = _normalize_extensions(extensions)
+        self._internal_extension_importers: dict[str, AbstractDagImporter] = {}
+        self._ordered_internal_importers: list[AbstractDagImporter] = []
+
+        if internal_importers is None:
+            from airflow.sdk.importers.python_importer import PythonDagImporter
+
+            self._register_internal(PythonDagImporter())
+        elif isinstance(internal_importers, list):
+            specs = _parse_importer_specs(internal_importers, 
context="internal_importers of ZipImporter")
+            for spec in specs:
+                importer = DagImporterRegistry._instantiate_spec(spec)
+                self._register_internal(importer, extensions=spec.extensions)

Review Comment:
   Claude code comment that makes sense to me.
   
   `ZipImporter.__init__` eagerly calls 
`DagImporterRegistry._instantiate_spec(spec)` for every configured internal 
importer, unlike `DagImporterRegistry` itself, which defers spec instantiation 
to first use via `_extension_specs` (see `get_importer`, base.py:375-396). A 
broken/optional-dependency classpath here breaks construction of the whole 
`ZipImporter` — and thus every `.zip` in the bundle — even if the extension it 
covers is never encountered.
   
   ```suggestion
           self._internal_extension_importers: dict[str, AbstractDagImporter] = 
{}
           self._internal_extension_specs: dict[str, _ImporterSpec] = {}
           self._ordered_internal_importers: list[AbstractDagImporter] = []
   
           if internal_importers is None:
               from airflow.sdk.importers.python_importer import 
PythonDagImporter
   
               self._register_internal(PythonDagImporter())
           elif isinstance(internal_importers, list):
               specs = _parse_importer_specs(internal_importers, 
context="internal_importers of ZipImporter")
               for spec in specs:
                   for ext in _normalize_extensions(spec.extensions or []):
                       self._internal_extension_specs[ext] = spec
   ```
   
   This also needs: `_ImporterSpec` added to the `airflow.sdk.importers.base` 
import block above, and `_get_internal_importer` updated to check 
`_internal_extension_specs` and lazily instantiate+cache on first lookup, 
mirroring `DagImporterRegistry.get_importer`.



##########
task-sdk/src/airflow/sdk/importers/base.py:
##########
@@ -0,0 +1,477 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Abstract base class for DAG importers."""
+
+from __future__ import annotations
+
+import contextlib
+import functools
+import logging
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk._shared.module_loading.file_discovery import 
find_path_from_directory
+from airflow.sdk.configuration import conf
+from airflow.sdk.exceptions import AirflowConfigException
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterable, Iterator
+
+    from typing_extensions import Self
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+    from airflow.sdk import DAG
+
+log = logging.getLogger(__name__)
+
+
+class DagDefinition(ABC):
+    """Abstract base class for a DAG source definition."""
+
+    @property
+    @abstractmethod
+    def freshness_token(self) -> str:
+        """Opaque, generalized token representing the current state of the 
source."""
+
+    @abstractmethod
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        """Get relative location of the definition to a root directory."""
+
+    @abstractmethod
+    def read_bytes(self) -> bytes:
+        """Read and return the content of the resource as bytes."""
+
+    def read_text(self, encoding: str = "utf-8") -> str:
+        """Read and return the content of the resource as a string."""
+        return self.read_bytes().decode(encoding)
+
+    @abstractmethod
+    def as_file(self) -> contextlib.AbstractContextManager[Path]:
+        """
+        Return a context manager yielding a Path pointing to a local file.
+
+        For file-backed resources, this is the actual file path.
+        For others, a temp file is created and cleaned up.
+        """
+
+    @abstractmethod
+    def __repr__(self) -> str:
+        """Return string representation used by import error and warning 
objects."""
+
+
+@dataclass
+class FileDagDefinition(DagDefinition):
+    """A DAG definition backed by a file on the local filesystem."""
+
+    path: Path
+
+    @property
+    def freshness_token(self) -> str:
+        try:
+            stat = self.path.stat()
+            return f"{stat.st_mtime_ns}-{stat.st_size}"
+        except OSError:
+            return ""
+
+    def get_relative_loc(self, root: Path | None = None) -> str:
+        if root is None:
+            return str(self.path)
+        try:
+            return str(self.path.relative_to(root))
+        except ValueError:
+            return str(self.path)
+
+    def read_bytes(self) -> bytes:
+        return self.path.read_bytes()
+
+    @contextlib.contextmanager
+    def as_file(self) -> Generator[Path, None, None]:
+        yield self.path
+
+    def __repr__(self) -> str:
+        return str(self.path)
+
+
+@dataclass
+class DagImportError:
+    """Structured error information for DAG import failures."""
+
+    source_reference: str
+    message: str
+    error_type: str = "import"
+    line_number: int | None = None
+    column_number: int | None = None
+    context: str | None = None
+    suggestion: str | None = None
+    stacktrace: str | None = None
+
+    def format_message(self) -> str:
+        """Format the error as a human-readable string."""
+        parts = [f"Error in {self.source_reference}"]
+        if self.line_number is not None:
+            loc = f"line {self.line_number}"
+            if self.column_number is not None:
+                loc += f", column {self.column_number}"
+            parts.append(f"Location: {loc}")
+        parts.append(f"Error ({self.error_type}): {self.message}")
+        if self.context:
+            parts.append(f"Context:\n{self.context}")
+        if self.suggestion:
+            parts.append(f"Suggestion: {self.suggestion}")
+        return "\n".join(parts)
+
+
+@dataclass
+class DagImportWarning:
+    """Warning information for non-fatal issues during DAG import."""
+
+    source_reference: str
+    message: str
+    warning_type: str = "import"
+    line_number: int | None = None
+    context: dict[str, Any] | None = None
+
+
+@dataclass
+class DagImportResult:
+    """Result of importing DAGs from a definition."""
+
+    definition: DagDefinition | None = None
+    dags: list[DAG] = field(default_factory=list)
+    errors: list[DagImportError] = field(default_factory=list)
+    skipped_definitions: list[DagDefinition] = field(default_factory=list)
+    warnings: list[DagImportWarning] = field(default_factory=list)
+    dependencies: list[DagDefinition] = field(default_factory=list)
+
+    @property
+    def success(self) -> bool:
+        """Return True if no fatal errors occurred."""
+        return not self.errors
+
+
+@dataclass
+class DagSourceCode:
+    """Raw source code and its language identifier for a DAG definition."""
+
+    source_code: str
+    language: str
+
+
+def _normalize_extensions(extensions: Iterable[str]) -> list[str]:
+    """Normalize file extensions to lowercase with leading dot."""
+    return [ext.lower() if ext.startswith(".") else f".{ext.lower()}" for ext 
in extensions]
+
+
+def _get_importer_extensions(importer: Any) -> list[str]:
+    """Extract supported extensions from an importer via duck typing."""
+    exts = getattr(importer, "supported_extensions", None)
+    if callable(exts):
+        return _normalize_extensions(exts())
+    if exts is not None:
+        return _normalize_extensions(exts)
+    return []
+
+
+class AbstractDagImporter(ABC):
+    """Abstract base class for DAG importers."""
+
+    @abstractmethod
+    def can_handle(self, definition: DagDefinition | str | Path) -> bool:
+        """Check if this importer can handle the given definition."""
+
+    @abstractmethod
+    def list_dag_definitions(
+        self,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> Iterator[DagDefinition]:
+        """List DAG definitions in a bundle that this importer can handle."""
+
+    @abstractmethod
+    def import_definition(
+        self,
+        definition: DagDefinition,
+        bundle: BaseDagBundle,
+        *,
+        safe_mode: bool = True,
+    ) -> DagImportResult:
+        """Import DAGs from a DAG definition."""
+
+    @abstractmethod
+    def get_source_code(self, definition: DagDefinition) -> DagSourceCode:
+        """Retrieve the raw source code and its language identifier for the 
specified DAG definition."""
+
+
+def get_file_suffix(definition: DagDefinition | str | Path) -> str | None:
+    """Extract lowercase file suffix from a definition, path, or filename."""
+    path = (
+        definition
+        if isinstance(definition, (str, Path))
+        else getattr(definition, "path", getattr(definition, "file_path", 
None))
+    )
+    return Path(path).suffix.lower() if path else None
+
+
+def find_file_dag_definitions(
+    bundle_path: Path,
+    supported_extensions: Iterable[str],
+) -> Iterator[DagDefinition]:
+    """Find file DAG definitions in a bundle matching given extensions and 
respecting .airflowignore."""
+    ignore_file_syntax = conf.get_mandatory_value("core", 
"DAG_IGNORE_FILE_SYNTAX", fallback="glob")
+    supported_exts = _normalize_extensions(supported_extensions)
+
+    for file_path in find_path_from_directory(bundle_path, ".airflowignore", 
ignore_file_syntax):
+        path = Path(file_path)
+
+        if not path.is_file():
+            continue
+
+        if path.suffix.lower() not in supported_exts:
+            continue
+
+        yield FileDagDefinition(path=path)

Review Comment:
   Claude code comment that makes sense to me.
   
   This never applies the `might_contain_dag` safe-mode heuristic while walking 
the bundle — the old `AbstractDagImporter.list_dag_files` did. As written, the 
`safe_mode` parameter both `PythonDagImporter.list_dag_definitions` and 
`ZipImporter.list_dag_definitions` accept is a no-op at listing time.
   
   ```suggestion
   def find_file_dag_definitions(
       bundle_path: Path,
       supported_extensions: Iterable[str],
       safe_mode: bool = True,
   ) -> Iterator[DagDefinition]:
       """Find file DAG definitions in a bundle matching given extensions and 
respecting .airflowignore."""
       from airflow.sdk._shared.module_loading.dag_file import might_contain_dag
   
       ignore_file_syntax = conf.get_mandatory_value("core", 
"DAG_IGNORE_FILE_SYNTAX", fallback="glob")
       supported_exts = _normalize_extensions(supported_extensions)
   
       for file_path in find_path_from_directory(bundle_path, ".airflowignore", 
ignore_file_syntax):
           path = Path(file_path)
   
           if not path.is_file():
               continue
   
           if path.suffix.lower() not in supported_exts:
               continue
   
           if safe_mode and not might_contain_dag(str(path), safe_mode):
               continue
   
           yield FileDagDefinition(path=path)
   ```
   
   Callers also need to actually pass `safe_mode` through — both 
`PythonDagImporter.list_dag_definitions` and `ZipImporter.list_dag_definitions` 
currently call `find_file_dag_definitions(bundle.path, 
self.supported_extensions)` without forwarding their own `safe_mode` argument.



##########
task-sdk/src/airflow/sdk/importers/zip_importer.py:
##########
@@ -0,0 +1,293 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Zip archive DAG importer."""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+import sys
+import tempfile
+import threading
+import zipfile
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+from airflow.sdk.exceptions import AirflowConfigException
+from airflow.sdk.importers.base import (
+    AbstractDagImporter,
+    DagDefinition,
+    DagImporterRegistry,
+    DagImportError,
+    DagImportResult,
+    DagSourceCode,
+    _get_importer_extensions,
+    _normalize_extensions,
+    _parse_importer_specs,
+    find_file_dag_definitions,
+    get_file_suffix,
+)
+
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterator
+
+    from airflow.dag_processing.bundles.base import BaseDagBundle  # noqa: 
SDK002
+
+log = logging.getLogger(__name__)
+
+_sys_path_lock = threading.Lock()

Review Comment:
   Claude code comment that makes sense to me.
   
   This plain `Lock` is held for the whole zip-member import loop 
(`import_definition` wraps its entire `for member_name in member_names` loop in 
this lock, not just the `sys.path` mutation). Since a `ZipImporter` can be 
configured with a nested `ZipImporter` as an internal importer, a zip-of-zips 
processed on one thread re-enters `_temporary_sys_path` and deadlocks trying to 
re-acquire this same non-reentrant lock. Making it reentrant fixes the 
self-deadlock (the cross-thread serialization is a separate, larger design 
question):
   
   ```suggestion
   _sys_path_lock = threading.RLock()
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to