diff --git a/doc/source/whatsnew/v1.4.1.rst b/doc/source/whatsnew/v1.4.1.rst index 9509b96055255..ec4a1b7348262 100644 --- a/doc/source/whatsnew/v1.4.1.rst +++ b/doc/source/whatsnew/v1.4.1.rst @@ -20,7 +20,8 @@ Fixed regressions - Regression in :meth:`DataFrame.iat` setting values leading to not propagating correctly in subsequent lookups (:issue:`45684`) - Regression in :meth:`DataFrame.loc.__setitem__` losing :class:`Index` name if :class:`DataFrame` was empty before (:issue:`45621`) - Regression in :func:`join` with overlapping :class:`IntervalIndex` raising an ``InvalidIndexError`` (:issue:`45661`) - +- Regression in :func:`read_sql` with a DBAPI2 connection that is not an instance of ``sqlite3.Connection`` incorrectly requiring SQLAlchemy be installed (:issue:`45660`) +- .. --------------------------------------------------------------------------- diff --git a/pandas/io/sql.py b/pandas/io/sql.py index fcb3f5177ae3f..ba18412856d6c 100644 --- a/pandas/io/sql.py +++ b/pandas/io/sql.py @@ -736,12 +736,15 @@ def pandasSQL_builder(con, schema: str | None = None): if isinstance(con, sqlite3.Connection) or con is None: return SQLiteDatabase(con) - sqlalchemy = import_optional_dependency("sqlalchemy") + sqlalchemy = import_optional_dependency("sqlalchemy", errors="ignore") if isinstance(con, str): - con = sqlalchemy.create_engine(con) + if sqlalchemy is None: + raise ImportError("Using URI string without sqlalchemy installed.") + else: + con = sqlalchemy.create_engine(con) - if isinstance(con, sqlalchemy.engine.Connectable): + if sqlalchemy is not None and isinstance(con, sqlalchemy.engine.Connectable): return SQLDatabase(con, schema=schema) warnings.warn( diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index 584308db3bae8..e383617c020aa 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -1532,9 +1532,25 @@ def test_sql_open_close(self, test_frame3): @pytest.mark.skipif(SQLALCHEMY_INSTALLED, reason="SQLAlchemy is installed") def test_con_string_import_error(self): conn = "mysql://root@localhost/pandas" - with pytest.raises(ImportError, match="SQLAlchemy"): + msg = "Using URI string without sqlalchemy installed" + with pytest.raises(ImportError, match=msg): sql.read_sql("SELECT * FROM iris", conn) + @pytest.mark.skipif(SQLALCHEMY_INSTALLED, reason="SQLAlchemy is installed") + def test_con_unknown_dbapi2_class_does_not_error_without_sql_alchemy_installed( + self, + ): + class MockSqliteConnection: + def __init__(self, *args, **kwargs): + self.conn = sqlite3.Connection(*args, **kwargs) + + def __getattr__(self, name): + return getattr(self.conn, name) + + conn = MockSqliteConnection(":memory:") + with tm.assert_produces_warning(UserWarning): + sql.read_sql("SELECT 1", conn) + def test_read_sql_delegate(self): iris_frame1 = sql.read_sql_query("SELECT * FROM iris", self.conn) iris_frame2 = sql.read_sql("SELECT * FROM iris", self.conn)