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

uranusjr 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 8b5306690ac Fix 'lineage' export in airflow.sdk (#75744)
8b5306690ac is described below

commit 8b5306690acb58d0584376272dfa2159aecea0dd
Author: Tzu-ping Chung <[email protected]>
AuthorDate: Tue Oct 27 22:15:19 2026 +0800

    Fix 'lineage' export in airflow.sdk (#75744)
---
 .../tests/unit/partition_mappers/test_init.py      |   4 +-
 task-sdk/src/airflow/sdk/__init__.py               | 227 +++++++++++----------
 task-sdk/tests/task_sdk/docs/test_public_api.py    |  26 ++-
 3 files changed, 136 insertions(+), 121 deletions(-)

diff --git a/airflow-core/tests/unit/partition_mappers/test_init.py 
b/airflow-core/tests/unit/partition_mappers/test_init.py
index e7579ec6eec..b7d7d7fc301 100644
--- a/airflow-core/tests/unit/partition_mappers/test_init.py
+++ b/airflow-core/tests/unit/partition_mappers/test_init.py
@@ -24,7 +24,7 @@ def test_package_root_exports_match_sdk():
     """Every partition-mapper symbol exported from airflow.sdk must also be 
exported from the core package root."""
     sdk_names = {
         name
-        for name, module in airflow.sdk.__lazy_imports.items()
-        if module.startswith(".definitions.partition_mappers")
+        for name, (module_path, _) in airflow.sdk.__lazy_imports.items()
+        if module_path.startswith(".definitions.partition_mappers")
     }
     assert sdk_names == set(airflow.partition_mappers.__all__)
diff --git a/task-sdk/src/airflow/sdk/__init__.py 
b/task-sdk/src/airflow/sdk/__init__.py
index 3a2a2fb4a0a..46418ceeaf1 100644
--- a/task-sdk/src/airflow/sdk/__init__.py
+++ b/task-sdk/src/airflow/sdk/__init__.py
@@ -126,6 +126,7 @@ __all__ = [
 __version__ = "1.4.0"
 
 if TYPE_CHECKING:
+    from airflow.sdk import lineage
     from airflow.sdk.api.datamodels._generated import DagRunState, 
TaskInstanceState, TriggerRule, WeightRule
     from airflow.sdk.bases.branch import BaseBranchOperator, BranchMixIn
     from airflow.sdk.bases.hook import BaseHook
@@ -237,120 +238,124 @@ if TYPE_CHECKING:
 
     conf: AirflowSDKConfigParser
 
-__lazy_imports: dict[str, str] = {
-    "AllowedKeyMapper": ".definitions.partition_mappers.allowed_key",
-    "Asset": ".definitions.asset",
-    "AssetAccessControl": ".definitions.asset",
-    "AssetAlias": ".definitions.asset",
-    "AssetAll": ".definitions.asset",
-    "AssetAndTimeSchedule": ".definitions.timetables.assets",
-    "AssetAny": ".definitions.asset",
-    "AssetOrTimeSchedule": ".definitions.timetables.assets",
-    "AssetTriggeredTimetable": ".definitions.timetables.assets",
-    "AssetWatcher": ".definitions.asset",
-    "AsyncCallback": ".definitions.callback",
-    "BaseAsyncOperator": ".bases.operator",
-    "BaseBranchOperator": ".bases.branch",
-    "BaseDeadlineReference": ".definitions.deadline",
-    "BaseHook": ".bases.hook",
-    "BaseNotifier": ".bases.notifier",
-    "BaseOperator": ".bases.operator",
-    "BaseOperatorLink": ".bases.operatorlink",
-    "BaseSensorOperator": ".bases.sensor",
-    "BaseXCom": ".bases.xcom",
-    "BranchMixIn": ".bases.branch",
-    "ChainMapper": ".definitions.partition_mappers.chain",
-    "ChainRetryPolicy": ".definitions.retry_policy",
-    "Connection": ".definitions.connection",
-    "Context": ".definitions.context",
-    "CronDataIntervalTimetable": ".definitions.timetables.interval",
-    "CronTriggerTimetable": ".definitions.timetables.trigger",
-    "CronPartitionTimetable": ".definitions.timetables.trigger",
-    "DAG": ".definitions.dag",
-    "DagRunState": ".api.datamodels._generated",
-    "DayWindow": ".definitions.partition_mappers.window",
-    "DeadlineAlert": ".definitions.deadline",
-    "DeadlineReference": ".definitions.deadline",
-    "DeltaDataIntervalTimetable": ".definitions.timetables.interval",
-    "DeltaTriggerTimetable": ".definitions.timetables.trigger",
-    "EdgeModifier": ".definitions.edges",
-    "EventsTimetable": ".definitions.timetables.events",
-    "ExceptionRetryPolicy": ".definitions.retry_policy",
-    "FanOutMapper": ".definitions.partition_mappers.temporal",
-    "FixedKeyMapper": ".definitions.partition_mappers.fixed_key",
-    "HourWindow": ".definitions.partition_mappers.window",
-    "IdentityMapper": ".definitions.partition_mappers.identity",
-    "Label": ".definitions.edges",
-    "LoopDecision": ".definitions.loop",
-    "Metadata": ".definitions.asset.metadata",
-    "MinimumCount": ".definitions.partition_mappers.wait_policy",
-    "MonthWindow": ".definitions.partition_mappers.window",
-    "MultipleCronTriggerTimetable": ".definitions.timetables.trigger",
-    "ObjectStoragePath": ".io.path",
-    "Param": ".definitions.param",
-    "ParamsDict": ".definitions.param",
-    "PartitionedAtRuntime": ".definitions.timetables.assets",
-    "PartitionedAssetTimetable": ".definitions.timetables.assets",
-    "PartitionMapper": ".definitions.partition_mappers.base",
-    "PokeReturnValue": ".bases.sensor",
-    "ProductMapper": ".definitions.partition_mappers.product",
-    "QuarterWindow": ".definitions.partition_mappers.window",
-    "ResumableJobMixin": ".bases.resumablejobmixin",
-    "RetryAction": ".definitions.retry_policy",
-    "RetryDecision": ".definitions.retry_policy",
-    "RetryPolicy": ".definitions.retry_policy",
-    "RetryRule": ".definitions.retry_policy",
-    "RollupMapper": ".definitions.partition_mappers.base",
-    "SecretCache": ".execution_time.cache",
-    "SegmentWindow": ".definitions.partition_mappers.window",
-    "SkipMixin": ".bases.skipmixin",
-    "SyncCallback": ".definitions.callback",
-    "StartOfDayMapper": ".definitions.partition_mappers.temporal",
-    "StartOfHourMapper": ".definitions.partition_mappers.temporal",
-    "StartOfMonthMapper": ".definitions.partition_mappers.temporal",
-    "StartOfQuarterMapper": ".definitions.partition_mappers.temporal",
-    "StartOfWeekMapper": ".definitions.partition_mappers.temporal",
-    "StartOfYearMapper": ".definitions.partition_mappers.temporal",
-    "TaskGroup": ".definitions.taskgroup",
-    "TaskInstance": ".types",
-    "TaskInstanceState": ".api.datamodels._generated",
-    "TriggerRule": ".api.datamodels._generated",
-    "Variable": ".definitions.variable",
-    "WaitForAll": ".definitions.partition_mappers.wait_policy",
-    "WeekWindow": ".definitions.partition_mappers.window",
-    "WeightRule": ".api.datamodels._generated",
-    "Window": ".definitions.partition_mappers.window",
-    "XComArg": ".definitions.xcom_arg",
-    "YearWindow": ".definitions.partition_mappers.window",
-    "asset": ".definitions.asset.decorators",
-    "chain": ".bases.operator",
-    "chain_linear": ".bases.operator",
-    "conf": ".configuration",
-    "cross_downstream": ".bases.operator",
-    "dag": ".definitions.dag",
-    "deadline_reference": ".definitions.deadline",
-    "NEVER_EXPIRE": ".execution_time.context",
-    "get_current_context": ".definitions.context",
-    "get_parsing_context": ".definitions.context",
-    "literal": ".definitions.template",
-    "lineage": ".lineage",
-    "macros": ".execution_time",
-    "result": ".definitions.decorators",
-    "setup": ".definitions.decorators",
-    "task": ".definitions.decorators",
-    "task_group": ".definitions.decorators",
-    "teardown": ".definitions.decorators",
+# Things to lazy import in form {local_name: (target_module, target_name)}.
+# An empty target_name exposes the target module itself.
+__lazy_imports: dict[str, tuple[str, str]] = {
+    "AllowedKeyMapper": (".definitions.partition_mappers.allowed_key", 
"AllowedKeyMapper"),
+    "Asset": (".definitions.asset", "Asset"),
+    "AssetAccessControl": (".definitions.asset", "AssetAccessControl"),
+    "AssetAlias": (".definitions.asset", "AssetAlias"),
+    "AssetAll": (".definitions.asset", "AssetAll"),
+    "AssetAndTimeSchedule": (".definitions.timetables.assets", 
"AssetAndTimeSchedule"),
+    "AssetAny": (".definitions.asset", "AssetAny"),
+    "AssetOrTimeSchedule": (".definitions.timetables.assets", 
"AssetOrTimeSchedule"),
+    "AssetTriggeredTimetable": (".definitions.timetables.assets", 
"AssetTriggeredTimetable"),
+    "AssetWatcher": (".definitions.asset", "AssetWatcher"),
+    "AsyncCallback": (".definitions.callback", "AsyncCallback"),
+    "BaseAsyncOperator": (".bases.operator", "BaseAsyncOperator"),
+    "BaseBranchOperator": (".bases.branch", "BaseBranchOperator"),
+    "BaseDeadlineReference": (".definitions.deadline", 
"BaseDeadlineReference"),
+    "BaseHook": (".bases.hook", "BaseHook"),
+    "BaseNotifier": (".bases.notifier", "BaseNotifier"),
+    "BaseOperator": (".bases.operator", "BaseOperator"),
+    "BaseOperatorLink": (".bases.operatorlink", "BaseOperatorLink"),
+    "BaseSensorOperator": (".bases.sensor", "BaseSensorOperator"),
+    "BaseXCom": (".bases.xcom", "BaseXCom"),
+    "BranchMixIn": (".bases.branch", "BranchMixIn"),
+    "ChainMapper": (".definitions.partition_mappers.chain", "ChainMapper"),
+    "ChainRetryPolicy": (".definitions.retry_policy", "ChainRetryPolicy"),
+    "Connection": (".definitions.connection", "Connection"),
+    "Context": (".definitions.context", "Context"),
+    "CronDataIntervalTimetable": (".definitions.timetables.interval", 
"CronDataIntervalTimetable"),
+    "CronTriggerTimetable": (".definitions.timetables.trigger", 
"CronTriggerTimetable"),
+    "CronPartitionTimetable": (".definitions.timetables.trigger", 
"CronPartitionTimetable"),
+    "DAG": (".definitions.dag", "DAG"),
+    "DagRunState": (".api.datamodels._generated", "DagRunState"),
+    "DayWindow": (".definitions.partition_mappers.window", "DayWindow"),
+    "DeadlineAlert": (".definitions.deadline", "DeadlineAlert"),
+    "DeadlineReference": (".definitions.deadline", "DeadlineReference"),
+    "DeltaDataIntervalTimetable": (".definitions.timetables.interval", 
"DeltaDataIntervalTimetable"),
+    "DeltaTriggerTimetable": (".definitions.timetables.trigger", 
"DeltaTriggerTimetable"),
+    "EdgeModifier": (".definitions.edges", "EdgeModifier"),
+    "EventsTimetable": (".definitions.timetables.events", "EventsTimetable"),
+    "ExceptionRetryPolicy": (".definitions.retry_policy", 
"ExceptionRetryPolicy"),
+    "FanOutMapper": (".definitions.partition_mappers.temporal", 
"FanOutMapper"),
+    "FixedKeyMapper": (".definitions.partition_mappers.fixed_key", 
"FixedKeyMapper"),
+    "HourWindow": (".definitions.partition_mappers.window", "HourWindow"),
+    "IdentityMapper": (".definitions.partition_mappers.identity", 
"IdentityMapper"),
+    "Label": (".definitions.edges", "Label"),
+    "LoopDecision": (".definitions.loop", "LoopDecision"),
+    "Metadata": (".definitions.asset.metadata", "Metadata"),
+    "MinimumCount": (".definitions.partition_mappers.wait_policy", 
"MinimumCount"),
+    "MonthWindow": (".definitions.partition_mappers.window", "MonthWindow"),
+    "MultipleCronTriggerTimetable": (".definitions.timetables.trigger", 
"MultipleCronTriggerTimetable"),
+    "ObjectStoragePath": (".io.path", "ObjectStoragePath"),
+    "Param": (".definitions.param", "Param"),
+    "ParamsDict": (".definitions.param", "ParamsDict"),
+    "PartitionedAtRuntime": (".definitions.timetables.assets", 
"PartitionedAtRuntime"),
+    "PartitionedAssetTimetable": (".definitions.timetables.assets", 
"PartitionedAssetTimetable"),
+    "PartitionMapper": (".definitions.partition_mappers.base", 
"PartitionMapper"),
+    "PokeReturnValue": (".bases.sensor", "PokeReturnValue"),
+    "ProductMapper": (".definitions.partition_mappers.product", 
"ProductMapper"),
+    "QuarterWindow": (".definitions.partition_mappers.window", 
"QuarterWindow"),
+    "ResumableJobMixin": (".bases.resumablejobmixin", "ResumableJobMixin"),
+    "RetryAction": (".definitions.retry_policy", "RetryAction"),
+    "RetryDecision": (".definitions.retry_policy", "RetryDecision"),
+    "RetryPolicy": (".definitions.retry_policy", "RetryPolicy"),
+    "RetryRule": (".definitions.retry_policy", "RetryRule"),
+    "RollupMapper": (".definitions.partition_mappers.base", "RollupMapper"),
+    "SecretCache": (".execution_time.cache", "SecretCache"),
+    "SegmentWindow": (".definitions.partition_mappers.window", 
"SegmentWindow"),
+    "SkipMixin": (".bases.skipmixin", "SkipMixin"),
+    "SyncCallback": (".definitions.callback", "SyncCallback"),
+    "StartOfDayMapper": (".definitions.partition_mappers.temporal", 
"StartOfDayMapper"),
+    "StartOfHourMapper": (".definitions.partition_mappers.temporal", 
"StartOfHourMapper"),
+    "StartOfMonthMapper": (".definitions.partition_mappers.temporal", 
"StartOfMonthMapper"),
+    "StartOfQuarterMapper": (".definitions.partition_mappers.temporal", 
"StartOfQuarterMapper"),
+    "StartOfWeekMapper": (".definitions.partition_mappers.temporal", 
"StartOfWeekMapper"),
+    "StartOfYearMapper": (".definitions.partition_mappers.temporal", 
"StartOfYearMapper"),
+    "TaskGroup": (".definitions.taskgroup", "TaskGroup"),
+    "TaskInstance": (".types", "TaskInstance"),
+    "TaskInstanceState": (".api.datamodels._generated", "TaskInstanceState"),
+    "TriggerRule": (".api.datamodels._generated", "TriggerRule"),
+    "Variable": (".definitions.variable", "Variable"),
+    "WaitForAll": (".definitions.partition_mappers.wait_policy", "WaitForAll"),
+    "WeekWindow": (".definitions.partition_mappers.window", "WeekWindow"),
+    "WeightRule": (".api.datamodels._generated", "WeightRule"),
+    "Window": (".definitions.partition_mappers.window", "Window"),
+    "XComArg": (".definitions.xcom_arg", "XComArg"),
+    "YearWindow": (".definitions.partition_mappers.window", "YearWindow"),
+    "asset": (".definitions.asset.decorators", "asset"),
+    "chain": (".bases.operator", "chain"),
+    "chain_linear": (".bases.operator", "chain_linear"),
+    "conf": (".configuration", "conf"),
+    "cross_downstream": (".bases.operator", "cross_downstream"),
+    "dag": (".definitions.dag", "dag"),
+    "deadline_reference": (".definitions.deadline", "deadline_reference"),
+    "NEVER_EXPIRE": (".execution_time.context", "NEVER_EXPIRE"),
+    "get_current_context": (".definitions.context", "get_current_context"),
+    "get_parsing_context": (".definitions.context", "get_parsing_context"),
+    "literal": (".definitions.template", "literal"),
+    "lineage": (".lineage", ""),
+    "macros": (".execution_time.macros", ""),
+    "result": (".definitions.decorators", "result"),
+    "setup": (".definitions.decorators", "setup"),
+    "task": (".definitions.decorators", "task"),
+    "task_group": (".definitions.decorators", "task_group"),
+    "teardown": (".definitions.decorators", "teardown"),
 }
 
 
 def __getattr__(name: str):
-    if module_path := __lazy_imports.get(name):
-        import importlib
+    module_path, attr_name = __lazy_imports.get(name, ("", ""))
+    if not module_path:
+        raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
 
-        mod = importlib.import_module(module_path, __name__)
-        val = getattr(mod, name)
+    import importlib
 
-        # Store for next time
-        globals()[name] = val
-        return val
-    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+    mod = importlib.import_module(module_path, __name__)
+    val = getattr(mod, attr_name) if attr_name else mod
+
+    # Store for next time
+    globals()[name] = val
+    return val
diff --git a/task-sdk/tests/task_sdk/docs/test_public_api.py 
b/task-sdk/tests/task_sdk/docs/test_public_api.py
index eea7740cc4b..0b00b32bc28 100644
--- a/task-sdk/tests/task_sdk/docs/test_public_api.py
+++ b/task-sdk/tests/task_sdk/docs/test_public_api.py
@@ -17,19 +17,29 @@
 from __future__ import annotations
 
 import importlib
+import subprocess
+import sys
+import textwrap
 
 
 def test_airflow_sdk_exports_exist():
     """
-    Ensure that all names declared in airflow.sdk.__all__ are present as 
attributes on the module.
+    Ensure every name in airflow.sdk.__all__ resolves through the lazy 
``__getattr__``.
+
+    This runs in a subprocess because in a shared test process, a submodule 
export
+    (e.g. ``lineage``) may already be bound on the package as an import side 
effect,
+    which would mask a broken lazy-import entry.
     """
-    sdk = importlib.import_module("airflow.sdk")
-    # Provide literal attribute for testing since it's declared in __all__
-    template_mod = importlib.import_module("airflow.sdk.definitions.template")
-    setattr(sdk, "literal", getattr(template_mod, "literal"))
-    public_names = getattr(sdk, "__all__", [])
-    missing = [name for name in public_names if not hasattr(sdk, name)]
-    assert not missing, f"Missing exports in airflow.sdk: {missing}"
+    code = textwrap.dedent(
+        """
+        import airflow.sdk as sdk
+
+        missing = [name for name in sdk.__all__ if not hasattr(sdk, name)]
+        assert not missing, f"Missing exports in airflow.sdk: {missing}"
+        """
+    )
+    result = subprocess.run([sys.executable, "-c", code], check=False, 
capture_output=True, text=True)
+    assert result.returncode == 0, result.stderr
 
 
 def test_airflow_sdk_no_unexpected_exports():

Reply via email to