Repository: Kludex/starlette-testclient (language: python)

Issue:
AttributeError: module 'anyio' has no attribute 'start_blocking_portal'
Using `anyio>=4.0.0` breaks:
```
  File "/home/grigi/work/magneto/.pyenv/lib64/python3.11/site-packages/starlette_testclient/_testclient.py", line 450, in _portal_factory
    with anyio.start_blocking_portal(**self.async_backend) as portal:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: module 'anyio' has no attribute 'start_blocking_portal'
```
Downgrading to `anyio<4` resolves the issue.


Discussion / hints:
@Kludex I am facing this issue while using [schemathesis](https://github.com/schemathesis/schemathesis/issues/2081). Could you maybe either drop anyio 3 or support both versions?

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4bd150f..032d854 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -14,7 +14,7 @@ jobs:
     timeout-minutes: 30
     strategy:
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
 
     steps:
       - uses: actions/checkout@v3
@@ -36,5 +36,17 @@ jobs:
           python -m pip install --upgrade pip setuptools wheel
           python -m pip install -r requirements.txt
 
+      - name: Downgrade trio
+        if: ${{ matrix.python-version == '3.7' }}
+        run: |
+          python -m pip install "trio==0.21"
+
       - name: Run tests
         run: python -m coverage run -m pytest
+
+      - name: Downgrade anyio
+        run: |
+          python -m pip install "anyio<4" "trio==0.21"
+
+      - name: Re-run tests
+        run: python -m coverage run -m pytest
diff --git a/pyproject.toml b/pyproject.toml
index c021eb0..e821cd3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,7 @@ classifiers = [
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
 ]
 license = "BSD-3-Clause"
 requires-python = ">=3.7"
diff --git a/requirements.txt b/requirements.txt
index 254fb51..01e479c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 -e .
 
-trio==0.21.0
+trio
 types-requests
 
 # Linter & Formatter


Test patch:
diff --git a/starlette_testclient/_testclient.py b/starlette_testclient/_testclient.py
index cba2bc4..475e3a1 100644
--- a/starlette_testclient/_testclient.py
+++ b/starlette_testclient/_testclient.py
@@ -24,6 +24,10 @@
 else:  # pragma: no cover
     from typing_extensions import TypedDict
 
+try:
+    start_blocking_portal = anyio.from_thread.start_blocking_portal
+except AttributeError:  # pragma: no cover
+    start_blocking_portal = anyio.start_blocking_portal
 
 _PortalFactoryType = typing.Callable[
     [], typing.ContextManager[anyio.abc.BlockingPortal]
@@ -447,7 +451,7 @@ def _portal_factory(
         if self.portal is not None:
             yield self.portal
         else:
-            with anyio.from_thread.start_blocking_portal(**self.async_backend) as portal:
+            with start_blocking_portal(**self.async_backend) as portal:
                 yield portal
 
     def request(  # type: ignore
@@ -512,7 +516,7 @@ def websocket_connect(
     def __enter__(self) -> "TestClient":
         with contextlib.ExitStack() as stack:
             self.portal = portal = stack.enter_context(
-                anyio.from_thread.start_blocking_portal(**self.async_backend)
+                start_blocking_portal(**self.async_backend)
             )
 
             @stack.callback
diff --git a/tests/test_testclient.py b/tests/test_testclient.py
index 20c3979..a5b1e32 100644
--- a/tests/test_testclient.py
+++ b/tests/test_testclient.py
@@ -39,11 +39,12 @@ def current_task():
     raise RuntimeError(f"unsupported asynclib={asynclib_name}")  # pragma: no cover
 
 
-def startup():
+@asynccontextmanager
+def lifespan(*args, **kwargs):
     raise RuntimeError()
 
 
-startup_error_app = Starlette(on_startup=[startup])
+startup_error_app = Starlette(lifespan=lifespan)
 
 
 def test_use_testclient_in_endpoint(test_client_factory):