Skip to content

Commit fb8cf6f

Browse files
committed
Move psycopg2-binary from a hard dependency to an optional extra
Mirrors the async default switch (apache#69089): PostgresHook no longer requires psycopg2 at import time. The psycopg2-specific connection, cursor, and execute_values imports are now lazy/guarded and raise a clear AirflowOptionalProviderFeatureException if psycopg2 genuinely needs to be used but isn't installed. psycopg2-binary moves to a new [psycopg2] extra. Part of the migration tracked in apache#68453.
1 parent e4fdf69 commit fb8cf6f

7 files changed

Lines changed: 86 additions & 23 deletions

File tree

providers/postgres/README.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,15 @@ The package supports the following python versions: 3.10,3.11,3.12,3.13,3.14
5050
Requirements
5151
------------
5252

53-
========================================== ======================================
53+
========================================== =====================================
5454
PIP package Version required
55-
========================================== ======================================
55+
========================================== =====================================
5656
``apache-airflow`` ``>=2.11.0``
5757
``apache-airflow-providers-common-compat`` ``>=1.12.0``
5858
``apache-airflow-providers-common-sql`` ``>=1.32.0``
59-
``psycopg2-binary`` ``>=2.9.9; python_version < "3.13"``
60-
``psycopg2-binary`` ``>=2.9.10; python_version >= "3.13"``
6159
``psycopg[binary]`` ``>=3.2.9; python_version < "3.14"``
6260
``psycopg[binary]`` ``>=3.3.3; python_version >= "3.14"``
63-
========================================== ======================================
61+
========================================== =====================================
6462

6563
Optional cross provider package dependencies
6664
--------------------------------------------
@@ -95,6 +93,7 @@ Extra Dependencies
9593
``openlineage`` ``apache-airflow-providers-openlineage``
9694
``pandas`` ``pandas>=2.1.2; python_version <"3.13"``, ``pandas>=2.2.3; python_version >="3.13" and python_version <"3.14"``, ``pandas>=2.3.3; python_version >="3.14"``
9795
``polars`` ``polars>=1.26.0``
96+
``psycopg2`` ``psycopg2-binary>=2.9.9; python_version < "3.13"``, ``psycopg2-binary>=2.9.10; python_version >= "3.13"``
9897
``sqlalchemy`` ``sqlalchemy>=1.4.54``
9998
=================== ============================================================================================================================================================
10099

providers/postgres/docs/changelog.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ Breaking changes
4242
``apache-airflow-providers-postgres[asyncpg]`` and set
4343
``[database] sql_alchemy_conn_async = postgresql+asyncpg://...`` explicitly.
4444

45+
.. note::
46+
The default synchronous metadata-database driver is now ``psycopg`` (psycopg3), mirroring the
47+
async default above. ``psycopg2-binary`` is no longer installed by default; it moved from a hard
48+
dependency to the new ``[psycopg2]`` optional extra.
49+
50+
A bare ``postgresql://`` or legacy ``postgres://`` / ``postgres+psycopg2://`` ``sql_alchemy_conn``
51+
is now rewritten to ``postgresql+psycopg://`` instead of ``postgresql+psycopg2://``. An explicit
52+
``postgresql+psycopg2://`` connection string is never rewritten. To keep using psycopg2, install
53+
``apache-airflow-providers-postgres[psycopg2]`` and keep (or set)
54+
``[database] sql_alchemy_conn = postgresql+psycopg2://...`` explicitly.
55+
4556
6.8.0
4657
.....
4758

providers/postgres/docs/index.rst

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,15 @@ Requirements
9898

9999
The minimum Apache Airflow version supported by this provider distribution is ``2.11.0``.
100100

101-
========================================== ======================================
101+
========================================== =====================================
102102
PIP package Version required
103-
========================================== ======================================
103+
========================================== =====================================
104104
``apache-airflow`` ``>=2.11.0``
105105
``apache-airflow-providers-common-compat`` ``>=1.12.0``
106106
``apache-airflow-providers-common-sql`` ``>=1.32.0``
107-
``psycopg2-binary`` ``>=2.9.9; python_version < "3.13"``
108-
``psycopg2-binary`` ``>=2.9.10; python_version >= "3.13"``
109107
``psycopg[binary]`` ``>=3.2.9; python_version < "3.14"``
110108
``psycopg[binary]`` ``>=3.3.3; python_version >= "3.14"``
111-
========================================== ======================================
109+
========================================== =====================================
112110

113111
Optional cross provider package dependencies
114112
--------------------------------------------

providers/postgres/pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,6 @@ dependencies = [
6262
"apache-airflow>=2.11.0",
6363
"apache-airflow-providers-common-compat>=1.12.0",
6464
"apache-airflow-providers-common-sql>=1.32.0",
65-
# psycopg2 remains the sync driver for now; its removal and the sync migration to
66-
# psycopg3 are tracked at https://github.com/apache/airflow/issues/68453
67-
"psycopg2-binary>=2.9.9; python_version < '3.13'",
68-
"psycopg2-binary>=2.9.10; python_version >= '3.13'",
6965
"psycopg[binary]>=3.2.9; python_version < '3.14'",
7066
"psycopg[binary]>=3.3.3; python_version >= '3.14'",
7167
]
@@ -93,6 +89,10 @@ dependencies = [
9389
"polars" = [
9490
"polars>=1.26.0"
9591
]
92+
"psycopg2" = [
93+
"psycopg2-binary>=2.9.9; python_version < '3.13'",
94+
"psycopg2-binary>=2.9.10; python_version >= '3.13'",
95+
]
9696
"sqlalchemy" = [
9797
"sqlalchemy>=1.4.54"
9898
]

providers/postgres/src/airflow/providers/postgres/hooks/postgres.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@
1818
from __future__ import annotations
1919

2020
import os
21-
from collections.abc import Iterable, Mapping
21+
from collections.abc import Callable, Iterable, Mapping
2222
from contextlib import closing
2323
from copy import deepcopy
24-
from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias, cast, overload
24+
from typing import TYPE_CHECKING, Any, Literal, NoReturn, Protocol, TypeAlias, cast, overload
2525

2626
from more_itertools import chunked
27-
from psycopg2 import connect as ppg2_connect
28-
from psycopg2.extras import DictCursor, NamedTupleCursor, RealDictCursor, execute_values
2927

3028
from airflow.providers.common.compat.sdk import (
3129
AirflowException,
@@ -54,9 +52,35 @@
5452
from psycopg.rows import dict_row, namedtuple_row
5553
from psycopg.types.json import register_default_adapters
5654

55+
try:
56+
import psycopg2 as _psycopg2
57+
import psycopg2.extras as _psycopg2_extras
58+
except (ImportError, ModuleNotFoundError):
59+
_psycopg2 = None
60+
_psycopg2_extras = None
61+
62+
ppg2_connect: Callable[..., Any] | None = _psycopg2.connect if _psycopg2 else None
63+
DictCursor: type | None = _psycopg2_extras.DictCursor if _psycopg2_extras else None
64+
NamedTupleCursor: type | None = _psycopg2_extras.NamedTupleCursor if _psycopg2_extras else None
65+
RealDictCursor: type | None = _psycopg2_extras.RealDictCursor if _psycopg2_extras else None
66+
execute_values: Callable[..., Any] | None = _psycopg2_extras.execute_values if _psycopg2_extras else None
67+
68+
69+
def _require_psycopg2() -> NoReturn:
70+
raise AirflowOptionalProviderFeatureException(
71+
"psycopg2 is not installed. Please install it with "
72+
"`pip install apache-airflow-providers-postgres[psycopg2]`."
73+
)
74+
75+
5776
if TYPE_CHECKING:
5877
from pandas import DataFrame as PandasDataFrame
5978
from polars import DataFrame as PolarsDataFrame
79+
from psycopg2.extras import (
80+
DictCursor as _DictCursorType,
81+
NamedTupleCursor as _NamedTupleCursorType,
82+
RealDictCursor as _RealDictCursorType,
83+
)
6084
from sqlalchemy.engine import URL
6185

6286
from airflow.providers.common.sql.dialects.dialect import Dialect
@@ -65,7 +89,7 @@
6589
if USE_PSYCOPG3:
6690
from psycopg.errors import Diagnostic
6791

68-
CursorType: TypeAlias = DictCursor | RealDictCursor | NamedTupleCursor
92+
CursorType: TypeAlias = _DictCursorType | _RealDictCursorType | _NamedTupleCursorType
6993
CursorRow: TypeAlias = dict[str, Any] | tuple[Any, ...]
7094

7195

@@ -204,6 +228,9 @@ def _get_cursor(self, raw_cursor: str) -> CursorType:
204228
valid_cursors = "dictcursor, namedtuplecursor"
205229
raise ValueError(f"Invalid cursor passed {_cursor}. Valid options are: {valid_cursors}")
206230

231+
if DictCursor is None:
232+
_require_psycopg2()
233+
207234
cursor_types = {
208235
"dictcursor": DictCursor,
209236
"realdictcursor": RealDictCursor,
@@ -235,6 +262,9 @@ def _create_connection(self, conn_args: dict[str, Any]) -> CompatConnection:
235262

236263
return connection
237264

265+
if ppg2_connect is None:
266+
_require_psycopg2()
267+
238268
return ppg2_connect(**conn_args)
239269

240270
def _generate_cursor_name(self):
@@ -691,6 +721,8 @@ def insert_rows(
691721
)
692722

693723
# if fast_executemany is enabled with psycopg2, use optimized execute_values from psycopg
724+
if execute_values is None:
725+
_require_psycopg2()
694726
self._insert_statement_format = "INSERT INTO {} {} VALUES %s"
695727

696728
nb_rows = 0

providers/postgres/tests/unit/postgres/hooks/test_postgres.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@
5858
import psycopg2.extras
5959

6060

61+
def test_hooks_postgres_raises_clear_error_without_psycopg2(monkeypatch):
62+
"""PostgresHook must fail loudly (not with a bare ImportError or a real connection attempt)
63+
when psycopg2-specific functionality is used but psycopg2 isn't installed."""
64+
import airflow.providers.postgres.hooks.postgres as postgres_module
65+
66+
monkeypatch.setattr(postgres_module, "USE_PSYCOPG3", False)
67+
monkeypatch.setattr(postgres_module, "ppg2_connect", None)
68+
monkeypatch.setattr(postgres_module, "DictCursor", None)
69+
monkeypatch.setattr(postgres_module, "RealDictCursor", None)
70+
monkeypatch.setattr(postgres_module, "NamedTupleCursor", None)
71+
monkeypatch.setattr(postgres_module, "execute_values", None)
72+
73+
hook = postgres_module.PostgresHook.__new__(postgres_module.PostgresHook)
74+
with pytest.raises(AirflowOptionalProviderFeatureException, match="psycopg2 is not installed"):
75+
hook._get_cursor("dictcursor")
76+
with pytest.raises(AirflowOptionalProviderFeatureException, match="psycopg2 is not installed"):
77+
hook._create_connection({})
78+
with pytest.raises(AirflowOptionalProviderFeatureException, match="psycopg2 is not installed"):
79+
hook.insert_rows(table="t", rows=[(1,)], fast_executemany=True)
80+
81+
6182
@pytest.fixture
6283
def mock_connect(mocker):
6384
"""Mock the connection object according to the correct psycopg version."""

uv.lock

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)