Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-starlette for
openSUSE:Factory checked in at 2023-02-13 16:39:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-starlette (Old)
and /work/SRC/openSUSE:Factory/.python-starlette.new.1848 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-starlette"
Mon Feb 13 16:39:25 2023 rev:16 rq:1064388 version:0.24.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-starlette/python-starlette.changes
2023-01-05 15:55:06.226922109 +0100
+++
/work/SRC/openSUSE:Factory/.python-starlette.new.1848/python-starlette.changes
2023-02-13 16:41:30.567579938 +0100
@@ -1,0 +2,20 @@
+Fri Feb 10 18:28:13 UTC 2023 - David Anes <[email protected]>
+
+- Disable broken tests for i586 and armv7l.
+
+- Update to 0.24.0
+ * Added
+ - Allow StaticFiles to follow symlinks
+ - Allow Request.form() as a context manager
+ - Add size attribute to UploadFile
+ - Add env_prefix argument to Config
+ - Add template context processors
+ - Support str and datetime on expires parameter on the Response.set_cookie
method
+ * Changed
+ - Lazily build the middleware stack
+ - Make the file argument required on UploadFile
+ - Use debug extension instead of custom response template extension
+ * Fixed
+ - Fix url parsing of ipv6 urls on URL.replace
+
+-------------------------------------------------------------------
Old:
----
starlette-0.23.1.tar.gz
New:
----
starlette-0.24.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-starlette.spec ++++++
--- /var/tmp/diff_new_pack.3SeBBE/_old 2023-02-13 16:41:31.083583277 +0100
+++ /var/tmp/diff_new_pack.3SeBBE/_new 2023-02-13 16:41:31.091583329 +0100
@@ -27,7 +27,7 @@
%define skip_python2 1
Name: python-starlette%{psuffix}
-Version: 0.23.1
+Version: 0.24.0
Release: 0
Summary: Lightweight ASGI framework/toolkit
License: BSD-3-Clause
@@ -58,6 +58,7 @@
BuildRequires: %{python_module trio}
# testing requires it for all flavors
BuildRequires: %{python_module typing_extensions}
+BuildRequires: %{python_module importlib-metadata}
# /SECITON
%endif
%python_subpackages
@@ -84,7 +85,15 @@
sed -i "s|--strict-config||" setup.cfg
sed -i "s|--strict-markers||" setup.cfg
sed -i "s| error$||" setup.cfg
-%pytest --asyncio-mode=strict
+
+# The following tests don't work in some archs because time_t cannot
+# hold the values the test expect, as they go beyond the maximum
+# value in i586 and armv7l. As we are using Buildarch: noarch, we
+# cannot just use ifarch conditionals here...
+ignored_tests="test_set_cookie"
+ignored_tests="$ignored_tests or test_expires_on_set_cookie"
+%pytest --asyncio-mode=strict -k "not ($ignored_tests)"
+
%endif
%if ! %{with test}
++++++ starlette-0.23.1.tar.gz -> starlette-0.24.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/config.md
new/starlette-0.24.0/docs/config.md
--- old/starlette-0.23.1/docs/config.md 2022-12-09 15:45:37.000000000 +0100
+++ new/starlette-0.24.0/docs/config.md 2023-02-06 17:01:14.000000000 +0100
@@ -113,6 +113,23 @@
environ['TESTING'] = 'TRUE'
```
+## Reading prefixed environment variables
+
+You can namespace the environment variables by setting `env_prefix` argument.
+
+```python title="myproject/settings.py"
+import os
+from starlette.config import Config
+
+os.environ['APP_DEBUG'] = 'yes'
+os.environ['ENVIRONMENT'] = 'dev'
+
+config = Config(env_prefix='APP_')
+
+DEBUG = config('DEBUG') # lookups APP_DEBUG, returns "yes"
+ENVIRONMENT = config('ENVIRONMENT') # lookups APP_ENVIRONMENT, raises KeyError
as variable is not defined
+```
+
## A full example
Structuring large applications can be complex. You need proper separation of
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/release-notes.md
new/starlette-0.24.0/docs/release-notes.md
--- old/starlette-0.23.1/docs/release-notes.md 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/docs/release-notes.md 2023-02-06 17:01:14.000000000
+0100
@@ -1,3 +1,23 @@
+## 0.24.0
+
+February 6, 2023
+
+### Added
+* Allow `StaticFiles` to follow symlinks
[#1683](https://github.com/encode/starlette/pull/1683).
+* Allow `Request.form()` as a context manager
[#1903](https://github.com/encode/starlette/pull/1903).
+* Add `size` attribute to `UploadFile`
[#1405](https://github.com/encode/starlette/pull/1405).
+* Add `env_prefix` argument to `Config`
[#1990](https://github.com/encode/starlette/pull/1990).
+* Add template context processors
[#1904](https://github.com/encode/starlette/pull/1904).
+* Support `str` and `datetime` on `expires` parameter on the
`Response.set_cookie` method
[#1908](https://github.com/encode/starlette/pull/1908).
+
+### Changed
+* Lazily build the middleware stack
[#2017](https://github.com/encode/starlette/pull/2017).
+* Make the `file` argument required on `UploadFile`
[#1413](https://github.com/encode/starlette/pull/1413).
+* Use debug extension instead of custom response template extension
[#1991](https://github.com/encode/starlette/pull/1991).
+
+### Fixed
+* Fix url parsing of ipv6 urls on `URL.replace`
[#1965](https://github.com/encode/starlette/pull/1965).
+
## 0.23.1
December 9, 2022
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/requests.md
new/starlette-0.24.0/docs/requests.md
--- old/starlette-0.23.1/docs/requests.md 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/docs/requests.md 2023-02-06 17:01:14.000000000
+0100
@@ -81,7 +81,7 @@
The request body as bytes: `await request.body()`
-The request body, parsed as form data or multipart: `await request.form()`
+The request body, parsed as form data or multipart: `async with request.form()
as form:`
The request body, parsed as JSON: `await request.json()`
@@ -114,7 +114,7 @@
Request files are normally sent as multipart form data (`multipart/form-data`).
-When you call `await request.form()` you receive a
`starlette.datastructures.FormData` which is an immutable
+When you call `async with request.form() as form` you receive a
`starlette.datastructures.FormData` which is an immutable
multidict, containing both file uploads and text input. File upload items are
represented as instances of `starlette.datastructures.UploadFile`.
`UploadFile` has the following attributes:
@@ -123,6 +123,7 @@
* `content_type`: A `str` with the content type (MIME type / media type) (e.g.
`image/jpeg`).
* `file`: A <a
href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile"
target="_blank">`SpooledTemporaryFile`</a> (a <a
href="https://docs.python.org/3/glossary.html#term-file-like-object"
target="_blank">file-like</a> object). This is the actual Python file that you
can pass directly to other functions or libraries that expect a "file-like"
object.
* `headers`: A `Headers` object. Often this will only be the `Content-Type`
header, but if additional headers were included in the multipart field they
will be included here. Note that these headers have no relationship with the
headers in `Request.headers`.
+* `size`: An `int` with uploaded file's size in bytes. This value is
calculated from request's contents, making it better choice to find uploaded
file's size than `Content-Length` header. `None` if not set.
`UploadFile` has the following `async` methods. They all call the
corresponding file methods underneath (using the internal
`SpooledTemporaryFile`).
@@ -137,9 +138,9 @@
For example, you can get the file name and the contents with:
```python
-form = await request.form()
-filename = form["upload_file"].filename
-contents = await form["upload_file"].read()
+async with request.form() as form:
+ filename = form["upload_file"].filename
+ contents = await form["upload_file"].read()
```
!!! info
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/responses.md
new/starlette-0.24.0/docs/responses.md
--- old/starlette-0.23.1/docs/responses.md 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/docs/responses.md 2023-02-06 17:01:14.000000000
+0100
@@ -36,7 +36,7 @@
* `key` - A string that will be the cookie's key.
* `value` - A string that will be the cookie's value.
* `max_age` - An integer that defines the lifetime of the cookie in seconds. A
negative integer or a value of `0` will discard the cookie immediately.
`Optional`
-* `expires` - An integer that defines the number of seconds until the cookie
expires. `Optional`
+* `expires` - Either an integer that defines the number of seconds until the
cookie expires, or a datetime. `Optional`
* `path` - A string that specifies the subset of routes to which the cookie
will apply. `Optional`
* `domain` - A string that specifies the domain for which the cookie is valid.
`Optional`
* `secure` - A bool indicating that the cookie will only be sent to the server
if request is made using SSL and the HTTPS protocol. `Optional`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/staticfiles.md
new/starlette-0.24.0/docs/staticfiles.md
--- old/starlette-0.23.1/docs/staticfiles.md 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/docs/staticfiles.md 2023-02-06 17:01:14.000000000
+0100
@@ -3,12 +3,13 @@
### StaticFiles
-Signature: `StaticFiles(directory=None, packages=None, check_dir=True)`
+Signature: `StaticFiles(directory=None, packages=None, check_dir=True,
follow_symlink=False)`
* `directory` - A string or [os.Pathlike][pathlike] denoting a directory path.
* `packages` - A list of strings or list of tuples of strings of python
packages.
* `html` - Run in HTML mode. Automatically loads `index.html` for directories
if such file exist.
* `check_dir` - Ensure that the directory exists upon instantiation. Defaults
to `True`.
+* `follow_symlink` - A boolean indicating if symbolic links for files and
directories should be followed. Defaults to `False`.
You can combine this ASGI application with Starlette's routing to provide
comprehensive static file serving.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/templates.md
new/starlette-0.24.0/docs/templates.md
--- old/starlette-0.23.1/docs/templates.md 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/docs/templates.md 2023-02-06 17:01:14.000000000
+0100
@@ -1,4 +1,4 @@
-Starlette is not *strictly* coupled to any particular templating engine, but
+Starlette is not _strictly_ coupled to any particular templating engine, but
Jinja2 provides an excellent choice.
Starlette provides a simple way to get `jinja2` configured. This is probably
@@ -33,7 +33,7 @@
For example, we can link to static files from within our HTML templates:
```html
-<link href="{{ url_for('static', path='/css/bootstrap.min.css') }}"
rel="stylesheet">
+<link href="{{ url_for('static', path='/css/bootstrap.min.css') }}"
rel="stylesheet" />
```
If you want to use [custom filters][jinja2], you will need to update the `env`
@@ -50,12 +50,51 @@
templates.env.filters['marked'] = marked_filter
```
+## Context processors
+
+A context processor is a function that returns a dictionary to be merged into
a template context.
+Every function takes only one argument `request` and must return a dictionary
to add to the context.
+
+A common use case of template processors is to extend the template context
with shared variables.
+
+```python
+import typing
+from starlette.requests import Request
+
+def app_context(request: Request) -> typing.Dict[str, typing.Any]:
+ return {'app': request.app}
+```
+
+### Registering context templates
+
+Pass context processors to `context_processors` argument of the
`Jinja2Templates` class.
+
+```python
+import typing
+
+from starlette.requests import Request
+from starlette.templating import Jinja2Templates
+
+def app_context(request: Request) -> typing.Dict[str, typing.Any]:
+ return {'app': request.app}
+
+templates = Jinja2Templates(
+ directory='templates', context_processors=[app_context]
+)
+```
+
+!!! info
+ Asynchronous functions as context processors are not supported.
+
## Testing template responses
When using the test client, template responses include `.template` and
`.context`
attributes.
```python
+from starlette.testclient import TestClient
+
+
def test_homepage():
client = TestClient(app)
response = client.get("/")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/third-party-packages.md
new/starlette-0.24.0/docs/third-party-packages.md
--- old/starlette-0.23.1/docs/third-party-packages.md 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/docs/third-party-packages.md 2023-02-06
17:01:14.000000000 +0100
@@ -147,7 +147,6 @@
to quickly generate fully customizable admin interface for your models. You
can export your data to many formats (*CSV*, *PDF*,
*Excel*, etc), filter your data with complex query including `AND` and `OR`
conditions, upload files, ...
-
## Frameworks
### FastAPI
@@ -158,6 +157,17 @@
High performance, easy to learn, fast to code, ready for production web API
framework.
Inspired by **APIStar**'s previous server system with type declarations for
route parameters, based on the OpenAPI specification version 3.0.0+ (with JSON
Schema), powered by **Pydantic** for the data handling.
+### Esmerald
+
+<a href="https://github.com/dymmond/esmerald" target="_blank">GitHub</a> |
+<a href="https://esmerald.dymmond.com/" target="_blank">Documentation</a>
+
+Highly scalable, performant, easy to learn, easy to code and for every
application web framework.
+Inspired by a lot of frameworks out there, Esmerald provides what every
application needs, from the
+smallest to complex. Includes, routes, middlewares, permissions, scheduler,
interceptors and lot more.
+
+Powered by **Starlette** and **Pydantic** with OpenAPI specification.
+
### Flama
<a href="https://github.com/perdy/flama/" target="_blank">GitHub</a> |
@@ -204,6 +214,15 @@
<a href="https://github.com/adriangb/xpresso" target="_blank">GitHub</a> |
<a href=https://xpresso-api.dev/" target="_blank">Documentation</a>
+### Ellar
+
+<a href="https://github.com/eadwinCode/ellar" target="_blank">GitHub</a> |
+<a href="https://eadwincode.github.io/ellar/" target="_blank">Documentation</a>
+
+Ellar is an ASGI web framework for building fast, efficient and scalable
RESTAPIs and server-side applications. It offers a high level of abstraction in
building server-side applications and combines elements of OOP (Object Oriented
Programming), and FP (Functional Programming) - Inspired by Nestjs.
+
+It is built on 3 core libraries **Starlette**, **Pydantic**, and **injector**.
+
### Apiman
An extension to integrate Swagger/OpenAPI document easily for Starlette
project and provide [SwaggerUI](http://swagger.io/swagger-ui/) and
[RedocUI](https://rebilly.github.io/ReDoc/).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/docs/websockets.md
new/starlette-0.24.0/docs/websockets.md
--- old/starlette-0.23.1/docs/websockets.md 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/docs/websockets.md 2023-02-06 17:01:14.000000000
+0100
@@ -73,6 +73,29 @@
JSON messages default to being received over text data frames, from version
0.10.0 onwards.
Use `websocket.receive_json(data, mode="binary")` to receive JSON over binary
data frames.
+### Iterating data
+
+* `websocket.iter_text()`
+* `websocket.iter_bytes()`
+* `websocket.iter_json()`
+
+Similar to `receive_text`, `receive_bytes`, and `receive_json` but returns an
+async iterator.
+
+```python hl_lines="7-8"
+from starlette.websockets import WebSocket
+
+
+async def app(scope, receive, send):
+ websocket = WebSocket(scope=scope, receive=receive, send=send)
+ await websocket.accept()
+ async for message in websocket.iter_text():
+ await websocket.send_text(f"Message text was: {message}")
+ await websocket.close()
+```
+
+When `starlette.websockets.WebSocketDisconnect` is raised, the iterator will
exit.
+
### Closing the connection
* `await websocket.close(code=1000, reason=None)`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/requirements.txt
new/starlette-0.24.0/requirements.txt
--- old/starlette-0.23.1/requirements.txt 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/requirements.txt 2023-02-06 17:01:14.000000000
+0100
@@ -3,15 +3,15 @@
# Testing
autoflake==1.5.3
-black==22.10.0
-coverage==6.5.0
+black==22.12.0
+coverage==7.1.0
flake8==3.9.2
importlib-metadata==4.13.0
isort==5.10.1
mypy==0.991
typing_extensions==4.4.0
types-contextvars==2.4.7
-types-PyYAML==6.0.12.2
+types-PyYAML==6.0.12.3
types-dataclasses==0.6.6
pytest==7.2.0
trio==0.21.0
@@ -22,5 +22,5 @@
mkautodoc==0.2.0
# Packaging
-build==0.8.0
-twine==4.0.1
+build==0.9.0
+twine==4.0.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/setup.py
new/starlette-0.24.0/setup.py
--- old/starlette-0.23.1/setup.py 2022-12-09 15:45:37.000000000 +0100
+++ new/starlette-0.24.0/setup.py 1970-01-01 01:00:00.000000000 +0100
@@ -1,29 +0,0 @@
-import sys
-
-from setuptools import setup
-
-sys.stderr.write(
- """
-===============================
-Unsupported installation method
-===============================
-Starlette no longer supports installation with `python setup.py install`.
-Please use `python -m pip install .` instead.
-"""
-)
-sys.exit(1)
-
-
-# The below code will never execute, however GitHub is particularly
-# picky about where it finds Python packaging metadata.
-# See: https://github.com/github/feedback/discussions/6456
-#
-# To be removed once GitHub catches up.
-
-setup(
- name="starlette",
- install_requires=[
- "anyio>=3.4.0,<5",
- "typing_extensions>=3.10.0; python_version < '3.10'",
- ],
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/__init__.py
new/starlette-0.24.0/starlette/__init__.py
--- old/starlette-0.23.1/starlette/__init__.py 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/starlette/__init__.py 2023-02-06 17:01:14.000000000
+0100
@@ -1 +1 @@
-__version__ = "0.23.1"
+__version__ = "0.24.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/_utils.py
new/starlette-0.24.0/starlette/_utils.py
--- old/starlette-0.23.1/starlette/_utils.py 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/starlette/_utils.py 2023-02-06 17:01:14.000000000
+0100
@@ -1,6 +1,13 @@
import asyncio
import functools
+import sys
import typing
+from types import TracebackType
+
+if sys.version_info < (3, 8): # pragma: no cover
+ from typing_extensions import Protocol
+else: # pragma: no cover
+ from typing import Protocol
def is_async_callable(obj: typing.Any) -> bool:
@@ -10,3 +17,58 @@
return asyncio.iscoroutinefunction(obj) or (
callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
)
+
+
+T_co = typing.TypeVar("T_co", covariant=True)
+
+
+# TODO: once 3.8 is the minimum supported version (27 Jun 2023)
+# this can just become
+# class AwaitableOrContextManager(
+# typing.Awaitable[T_co],
+# typing.AsyncContextManager[T_co],
+# typing.Protocol[T_co],
+# ):
+# pass
+class AwaitableOrContextManager(Protocol[T_co]):
+ def __await__(self) -> typing.Generator[typing.Any, None, T_co]:
+ ... # pragma: no cover
+
+ async def __aenter__(self) -> T_co:
+ ... # pragma: no cover
+
+ async def __aexit__(
+ self,
+ __exc_type: typing.Optional[typing.Type[BaseException]],
+ __exc_value: typing.Optional[BaseException],
+ __traceback: typing.Optional[TracebackType],
+ ) -> typing.Union[bool, None]:
+ ... # pragma: no cover
+
+
+class SupportsAsyncClose(Protocol):
+ async def close(self) -> None:
+ ... # pragma: no cover
+
+
+SupportsAsyncCloseType = typing.TypeVar(
+ "SupportsAsyncCloseType", bound=SupportsAsyncClose, covariant=False
+)
+
+
+class AwaitableOrContextManagerWrapper(typing.Generic[SupportsAsyncCloseType]):
+ __slots__ = ("aw", "entered")
+
+ def __init__(self, aw: typing.Awaitable[SupportsAsyncCloseType]) -> None:
+ self.aw = aw
+
+ def __await__(self) -> typing.Generator[typing.Any, None,
SupportsAsyncCloseType]:
+ return self.aw.__await__()
+
+ async def __aenter__(self) -> SupportsAsyncCloseType:
+ self.entered = await self.aw
+ return self.entered
+
+ async def __aexit__(self, *args: typing.Any) -> typing.Union[None, bool]:
+ await self.entered.close()
+ return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/applications.py
new/starlette-0.24.0/starlette/applications.py
--- old/starlette-0.23.1/starlette/applications.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/starlette/applications.py 2023-02-06
17:01:14.000000000 +0100
@@ -65,7 +65,7 @@
on_startup is None and on_shutdown is None
), "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both."
- self._debug = debug
+ self.debug = debug
self.state = State()
self.router = Router(
routes, on_startup=on_startup, on_shutdown=on_shutdown,
lifespan=lifespan
@@ -74,7 +74,7 @@
{} if exception_handlers is None else dict(exception_handlers)
)
self.user_middleware = [] if middleware is None else list(middleware)
- self.middleware_stack = self.build_middleware_stack()
+ self.middleware_stack: typing.Optional[ASGIApp] = None
def build_middleware_stack(self) -> ASGIApp:
debug = self.debug
@@ -108,20 +108,13 @@
def routes(self) -> typing.List[BaseRoute]:
return self.router.routes
- @property
- def debug(self) -> bool:
- return self._debug
-
- @debug.setter
- def debug(self, value: bool) -> None:
- self._debug = value
- self.middleware_stack = self.build_middleware_stack()
-
def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
return self.router.url_path_for(name, **path_params)
async def __call__(self, scope: Scope, receive: Receive, send: Send) ->
None:
scope["app"] = self
+ if self.middleware_stack is None:
+ self.middleware_stack = self.build_middleware_stack()
await self.middleware_stack(scope, receive, send)
def on_event(self, event_type: str) -> typing.Callable: # pragma: nocover
@@ -137,11 +130,10 @@
) -> None: # pragma: no cover
self.router.host(host, app=app, name=name)
- def add_middleware(
- self, middleware_class: type, **options: typing.Any
- ) -> None: # pragma: no cover
+ def add_middleware(self, middleware_class: type, **options: typing.Any) ->
None:
+ if self.middleware_stack is not None: # pragma: no cover
+ raise RuntimeError("Cannot add middleware after an application has
started")
self.user_middleware.insert(0, Middleware(middleware_class, **options))
- self.middleware_stack = self.build_middleware_stack()
def add_exception_handler(
self,
@@ -149,7 +141,6 @@
handler: typing.Callable,
) -> None: # pragma: no cover
self.exception_handlers[exc_class_or_status_code] = handler
- self.middleware_stack = self.build_middleware_stack()
def add_event_handler(
self, event_type: str, func: typing.Callable
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/config.py
new/starlette-0.24.0/starlette/config.py
--- old/starlette-0.23.1/starlette/config.py 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/starlette/config.py 2023-02-06 17:01:14.000000000
+0100
@@ -54,8 +54,10 @@
self,
env_file: typing.Optional[typing.Union[str, Path]] = None,
environ: typing.Mapping[str, str] = environ,
+ env_prefix: str = "",
) -> None:
self.environ = environ
+ self.env_prefix = env_prefix
self.file_values: typing.Dict[str, str] = {}
if env_file is not None and os.path.isfile(env_file):
self.file_values = self._read_file(env_file)
@@ -103,6 +105,7 @@
cast: typing.Optional[typing.Callable] = None,
default: typing.Any = undefined,
) -> typing.Any:
+ key = self.env_prefix + key
if key in self.environ:
value = self.environ[key]
return self._perform_cast(key, value, cast)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/datastructures.py
new/starlette-0.24.0/starlette/datastructures.py
--- old/starlette-0.23.1/starlette/datastructures.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/starlette/datastructures.py 2023-02-06
17:01:14.000000000 +0100
@@ -1,4 +1,3 @@
-import tempfile
import typing
from collections.abc import Sequence
from shlex import shlex
@@ -114,11 +113,18 @@
or "hostname" in kwargs
or "port" in kwargs
):
- hostname = kwargs.pop("hostname", self.hostname)
+ hostname = kwargs.pop("hostname", None)
port = kwargs.pop("port", self.port)
username = kwargs.pop("username", self.username)
password = kwargs.pop("password", self.password)
+ if hostname is None:
+ netloc = self.netloc
+ _, _, hostname = netloc.rpartition("@")
+
+ if hostname[-1] != "]":
+ hostname = hostname.rsplit(":", 1)[0]
+
netloc = hostname
if port is not None:
netloc += f":{port}"
@@ -428,32 +434,33 @@
An uploaded file included as part of the request data.
"""
- spool_max_size = 1024 * 1024
- file: typing.BinaryIO
- headers: "Headers"
-
def __init__(
self,
- filename: str,
- file: typing.Optional[typing.BinaryIO] = None,
- content_type: str = "",
+ file: typing.BinaryIO,
*,
+ size: typing.Optional[int] = None,
+ filename: typing.Optional[str] = None,
headers: "typing.Optional[Headers]" = None,
) -> None:
self.filename = filename
- self.content_type = content_type
- if file is None:
- self.file =
tempfile.SpooledTemporaryFile(max_size=self.spool_max_size) # type:
ignore[assignment] # noqa: E501
- else:
- self.file = file
+ self.file = file
+ self.size = size
self.headers = headers or Headers()
@property
+ def content_type(self) -> typing.Optional[str]:
+ return self.headers.get("content-type", None)
+
+ @property
def _in_memory(self) -> bool:
+ # check for SpooledTemporaryFile._rolled
rolled_to_disk = getattr(self.file, "_rolled", True)
return not rolled_to_disk
async def write(self, data: bytes) -> None:
+ if self.size is not None:
+ self.size += len(data)
+
if self._in_memory:
self.file.write(data)
else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/formparsers.py
new/starlette-0.24.0/starlette/formparsers.py
--- old/starlette-0.23.1/starlette/formparsers.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/starlette/formparsers.py 2023-02-06
17:01:14.000000000 +0100
@@ -1,5 +1,6 @@
import typing
from enum import Enum
+from tempfile import SpooledTemporaryFile
from urllib.parse import unquote_plus
from starlette.datastructures import FormData, Headers, UploadFile
@@ -116,6 +117,8 @@
class MultiPartParser:
+ max_file_size = 1024 * 1024
+
def __init__(
self, headers: Headers, stream: typing.AsyncGenerator[bytes, None]
) -> None:
@@ -160,7 +163,7 @@
async def parse(self) -> FormData:
# Parse the Content-Type header to get the multipart boundary.
- content_type, params =
parse_options_header(self.headers["Content-Type"])
+ _, params = parse_options_header(self.headers["Content-Type"])
charset = params.get(b"charset", "utf-8")
if type(charset) == bytes:
charset = charset.decode("latin-1")
@@ -186,7 +189,6 @@
header_field = b""
header_value = b""
content_disposition = None
- content_type = b""
field_name = ""
data = b""
file: typing.Optional[UploadFile] = None
@@ -202,7 +204,6 @@
for message_type, message_bytes in messages:
if message_type == MultiPartMessage.PART_BEGIN:
content_disposition = None
- content_type = b""
data = b""
item_headers = []
elif message_type == MultiPartMessage.HEADER_FIELD:
@@ -213,8 +214,6 @@
field = header_field.lower()
if field == b"content-disposition":
content_disposition = header_value
- elif field == b"content-type":
- content_type = header_value
item_headers.append((field, header_value))
header_field = b""
header_value = b""
@@ -229,9 +228,11 @@
)
if b"filename" in options:
filename = _user_safe_decode(options[b"filename"],
charset)
+ tempfile =
SpooledTemporaryFile(max_size=self.max_file_size)
file = UploadFile(
+ file=tempfile, # type: ignore[arg-type]
+ size=0,
filename=filename,
- content_type=content_type.decode("latin-1"),
headers=Headers(raw=item_headers),
)
else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/middleware/base.py
new/starlette-0.24.0/starlette/middleware/base.py
--- old/starlette-0.23.1/starlette/middleware/base.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/starlette/middleware/base.py 2023-02-06
17:01:14.000000000 +0100
@@ -2,8 +2,9 @@
import anyio
+from starlette.background import BackgroundTask
from starlette.requests import Request
-from starlette.responses import Response, StreamingResponse
+from starlette.responses import ContentStream, Response, StreamingResponse
from starlette.types import ASGIApp, Message, Receive, Scope, Send
RequestResponseEndpoint = typing.Callable[[Request],
typing.Awaitable[Response]]
@@ -75,6 +76,9 @@
try:
message = await recv_stream.receive()
+ info = message.get("info", None)
+ if message["type"] == "http.response.debug" and info is not
None:
+ message = await recv_stream.receive()
except anyio.EndOfStream:
if app_exc is not None:
raise app_exc
@@ -93,8 +97,8 @@
if app_exc is not None:
raise app_exc
- response = StreamingResponse(
- status_code=message["status"], content=body_stream()
+ response = _StreamingResponse(
+ status_code=message["status"], content=body_stream(), info=info
)
response.raw_headers = message["headers"]
return response
@@ -109,3 +113,22 @@
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
raise NotImplementedError() # pragma: no cover
+
+
+class _StreamingResponse(StreamingResponse):
+ def __init__(
+ self,
+ content: ContentStream,
+ status_code: int = 200,
+ headers: typing.Optional[typing.Mapping[str, str]] = None,
+ media_type: typing.Optional[str] = None,
+ background: typing.Optional[BackgroundTask] = None,
+ info: typing.Optional[typing.Mapping[str, typing.Any]] = None,
+ ) -> None:
+ self._info = info
+ super().__init__(content, status_code, headers, media_type, background)
+
+ async def stream_response(self, send: Send) -> None:
+ if self._info:
+ await send({"type": "http.response.debug", "info": self._info})
+ return await super().stream_response(send)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/requests.py
new/starlette-0.24.0/starlette/requests.py
--- old/starlette-0.23.1/starlette/requests.py 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/starlette/requests.py 2023-02-06 17:01:14.000000000
+0100
@@ -4,6 +4,7 @@
import anyio
+from starlette._utils import AwaitableOrContextManager,
AwaitableOrContextManagerWrapper
from starlette.datastructures import URL, Address, FormData, Headers,
QueryParams, State
from starlette.exceptions import HTTPException
from starlette.formparsers import FormParser, MultiPartException,
MultiPartParser
@@ -187,6 +188,8 @@
class Request(HTTPConnection):
+ _form: typing.Optional[FormData]
+
def __init__(
self, scope: Scope, receive: Receive = empty_receive, send: Send =
empty_send
):
@@ -196,6 +199,7 @@
self._send = send
self._stream_consumed = False
self._is_disconnected = False
+ self._form = None
@property
def method(self) -> str:
@@ -210,10 +214,8 @@
yield self._body
yield b""
return
-
if self._stream_consumed:
raise RuntimeError("Stream consumed")
-
self._stream_consumed = True
while True:
message = await self._receive()
@@ -242,8 +244,8 @@
self._json = json.loads(body)
return self._json
- async def form(self) -> FormData:
- if not hasattr(self, "_form"):
+ async def _get_form(self) -> FormData:
+ if self._form is None:
assert (
parse_options_header is not None
), "The `python-multipart` library must be installed to use form
parsing."
@@ -265,8 +267,11 @@
self._form = FormData()
return self._form
+ def form(self) -> AwaitableOrContextManager[FormData]:
+ return AwaitableOrContextManagerWrapper(self._get_form())
+
async def close(self) -> None:
- if hasattr(self, "_form"):
+ if self._form is not None:
await self._form.close()
async def is_disconnected(self) -> bool:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/responses.py
new/starlette-0.24.0/starlette/responses.py
--- old/starlette-0.23.1/starlette/responses.py 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/starlette/responses.py 2023-02-06 17:01:14.000000000
+0100
@@ -4,7 +4,8 @@
import stat
import sys
import typing
-from email.utils import formatdate
+from datetime import datetime
+from email.utils import format_datetime, formatdate
from functools import partial
from mimetypes import guess_type as mimetypes_guess_type
from urllib.parse import quote
@@ -105,7 +106,7 @@
key: str,
value: str = "",
max_age: typing.Optional[int] = None,
- expires: typing.Optional[int] = None,
+ expires: typing.Optional[typing.Union[datetime, str, int]] = None,
path: str = "/",
domain: typing.Optional[str] = None,
secure: bool = False,
@@ -117,7 +118,10 @@
if max_age is not None:
cookie[key]["max-age"] = max_age
if expires is not None:
- cookie[key]["expires"] = expires
+ if isinstance(expires, datetime):
+ cookie[key]["expires"] = format_datetime(expires, usegmt=True)
+ else:
+ cookie[key]["expires"] = expires
if path is not None:
cookie[key]["path"] = path
if domain is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/staticfiles.py
new/starlette-0.24.0/starlette/staticfiles.py
--- old/starlette-0.23.1/starlette/staticfiles.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/starlette/staticfiles.py 2023-02-06
17:01:14.000000000 +0100
@@ -45,12 +45,14 @@
] = None,
html: bool = False,
check_dir: bool = True,
+ follow_symlink: bool = False,
) -> None:
self.directory = directory
self.packages = packages
self.all_directories = self.get_directories(directory, packages)
self.html = html
self.config_checked = False
+ self.follow_symlink = follow_symlink
if check_dir and directory is not None and not
os.path.isdir(directory):
raise RuntimeError(f"Directory '{directory}' does not exist")
@@ -161,7 +163,11 @@
self, path: str
) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
for directory in self.all_directories:
- full_path = os.path.realpath(os.path.join(directory, path))
+ joined_path = os.path.join(directory, path)
+ if self.follow_symlink:
+ full_path = os.path.abspath(joined_path)
+ else:
+ full_path = os.path.realpath(joined_path)
directory = os.path.realpath(directory)
if os.path.commonprefix([full_path, directory]) != directory:
# Don't allow misbehaving clients to break out of the static
files
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/templating.py
new/starlette-0.24.0/starlette/templating.py
--- old/starlette-0.23.1/starlette/templating.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/starlette/templating.py 2023-02-06
17:01:14.000000000 +0100
@@ -2,6 +2,7 @@
from os import PathLike
from starlette.background import BackgroundTask
+from starlette.requests import Request
from starlette.responses import Response
from starlette.types import Receive, Scope, Send
@@ -40,12 +41,14 @@
async def __call__(self, scope: Scope, receive: Receive, send: Send) ->
None:
request = self.context.get("request", {})
extensions = request.get("extensions", {})
- if "http.response.template" in extensions:
+ if "http.response.debug" in extensions:
await send(
{
- "type": "http.response.template",
- "template": self.template,
- "context": self.context,
+ "type": "http.response.debug",
+ "info": {
+ "template": self.template,
+ "context": self.context,
+ },
}
)
await super().__call__(scope, receive, send)
@@ -59,10 +62,16 @@
"""
def __init__(
- self, directory: typing.Union[str, PathLike], **env_options: typing.Any
+ self,
+ directory: typing.Union[str, PathLike],
+ context_processors: typing.Optional[
+ typing.List[typing.Callable[[Request], typing.Dict[str,
typing.Any]]]
+ ] = None,
+ **env_options: typing.Any
) -> None:
assert jinja2 is not None, "jinja2 must be installed to use
Jinja2Templates"
self.env = self._create_env(directory, **env_options)
+ self.context_processors = context_processors or []
def _create_env(
self, directory: typing.Union[str, PathLike], **env_options: typing.Any
@@ -94,6 +103,11 @@
) -> _TemplateResponse:
if "request" not in context:
raise ValueError('context must include a "request" key')
+
+ request = typing.cast(Request, context["request"])
+ for context_processor in self.context_processors:
+ context.update(context_processor(request))
+
template = self.get_template(name)
return _TemplateResponse(
template,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/starlette/testclient.py
new/starlette-0.24.0/starlette/testclient.py
--- old/starlette-0.23.1/starlette/testclient.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/starlette/testclient.py 2023-02-06
17:01:14.000000000 +0100
@@ -259,7 +259,7 @@
"headers": headers,
"client": ["testclient", 50000],
"server": [host, port],
- "extensions": {"http.response.template": {}},
+ "extensions": {"http.response.debug": {}},
}
request_complete = False
@@ -324,9 +324,9 @@
if not more_body:
raw_kwargs["stream"].seek(0)
response_complete.set()
- elif message["type"] == "http.response.template":
- template = message["template"]
- context = message["context"]
+ elif message["type"] == "http.response.debug":
+ template = message["info"]["template"]
+ context = message["info"]["context"]
try:
with self.portal_factory() as portal:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_applications.py
new/starlette-0.24.0/tests/test_applications.py
--- old/starlette-0.23.1/tests/test_applications.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/tests/test_applications.py 2023-02-06
17:01:14.000000000 +0100
@@ -1,7 +1,9 @@
import os
from contextlib import asynccontextmanager
+from typing import Any, Callable
import anyio
+import httpx
import pytest
from starlette import status
@@ -13,6 +15,7 @@
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Host, Mount, Route, Router, WebSocketRoute
from starlette.staticfiles import StaticFiles
+from starlette.types import ASGIApp
from starlette.websockets import WebSocket
@@ -486,3 +489,45 @@
app.on_event("startup")(startup)
assert len(record) == 1
+
+
+def test_middleware_stack_init(test_client_factory: Callable[[ASGIApp],
httpx.Client]):
+ class NoOpMiddleware:
+ def __init__(self, app: ASGIApp):
+ self.app = app
+
+ async def __call__(self, *args: Any):
+ await self.app(*args)
+
+ class SimpleInitializableMiddleware:
+ counter = 0
+
+ def __init__(self, app: ASGIApp):
+ self.app = app
+ SimpleInitializableMiddleware.counter += 1
+
+ async def __call__(self, *args: Any):
+ await self.app(*args)
+
+ def get_app() -> ASGIApp:
+ app = Starlette()
+ app.add_middleware(SimpleInitializableMiddleware)
+ app.add_middleware(NoOpMiddleware)
+ return app
+
+ app = get_app()
+
+ with test_client_factory(app):
+ pass
+
+ assert SimpleInitializableMiddleware.counter == 1
+
+ test_client_factory(app).get("/foo")
+
+ assert SimpleInitializableMiddleware.counter == 1
+
+ app = get_app()
+
+ test_client_factory(app).get("/foo")
+
+ assert SimpleInitializableMiddleware.counter == 2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_config.py
new/starlette-0.24.0/tests/test_config.py
--- old/starlette-0.23.1/tests/test_config.py 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/tests/test_config.py 2023-02-06 17:01:14.000000000
+0100
@@ -127,3 +127,13 @@
environ = Environ()
assert list(iter(environ)) == list(iter(os.environ))
assert len(environ) == len(os.environ)
+
+
+def test_config_with_env_prefix(tmpdir, monkeypatch):
+ config = Config(
+ environ={"APP_DEBUG": "value", "ENVIRONMENT": "dev"}, env_prefix="APP_"
+ )
+ assert config.get("DEBUG") == "value"
+
+ with pytest.raises(KeyError):
+ config.get("ENVIRONMENT")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_datastructures.py
new/starlette-0.24.0/tests/test_datastructures.py
--- old/starlette-0.23.1/tests/test_datastructures.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/tests/test_datastructures.py 2023-02-06
17:01:14.000000000 +0100
@@ -1,4 +1,6 @@
import io
+from tempfile import SpooledTemporaryFile
+from typing import BinaryIO
import pytest
@@ -38,6 +40,24 @@
assert new == "https://example.com:123/path/to/somewhere?abc=123#anchor"
assert new.hostname == "example.com"
+ ipv6_url = URL("https://[fe::2]:12345")
+ new = ipv6_url.replace(port=8080)
+ assert new == "https://[fe::2]:8080"
+
+ new = ipv6_url.replace(username="username", password="password")
+ assert new == "https://username:password@[fe::2]:12345"
+ assert new.netloc == "username:password@[fe::2]:12345"
+
+ ipv6_url = URL("https://[fe::2]")
+ new = ipv6_url.replace(port=123)
+ assert new == "https://[fe::2]:123"
+
+ url = URL("http://u:p@host/")
+ assert url.replace(hostname="bar") == URL("http://u:p@bar/")
+
+ url = URL("http://u:p@host:80")
+ assert url.replace(port=88) == URL("http://u:p@host:88")
+
def test_url_query_params():
u = URL("https://example.org/path/?page=3")
@@ -269,35 +289,63 @@
assert QueryParams(q) == q
-class BigUploadFile(UploadFile):
- spool_max_size = 1024
-
-
@pytest.mark.anyio
-async def test_upload_file():
- big_file = BigUploadFile("big-file")
- await big_file.write(b"big-data" * 512)
- await big_file.write(b"big-data")
- await big_file.seek(0)
- assert await big_file.read(1024) == b"big-data" * 128
- await big_file.close()
+async def test_upload_file_file_input():
+ """Test passing file/stream into the UploadFile constructor"""
+ stream = io.BytesIO(b"data")
+ file = UploadFile(filename="file", file=stream, size=4)
+ assert await file.read() == b"data"
+ assert file.size == 4
+ await file.write(b" and more data!")
+ assert await file.read() == b""
+ assert file.size == 19
+ await file.seek(0)
+ assert await file.read() == b"data and more data!"
@pytest.mark.anyio
-async def test_upload_file_file_input():
- """Test passing file/stream into the UploadFile constructor"""
+async def test_upload_file_without_size():
+ """Test passing file/stream into the UploadFile constructor without size"""
stream = io.BytesIO(b"data")
file = UploadFile(filename="file", file=stream)
assert await file.read() == b"data"
+ assert file.size is None
await file.write(b" and more data!")
assert await file.read() == b""
+ assert file.size is None
await file.seek(0)
assert await file.read() == b"data and more data!"
[email protected]
[email protected]("max_size", [1, 1024], ids=["rolled", "unrolled"])
+async def test_uploadfile_rolling(max_size: int) -> None:
+ """Test that we can r/w to a SpooledTemporaryFile
+ managed by UploadFile before and after it rolls to disk
+ """
+ stream: BinaryIO = SpooledTemporaryFile( # type: ignore[assignment]
+ max_size=max_size
+ )
+ file = UploadFile(filename="file", file=stream, size=0)
+ assert await file.read() == b""
+ assert file.size == 0
+ await file.write(b"data")
+ assert await file.read() == b""
+ assert file.size == 4
+ await file.seek(0)
+ assert await file.read() == b"data"
+ await file.write(b" more")
+ assert await file.read() == b""
+ assert file.size == 9
+ await file.seek(0)
+ assert await file.read() == b"data more"
+ assert file.size == 9
+ await file.close()
+
+
def test_formdata():
stream = io.BytesIO(b"data")
- upload = UploadFile(filename="file", file=stream)
+ upload = UploadFile(filename="file", file=stream, size=4)
form = FormData([("a", "123"), ("a", "456"), ("b", upload)])
assert "a" in form
assert "A" not in form
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_formparsers.py
new/starlette-0.24.0/tests/test_formparsers.py
--- old/starlette-0.23.1/tests/test_formparsers.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/tests/test_formparsers.py 2023-02-06
17:01:14.000000000 +0100
@@ -29,6 +29,7 @@
content = await value.read()
output[key] = {
"filename": value.filename,
+ "size": value.size,
"content": content.decode(),
"content_type": value.content_type,
}
@@ -51,6 +52,7 @@
output[key].append(
{
"filename": value.filename,
+ "size": value.size,
"content": content.decode(),
"content_type": value.content_type,
}
@@ -71,6 +73,7 @@
content = await value.read()
output[key] = {
"filename": value.filename,
+ "size": value.size,
"content": content.decode(),
"content_type": value.content_type,
"headers": list(value.headers.items()),
@@ -112,6 +115,7 @@
assert response.json() == {
"test": {
"filename": "test.txt",
+ "size": 14,
"content": "<file content>",
"content_type": "text/plain",
}
@@ -129,6 +133,7 @@
assert response.json() == {
"test": {
"filename": "test.txt",
+ "size": 14,
"content": "<file content>",
"content_type": "text/plain",
}
@@ -152,11 +157,13 @@
assert response.json() == {
"test1": {
"filename": "test1.txt",
+ "size": 15,
"content": "<file1 content>",
"content_type": "text/plain",
},
"test2": {
"filename": "test2.txt",
+ "size": 15,
"content": "<file2 content>",
"content_type": "text/plain",
},
@@ -185,6 +192,7 @@
"test1": "<file1 content>",
"test2": {
"filename": "test2.txt",
+ "size": 15,
"content": "<file2 content>",
"content_type": "text/plain",
"headers": [
@@ -220,11 +228,13 @@
"abc",
{
"filename": "test1.txt",
+ "size": 15,
"content": "<file1 content>",
"content_type": "text/plain",
},
{
"filename": "test2.txt",
+ "size": 15,
"content": "<file2 content>",
"content_type": "text/plain",
},
@@ -261,6 +271,7 @@
assert response.json() == {
"file": {
"filename": "file.txt",
+ "size": 14,
"content": "<file content>",
"content_type": "text/plain",
},
@@ -291,6 +302,7 @@
assert response.json() == {
"file": {
"filename": "ææ¸.txt",
+ "size": 14,
"content": "<file content>",
"content_type": "text/plain",
}
@@ -318,6 +330,7 @@
assert response.json() == {
"file": {
"filename": "ç»å.jpg",
+ "size": 14,
"content": "<file content>",
"content_type": "image/jpeg",
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_requests.py
new/starlette-0.24.0/tests/test_requests.py
--- old/starlette-0.23.1/tests/test_requests.py 2022-12-09 15:45:37.000000000
+0100
+++ new/starlette-0.24.0/tests/test_requests.py 2023-02-06 17:01:14.000000000
+0100
@@ -128,6 +128,19 @@
assert response.json() == {"form": {"abc": "123 @"}}
+def test_request_form_context_manager(test_client_factory):
+ async def app(scope, receive, send):
+ request = Request(scope, receive)
+ async with request.form() as form:
+ response = JSONResponse({"form": dict(form)})
+ await response(scope, receive, send)
+
+ client = test_client_factory(app)
+
+ response = client.post("/", data={"abc": "123 @"})
+ assert response.json() == {"form": {"abc": "123 @"}}
+
+
def test_request_body_then_stream(test_client_factory):
async def app(scope, receive, send):
request = Request(scope, receive)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_responses.py
new/starlette-0.24.0/tests/test_responses.py
--- old/starlette-0.23.1/tests/test_responses.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/tests/test_responses.py 2023-02-06
17:01:14.000000000 +0100
@@ -1,4 +1,7 @@
+import datetime as dt
import os
+import time
+from http.cookies import SimpleCookie
import anyio
import pytest
@@ -288,7 +291,11 @@
assert response.headers["content-disposition"] == expected_disposition
-def test_set_cookie(test_client_factory):
+def test_set_cookie(test_client_factory, monkeypatch):
+ # Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
+ mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
+ monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
+
async def app(scope, receive, send):
response = Response("Hello, world!", media_type="text/plain")
response.set_cookie(
@@ -307,6 +314,37 @@
client = test_client_factory(app)
response = client.get("/")
assert response.text == "Hello, world!"
+ assert (
+ response.headers["set-cookie"]
+ == "mycookie=myvalue; Domain=localhost; expires=Fri, 22 Jan 2100
12:00:10 GMT; "
+ "HttpOnly; Max-Age=10; Path=/; SameSite=none; Secure"
+ )
+
+
[email protected](
+ "expires",
+ [
+ pytest.param(
+ dt.datetime(2100, 1, 22, 12, 0, 10, tzinfo=dt.timezone.utc),
id="datetime"
+ ),
+ pytest.param("Fri, 22 Jan 2100 12:00:10 GMT", id="str"),
+ pytest.param(10, id="int"),
+ ],
+)
+def test_expires_on_set_cookie(test_client_factory, monkeypatch, expires):
+ # Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
+ mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
+ monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
+
+ async def app(scope, receive, send):
+ response = Response("Hello, world!", media_type="text/plain")
+ response.set_cookie("mycookie", "myvalue", expires=expires)
+ await response(scope, receive, send)
+
+ client = test_client_factory(app)
+ response = client.get("/")
+ cookie: SimpleCookie = SimpleCookie(response.headers.get("set-cookie"))
+ assert cookie["mycookie"]["expires"] == "Fri, 22 Jan 2100 12:00:10 GMT"
def test_delete_cookie(test_client_factory):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_staticfiles.py
new/starlette-0.24.0/tests/test_staticfiles.py
--- old/starlette-0.23.1/tests/test_staticfiles.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/tests/test_staticfiles.py 2023-02-06
17:01:14.000000000 +0100
@@ -1,6 +1,7 @@
import os
import pathlib
import stat
+import tempfile
import time
import anyio
@@ -448,3 +449,70 @@
response = client.get("/example.txt")
assert response.status_code == 500
assert response.text == "Internal Server Error"
+
+
+def test_staticfiles_follows_symlinks(tmpdir, test_client_factory):
+ statics_path = os.path.join(tmpdir, "statics")
+ os.mkdir(statics_path)
+
+ source_path = tempfile.mkdtemp()
+ source_file_path = os.path.join(source_path, "page.html")
+ with open(source_file_path, "w") as file:
+ file.write("<h1>Hello</h1>")
+
+ statics_file_path = os.path.join(statics_path, "index.html")
+ os.symlink(source_file_path, statics_file_path)
+
+ app = StaticFiles(directory=statics_path, follow_symlink=True)
+ client = test_client_factory(app)
+
+ response = client.get("/index.html")
+ assert response.url == "http://testserver/index.html"
+ assert response.status_code == 200
+ assert response.text == "<h1>Hello</h1>"
+
+
+def test_staticfiles_follows_symlink_directories(tmpdir, test_client_factory):
+ statics_path = os.path.join(tmpdir, "statics")
+ statics_html_path = os.path.join(statics_path, "html")
+ os.mkdir(statics_path)
+
+ source_path = tempfile.mkdtemp()
+ source_file_path = os.path.join(source_path, "page.html")
+ with open(source_file_path, "w") as file:
+ file.write("<h1>Hello</h1>")
+
+ os.symlink(source_path, statics_html_path)
+
+ app = StaticFiles(directory=statics_path, follow_symlink=True)
+ client = test_client_factory(app)
+
+ response = client.get("/html/page.html")
+ assert response.url == "http://testserver/html/page.html"
+ assert response.status_code == 200
+ assert response.text == "<h1>Hello</h1>"
+
+
+def test_staticfiles_disallows_path_traversal_with_symlinks(tmpdir):
+ statics_path = os.path.join(tmpdir, "statics")
+
+ root_source_path = tempfile.mkdtemp()
+ source_path = os.path.join(root_source_path, "statics")
+ os.mkdir(source_path)
+
+ source_file_path = os.path.join(root_source_path, "index.html")
+ with open(source_file_path, "w") as file:
+ file.write("<h1>Hello</h1>")
+
+ os.symlink(source_path, statics_path)
+
+ app = StaticFiles(directory=statics_path, follow_symlink=True)
+ # We can't test this with 'httpx', so we test the app directly here.
+ path = app.get_path({"path": "/../index.html"})
+ scope = {"method": "GET"}
+
+ with pytest.raises(HTTPException) as exc_info:
+ anyio.run(app.get_response, path, scope)
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Not Found"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/starlette-0.23.1/tests/test_templates.py
new/starlette-0.24.0/tests/test_templates.py
--- old/starlette-0.23.1/tests/test_templates.py 2022-12-09
15:45:37.000000000 +0100
+++ new/starlette-0.24.0/tests/test_templates.py 2023-02-06
17:01:14.000000000 +0100
@@ -3,6 +3,8 @@
import pytest
from starlette.applications import Starlette
+from starlette.middleware import Middleware
+from starlette.middleware.base import BaseHTTPMiddleware
from starlette.routing import Route
from starlette.templating import Jinja2Templates
@@ -32,3 +34,57 @@
templates = Jinja2Templates(str(tmpdir))
with pytest.raises(ValueError):
templates.TemplateResponse("", {})
+
+
+def test_calls_context_processors(tmp_path, test_client_factory):
+ path = tmp_path / "index.html"
+ path.write_text("<html>Hello {{ username }}</html>")
+
+ async def homepage(request):
+ return templates.TemplateResponse("index.html", {"request": request})
+
+ def hello_world_processor(request):
+ return {"username": "World"}
+
+ app = Starlette(
+ debug=True,
+ routes=[Route("/", endpoint=homepage)],
+ )
+ templates = Jinja2Templates(
+ directory=tmp_path,
+ context_processors=[
+ hello_world_processor,
+ ],
+ )
+
+ client = test_client_factory(app)
+ response = client.get("/")
+ assert response.text == "<html>Hello World</html>"
+ assert response.template.name == "index.html"
+ assert set(response.context.keys()) == {"request", "username"}
+
+
+def test_template_with_middleware(tmpdir, test_client_factory):
+ path = os.path.join(tmpdir, "index.html")
+ with open(path, "w") as file:
+ file.write("<html>Hello, <a href='{{ url_for('homepage')
}}'>world</a></html>")
+
+ async def homepage(request):
+ return templates.TemplateResponse("index.html", {"request": request})
+
+ class CustomMiddleware(BaseHTTPMiddleware):
+ async def dispatch(self, request, call_next):
+ return await call_next(request)
+
+ app = Starlette(
+ debug=True,
+ routes=[Route("/", endpoint=homepage)],
+ middleware=[Middleware(CustomMiddleware)],
+ )
+ templates = Jinja2Templates(directory=str(tmpdir))
+
+ client = test_client_factory(app)
+ response = client.get("/")
+ assert response.text == "<html>Hello, <a
href='http://testserver/'>world</a></html>"
+ assert response.template.name == "index.html"
+ assert set(response.context.keys()) == {"request"}