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 <david.a...@suse.com>
+
+- 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!"
 
 
+@pytest.mark.anyio
+@pytest.mark.parametrize("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"
+    )
+
+
+@pytest.mark.parametrize(
+    "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"}

Reply via email to