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

mobuchowski 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 a3bf92fe855 Fix lineage entities sharing mutable defaults across 
instances (#72509)
a3bf92fe855 is described below

commit a3bf92fe855e1d97acf9ee8712582df222e98add
Author: Keith <[email protected]>
AuthorDate: Thu Sep 10 06:51:50 2026 +0900

    Fix lineage entities sharing mutable defaults across instances (#72509)
    
    Bare mutable defaults on attrs classes are evaluated once and shared
    class-wide, so tagging one Table (or Column) silently mutated the
    tags/columns/owners/extra of every other instance — corrupting lineage
    metadata whenever more than one entity was in play.
---
 .../providers/common/compat/lineage/entities.py    | 10 ++---
 .../unit/common/compat/lineage/test_entities.py    | 47 ++++++++++++++++++++++
 2 files changed, 52 insertions(+), 5 deletions(-)

diff --git 
a/providers/common/compat/src/airflow/providers/common/compat/lineage/entities.py
 
b/providers/common/compat/src/airflow/providers/common/compat/lineage/entities.py
index 74023f4ddba..f6103f796af 100644
--- 
a/providers/common/compat/src/airflow/providers/common/compat/lineage/entities.py
+++ 
b/providers/common/compat/src/airflow/providers/common/compat/lineage/entities.py
@@ -61,7 +61,7 @@ class Column:
     name: str = attr.ib()
     description: str | None = None
     data_type: str = attr.ib()
-    tags: list[Tag] = []
+    tags: list[Tag] = attr.Factory(list)
 
     template_fields: ClassVar = ("name", "description", "data_type", "tags")
 
@@ -83,11 +83,11 @@ class Table:
     database: str = attr.ib()
     cluster: str = attr.ib()
     name: str = attr.ib()
-    tags: list[Tag] = []
+    tags: list[Tag] = attr.Factory(list)
     description: str | None = None
-    columns: list[Column] = []
-    owners: list[User] = []
-    extra: dict[str, Any] = {}
+    columns: list[Column] = attr.Factory(list)
+    owners: list[User] = attr.Factory(list)
+    extra: dict[str, Any] = attr.Factory(dict)
     type_hint: str | None = None
 
     template_fields: ClassVar = (
diff --git 
a/providers/common/compat/tests/unit/common/compat/lineage/test_entities.py 
b/providers/common/compat/tests/unit/common/compat/lineage/test_entities.py
new file mode 100644
index 00000000000..9ec0ea72675
--- /dev/null
+++ b/providers/common/compat/tests/unit/common/compat/lineage/test_entities.py
@@ -0,0 +1,47 @@
+# 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.
+from __future__ import annotations
+
+from airflow.providers.common.compat.lineage.entities import Column, Table, 
Tag, User
+
+
+class TestMutableDefaultsAreNotShared:
+    """Each instance must get its own container; a bare ``= []`` default is 
shared class-wide."""
+
+    def test_column_tags_are_isolated_between_instances(self):
+        first = Column(name="a", data_type="INT")
+        second = Column(name="b", data_type="INT")
+
+        first.tags.append(Tag(tag_name="pii"))
+
+        assert first.tags == [Tag(tag_name="pii")]
+        assert second.tags == []
+        assert first.tags is not second.tags
+
+    def test_table_containers_are_isolated_between_instances(self):
+        first = Table(database="db", cluster="cluster", name="first")
+        second = Table(database="db", cluster="cluster", name="second")
+
+        first.tags.append(Tag(tag_name="pii"))
+        first.columns.append(Column(name="id", data_type="INT"))
+        first.owners.append(User(email="[email protected]"))
+        first.extra["source"] = "warehouse"
+
+        assert second.tags == []
+        assert second.columns == []
+        assert second.owners == []
+        assert second.extra == {}

Reply via email to