Repository: dialoguemd/fastapi-sqla (language: python)

Issue:
Async dependencies of the pytest plugin are required even when you only use the sync variants
In my project I chose to use pyscopg2 instead of asyncpg. I did not have any issues with this until I got to the tests. I had to install `pytest-asyncio` and `asyncpg` to please the pytest fixtures.

Unfortunately even after installing the dependencies I still get an error:
```python
    @fixture(scope="session")
    def asyncpg_url(db_url):
>       scheme, parts = db_url.split(":")
E       ValueError: too many values to unpack (expected 2)
```

My connection string looks like `postgresql+psycopg2://app:*****@localhost:4200/app_test`, so there's more than 2 parts created by splitting on `':'`.

I found a work-around in the meantime, it's to override the `patch_async_sessionmaker` fixture which has `autouse=True`, with this
```python
@fixture(autouse=True)
async def patch_async_sessionmaker():
    pass
```
It doesn't cause me trouble to disable it because I only use the sync version of the fixtures.


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 95d72cf7..ef0e0fdf 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -23,6 +23,7 @@ workflows:
           matrix:
             parameters:
               sqlalchemy_version: ["1.3", "1.4"]
+              asyncpg: ["asyncpg", "noasyncpg"]
       - release/release:
           name: release
           requires:
@@ -49,19 +50,25 @@ workflows:
 jobs:
 
   test:
+    environment:
+      CACHE_VERSION: "2021-05-02T10:18:17.640582"
     parameters:
       sqlalchemy_version:
         type: enum
         enum: ["1.4", "1.3"]
         description: |
           Specify which version of sqlalchemy to run the tests against
+      asyncpg:
+        type: enum
+        enum: ["asyncpg", "noasyncpg"]
+        description: To run tests with and without asyncpg installed.
     executor: python-postgres
     working_directory: ~/project/.
     steps:
       - base/setup
       - python/setup
       - utils/with_cache:
-          key: 'sqlalchemy<<parameters.sqlalchemy_version>>-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }}'
+          key: 'sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>-{{ checksum "pyproject.toml" }}-{{ checksum "poetry.lock" }}'
           namespace: tox
           path: ~/project/.tox
           steps:
@@ -70,11 +77,13 @@ jobs:
                 command: |
                   poetry run pip install -U tox
             - run:
-                name: "run tox using sqlalchemy <<parameters.sqlalchemy_version>>.*"
+                name: "run tox using sqlalchemy <<parameters.sqlalchemy_version>>.* and -<<parameters.asyncpg>>"
                 command: |
-                  poetry run tox -e sqlalchemy<<parameters.sqlalchemy_version>>
+                  poetry run tox -e sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>
+      - store_test_results:
+          path: test-reports
       - utils/send_coverage_to_codecov:
-          codecov_flag: sqlalchemy<<parameters.sqlalchemy_version>>
+          codecov_flag: sqlalchemy<<parameters.sqlalchemy_version>>-<<parameters.asyncpg>>
 
   publish:
     docker:
diff --git a/pyproject.toml b/pyproject.toml
index 1aec5057..d34da498 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -64,7 +64,6 @@ tox = { version = "^3.23.0", optional = true}
 tests = [
     "alembic",
     "asgi_lifespan",
-    "asyncpg",
     "black",
     "Faker",
     "httpx",
@@ -97,15 +96,15 @@ fastapi-sqla = "fastapi_sqla._pytest_plugin"
 legacy_tox_ini = """
 [tox]
 isolated_build = True
-envlist = sqlalchemy{1.3,1.4}
+envlist = sqlalchemy{ 1.3, 1.4 }-{ asyncpg, noasyncpg }
 
 [testenv]
 passenv = CI
 deps =
     sqlalchemy1.3: sqlalchemy<1.4
     sqlalchemy1.4: sqlalchemy>=1.4,<2
+    asyncpg: asyncpg
 extras =
-    asyncpg
     tests
 commands = pytest -vv --cov={envsitepackagesdir}/fastapi_sqla --cov-report xml --cov-report html --junitxml=test-reports/pytest/junit.xml
 """


Test patch:
diff --git a/fastapi_sqla/_pytest_plugin.py b/fastapi_sqla/_pytest_plugin.py
index c772e88b..f837136f 100644
--- a/fastapi_sqla/_pytest_plugin.py
+++ b/fastapi_sqla/_pytest_plugin.py
@@ -1,5 +1,6 @@
 import os
 from unittest.mock import patch
+from urllib.parse import urlsplit, urlunsplit
 
 from alembic import command
 from alembic.config import Config
@@ -7,6 +8,7 @@
 from sqlalchemy import create_engine, text
 
 try:
+    import asyncpg  # noqa
     from sqlalchemy.ext.asyncio import create_async_engine
 
     asyncio_support = True
@@ -102,16 +104,21 @@ def session(sqla_transaction, sqla_connection):
     session.close()
 
 
-if asyncio_support:
+def format_async_async_sqlalchemy_url(url):
+    scheme, location, path, query, fragment = urlsplit(url)
+    return urlunsplit([f"{scheme}+asyncpg", location, path, query, fragment])
+
 
-    @fixture(scope="session")
-    def async_sqlalchemy_url(db_url):
-        """Default async db url.
+@fixture(scope="session")
+def async_sqlalchemy_url(db_url):
+    """Default async db url.
 
-        It is the same as `db_url` with `postgresql+asynpg://` as scheme.
-        """
-        scheme, parts = db_url.split(":")
-        return f"{scheme}+asyncpg:{parts}"
+    It is the same as `db_url` with `postgresql+asyncpg://` as scheme.
+    """
+    return format_async_async_sqlalchemy_url(db_url)
+
+
+if asyncio_support:
 
     @fixture
     async def async_engine(async_sqlalchemy_url):
diff --git a/tests/conftest.py b/tests/conftest.py
index e49b5cd1..ccd3d932 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -17,6 +17,9 @@ def pytest_configure(config):
             "sqlalchemy version"
         ),
     )
