anyquart 0.3.0__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.3.0 → anyquart-0.3.1}/PKG-INFO +24 -15
  2. {anyquart-0.3.0 → anyquart-0.3.1}/README.md +20 -11
  3. {anyquart-0.3.0 → anyquart-0.3.1}/pyproject.toml +7 -8
  4. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/app.py +14 -6
  5. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/cli.py +32 -15
  6. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/ctx.py +7 -4
  7. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/di.py +13 -16
  8. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/helpers.py +2 -2
  9. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/sessions.py +3 -5
  10. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/templating.py +1 -3
  11. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/testing/__init__.py +11 -14
  12. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/testing/utils.py +10 -5
  13. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/typing.py +35 -39
  14. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/wrappers/response.py +1 -1
  15. {anyquart-0.3.0 → anyquart-0.3.1}/LICENSE.txt +0 -0
  16. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/__init__.py +0 -0
  17. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/__main__.py +0 -0
  18. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/asgi.py +0 -0
  19. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/blueprints.py +0 -0
  20. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/config.py +0 -0
  21. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/datastructures.py +0 -0
  22. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/debug.py +0 -0
  23. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/formparser.py +0 -0
  24. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/globals.py +0 -0
  25. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/json/__init__.py +0 -0
  26. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/json/provider.py +0 -0
  27. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/json/tag.py +0 -0
  28. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/logging.py +0 -0
  29. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/py.typed +0 -0
  30. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/routing.py +0 -0
  31. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/signals.py +0 -0
  32. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/testing/app.py +0 -0
  33. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/testing/client.py +0 -0
  34. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/testing/connections.py +0 -0
  35. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/utils.py +0 -0
  36. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/views.py +0 -0
  37. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/wrappers/__init__.py +6 -6
  38. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/wrappers/base.py +0 -0
  39. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/wrappers/request.py +0 -0
  40. {anyquart-0.3.0 → anyquart-0.3.1}/src/anyquart/wrappers/websocket.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: anyquart
3
- Version: 0.3.0
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
@@ -31,7 +31,7 @@ Requires-Dist: importlib-metadata; python_version < '3.13'
31
31
  Requires-Dist: itsdangerous
32
32
  Requires-Dist: jinja2
33
33
  Requires-Dist: markupsafe
34
- Requires-Dist: typing-extensions; python_version < '3.13'
34
+ Requires-Dist: typing-extensions >= 4.16.0; python_version < '3.15'
35
35
  Requires-Dist: werkzeug>=3.0
36
36
  Requires-Dist: anycorn>=0.11.2 ; extra == "anycorn"
37
37
  Requires-Dist: python-dotenv ; extra == "dotenv"
@@ -67,32 +67,35 @@ e.g:
67
67
  ```python
68
68
  from anyquart import Needs
69
69
 
70
+
70
71
  async def get_db() -> AsyncGenerator[AsyncSession, None]:
71
72
  async with async_session() as session:
72
73
  yield session
73
74
 
75
+
74
76
  @app.route("/users")
75
- async def get_users(db: AsyncSession = Needs(get_db)) -> None:
76
- ...
77
+ async def get_users(db: AsyncSession = Needs(get_db)) -> None: ...
78
+
77
79
 
78
80
  # Or:
79
81
  DBSession = Annotated[AsyncSession, Needs(get_db)]
80
82
 
83
+
81
84
  @app.route("/users")
82
- async def get_users(db: DBSession) -> None:
83
- ...
85
+ async def get_users(db: DBSession) -> None: ...
86
+
84
87
 
85
88
  # Tests can use `app.dependency_overrides` dictionary to replace route handler's
86
89
  # dependency with test dependency
87
90
  ```
88
- 6. Runs on Python 3.10+
91
+ 6. Runs on Python 3.11+
89
92
 
90
93
  ## Usage
91
94
  You will have to replace `quart` with `anyquart` and `Quart` with `AnyQuart`.
92
95
 
93
- 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+.
94
97
 
95
- ```
98
+ ```python
96
99
  $ pip install anyquart
