anyquart 0.2.1__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {anyquart-0.2.1 → anyquart-0.3.1}/PKG-INFO +58 -18
  2. {anyquart-0.2.1 → anyquart-0.3.1}/README.md +50 -12
  3. {anyquart-0.2.1 → anyquart-0.3.1}/pyproject.toml +9 -9
  4. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/__init__.py +1 -0
  5. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/app.py +80 -63
  6. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/asgi.py +27 -26
  7. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/cli.py +32 -15
  8. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/ctx.py +7 -4
  9. anyquart-0.3.1/src/anyquart/di.py +260 -0
  10. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/helpers.py +2 -2
  11. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/sessions.py +3 -5
  12. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/templating.py +1 -3
  13. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/testing/__init__.py +11 -14
  14. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/testing/app.py +5 -4
  15. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/testing/connections.py +9 -6
  16. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/testing/utils.py +15 -9
  17. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/typing.py +237 -15
  18. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/wrappers/base.py +2 -1
  19. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/wrappers/request.py +2 -1
  20. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/wrappers/response.py +1 -1
  21. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/wrappers/websocket.py +3 -2
  22. {anyquart-0.2.1 → anyquart-0.3.1}/LICENSE.txt +0 -0
  23. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/__main__.py +0 -0
  24. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/blueprints.py +0 -0
  25. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/config.py +0 -0
  26. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/datastructures.py +0 -0
  27. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/debug.py +0 -0
  28. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/formparser.py +0 -0
  29. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/globals.py +0 -0
  30. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/json/__init__.py +0 -0
  31. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/json/provider.py +0 -0
  32. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/json/tag.py +0 -0
  33. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/logging.py +0 -0
  34. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/py.typed +0 -0
  35. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/routing.py +0 -0
  36. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/signals.py +0 -0
  37. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/testing/client.py +0 -0
  38. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/utils.py +0 -0
  39. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/views.py +0 -0
  40. {anyquart-0.2.1 → anyquart-0.3.1}/src/anyquart/wrappers/__init__.py +6 -6
@@ -1,10 +1,10 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: anyquart
3
- Version: 0.2.1
3
+ Version: 0.3.1
4
4
  Summary: A fork of Quart that uses AnyIO
5
5
  Author-email: pgjones <philip.graham.jones@googlemail.com>, NIYONSHUTI Emmanuel <emmanuelniyonshuti13@gmail.com>
6
6
  Maintainer-email: NIYONSHUTI Emmanuel <emmanuelniyonshuti13@gmail.com>
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  License-Expression: MIT
10
10
  Classifier: Development Status :: 4 - Beta
@@ -13,12 +13,12 @@ Classifier: Framework :: Flask
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3 :: Only
16
- Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
19
18
  Classifier: Programming Language :: Python :: 3.13
20
19
  Classifier: Programming Language :: Python :: 3.14
21
20
  Classifier: Programming Language :: Python :: 3.15
21
+ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
22
22
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
23
23
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
24
24
  Classifier: Typing :: Typed
@@ -27,18 +27,20 @@ Requires-Dist: anyio>=4.14.0,<5
27
27
  Requires-Dist: blinker>=1.6
28
28
  Requires-Dist: click>=8.0
29
29
  Requires-Dist: flask>=3.0
30
- Requires-Dist: anycorn>=0.11.2
31
30
  Requires-Dist: importlib-metadata; python_version < '3.13'
32
31
  Requires-Dist: itsdangerous
33
32
  Requires-Dist: jinja2
34
33
  Requires-Dist: markupsafe
35
- Requires-Dist: typing-extensions; python_version < '3.13'
34
+ Requires-Dist: typing-extensions >= 4.16.0; python_version < '3.15'
36
35
  Requires-Dist: werkzeug>=3.0
36
+ Requires-Dist: anycorn>=0.11.2 ; extra == "anycorn"
37
37
  Requires-Dist: python-dotenv ; extra == "dotenv"
38
38
  Project-URL: Documentation, https://quart.palletsprojects.com
39
39
  Project-URL: Donate, https://palletsprojects.com/donate
40
40
  Project-URL: Source, https://github.com/EmmanuelNiyonshuti/anyquart
41
+ Provides-Extra: anycorn
41
42
  Provides-Extra: dotenv
43
+ Import-Name: anyquart
42
44
 
43
45
  # AnyQuart
