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

vincbeck 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 9b02bafa0a6 Add SQLA's `mapped_column` to common-compat (#56880)
9b02bafa0a6 is described below

commit 9b02bafa0a60d24bdc65ac12f726b605a8e89b68
Author: Dev-iL <[email protected]>
AuthorDate: Mon Oct 20 20:53:06 2025 +0300

    Add SQLA's `mapped_column` to common-compat (#56880)
    
    This code is copied from airflow.utils.sqlalchemy so it can be used in 
providers.
---
 .../providers/common/compat/sqlalchemy/__init__.py |  16 +++
 .../providers/common/compat/sqlalchemy/orm.py      |  27 ++++
 .../unit/common/compat/sqlalchemy/__init__.py      |  16 +++
 .../unit/common/compat/sqlalchemy/test_orm.py      | 145 +++++++++++++++++++++
 4 files changed, 204 insertions(+)

diff --git 
a/providers/common/compat/src/airflow/providers/common/compat/sqlalchemy/__init__.py
 
b/providers/common/compat/src/airflow/providers/common/compat/sqlalchemy/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ 
b/providers/common/compat/src/airflow/providers/common/compat/sqlalchemy/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git 
a/providers/common/compat/src/airflow/providers/common/compat/sqlalchemy/orm.py 
b/providers/common/compat/src/airflow/providers/common/compat/sqlalchemy/orm.py
new file mode 100644
index 00000000000..47b8d412eb8
--- /dev/null
+++ 
b/providers/common/compat/src/airflow/providers/common/compat/sqlalchemy/orm.py
@@ -0,0 +1,27 @@
+# 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
+
+try:
+    from sqlalchemy.orm import mapped_column
+except ImportError:
+    # fallback for SQLAlchemy < 2.0
+    def mapped_column(*args, **kwargs):
+        from sqlalchemy import Column
+
+        return Column(*args, **kwargs)
diff --git 
a/providers/common/compat/tests/unit/common/compat/sqlalchemy/__init__.py 
b/providers/common/compat/tests/unit/common/compat/sqlalchemy/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/providers/common/compat/tests/unit/common/compat/sqlalchemy/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git 
a/providers/common/compat/tests/unit/common/compat/sqlalchemy/test_orm.py 
b/providers/common/compat/tests/unit/common/compat/sqlalchemy/test_orm.py
new file mode 100644
index 00000000000..1efa4783899
--- /dev/null
+++ b/providers/common/compat/tests/unit/common/compat/sqlalchemy/test_orm.py
@@ -0,0 +1,145 @@
+# 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
+
+import builtins
+import importlib
+import sys
+import types
+from collections.abc import Callable
+from typing import Any, cast
+
+import pytest
+
+TARGET = "airflow.providers.common.compat.sqlalchemy.orm"
+
+
[email protected](autouse=True)
+def clean_target():
+    """Ensure the target module is removed from sys.modules before each 
test."""
+    sys.modules.pop(TARGET, None)
+    yield
+    sys.modules.pop(TARGET, None)
+
+
+def reload_target() -> Any:
+    """Import the compatibility shim after the monkey‑patched environment is 
set."""
+    return importlib.import_module(TARGET)
+
+
+# ----------------------------------------------------------------------
+# Helper factories for the fake sqlalchemy packages
+# ----------------------------------------------------------------------
+def make_fake_sqlalchemy(
+    *,
+    has_mapped_column: bool = False,
+    column_impl: Callable[..., tuple] | None = None,
+) -> tuple[Any, Any]:
+    """Return a tuple `(sqlalchemy_pkg, orm_pkg)` that mimics the requested 
feature set."""
+    # Cast the ModuleType to Any so static type checkers don't complain when we
+    # dynamically add attributes like `Column`, `orm` or `mapped_column`.
+    sqlalchemy_pkg = cast("Any", types.ModuleType("sqlalchemy"))
+    orm_pkg = cast("Any", types.ModuleType("sqlalchemy.orm"))
+
+    # Provide Column implementation (used by the fallback)
+    if column_impl is None:
+        column_impl = lambda *a, **kw: ("Column_called", a, kw)
+
+    sqlalchemy_pkg.Column = column_impl
+
+    if has_mapped_column:
+        orm_pkg.mapped_column = lambda *a, **kw: ("mapped_column_called", a, 
kw)
+
+    sqlalchemy_pkg.orm = orm_pkg
+    return sqlalchemy_pkg, orm_pkg
+
+
+# ----------------------------------------------------------------------
+# Parametrised tests
+# ----------------------------------------------------------------------
[email protected](
+    ("has_mapped", "expect_fallback"),
+    [
+        (True, False),  # real mapped_column present
+        (False, True),  # fallback to Column
+    ],
+)
+def test_mapped_column_resolution(monkeypatch, has_mapped, expect_fallback):
+    sqlalchemy_pkg, orm_pkg = 
make_fake_sqlalchemy(has_mapped_column=has_mapped)
+    monkeypatch.setitem(sys.modules, "sqlalchemy", sqlalchemy_pkg)
+    monkeypatch.setitem(sys.modules, "sqlalchemy.orm", orm_pkg)
+
+    mod = reload_target()
+
+    # The shim must expose a callable named `mapped_column`
+    assert callable(mod.mapped_column)
+
+    # Verify that the correct implementation is used
+    result = mod.mapped_column(1, a=2)
+
+    if expect_fallback:
+        assert result == ("Column_called", (1,), {"a": 2})
+    else:
+        assert result == ("mapped_column_called", (1,), {"a": 2})
+
+
+def test_fallback_call_shapes(monkeypatch):
+    """Exercise a handful of call signatures on the fallback."""
+    sqlalchemy_pkg, orm_pkg = make_fake_sqlalchemy(has_mapped_column=False)
+    monkeypatch.setitem(sys.modules, "sqlalchemy", sqlalchemy_pkg)
+    monkeypatch.setitem(sys.modules, "sqlalchemy.orm", orm_pkg)
+
+    mod = reload_target()
+
+    # No‑arg call
+    assert mod.mapped_column() == ("Column_called", (), {})
+
+    # Mixed positional / keyword
+    assert mod.mapped_column(1, 2, a=3, b=4) == (
+        "Column_called",
+        (1, 2),
+        {"a": 3, "b": 4},
+    )
+
+
+def test_importerror_while_importing_sqlalchemy_orm(monkeypatch):
+    """Simulate an ImportError raised *during* the import of sqlalchemy.orm."""
+    sqlalchemy_pkg = cast("Any", types.ModuleType("sqlalchemy"))
+    sqlalchemy_pkg.Column = lambda *a, **kw: ("Column_called", a, kw)
+
+    monkeypatch.setitem(sys.modules, "sqlalchemy", sqlalchemy_pkg)
+
+    # Force ImportError for any attempt to import sqlalchemy.orm
+    real_import = __import__
+
+    def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
+        if name.startswith("sqlalchemy.orm"):
+            raise ImportError("simulated failure")
+        return real_import(name, globals, locals, fromlist, level)
+
+    monkeypatch.setattr(builtins, "__import__", fake_import)
+
+    try:
+        mod = reload_target()
+    finally:
+        # Restore the original import function - pytest's monkeypatch will also
+        # do this, but we keep the explicit finally for clarity.
+        monkeypatch.setattr(builtins, "__import__", real_import)
+
+    assert callable(mod.mapped_column)
+    assert mod.mapped_column("abc") == ("Column_called", ("abc",), {})

Reply via email to