97
100
  ```
98
101
 
@@ -103,14 +106,17 @@ from anyquart import AnyQuart, websocket, render_template
103
106
 
104
107
  app = AnyQuart(__name__)
105
108
 
109
+
106
110
  @app.route("/")
107
111
  async def hello():
108
112
  return await render_template("index.html")
109
113
 
114
+
110
115
  @app.route("/api")
111
116
  async def json():
112
117
  return {"hello": "world"}
113
118
 
119
+
114
120
  @app.websocket("/ws")
115
121
  async def ws():
116
122
  while True:
@@ -119,14 +125,15 @@ async def ws():
119
125
  ```
120
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:
121
127
  ```bash
122
- $ uv add anyquart[anycorn]
128
+ $ pip install anyquart[anycorn]
123
129
  ```
124
130
  run the application:
125
131
  ```bash
126
132
  $ anycorn app:app
127
133
  * [2026-08-01 11:49:46 +0200] [30809] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit)
128
134
  ```
129
- The built-in `anyquart run` command is only available on anyquart <= 0.2.1 — from 0.3.0 onward, run the ASGI server directly as shown above.
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.
130
137
 
131
138
  # Testing
132
139
  Pytest requires a plugin to run asynchronous test functions and fixtures.
@@ -137,20 +144,22 @@ import pytest
137
144
 
138
145
  from app import app
139
146
 
147
+
140
148
  @pytest.fixture()
141
149
  def anyio_backend():
142
- return "trio" # you can replace with "asyncio"
150
+ return "trio" # you can replace with "asyncio"
151
+
143
152
 
144
153
  @pytest.fixture()
145
154
  def test_client():
146
155
  return app.test_client()
147
156
 
157
+
148
158
  @pytest.mark.anyio
149
159
  async def test_do_something(test_client) -> None:
150
160
  response = await test_client.get("/")
151
161
  assert response.status_code == 200
152
162
  assert await response.json == {"hello": "world"}
153
-
154
163
  ```
155
164
 
156
165
  Refer to the [Quart documentation](https://quart.palletsprojects.com) for more details.
@@ -23,32 +23,35 @@ e.g:
23
23
  ```python
24
24
  from anyquart import Needs
25
25
 
26
+
26
27
  async def get_db() -> AsyncGenerator[AsyncSession, None]:
27
28
  async with async_session() as session:
28
29
  yield session
29
30
 
31
+
30
32
  @app.route("/users")
31
- async def get_users(db: AsyncSession = Needs(get_db)) -> None:
32
- ...
33
+ async def get_users(db: AsyncSession = Needs(get_db)) -> None: ...
34
+
33
35
 
34
36
  # Or:
35
37
  DBSession = Annotated[AsyncSession, Needs(get_db)]
36
38
 
39
+
37
40
  @app.route("/users")
38
- async def get_users(db: DBSession) -> None:
39
- ...
41
+ async def get_users(db: DBSession) -> None: ...
42
+
40
43
 
41
44
  # Tests can use `app.dependency_overrides` dictionary to replace route handler's
42
45
  # dependency with test dependency
43
46
  ```
44
- 6. Runs on Python 3.10+
47
+ 6. Runs on Python 3.11+
45
48
 
46
49
  ## Usage
47
50
  You will have to replace `quart` with `anyquart` and `Quart` with `AnyQuart`.
48
51
 
49
- 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+.
50
53
 
51
- ```
54
+ ```python
52
55
  $ pip install anyquart
53
56
  ```
54
57
 
@@ -59,14 +62,17 @@ from anyquart import AnyQuart, websocket, render_template
59
62
 
60
63
  app = AnyQuart(__name__)
61
64
 
65
+
62
66
  @app.route("/")
63
67
  async def hello():
64
68
  return await render_template("index.html")
65
69
 
70
+
66
71
  @app.route("/api")
67
72
  async def json():
68
73
  return {"hello": "world"}
69
74
 
75
+
70
76
  @app.websocket("/ws")
71
77
  async def ws():
72
78
  while True:
@@ -75,14 +81,15 @@ async def ws():
75
81
  ```
76
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:
77
83
  ```bash
78
- $ uv add anyquart[anycorn]
84
+ $ pip install anyquart[anycorn]
79
85
  ```
80
86
  run the application:
81
87
  ```bash
82
88
  $ anycorn app:app
83
89
  * [2026-08-01 11:49:46 +0200] [30809] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit)