44
46
 
@@ -55,18 +57,45 @@ AnyQuart is [Quart](https://github.com/pallets/quart) running on [AnyIO](https:/
55
57
  ## Differences from Quart
56
58
  `AnyQuart` and `Quart` are essentially the same thing. The only difference is the name and the internals(Asyncio replaced with AnyIO). This also means the testing setup changes, which is explained in the [Testing](#testing) section below.
57
59
 
58
- - Works with both asyncio and Trio code via AnyIO, giving you structured concurrency out of the box.
59
- - Uses [Anycorn](https://github.com/davidbrochart/anycorn) instead of Hypercorn as the development server.
60
- - [aiofiles](https://github.com/Tinche/aiofiles) dropped, AnyIO's file I/O is used instead.
61
- - [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) replaced by [AnyIO pytest plugin](https://anyio.readthedocs.io/en/stable/testing.html).
62
- - Runs on Python 3.10+ .
60
+ 1. Works with both asyncio and Trio code via AnyIO, giving you structured concurrency out of the box.
61
+ 2. [aiofiles](https://github.com/Tinche/aiofiles) dropped, AnyIO's file I/O is used instead.
62
+ 3. [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) replaced by [AnyIO pytest plugin](https://anyio.readthedocs.io/en/stable/testing.html).
63
+ 4. As of 0.3.0, [Anycorn](https://github.com/davidbrochart/anycorn) is an optional dependency for the development server, while Quart installs [Hypercorn](https://hypercorn.readthedocs.io) as a required dependency for the same purpose.
64
+ 5. As of 0.3.0, AnyQuart provides a Request-scoped dependency injection for route handlers.
65
+ Request handlers may mark parameters with :class:`Needs` to resolve a request-scoped value.
66
+ e.g:
67
+ ```python
68
+ from anyquart import Needs
69
+
70
+
71
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
72
+ async with async_session() as session:
73
+ yield session
74
+
75
+
76
+ @app.route("/users")
77
+ async def get_users(db: AsyncSession = Needs(get_db)) -> None: ...
78
+
79
+
80
+ # Or:
81
+ DBSession = Annotated[AsyncSession, Needs(get_db)]
82
+
83
+
84
+ @app.route("/users")
85
+ async def get_users(db: DBSession) -> None: ...
86
+
87
+
88
+ # Tests can use `app.dependency_overrides` dictionary to replace route handler's
89
+ # dependency with test dependency
90
+ ```
91
+ 6. Runs on Python 3.11+
63
92
 
64
93
  ## Usage
65
94
  You will have to replace `quart` with `anyquart` and `Quart` with `AnyQuart`.
66
95
 
67
- Install from PyPI using an installer such as pip. Requires Python 3.10+.
96
+ Install from PyPI using an installer such as pip. Requires Python 3.11+.
68
97
 
69
- ```
98
+ ```python
70
99
  $ pip install anyquart
71
100
  ```
72
101
 
@@ -77,26 +106,35 @@ from anyquart import AnyQuart, websocket, render_template
77
106
 
78
107
  app = AnyQuart(__name__)
79
108
 
109
+
80
110
  @app.route("/")
81
111
  async def hello():
82
112
  return await render_template("index.html")
83
113
 
114
+
84
115
  @app.route("/api")
85
116
  async def json():
86
117
  return {"hello": "world"}
87
118
 
119
+
88
120
  @app.websocket("/ws")
89
121
  async def ws():
90
122
  while True:
91
123
  await websocket.send("hello")
92
124
  await websocket.send_json({"hello": "world"})
93
125
  ```
94
-
126
+ Install an ASGI web server, pick one from [this list](https://asgi.readthedocs.io/en/latest/implementations.html), or install anyquart with Anycorn directly:
127
+ ```bash
128
+ $ pip install anyquart[anycorn]
95
129
  ```
96
- $ anyquart run
97
- * Running on http://127.0.0.1:5000 (CTRL + C to quit)
130
+ run the application:
131
+ ```bash
132
+ $ anycorn app:app
133
+ * [2026-08-01 11:49:46 +0200] [30809] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit)
98
134
  ```
99
135
 
136
+ **Note**: As of 0.3.0, `anyquart run` requires an ASGI web server to be installed. If no ASGI web server is available, running the application in development mode will raise a `RuntimeError`. Install AnyQuart with Anycorn using the command above, or install another ASGI web server from the list above.
137
+
100
138
  # Testing
101
139
  Pytest requires a plugin to run asynchronous test functions and fixtures.
102
140
  Quart uses pytest-asyncio, while AnyQuart uses AnyIO's pytest plugin. You will need to specify which backend your tests run on via the `anyio_backend` fixture and decorate your asynchronous tests with `@pytest.mark.anyio`.
@@ -106,20 +144,22 @@ import pytest
106
144
 
107
145
  from app import app
108
146
 
147
+
109
148
  @pytest.fixture()
110
149
  def anyio_backend():
111
- return "trio" # you can replace with "asyncio"
150
+ return "trio" # you can replace with "asyncio"
151
+
112
152
 
113
153
  @pytest.fixture()
114
154
  def test_client():
115
155
  return app.test_client()
116
156
 
157
+
117
158
  @pytest.mark.anyio
118
159
  async def test_do_something(test_client) -> None:
119
160
  response = await test_client.get("/")
120
161
  assert response.status_code == 200
121
162
  assert await response.json == {"hello": "world"}
122
-
123
163
  ```
124
164
 
125
165
  Refer to the [Quart documentation](https://quart.palletsprojects.com) for more details.
@@ -13,18 +13,45 @@ AnyQuart is [Quart](https://github.com/pallets/quart) running on [AnyIO](https:/
13
13
  ## Differences from Quart
14
14
  `AnyQuart` and `Quart` are essentially the same thing. The only difference is the name and the internals(Asyncio replaced with AnyIO). This also means the testing setup changes, which is explained in the [Testing](#testing) section below.
15
15
 
16
- - Works with both asyncio and Trio code via AnyIO, giving you structured concurrency out of the box.
17
- - Uses [Anycorn](https://github.com/davidbrochart/anycorn) instead of Hypercorn as the development server.
18
- - [aiofiles](https://github.com/Tinche/aiofiles) dropped, AnyIO's file I/O is used instead.
19
- - [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) replaced by [AnyIO pytest plugin](https://anyio.readthedocs.io/en/stable/testing.html).
20
- - Runs on Python 3.10+ .
16
+ 1. Works with both asyncio and Trio code via AnyIO, giving you structured concurrency out of the box.
17
+ 2. [aiofiles](https://github.com/Tinche/aiofiles) dropped, AnyIO's file I/O is used instead.
18
+ 3. [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) replaced by [AnyIO pytest plugin](https://anyio.readthedocs.io/en/stable/testing.html).
19
+ 4. As of 0.3.0, [Anycorn](https://github.com/davidbrochart/anycorn) is an optional dependency for the development server, while Quart installs [Hypercorn](https://hypercorn.readthedocs.io) as a required dependency for the same purpose.
20
+ 5. As of 0.3.0, AnyQuart provides a Request-scoped dependency injection for route handlers.
21
+ Request handlers may mark parameters with :class:`Needs` to resolve a request-scoped value.
22
+ e.g:
23
+ ```python
24
+ from anyquart import Needs
25
+
26
+
27
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
28
+ async with async_session() as session:
29
+ yield session
30
+
31
+
32
+ @app.route("/users")
33
+ async def get_users(db: AsyncSession = Needs(get_db)) -> None: ...
34
+
35
+
36
+ # Or:
37
+ DBSession = Annotated[AsyncSession, Needs(get_db)]
38
+
39
+
40
+ @app.route("/users")
41
+ async def get_users(db: DBSession) -> None: ...
42
+
43
+
44
+ # Tests can use `app.dependency_overrides` dictionary to replace route handler's
45
+ # dependency with test dependency
46
+ ```
47
+ 6. Runs on Python 3.11+
21
48
 
22
49
  ## Usage
23
50
  You will have to replace `quart` with `anyquart` and `Quart` with `AnyQuart`.
24
51
 
25
- Install from PyPI using an installer such as pip. Requires Python 3.10+.
52
+ Install from PyPI using an installer such as pip. Requires Python 3.11+.
26
53
 
27
- ```
54
+ ```python
28
55
  $ pip install anyquart
29
56
  ```
30
57
 
@@ -35,26 +62,35 @@ from anyquart import AnyQuart, websocket, render_template
35
62
 
36
63
  app = AnyQuart(__name__)
37
64
 
65
+
38
66
  @app.route("/")
39
67
  async def hello():
40
68
  return await render_template("index.html")
41
69
 
70
+
42
71
  @app.route("/api")
43
72
  async def json():
44
73
  return {"hello": "world"}
45
74
 
75
+
46
76
  @app.websocket("/ws")
47
77
  async def ws():
48
78
  while True:
49
79
  await websocket.send("hello")
50
80
  await websocket.send_json({"hello": "world"})
51
81
  ```
52
-
82
+ Install an ASGI web server, pick one from [this list](https://asgi.readthedocs.io/en/latest/implementations.html), or install anyquart with Anycorn directly:
83
+ ```bash
84
+ $ pip install anyquart[anycorn]
53
85
  ```
54
- $ anyquart run
55
- * Running on http://127.0.0.1:5000 (CTRL + C to quit)
86
+ run the application:
87
+ ```bash
88
+ $ anycorn app:app
89
+ * [2026-08-01 11:49:46 +0200] [30809] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit)
56
90
  ```
57
91
 
92
+ **Note**: As of 0.3.0, `anyquart run` requires an ASGI web server to be installed. If no ASGI web server is available, running the application in development mode will raise a `RuntimeError`. Install AnyQuart with Anycorn using the command above, or install another ASGI web server from the list above.
93
+
58
94
  # Testing
59
95
  Pytest requires a plugin to run asynchronous test functions and fixtures.
60
96
  Quart uses pytest-asyncio, while AnyQuart uses AnyIO's pytest plugin. You will need to specify which backend your tests run on via the `anyio_backend` fixture and decorate your asynchronous tests with `@pytest.mark.anyio`.
@@ -64,20 +100,22 @@ import pytest
64
100
 
65
101
  from app import app
66
102
 
103
+
67
104
  @pytest.fixture()
68
105
  def anyio_backend():
69
- return "trio" # you can replace with "asyncio"
106
+ return "trio" # you can replace with "asyncio"
107
+
70
108
 
71
109
  @pytest.fixture()
72
110
  def test_client():
73
111
  return app.test_client()
74
112
 
113
+
75
114
  @pytest.mark.anyio
76
115
  async def test_do_something(test_client) -> None:
77
116
  response = await test_client.get("/")
78
117
  assert response.status_code == 200
79
118
  assert await response.json == {"hello": "world"}
80
-
81
119
  ```
82
120
 
83
121
  Refer to the [Quart documentation](https://quart.palletsprojects.com) for more details.
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "anyquart"
7
- version = "0.2.1"
7
+ version = "0.3.1"
8
8
  description = "A fork of Quart that uses AnyIO"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -21,28 +21,27 @@ classifiers = [
21
21
  "Intended Audience :: Developers",
22
22
  "Operating System :: OS Independent",
23
23
  "Programming Language :: Python :: 3 :: Only",
24
- "Programming Language :: Python :: 3.10",
25
24
  "Programming Language :: Python :: 3.11",
26
25
  "Programming Language :: Python :: 3.12",
27
26
  "Programming Language :: Python :: 3.13",
28
27
  "Programming Language :: Python :: 3.14",
29
28
  "Programming Language :: Python :: 3.15",
29
+ "Programming Language :: Python :: Free Threading :: 2 - Beta",
30
30
  "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
31
31
  "Topic :: Software Development :: Libraries :: Application Frameworks",
32
32
  "Typing :: Typed",
33
33
  ]
34
- requires-python = ">=3.10"
34
+ requires-python = ">=3.11"
35
35
  dependencies = [
36
36
  "anyio>=4.14.0,<5",
37
37
  "blinker>=1.6",
38
38
  "click>=8.0",
39
39
  "flask>=3.0",
40
- "anycorn>=0.11.2",
41
40
  "importlib-metadata; python_version < '3.13'",
42
41
  "itsdangerous",
43
42
  "jinja2",
44
43
  "markupsafe",
45
- "typing-extensions; python_version < '3.13'",
44
+ "typing-extensions >= 4.16.0; python_version < '3.15'",
46
45
  "werkzeug>=3.0",
47
46
  ]
48
47
 
@@ -53,13 +52,14 @@ Source = "https://github.com/EmmanuelNiyonshuti/anyquart"
53
52
 
54
53
  [project.optional-dependencies]
55
54
  dotenv = ["python-dotenv"]
55
+ anycorn = ["anycorn>=0.11.2"]
56
56
 
57
57
  [project.scripts]
58
58
  anyquart = "anyquart.cli:main"
59
59
 
60
60
  [dependency-groups]
61
61
  dev = [
62
- "trio==0.33.0",
62
+ "trio==0.35.0",
63
63
  "ruff",
64
64
  "tox",
65
65
  "tox-uv",
@@ -78,6 +78,7 @@ tests = [
78
78
  "pytest-cov",
79
79
  "pytest-sugar",
80
80
  "python-dotenv",
81
+ "anycorn>=0.11.2" # need in typing ci job
81
82
  ]
82
83
  typing = [
83
84
  "mypy",
@@ -122,7 +123,7 @@ warn_return_any = false
122
123
  files = ["src", "tests"]
123
124
 
124
125
  [tool.pyright]
125
- pythonVersion = "3.10"
126
+ pythonVersion = "3.11"
126
127
  include = ["src", "tests"]
127
128
  typeCheckingMode = "basic"
128
129
 
@@ -144,7 +145,6 @@ select = [
144
145
  "W", # pycodestyle warning
145
146
  ]
146
147
  ignore = [
147
- "UP038", # keep isinstance tuple
148
148
  "UP007", # Union[] is needed for forward-referenced type aliases
149
149
  "UP045"
150
150
  ]
@@ -155,7 +155,7 @@ order-by-type = false
155
155
 
156
156
  [tool.tox]
157
157
  env_list = [
158
- "py3.15", "py3.14", "py3.13", "py3.12", "py3.11", "py3.10",
158
+ "py3.15", "py3.14", "py3.14t", "py3.13", "py3.12", "py3.11",
159
159
  "style",
160
160
  "typing",
161
161
  "docs",
@@ -13,6 +13,7 @@ from .ctx import copy_current_websocket_context as copy_current_websocket_contex
13
13
  from .ctx import has_app_context as has_app_context
14
14
  from .ctx import has_request_context as has_request_context
15
15
  from .ctx import has_websocket_context as has_websocket_context
16
+ from .di import Needs as Needs
16
17
  from .globals import current_app as current_app
17
18
  from .globals import g as g
18
19
  from .globals import request as request
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import os
5
4
  import signal
6
5
  import sys
@@ -12,7 +11,6 @@ from collections.abc import Callable
12
11
  from contextlib import AbstractAsyncContextManager
13
12
  from datetime import timedelta
14
13
  from inspect import isasyncgen
15
- from inspect import iscoroutinefunction as _inspect_iscoroutinefunction
16
14
  from inspect import isgenerator
17
15
  from types import TracebackType
18
16
  from typing import Any
@@ -25,11 +23,6 @@ from typing import TypeVar
25
23
  from urllib.parse import quote
26
24
 
27
25
  import anyio
28
- from anycorn import serve
29
- from anycorn.config import Config as HyperConfig
30
- from anycorn.typing import ASGIReceiveCallable
31
- from anycorn.typing import ASGISendCallable
32
- from anycorn.typing import Scope
33
26
  from anyio import AsyncFile
34
27
  from anyio import create_task_group
35
28
  from anyio import current_time
@@ -52,6 +45,10 @@ from werkzeug.routing import MapAdapter
52
45
  from werkzeug.routing import RoutingException
53
46
  from werkzeug.wrappers import Response as WerkzeugResponse
54
47
 
48
+ from anyquart.typing import ASGIReceiveCallable
49
+ from anyquart.typing import ASGISendCallable
50
+ from anyquart.typing import Scope
51
+
55
52
  from .asgi import ASGIHTTPConnection
56
53
  from .asgi import ASGILifespan
57
54
  from .asgi import ASGIWebsocketConnection
@@ -63,6 +60,8 @@ from .ctx import has_request_context
63
60
  from .ctx import has_websocket_context
64
61
  from .ctx import RequestContext
65
62
  from .ctx import WebsocketContext
63
+ from .di import check_name_conflicts
64
+ from .di import invoke_with_di
66
65
  from .globals import _cv_app
67
66
  from .globals import _cv_request
68
67
  from .globals import _cv_websocket
@@ -97,8 +96,8 @@ from .testing import make_test_body_with_headers
97
96
  from .testing import make_test_headers_path_and_query_string
98
97
  from .testing import make_test_scope
99
98
  from .testing import no_op_push
100
- from .testing import sentinel
101
99
  from .testing import TestApp
100
+ from .testing.utils import sentinel
102
101
  from .typing import AfterServingCallable
103
102
  from .typing import AfterWebsocketCallable
104
103
  from .typing import ASGIHTTPProtocol
@@ -127,14 +126,12 @@ from .wrappers import Request
127
126
  from .wrappers import Response
128
127
  from .wrappers import Websocket
129
128
 
130
- # Python 3.14 deprecated asyncio.iscoroutinefunction, but suggested
131
- # inspect.iscoroutinefunction does not work correctly in some Python
132
- # versions before 3.12.
133
- # See https://github.com/python/cpython/issues/122858#issuecomment-2466239748
134
- if sys.version_info >= (3, 12):
135
- iscoroutinefunction = _inspect_iscoroutinefunction
129
+ # inspect.iscoroutinefunction behaves differently on python < 3.13
130
+ # See: https://github.com/python/cpython/issues/94924
131
+ if sys.version_info >= (3, 13):
132
+ from inspect import iscoroutinefunction
136
133
  else:
137
- iscoroutinefunction = asyncio.iscoroutinefunction
134
+ from asyncio import iscoroutinefunction
138
135
 
139
136
  AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named
140
137
  T_after_serving = TypeVar("T_after_serving", bound=AfterServingCallable)
@@ -345,6 +342,10 @@ class AnyQuart(App):
345
342
  self.cli = AppGroup()
346
343
  self.cli.name = self.name
347
344
 
345
+ # overrides route handler's dependencies, useful for tests for replacing
346
+ # expensive dependency
347
+ self.dependency_overrides: dict[Callable[..., Any], Callable[..., Any]] = {}
348
+
348
349
  if self.has_static_folder:
349
350
  assert bool(static_host) == host_matching, (
350
351
  "Invalid static_host/host_matching combination"
@@ -573,6 +574,42 @@ class AnyQuart(App):
573
574
  return self.url_map.bind(self.config["SERVER_NAME"], url_scheme=scheme)
574
575
  return None
575
576
 
577
+ @setupmethod
578
+ def add_url_rule(
579
+ self,
580
+ rule: str,
581
+ endpoint: str | None = None,
582
+ view_func: Callable[..., Any] | None = None,
583
+ provide_automatic_options: bool | None = None,
584
+ **options: Any,
585
+ ) -> None:
586
+ """Register a rule for routing incoming requests and building URLs.
587
+
588
+ In addition to the default behaviour this raises
589
+ :exc:`ValueError` at registration time if any parameter of the view
590
+ function is marked as a :class:`~anyquart.di.Needs` dependency but its
591
+ name also appears as a URL rule converter in ``rule``, since the two
592
+ sources would otherwise clobber one another when dispatching.
593
+
594
+ Arguments:
595
+ rule: The URL rule string.
596
+ endpoint: The endpoint name to associate with the rule
597
+ and view function.
598
+ view_func: The view function to associate with the
599
+ endpoint name.
600
+ provide_automatic_options: Add the ``OPTIONS`` method and
601
+ respond to ``OPTIONS`` requests automatically.
602
+ """
603
+ if view_func is not None:
604
+ check_name_conflicts(view_func, rule, options.get("defaults"))
605
+ super().add_url_rule(
606
+ rule,
607
+ endpoint,
608
+ view_func,
609
+ provide_automatic_options,
610
+ **options,
611
+ )
612
+
576
613
  def websocket(
577
614
  self,
578
615
  rule: str,
@@ -785,10 +822,9 @@ class AnyQuart(App):
785
822
  app_import_path: str | None = None,
786
823
  **kwargs: Any,
787
824
  ) -> None:
788
- """Run this application.
789
-
790
- This is best used for development only, see Hypercorn for
791
- production servers.
825
+ """Run this application. This is best used for development only,
826
+ See from the list: https://asgi.readthedocs.io/en/latest/implementations.html
827
+ for production servers.
792
828
 
793
829
  Arguments:
794
830
  host: Hostname to listen on. By default this is loopback
@@ -800,6 +836,15 @@ class AnyQuart(App):
800
836
  certfile: Path to the SSL certificate file.
801
837
  keyfile: Path to the SSL key file.
802
838
  """
839
+ try:
840
+ from anycorn.config import Config as HyperConfig
841
+ except ImportError:
842
+ raise RuntimeError(
843
+ "Running anyquart requires an ASGI web server to be installed\n"
844
+ "You can install one from this list https://asgi.readthedocs.io/en/latest/implementations.html\n"
845
+ "e.g:\n"
846
+ " $ pip install anycorn\n"
847
+ ) from None
803
848
  if kwargs:
804
849
  warnings.warn(
805
850
  f"Additional arguments, {','.join(kwargs.keys())}, are not supported.\n"
@@ -830,25 +875,6 @@ class AnyQuart(App):
830
875
  scheme = "https" if certfile is not None and keyfile is not None else "http"
831
876
  print(f" * Running on {scheme}://{host}:{port} (CTRL + C to quit)")
832
877
 
833
- if use_reloader:
834
- from anycorn.run import run as anycorn_run
835
-
836
- config = self._build_config(host, port, debug, ca_certs, certfile, keyfile)
837
- config.use_reloader = True
838
- config.application_path = app_import_path
839
- anycorn_run(config)
840
- else:
841
- anyio.run(self._run_serve, host, port, debug, ca_certs, certfile, keyfile)
842
-
843
- def _build_config(
844
- self,
845
- host: str,
846
- port: int,
847
- debug: bool | None,
848
- ca_certs: str | None,
849
- certfile: str | None,
850
- keyfile: str | None,
851
- ) -> HyperConfig:
852
878
  config = HyperConfig()
853
879
  config.access_log_format = "%(h)s %(r)s %(s)s %(b)s %(D)s"
854
880
  config.accesslog = "-"
@@ -860,32 +886,20 @@ class AnyQuart(App):
860
886
  config.debug = debug
861
887
  config.errorlog = config.accesslog
862
888
  config.keyfile = keyfile
863
- return config
864
889
 
865
- async def _run_serve(
866
- self,
867
- host: str = "127.0.0.1",
868
- port: int = 5000,
869
- debug: bool = False,
870
- ca_certs: str | None = None,
871
- certfile: str | None = None,
872
- keyfile: str | None = None,
873
- ) -> None:
874
- """runs this application.
890
+ if use_reloader:
891
+ # run directly with anycorn when in reload mode.
892
+ from anycorn.run import run as anycorn_run
875
893
 
876
- This is best used for development only, see Hypercorn for
877
- production servers.
894
+ config.use_reloader = True
895
+ config.application_path = app_import_path
896
+ anycorn_run(config)
897
+ else:
898
+ anyio.run(self._run_serve, config)
878
899
 
879
- Arguments:
880
- host: Hostname to listen on. By default this is loopback
881
- only, use 0.0.0.0 to have the server listen externally.
882
- port: Port number to listen on.
883
- debug: If set enable (or disable) debug mode and debug output.
884
- ca_certs: Path to the SSL CA certificate file.
885
- certfile: Path to the SSL certificate file.
886
- keyfile: Path to the SSL key file.
900
+ async def _run_serve(self, config) -> None: # type: ignore
901
+ from anycorn import serve
887
902
 
888
- """
889
903
  shutdown_event = Event()
890
904
 
891
905
  async def watch_signals() -> None:
@@ -894,10 +908,13 @@ class AnyQuart(App):
894
908
  shutdown_event.set()
895
909
  return
896
910
 
897
- config = self._build_config(host, port, debug, ca_certs, certfile, keyfile)
898
911
  async with create_task_group() as tg:
899
912
  tg.start_soon(watch_signals)
900
- await serve(self, config, shutdown_trigger=shutdown_event.wait)
913
+ await serve(
914
+ self, # type: ignore[arg-type]
915
+ config,
916
+ shutdown_trigger=shutdown_event.wait,
917
+ )
901
918
  tg.cancel_scope.cancel()
902
919
 
903
920
  def test_client(
@@ -1574,7 +1591,7 @@ class AnyQuart(App):
1574
1591
  return await self.make_default_options_response()
1575
1592
 
1576
1593
  handler = self.view_functions[request_.url_rule.endpoint]
1577
- return await self.ensure_async(handler)(**request_.view_args) # type: ignore[return-value]
1594
+ return await invoke_with_di(self, handler, request_.view_args)
1578
1595
 
1579
1596
  async def dispatch_websocket(
1580
1597
  self, websocket_context: WebsocketContext | None = None