+    config.addinivalue_line(
+        "markers", "require_asyncpg: skip test if asyncpg is not installed"
+    )
 
 
 @fixture(scope="session")
@@ -26,13 +29,21 @@ def sqla_version_tuple():
     return tuple(int(i) for i in __version__.split("."))
 
 
+def is_asyncpg_installed():
+    try:
+        import asyncpg  # noqa
+    except ImportError:
+        return False
+    else:
+        return True
+
+
 @fixture(scope="session", autouse=True)
-def environ(db_url, sqla_version_tuple):
+def environ(db_url, sqla_version_tuple, async_sqlalchemy_url):
     values = {"sqlalchemy_url": db_url, "PYTHONASYNCIODEBUG": "1"}
 
-    if sqla_version_tuple >= (1, 4, 0):
-        scheme, parts = db_url.split(":")
-        values["async_sqlalchemy_url"] = f"{scheme}+asyncpg:{parts}"
+    if sqla_version_tuple >= (1, 4, 0) and is_asyncpg_installed():
+        values["async_sqlalchemy_url"] = async_sqlalchemy_url
 
     with patch.dict("os.environ", values=values, clear=True):
         yield values
@@ -76,6 +87,14 @@ def check_sqlalchemy_version(request, sqla_version_tuple):
             )
 
 
+@fixture(autouse=True)
+def check_asyncpg(request):
+    "Skip test marked with mark.require_asyncpg if asyncpg is not installed."
+    marker = request.node.get_closest_marker("require_asyncpg")
+    if marker and not is_asyncpg_installed():
+        skip("This test requires asyncpg. Skipping as asyncpg is not installed.")
+
+
 @fixture(scope="session")
 def faker():
     return Faker()
diff --git a/tests/test_asyncio_support.py b/tests/test_asyncio_support.py
index 62adde59..d22bf431 100644
--- a/tests/test_asyncio_support.py
+++ b/tests/test_asyncio_support.py
@@ -1,7 +1,7 @@
 from pytest import fixture, mark
 from sqlalchemy import text
 
-pytestmark = [mark.asyncio, mark.sqlalchemy("1.4")]
+pytestmark = [mark.asyncio, mark.sqlalchemy("1.4"), mark.require_asyncpg]
 
 
 @fixture(autouse=True)
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index b2b6c041..cb3b3890 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -95,6 +95,7 @@ async def test_session_dependency(client, faker, session):
     assert row == (userid, first_name, last_name)
 
 
+@mark.require_asyncpg
 @mark.sqlalchemy("1.4")
 async def test_async_session_dependency(client, faker, async_session):
     userid = faker.unique.random_int()
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 20e97f8f..5dbf78db 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -222,6 +222,7 @@ async def client(app):
             yield client
 
 
+@mark.require_asyncpg
 @mark.asyncio
 @mark.parametrize(
     "offset,items_number,path",
diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py
index c9e66176..b2f2fb77 100644
--- a/tests/test_pytest_plugin.py
+++ b/tests/test_pytest_plugin.py
@@ -41,6 +41,7 @@ def test_session_fixture_does_not_write_in_db(session, singer_cls, engine):
         assert connection.execute(text("select count(*) from singer")).scalar() == 0
 
 
+@mark.require_asyncpg
 @mark.asyncio
 @mark.sqlalchemy("1.4")
 async def test_async_session_fixture_does_not_write_in_db(
@@ -66,6 +67,7 @@ def test_all_opened_sessions_are_within_the_same_transaction(
     assert other_session.query(singer_cls).get(1)
 
 
+@mark.require_asyncpg
 @mark.asyncio
 @mark.sqlalchemy("1.4")
 async def test_all_opened_async_sessions_are_within_the_same_transaction(
@@ -126,3 +128,16 @@ def test_anything(session):
     result = testdir.runpytest()
     result.assert_outcomes(errors=1)
     result.stdout.fnmatch_lines(["*sqla_modules fixture is not defined*"])
+
+
+@mark.parametrize(
+    "url,expected",
+    [
+        ("postgresql://localhost/db", "postgresql+asyncpg://localhost/db"),
+        ("postgresql://u:p@localhost/db", "postgresql+asyncpg://u:p@localhost/db"),
+    ],
+)
+def test_format_async_sqlalchemy_url(monkeypatch, conftest, testdir, url, expected):
+    from fastapi_sqla._pytest_plugin import format_async_async_sqlalchemy_url
+
+    assert format_async_async_sqlalchemy_url(url) == expected