84
90
  ```
85
- The built-in `anyquart run` command is only available on anyquart <= 0.2.1 — from 0.3.0 onward, run the ASGI server directly as shown above.
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.
86
93
 
87
94
  # Testing
88
95
  Pytest requires a plugin to run asynchronous test functions and fixtures.
@@ -93,20 +100,22 @@ import pytest
93
100
 
94
101
  from app import app
95
102
 
103
+
96
104
  @pytest.fixture()
97
105
  def anyio_backend():
98
- return "trio" # you can replace with "asyncio"
106
+ return "trio" # you can replace with "asyncio"
107
+
99
108
 
100
109
  @pytest.fixture()
101
110
  def test_client():
102
111
  return app.test_client()
103
112
 
113
+
104
114
  @pytest.mark.anyio
105
115
  async def test_do_something(test_client) -> None:
106
116
  response = await test_client.get("/")
107
117
  assert response.status_code == 200
108
118
  assert await response.json == {"hello": "world"}
109
-
110
119
  ```
111
120
 
112
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.3.0"
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,17 +21,17 @@ 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",
@@ -41,7 +41,7 @@ dependencies = [
41
41
  "itsdangerous",
42
42
  "jinja2",
43
43
  "markupsafe",
44
- "typing-extensions; python_version < '3.13'",
44
+ "typing-extensions >= 4.16.0; python_version < '3.15'",
45
45
  "werkzeug>=3.0",
46
46
  ]
47
47
 
@@ -59,7 +59,7 @@ 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",
@@ -123,7 +123,7 @@ warn_return_any = false
123
123
  files = ["src", "tests"]
124
124
 
125
125
  [tool.pyright]
126
- pythonVersion = "3.10"
126
+ pythonVersion = "3.11"
127
127
  include = ["src", "tests"]
128
128
  typeCheckingMode = "basic"
129
129
 
@@ -145,7 +145,6 @@ select = [
145
145
  "W", # pycodestyle warning
146
146
  ]
147
147
  ignore = [
148
- "UP038", # keep isinstance tuple
149
148
  "UP007", # Union[] is needed for forward-referenced type aliases
150
149
  "UP045"
151
150
  ]
@@ -156,7 +155,7 @@ order-by-type = false
156
155
 
157
156
  [tool.tox]
