Skip to content

Commit c93caef

Browse files
committed
Adjusted import and error message behavior based on types
1 parent cc40732 commit c93caef

File tree

4 files changed

+90
-10
lines changed

4 files changed

+90
-10
lines changed

pandas/compat/_optional.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def import_optional_dependency(
151151
install_name = package_name if package_name is not None else name
152152

153153
msg = (
154-
f"`Import {install_name}` failed. {extra} "
154+
f"`Import {install_name} failed. {extra} "
155155
f"Use pip or conda to install the {install_name} package."
156156
)
157157
try:

pandas/io/sql.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -895,13 +895,23 @@ def pandasSQL_builder(
895895
if isinstance(con, sqlite3.Connection) or con is None:
896896
return SQLiteDatabase(con)
897897

898-
sqlalchemy = import_optional_dependency("sqlalchemy", errors="ignore")
898+
is_sqlalchemy_type = isinstance(con, str) or con.__module__.startswith("sqlalchemy")
899899

900-
if isinstance(con, str) and sqlalchemy is None:
901-
raise ImportError("Using URI string without sqlalchemy installed.")
900+
if is_sqlalchemy_type:
901+
try:
902+
sqlalchemy = import_optional_dependency(
903+
"sqlalchemy", min_version="2.0.36", errors="raise"
904+
)
905+
except ImportError as e:
906+
if isinstance(con, str):
907+
raise ImportError(
908+
"Using URI string without matching version of sqlalchemy installed."
909+
) from e
910+
else:
911+
raise e
902912

903-
if sqlalchemy is not None and isinstance(con, (str, sqlalchemy.engine.Connectable)):
904-
return SQLDatabase(con, schema, need_transaction)
913+
if isinstance(con, (str, sqlalchemy.engine.Connectable)):
914+
return SQLDatabase(con, schema, need_transaction)
905915

906916
adbc = import_optional_dependency("adbc_driver_manager.dbapi", errors="ignore")
907917
if adbc and isinstance(con, adbc.Connection):

pandas/tests/io/test_sql.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2598,11 +2598,38 @@ def test_sql_open_close(test_frame3):
25982598
@td.skip_if_installed("sqlalchemy")
25992599
def test_con_string_import_error():
26002600
conn = "mysql://root@localhost/pandas"
2601-
msg = "Using URI string without sqlalchemy installed"
2601+
msg = "Using URI string without matching version of sqlalchemy installed."
26022602
with pytest.raises(ImportError, match=msg):
26032603
sql.read_sql("SELECT * FROM iris", conn)
26042604

26052605

2606+
@td.skip_if_installed("sqlalchemy")
2607+
@pytest.mark.parametrize("conn", sqlalchemy_connectable)
2608+
def test_con_sqlalchemy_import_error(conn, request):
2609+
conn = request.getfixturevalue(conn)
2610+
msg = r"Pandas requires version '2\.0\.36' or newer of 'sqlalchemy'.*"
2611+
with pytest.raises(ImportError, match=msg):
2612+
_ = pandasSQL_builder(conn)
2613+
2614+
2615+
@td.skip_if_no_unsupported_installed("sqlalchemy", min_version="2.0.36")
2616+
@pytest.mark.parametrize("conn", sqlalchemy_connectable)
2617+
def test_con_sqlalchemy_import_error_unsupported(conn, request):
2618+
conn = request.getfixturevalue(conn)
2619+
msg = r"Import SQLAlchemy failed\..*"
2620+
with pytest.raises(ImportError, match=msg):
2621+
_ = pandasSQL_builder(conn)
2622+
2623+
2624+
@td.skip_if_no("sqlalchemy", min_version="2.0.36")
2625+
@pytest.mark.parametrize("conn", sqlalchemy_connectable)
2626+
def test_con_sqlalchemy_connectable(conn, request):
2627+
conn = request.getfixturevalue(conn)
2628+
pandas_sql = pandasSQL_builder(conn)
2629+
2630+
assert isinstance(pandas_sql, SQLDatabase)
2631+
2632+
26062633
@td.skip_if_installed("sqlalchemy")
26072634
def test_con_unknown_dbapi2_class_does_not_error_without_sql_alchemy_installed():
26082635
class MockSqliteConnection:

pandas/util/_test_decorators.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,67 @@ def test_foo():
4343
from pandas.compat._optional import import_optional_dependency
4444

4545

46-
def skip_if_installed(package: str) -> pytest.MarkDecorator:
46+
def skip_if_installed(
47+
package: str, min_version: str | None = None
48+
) -> pytest.MarkDecorator:
4749
"""
4850
Skip a test if a package is installed.
4951
5052
Parameters
5153
----------
5254
package : str
5355
The name of the package.
56+
min_version: str or None, default None
57+
Optional minimum version of the package.
5458
5559
Returns
5660
-------
5761
pytest.MarkDecorator
5862
a pytest.mark.skipif to use as either a test decorator or a
5963
parametrization mark.
6064
"""
65+
msg = f"Skipping because '{package}'"
66+
if min_version:
67+
msg += f" satisfying a min_version of {min_version}"
68+
msg += " is installed."
6169
return pytest.mark.skipif(
62-
bool(import_optional_dependency(package, errors="ignore")),
63-
reason=f"Skipping because {package} is installed.",
70+
bool(
71+
import_optional_dependency(
72+
package, min_version=min_version, errors="ignore"
73+
)
74+
),
75+
reason=msg,
76+
)
77+
78+
79+
def skip_if_no_unsupported_installed(
80+
package: str, min_version: str
81+
) -> pytest.MarkDecorator:
82+
"""
83+
Skip a test if there is no unsupported version of a package installed.
84+
The test will hence be executed only if an unsupported version is installed.
85+
86+
Parameters
87+
----------
88+
package : str
89+
The name of the package.
90+
min_version: str or None, default None
91+
The minimum version of the package.
92+
93+
Returns
94+
-------
95+
pytest.MarkDecorator
96+
a pytest.mark.skipif to use as either a test decorator or a
97+
parametrization mark.
98+
"""
99+
return pytest.mark.skipif(
100+
bool(import_optional_dependency(package, errors="ignore"))
101+
and not bool(
102+
import_optional_dependency(
103+
package, min_version=min_version, errors="ignore"
104+
)
105+
),
106+
reason=f"Skipping because no unsupported version of {package} is installed",
64107
)
65108

66109

0 commit comments

Comments
 (0)