158
157
  env_list = [
159
- "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",
160
159
  "style",
161
160
  "typing",
162
161
  "docs",
@@ -11,7 +11,6 @@ from collections.abc import Callable
11
11
  from contextlib import AbstractAsyncContextManager
12
12
  from datetime import timedelta
13
13
  from inspect import isasyncgen
14
- from inspect import iscoroutinefunction
15
14
  from inspect import isgenerator
16
15
  from types import TracebackType
17
16
  from typing import Any
@@ -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,6 +126,13 @@ from .wrappers import Request
127
126
  from .wrappers import Response
128
127
  from .wrappers import Websocket
129
128
 
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
133
+ else:
134
+ from asyncio import iscoroutinefunction
135
+
130
136
  AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named
131
137
  T_after_serving = TypeVar("T_after_serving", bound=AfterServingCallable)
132
138
  T_after_websocket = TypeVar("T_after_websocket", bound=AfterWebsocketCallable)
@@ -833,10 +839,12 @@ class AnyQuart(App):
833
839
  try:
834
840
  from anycorn.config import Config as HyperConfig
835
841
  except ImportError:
836
- raise RuntimeError("""Install ASGI webserver to run in development mode
837
- e.g: `uv add anyquart[anycorn]
838
- or choose one from this list https://asgi.readthedocs.io/en/latest/implementations.html
839
- """) from None
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
840
848
  if kwargs:
841
849
  warnings.warn(
842
850
  f"Additional arguments, {','.join(kwargs.keys())}, are not supported.\n"
@@ -15,6 +15,7 @@ from importlib.metadata import entry_points
15
15
  from operator import attrgetter
16
16
  from types import ModuleType
17
17
  from typing import Any
18
+ from typing import overload
18
19
  from typing import TYPE_CHECKING
19
20
 
20
21
  import anyio
@@ -38,7 +39,7 @@ class NoAppException(click.UsageError):
38
39
  pass
39
40
 
40
41
 
41
- def _called_with_wrong_args(f: Callable) -> bool:
42
+ def _called_with_wrong_args(f: Callable[..., Any]) -> bool:
42
43
  """Check whether calling a function raised a ``TypeError`` because
43
44
  the call failed or because something in the factory raised the
44
45
  error.
@@ -265,7 +266,7 @@ class ScriptInfo:
265
266
  if not app:
266
267
  raise NoAppException(
267
268
  "Could not locate a AnyQuart application. Use the"
268
- " 'AnyQuart --app' option, 'AnyQuart_APP' environment"
269
+ " 'AnyQuart --app' option, 'ANYQUART_APP' environment"
269
270
  " variable, or an 'app.py' file in the"
270
271
  " current directory."
271
272
  )
@@ -317,21 +318,41 @@ class AppGroup(click.Group):
317
318
  Not to be confused with :class:`AnyQuartGroup`.
318
319
  """
319
320
 
320
- def command(self, *args: Any, **kwargs: Any) -> Callable: # type: ignore
321
+ @overload
322
+ def command(self, __func: Callable[..., Any]) -> click.Command: ...
323
+
324
+ @overload
325
+ def command(
326
+ self, *args: Any, **kwargs: Any
327
+ ) -> Callable[[Callable[..., Any]], click.Command]: ...
328
+
329
+ def command(
330
+ self, *args: Any, **kwargs: Any
331
+ ) -> Callable[[Callable[..., Any]], click.Command]:
321
332
  """This works exactly like the method of the same name on a regular
322
333
  :class:`click.Group` but it wraps callbacks in :func:`with_appcontext`
323
334
  if it's enabled by passing ``with_appcontext=True``.
324
335
  """
325
336
  wrap_for_ctx = kwargs.pop("with_appcontext", False)
326
337
 
327
- def decorator(f: Callable) -> Callable:
338
+ def decorator(f: Callable[..., Any]) -> click.Command:
328
339
  if wrap_for_ctx:
329
340
  f = with_appcontext(f)
330
341
  return click.Group.command(self, *args, **kwargs)(f)
331
342
 
332
343
  return decorator
333
344
 
334
- def group(self, *args: Any, **kwargs: Any) -> Callable: # type: ignore
345
+ @overload
346
+ def group(self, __func: Callable[..., Any]) -> click.Group: ...
347
+
348
+ @overload
349
+ def group(
350
+ self, *args: Any, **kwargs: Any
351
+ ) -> Callable[[Callable[..., Any]], click.Group]: ...
352
+
353
+ def group(
354
+ self, *args: Any, **kwargs: Any
355
+ ) -> Callable[[Callable[..., Any]], click.Group] | click.Group:
335
356
  kwargs.setdefault("cls", AppGroup)
336
357
  return super().group(*args, **kwargs)
337
358
 
@@ -374,7 +395,7 @@ def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str
374
395
  # This option is eager so the app will be available if --help is given.
375
396
  # --help is also eager, so --app must be before it in the param list.
376
397
  # no_args_is_help bypasses eager processing, so this option must be
377
- # processed manually in that case to ensure AnyQuart_APP gets picked up.
398
+ # processed manually in that case to ensure ANYQUART_APP gets picked up.
378
399
  _app_option = click.Option(
379
400
  ["-A", "--app"],
380
401
  metavar="IMPORT",
@@ -461,7 +482,7 @@ class AnyQuartGroup(AppGroup):
461
482
  set_debug_flag: bool = True,
462
483
  **extra: Any,
463
484
  ) -> None:
464
- params = list(extra.pop("params", None) or ())
485
+ params: list[click.Parameter] = list(extra.pop("params", None) or ())
465
486
  # Processing is done with option callbacks instead of a group
466
487
  # callback. This allows users to make a custom group callback
467
488
  # without losing the behavior. --env-file must come first so
@@ -474,7 +495,7 @@ class AnyQuartGroup(AppGroup):
474
495
  if "context_settings" not in extra:
475
496
  extra["context_settings"] = {}
476
497
 
477
- extra["context_settings"].setdefault("auto_envvar_prefix", "Quart")
498
+ extra["context_settings"].setdefault("auto_envvar_prefix", "anyquart")
478
499
 
479
500
  super().__init__(params=params, **extra)
480
501
 
@@ -558,12 +579,8 @@ class AnyQuartGroup(AppGroup):
558
579
  return super().make_context(info_name, args, parent=parent, **extra)
559
580
 
560
581
  def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
561
- if not args and self.no_args_is_help:
562
- # Attempt to load --env-file and --app early in case they
563
- # were given as env vars. Otherwise no_args_is_help will not
564
- # see commands from app.cli.
565
- _env_file_option.handle_parse_result(ctx, {}, [])
566
- _app_option.handle_parse_result(ctx, {}, [])
582
+ _env_file_option.handle_parse_result(ctx, {}, [])
583
+ _app_option.handle_parse_result(ctx, {}, [])
567
584
 
568
585
  return super().parse_args(ctx, args)
569
586
 
@@ -776,7 +793,7 @@ cli = AnyQuartGroup(
776
793
  help="""\
777
794
  A general utility script for AnyQuart applications.
778
795
  An application to load must be given with the '--app' option,
779
- 'AnyQuart_APP' environment variable, or with an 'app.py' file
796
+ 'ANYQUART_APP' environment variable, or with an 'app.py' file
780
797
  in the current directory.
781
798
  """,
782
799
  )
@@ -27,7 +27,10 @@ from .wrappers import Websocket
27
27
  if TYPE_CHECKING:
28
28
  from .app import AnyQuart # noqa
29
29
 
30
- _sentinel = object()
30
+ if sys.version_info < (3, 15):
31
+ from typing_extensions import sentinel
32
+
33
+ _sentinel = sentinel("_sentinel")
31
34
 
32
35
 
33
36
  class _BaseRequestWebsocketContext:
@@ -153,7 +156,7 @@ class RequestContext(_BaseRequestWebsocketContext):
153
156
  await super()._push_appctx(_cv_request.set(self))
154
157
  await super()._push()
155
158
 
156
- async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
159
+ async def pop(self, exc: BaseException | _sentinel = _sentinel) -> None:
157
160
  try:
158
161
  if len(self._cv_tokens) == 1:
159
162
  if exc is _sentinel:
@@ -210,7 +213,7 @@ class WebsocketContext(_BaseRequestWebsocketContext):
210
213
  await super()._push_appctx(_cv_websocket.set(self))
211
214
  await super()._push()
212
215
 
213
- async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
216
+ async def pop(self, exc: BaseException | _sentinel = _sentinel) -> None:
214
217
  try:
215
218
  if len(self._cv_tokens) == 1:
216
219
  if exc is _sentinel:
@@ -265,7 +268,7 @@ class AppContext:
265
268
  _sync_wrapper=self.app.ensure_async, # type: ignore[arg-type]
266
269
  )
267
270
 
268
- async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
271
+ async def pop(self, exc: BaseException | _sentinel = _sentinel) -> None:
269
272
  try:
270
273
  if len(self._cv_tokens) == 1:
271
274
  if exc is _sentinel:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  import re
5
+ import sys
5
6
  import typing
6
7
  import weakref
7
8
  from collections.abc import AsyncGenerator
@@ -18,6 +19,8 @@ from anyio import to_thread
18
19
  if typing.TYPE_CHECKING:
19
20
  from .app import AnyQuart
20
21
 
22
+ if sys.version_info < (3, 15):
23
+ from typing_extensions import sentinel
21
24
 
22
25
  # Matches the converter portions of a URL rule so that we can extract the
23
26
  # variable names (e.g. ``<int:user_id>`` yields ``user_id``).
@@ -25,7 +28,7 @@ _rule_variable_re = re.compile(
25
28
  r"<(?:(?P<converter>[a-zA-Z_][a-zA-Z0-9_]*)(?:\([^>]*\))?:)?"
26
29
  r"(?P<variable>[a-zA-Z_][a-zA-Z0-9_]*)>"
27
30
  )
28
- _NO_VALUE = object()
31
+ _NO_VALUE = sentinel("_NO_VALUE")
29
32
 
30
33
  _dependency_map_cache: weakref.WeakKeyDictionary[
31
34
  Callable[..., Any], dict[str, Callable[..., Any]]
@@ -34,25 +37,23 @@ _dependency_map_cache: weakref.WeakKeyDictionary[
34
37
 
35
38
  @dataclass(frozen=True)
36
39
  class _Needs:
40
+ dependency: Callable[..., Any]
41
+
42
+
43
+ def Needs(dependency: Callable[..., Any]) -> Any: # noqa 802
37
44
  """Mark a handler or dependency parameter as a request-scoped dependency.
38
45
 
39
46
  Arguments:
40
47
  dependency: The callable that produces the value for the marked
41
48
  parameter.
42
49
  """
43
-
44
- dependency: Callable[..., Any]
45
-
46
-
47
- def Needs(dependency: Callable[..., Any]) -> Any: # noqa 802
48
50
  return _Needs(dependency=dependency)
49
51
 
50
52
 
51
53
  def build_route_handler_dependency_map(
52
54
  func: Callable[..., Any],
53
55
  ) -> dict[str, Callable[..., Any]]:
54
- """
55
- Build route handler dependency map(a dictionary)
56
+ """Build route handler dependency map(a dictionary)
56
57
  i.e:``{parameter_name: dependency_func}``
57
58
 
58
59
  Any handler or dependency parameter whose default value is a
@@ -106,8 +107,7 @@ def build_route_handler_dependency_map(
106
107
 
107
108
 
108
109
  def rule_arguments(rule: str) -> set[str]:
109
- """
110
- Get a set of url converter variables
110
+ """Get a set of url converter variables
111
111
  present in a url path(a Url Rule in flask lingo).
112
112
  Arguments:
113
113
  rule: a url path
@@ -120,8 +120,7 @@ def rule_arguments(rule: str) -> set[str]:
120
120
  def check_name_conflicts(
121
121
  view_func: Callable[..., Any], rule: str, defaults: dict[str, Any] | None = None
122
122
  ) -> None:
123
- """
124
- A handler parameter cannot be both a ``Needs`` dependency injected value
123
+ """A handler parameter cannot be both a ``Needs`` dependency injected value
125
124
  and a URL rule converter (or a rule ``default``) on the same route.
126
125
  e.g:
127
126
  ```
@@ -155,8 +154,7 @@ def check_name_conflicts(
155
154
  async def invoke_with_di(
156
155
  app: AnyQuart, func: Callable[..., Any], view_args: dict[str, Any] | None
157
156
  ) -> Any:
158
- """
159
- Invoke the ``func`` resolving any of its dependency injected parameters.
157
+ """Invoke the ``func`` resolving any of its dependency injected parameters.
160
158
 
161
159
  When ``func`` declares no dependencies this is exactly equivalent to
162
160
  ``await app.ensure_async(func)(**view_args)`` so non-injected routes carry
@@ -174,8 +172,7 @@ async def invoke_with_di(
174
172
 
175
173
 
176
174
  class _Resolver:
177
- """
178
- Resolves a dependency tree for a single request.
175
+ """Resolves a dependency tree for a single request.
179
176
 
180
177
  A resolver instance is created per request and holds that request's cache
181
178
  of resolved values plus its generator teardowns, guaranteeing that two
@@ -6,7 +6,7 @@ from collections.abc import Callable
6
6
  from collections.abc import Iterable
7
7
  from datetime import datetime
8
8
  from datetime import timedelta
9
- from datetime import timezone
9
+ from datetime import UTC
10
10
  from functools import cache
11
11
  from functools import wraps
12
12
  from io import BytesIO
@@ -339,7 +339,7 @@ async def send_file(
339
339
  response.cache_control.public = True
340
340
  if cache_timeout is not None:
341
341
  response.cache_control.max_age = cache_timeout
342
- response.expires = datetime.now(timezone.utc) + timedelta(seconds=cache_timeout)
342
+ response.expires = datetime.now(UTC) + timedelta(seconds=cache_timeout)
343
343
 
344
344
  if add_etags and etag is not None:
345
345
  response.set_etag(etag)
@@ -2,14 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  from datetime import datetime
5
- from datetime import timezone
5
+ from datetime import UTC
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from flask.sessions import NullSession as NullSession # noqa: F401
9
9
  from flask.sessions import SecureCookieSession as SecureCookieSession # noqa: F401
10
- from flask.sessions import ( # noqa: F401
11
- session_json_serializer as session_json_serializer,
12
- )
10
+ from flask.sessions import session_json_serializer as session_json_serializer # noqa: F401
13
11
  from flask.sessions import SessionMixin as SessionMixin # noqa: F401
14
12
  from itsdangerous import BadSignature
15
13
  from itsdangerous import URLSafeTimedSerializer
@@ -84,7 +82,7 @@ class SessionInterface:
84
82
  the browser stops accessing the app.
85
83
  """
86
84
  if session.permanent:
87
- return datetime.now(timezone.utc) + app.permanent_session_lifetime
85
+ return datetime.now(UTC) + app.permanent_session_lifetime
88
86
  else:
89
87
  return None
90
88
 
@@ -4,9 +4,7 @@ from collections.abc import AsyncIterator
4
4
  from typing import Any
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from flask.templating import (
8
- DispatchingJinjaLoader as DispatchingJinjaLoader, # noqa: F401
9
- )
7
+ from flask.templating import DispatchingJinjaLoader as DispatchingJinjaLoader # noqa: F401
10
8
  from jinja2 import Environment as BaseEnvironment
11
9
  from jinja2 import Template
12
10
 
@@ -1,5 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
+ __all__ = (
4
+ "make_test_body_with_headers",
5
+ "make_test_headers_path_and_query_string",
6
+ "make_test_scope",
7
+ "no_op_push",
8
+ "AnyQuartClient",
9
+ "AnyQuartCliRunner",
10
+ "TestApp",
11
+ "WebsocketResponseError",
12
+ )
13
+
3
14
  from typing import Any
4
15
  from typing import TYPE_CHECKING
5
16
 
@@ -13,7 +24,6 @@ from .utils import make_test_body_with_headers
13
24
  from .utils import make_test_headers_path_and_query_string
14
25
  from .utils import make_test_scope
15
26
  from .utils import no_op_push
16
- from .utils import sentinel
17
27
 
18
28
  if TYPE_CHECKING:
19
29
  from ..app import AnyQuart
@@ -32,16 +42,3 @@ class AnyQuartCliRunner(CliRunner):
32
42
  kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
33
43
 
34
44
  return super().invoke(cli, args, **kwargs)
35
-
36
-
37
- __all__ = (
38
- "make_test_body_with_headers",
39
- "make_test_headers_path_and_query_string",
40
- "make_test_scope",
41
- "no_op_push",
42
- "AnyQuartClient",
43
- "AnyQuartCliRunner",
44
- "sentinel",
45
- "TestApp",
46
- "WebsocketResponseError",
47
- )
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  from typing import Any
4
5
  from typing import AnyStr
5
6
  from typing import cast
@@ -30,7 +31,11 @@ from ..utils import encode_headers
30
31
  if TYPE_CHECKING:
31
32
  from ..app import AnyQuart # noqa
32
33
 
33
- sentinel = object()
34
+ if sys.version_info < (3, 15):
35
+ from typing_extensions import sentinel as sentinel
36
+
37
+ _sentinel = sentinel("_sentinel")
38
+ sentinel = _sentinel # type: ignore
34
39
 
35
40
 
36
41
  def make_test_headers_path_and_query_string(
@@ -84,7 +89,7 @@ def make_test_body_with_headers(
84
89
  data: AnyStr | None = None,
85
90
  form: dict | None = None,
86
91
  files: dict[str, FileStorage] | None = None,
87
- json: Any = sentinel,
92
+ json: Any = _sentinel,
88
93
  app: AnyQuart | None = None,
89
94
  ) -> tuple[bytes, Headers]:
90
95
  """Make the body bytes with associated headers.
@@ -98,11 +103,11 @@ def make_test_body_with_headers(
98
103
  json: Data to send json encoded in the request body.
99
104
 
100
105
  """
101
- if [json is not sentinel, form is not None, data is not None].count(True) > 1:
106
+ if [json is not _sentinel, form is not None, data is not None].count(True) > 1:
102
107
  raise ValueError(
103
108
  "anyquart test args 'json', 'form', and 'data' are mutually exclusive"
104
109
  )
105
- if [json is not sentinel, files is not None, data is not None].count(True) > 1:
110
+ if [json is not _sentinel, files is not None, data is not None].count(True) > 1:
106
111
  raise ValueError(
107
112
  "anyquart test args 'files', 'json', and 'data' are mutually exclusive"
108
113
  )
@@ -116,7 +121,7 @@ def make_test_body_with_headers(
116
121
  elif isinstance(data, bytes):
117
122
  request_data = data
118
123
 
119
- if json is not sentinel:
124
+ if json is not _sentinel:
120
125
  request_data = dumps(json).encode("utf-8")
121
126
  headers["Content-Type"] = "application/json"
122
127
  elif files is not None:
@@ -1,44 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
- import sys
5
- from collections.abc import AsyncGenerator
6
- from collections.abc import Awaitable
7
- from collections.abc import Callable
8
- from collections.abc import Iterable
9
- from collections.abc import Iterator
10
- from collections.abc import Mapping
11
- from collections.abc import Sequence
12
- from contextlib import AbstractAsyncContextManager
13
- from datetime import datetime
14
- from datetime import timedelta
15
- from http.cookiejar import CookieJar
16
- from types import TracebackType
17
- from typing import Any
18
- from typing import AnyStr
19
- from typing import Literal
20
- from typing import Optional
21
- from typing import Protocol
22
- from typing import TYPE_CHECKING
23
- from typing import TypedDict
24
- from typing import Union
25
-
26
- from .datastructures import FileStorage
27
-
28
- if sys.version_info >= (3, 11):
29
- from typing import NotRequired
30
- else:
31
- from typing_extensions import NotRequired
32
-
33
- if TYPE_CHECKING:
34
- from werkzeug.datastructures import Authorization # noqa: F401
35
- from werkzeug.datastructures import Headers # noqa: F401
36
- from werkzeug.wrappers import Response as WerkzeugResponse
37
-
38
- from .app import AnyQuart
39
- from .sessions import SessionMixin
40
- from .wrappers.response import Response
41
-
42
3
  __all__ = (
43
4
  "ASGIVersions",
44
5
  "HTTPScope",
@@ -73,6 +34,41 @@ __all__ = (
73
34
  "ASGISendCallable",
74
35
  )
75
36
 
37
+ import os
38
+ from collections.abc import AsyncGenerator
39
+ from collections.abc import Awaitable
40
+ from collections.abc import Callable
41
+ from collections.abc import Iterable
42
+ from collections.abc import Iterator
43
+ from collections.abc import Mapping
44
+ from collections.abc import Sequence
45
+ from contextlib import AbstractAsyncContextManager
46
+ from datetime import datetime
47
+ from datetime import timedelta
48
+ from http.cookiejar import CookieJar
49
+ from types import TracebackType
50
+ from typing import Any
51
+ from typing import AnyStr
52
+ from typing import Literal
53
+ from typing import NotRequired
54
+ from typing import Optional
55
+ from typing import Protocol
56
+ from typing import TYPE_CHECKING
57
+ from typing import TypedDict
58
+ from typing import Union
59
+
60
+ from .datastructures import FileStorage
61
+
62
+ if TYPE_CHECKING:
63
+ from werkzeug.datastructures import Authorization # noqa: F401
64
+ from werkzeug.datastructures import Headers # noqa: F401
65
+ from werkzeug.wrappers import Response as WerkzeugResponse
66
+
67
+ from .app import AnyQuart
68
+ from .sessions import SessionMixin
69
+ from .wrappers.response import Response
70
+
71
+
76
72
  FilePath = Union[bytes, str, os.PathLike]
77
73
 
78
74
  # The possible types that are directly convertible or are a Response object.
@@ -105,7 +105,7 @@ class IterableBody(ResponseBody):
105
105
  if isinstance(iterable, Iterable):
106
106
  self.iter = run_sync_iterable(iter(iterable))
107
107
  else:
108
- self.iter = iterable.__aiter__() # Can't use aiter() until 3.10
108
+ self.iter = aiter(iterable)
109
109
 
110
110
  async def __aenter__(self) -> IterableBody:
111
111
  return self
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,11 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from .base import BaseRequestWebsocket
4
- from .request import Body
5
- from .request import Request
6
- from .response import Response
7
- from .websocket import Websocket
8
-
9
3
  __all__ = (
10
4
  "BaseRequestWebsocket",
11
5
  "Body",
@@ -13,3 +7,9 @@ __all__ = (
13
7
  "Response",
14
8
  "Websocket",
15
9
  )
10
+
11
+ from .base import BaseRequestWebsocket
12
+ from .request import Body
13
+ from .request import Request
14
+ from .response import Response
15
+ from .websocket import Websocket