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 2022-11-28 11:07:28 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-starlette (Old) and /work/SRC/openSUSE:Factory/.python-starlette.new.1597 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-starlette" Mon Nov 28 11:07:28 2022 rev:13 rq:1038588 version:0.22.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-starlette/python-starlette.changes 2022-09-29 18:15:17.119481642 +0200 +++ /work/SRC/openSUSE:Factory/.python-starlette.new.1597/python-starlette.changes 2022-11-28 11:07:34.751894604 +0100 @@ -1,0 +2,11 @@ +Sun Nov 27 22:53:40 UTC 2022 - Michael Ströder <mich...@stroeder.com> + +- Update to 0.22.0 + * Changed + - Bypass GZipMiddleware when response includes Content-Encoding #1901. + * Fixed + - Remove unneeded unquote() from query parameters on the TestClient #1953. + - Make sure MutableHeaders._list is actually a list #1917. + - Import compatibility with the next version of AnyIO #1936. + +------------------------------------------------------------------- Old: ---- starlette-0.21.0.tar.gz New: ---- starlette-0.22.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-starlette.spec ++++++ --- /var/tmp/diff_new_pack.gDcDMP/_old 2022-11-28 11:07:35.363897348 +0100 +++ /var/tmp/diff_new_pack.gDcDMP/_new 2022-11-28 11:07:35.375897402 +0100 @@ -27,7 +27,7 @@ %{?!python_module:%define python_module() python3-%{**}} %define skip_python2 1 Name: python-starlette%{psuffix} -Version: 0.21.0 +Version: 0.22.0 Release: 0 Summary: Lightweight ASGI framework/toolkit License: BSD-3-Clause ++++++ starlette-0.21.0.tar.gz -> starlette-0.22.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/.github/workflows/test-suite.yml new/starlette-0.22.0/.github/workflows/test-suite.yml --- old/starlette-0.21.0/.github/workflows/test-suite.yml 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/.github/workflows/test-suite.yml 2022-11-17 07:24:34.000000000 +0100 @@ -14,7 +14,7 @@ strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: "actions/checkout@v3" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/docs/middleware.md new/starlette-0.22.0/docs/middleware.md --- old/starlette-0.21.0/docs/middleware.md 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/docs/middleware.md 2022-11-17 07:24:34.000000000 +0100 @@ -114,6 +114,7 @@ ```python from starlette.applications import Starlette +from starlette.middleware import Middleware from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware routes = ... @@ -179,6 +180,8 @@ * `minimum_size` - Do not GZip responses that are smaller than this minimum size in bytes. Defaults to `500`. +The middleware won't GZip responses that already have a `Content-Encoding` set, to prevent them from being encoded twice. + ## BaseHTTPMiddleware An abstract class that allows you to write ASGI middleware against a request/response @@ -242,7 +245,6 @@ Currently, the `BaseHTTPMiddleware` has some known limitations: -- It's not possible to use `BackgroundTasks` with `BaseHTTPMiddleware`. Check [#1438](https://github.com/encode/starlette/issues/1438) for more details. - Using `BaseHTTPMiddleware` will prevent changes to [`contextlib.ContextVar`](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar)s from propagating upwards. That is, if you set a value for a `ContextVar` in your endpoint and try to read it from a middleware you will find that the value is not the same value you set in your endpoint (see [this test](https://github.com/encode/starlette/blob/621abc747a6604825190b93467918a0ec6456a24/tests/middleware/test_base.py#L192-L223) for an example of this behavior). To overcome these limitations, use [pure ASGI middleware](#pure-asgi-middleware), as shown below. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/docs/release-notes.md new/starlette-0.22.0/docs/release-notes.md --- old/starlette-0.21.0/docs/release-notes.md 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/docs/release-notes.md 2022-11-17 07:24:34.000000000 +0100 @@ -1,3 +1,15 @@ +## 0.22.0 + +November 17, 2022 + +### Changed +* Bypass `GZipMiddleware` when response includes `Content-Encoding` [#1901](https://github.com/encode/starlette/pull/1901). + +### Fixed +* Remove unneeded `unquote()` from query parameters on the `TestClient` [#1953](https://github.com/encode/starlette/pull/1953). +* Make sure `MutableHeaders._list` is actually a `list` [#1917](https://github.com/encode/starlette/pull/1917). +* Import compatibility with the next version of `AnyIO` [#1936](https://github.com/encode/starlette/pull/1936). + ## 0.21.0 September 26, 2022 @@ -167,7 +179,7 @@ * The ClassVar `starlette.testclient.TestClient.async_backend` was removed, the backend is now configured using constructor kwargs [#1211](https://github.com/encode/starlette/pull/1211) - * Passing an Async Generator Function or a Generator Function to `starlette.router.Router(lifespan_context=)` is deprecated. You should wrap your lifespan in `@contextlib.asynccontextmanager`. + * Passing an Async Generator Function or a Generator Function to `starlette.routing.Router(lifespan=)` is deprecated. You should wrap your lifespan in `@contextlib.asynccontextmanager`. [#1227](https://github.com/encode/starlette/pull/1227) [#1110](https://github.com/encode/starlette/pull/1110) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/docs/testclient.md new/starlette-0.22.0/docs/testclient.md --- old/starlette-0.21.0/docs/testclient.md 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/docs/testclient.md 2022-11-17 07:24:34.000000000 +0100 @@ -55,7 +55,7 @@ response = client.post("/form", files=files) ``` -For more information you can check the `requests` [documentation](https://requests.readthedocs.io/en/master/user/advanced/). +For more information you can check the `httpx` [documentation](https://www.python-httpx.org/advanced/). By default the `TestClient` will raise any exceptions that occur in the application. Occasionally you might want to test the content of 500 error diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/docs/third-party-packages.md new/starlette-0.22.0/docs/third-party-packages.md --- old/starlette-0.21.0/docs/third-party-packages.md 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/docs/third-party-packages.md 2022-11-17 07:24:34.000000000 +0100 @@ -209,3 +209,9 @@ 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/). <a href="https://github.com/strongbugman/apiman" target="_blank">GitHub</a> + +### Starlette-Babel + +Provides translations, localization, and timezone support via Babel integration. + +<a href="https://github.com/alex-oleshkevich/starlette_babel" target="_blank">GitHub</a> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/mkdocs.yml new/starlette-0.22.0/mkdocs.yml --- old/starlette-0.21.0/mkdocs.yml 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/mkdocs.yml 2022-11-17 07:24:34.000000000 +0100 @@ -17,10 +17,12 @@ toggle: icon: 'material/lightbulb-outline' name: 'Switch to light mode' + icon: + repo: fontawesome/brands/github repo_name: encode/starlette repo_url: https://github.com/encode/starlette -edit_uri: "" +edit_uri: edit/master/docs/ nav: - Introduction: 'index.md' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/pyproject.toml new/starlette-0.22.0/pyproject.toml --- old/starlette-0.21.0/pyproject.toml 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/pyproject.toml 2022-11-17 07:24:34.000000000 +0100 @@ -43,6 +43,10 @@ [project.urls] Homepage = "https://github.com/encode/starlette" +Documentation = "https://www.starlette.io/" +Changelog = "https://www.starlette.io/release-notes/" +Funding = "https://github.com/sponsors/encode" +Source = "https://github.com/encode/starlette" [tool.hatch.version] path = "starlette/__init__.py" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/requirements.txt new/starlette-0.22.0/requirements.txt --- old/starlette-0.21.0/requirements.txt 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/requirements.txt 2022-11-17 07:24:34.000000000 +0100 @@ -4,24 +4,22 @@ # Testing autoflake==1.5.3 black==22.8.0 -coverage==6.4.2 -databases[sqlite]==0.6.1 +coverage==6.5.0 flake8==3.9.2 +importlib-metadata==4.13.0 isort==5.10.1 mypy==0.971 -typing_extensions==4.3.0 +typing_extensions==4.4.0 types-contextvars==2.4.7 -types-PyYAML==6.0.11 +types-PyYAML==6.0.12 types-dataclasses==0.6.6 -pytest==7.1.2 +pytest==7.2.0 trio==0.21.0 -# NOTE: Remove once greenlet releases 2.0.0. -greenlet==2.0.0a2; python_version >= "3.11" # Documentation -mkdocs==1.3.1 -mkdocs-material==8.4.2 -mkautodoc==0.1.0 +mkdocs==1.4.0 +mkdocs-material==8.5.7 +mkautodoc==0.2.0 # Packaging build==0.8.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/scripts/install new/starlette-0.22.0/scripts/install --- old/starlette-0.21.0/scripts/install 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/scripts/install 2022-11-17 07:24:34.000000000 +0100 @@ -15,5 +15,5 @@ PIP="pip" fi +"$PIP" install -U pip "$PIP" install -r "$REQUIREMENTS" -"$PIP" install -e . diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/setup.cfg new/starlette-0.22.0/setup.cfg --- old/starlette-0.21.0/setup.cfg 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/setup.cfg 2022-11-17 07:24:34.000000000 +0100 @@ -7,6 +7,7 @@ ignore_missing_imports = True no_implicit_optional = True show_error_codes = True +enable_error_code = ignore-without-code [mypy-starlette.testclient] no_implicit_optional = False @@ -35,6 +36,7 @@ ignore: Use 'content=<...>' to upload raw bytes/text content.:DeprecationWarning ignore: The `allow_redirects` argument is deprecated. Use `follow_redirects` instead.:DeprecationWarning ignore: 'cgi' is deprecated and slated for removal in Python 3\.13:DeprecationWarning + ignore: You seem to already have a custom sys.excepthook handler installed. I'll skip installing Trio's custom handler, but this means MultiErrors will not show full tracebacks.:RuntimeWarning [coverage:run] source_pkgs = starlette, tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/__init__.py new/starlette-0.22.0/starlette/__init__.py --- old/starlette-0.21.0/starlette/__init__.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/__init__.py 2022-11-17 07:24:34.000000000 +0100 @@ -1 +1 @@ -__version__ = "0.21.0" +__version__ = "0.22.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/datastructures.py new/starlette-0.22.0/starlette/datastructures.py --- old/starlette-0.21.0/starlette/datastructures.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/datastructures.py 2022-11-17 07:24:34.000000000 +0100 @@ -410,7 +410,7 @@ parse_qsl(value.decode("latin-1"), keep_blank_values=True), **kwargs ) else: - super().__init__(*args, **kwargs) # type: ignore + super().__init__(*args, **kwargs) # type: ignore[arg-type] self._list = [(str(k), str(v)) for k, v in self._list] self._dict = {str(k): str(v) for k, v in self._dict.items()} @@ -443,7 +443,7 @@ self.filename = filename self.content_type = content_type if file is None: - self.file = tempfile.SpooledTemporaryFile(max_size=self.spool_max_size) # type: ignore # noqa: E501 + self.file = tempfile.SpooledTemporaryFile(max_size=self.spool_max_size) # type: ignore[assignment] # noqa: E501 else: self.file = file self.headers = headers or Headers() @@ -508,7 +508,7 @@ self, headers: typing.Optional[typing.Mapping[str, str]] = None, raw: typing.Optional[typing.List[typing.Tuple[bytes, bytes]]] = None, - scope: typing.Optional[typing.Mapping[str, typing.Any]] = None, + scope: typing.Optional[typing.MutableMapping[str, typing.Any]] = None, ) -> None: self._list: typing.List[typing.Tuple[bytes, bytes]] = [] if headers is not None: @@ -522,19 +522,21 @@ assert scope is None, 'Cannot set both "raw" and "scope".' self._list = raw elif scope is not None: - self._list = scope["headers"] + # scope["headers"] isn't necessarily a list + # it might be a tuple or other iterable + self._list = scope["headers"] = list(scope["headers"]) @property def raw(self) -> typing.List[typing.Tuple[bytes, bytes]]: return list(self._list) - def keys(self) -> typing.List[str]: # type: ignore + def keys(self) -> typing.List[str]: # type: ignore[override] return [key.decode("latin-1") for key, value in self._list] - def values(self) -> typing.List[str]: # type: ignore + def values(self) -> typing.List[str]: # type: ignore[override] return [value.decode("latin-1") for key, value in self._list] - def items(self) -> typing.List[typing.Tuple[str, str]]: # type: ignore + def items(self) -> typing.List[typing.Tuple[str, str]]: # type: ignore[override] return [ (key.decode("latin-1"), value.decode("latin-1")) for key, value in self._list @@ -593,7 +595,7 @@ set_key = key.lower().encode("latin-1") set_value = value.encode("latin-1") - found_indexes = [] + found_indexes: "typing.List[int]" = [] for idx, (item_key, item_value) in enumerate(self._list): if item_key == set_key: found_indexes.append(idx) @@ -613,7 +615,7 @@ """ del_key = key.lower().encode("latin-1") - pop_indexes = [] + pop_indexes: "typing.List[int]" = [] for idx, (item_key, item_value) in enumerate(self._list): if item_key == del_key: pop_indexes.append(idx) @@ -621,13 +623,13 @@ for idx in reversed(pop_indexes): del self._list[idx] - def __ior__(self, other: typing.Mapping) -> "MutableHeaders": + def __ior__(self, other: typing.Mapping[str, str]) -> "MutableHeaders": if not isinstance(other, typing.Mapping): raise TypeError(f"Expected a mapping but got {other.__class__.__name__}") self.update(other) return self - def __or__(self, other: typing.Mapping) -> "MutableHeaders": + def __or__(self, other: typing.Mapping[str, str]) -> "MutableHeaders": if not isinstance(other, typing.Mapping): raise TypeError(f"Expected a mapping but got {other.__class__.__name__}") new = self.mutablecopy() @@ -652,7 +654,7 @@ self._list.append((set_key, set_value)) return value - def update(self, other: typing.Mapping) -> None: + def update(self, other: typing.Mapping[str, str]) -> None: for key, val in other.items(): self[key] = val diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/middleware/errors.py new/starlette-0.22.0/starlette/middleware/errors.py --- old/starlette-0.21.0/starlette/middleware/errors.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/middleware/errors.py 2022-11-17 07:24:34.000000000 +0100 @@ -198,7 +198,9 @@ def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str: code_context = "".join( - self.format_line(index, line, frame.lineno, frame.index) # type: ignore + self.format_line( + index, line, frame.lineno, frame.index # type: ignore[arg-type] + ) for index, line in enumerate(frame.code_context or []) ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/middleware/gzip.py new/starlette-0.22.0/starlette/middleware/gzip.py --- old/starlette-0.21.0/starlette/middleware/gzip.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/middleware/gzip.py 2022-11-17 07:24:34.000000000 +0100 @@ -33,6 +33,7 @@ self.send: Send = unattached_send self.initial_message: Message = {} self.started = False + self.content_encoding_set = False self.gzip_buffer = io.BytesIO() self.gzip_file = gzip.GzipFile( mode="wb", fileobj=self.gzip_buffer, compresslevel=compresslevel @@ -48,6 +49,13 @@ # Don't send the initial message until we've determined how to # modify the outgoing headers correctly. self.initial_message = message + headers = Headers(raw=self.initial_message["headers"]) + self.content_encoding_set = "content-encoding" in headers + elif message_type == "http.response.body" and self.content_encoding_set: + if not self.started: + self.started = True + await self.send(self.initial_message) + await self.send(message) elif message_type == "http.response.body" and not self.started: self.started = True body = message.get("body", b"") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/responses.py new/starlette-0.22.0/starlette/responses.py --- old/starlette-0.21.0/starlette/responses.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/responses.py 2022-11-17 07:24:34.000000000 +0100 @@ -23,7 +23,7 @@ from typing_extensions import Literal # Workaround for adding samesite support to pre 3.8 python -http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore +http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore[attr-defined] # Compatibility wrapper for `mimetypes.guess_type` to support `os.PathLike` on <py3.8 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/schemas.py new/starlette-0.22.0/starlette/schemas.py --- old/starlette-0.21.0/starlette/schemas.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/schemas.py 2022-11-17 07:24:34.000000000 +0100 @@ -9,7 +9,7 @@ try: import yaml except ImportError: # pragma: nocover - yaml = None # type: ignore + yaml = None # type: ignore[assignment] class OpenAPIResponse(Response): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/templating.py new/starlette-0.22.0/starlette/templating.py --- old/starlette-0.21.0/starlette/templating.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/templating.py 2022-11-17 07:24:34.000000000 +0100 @@ -17,7 +17,7 @@ else: # pragma: nocover pass_context = jinja2.contextfunction # type: ignore[attr-defined] except ImportError: # pragma: nocover - jinja2 = None # type: ignore + jinja2 = None # type: ignore[assignment] class _TemplateResponse(Response): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/starlette/testclient.py new/starlette-0.22.0/starlette/testclient.py --- old/starlette-0.21.0/starlette/testclient.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/starlette/testclient.py 2022-11-17 07:24:34.000000000 +0100 @@ -12,6 +12,7 @@ from urllib.parse import unquote, urljoin import anyio +import anyio.from_thread import httpx from anyio.streams.stapled import StapledObjectStream @@ -33,6 +34,9 @@ ASGI3App = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]] +_RequestData = typing.Mapping[str, typing.Union[str, typing.Iterable[str]]] + + def _is_asgi3(app: typing.Union[ASGI2App, ASGI3App]) -> bool: if inspect.isclass(app): return hasattr(app, "__await__") @@ -192,10 +196,10 @@ def handle_request(self, request: httpx.Request) -> httpx.Response: scheme = request.url.scheme - netloc = unquote(request.url.netloc.decode(encoding="ascii")) + netloc = request.url.netloc.decode(encoding="ascii") path = request.url.path raw_path = request.url.raw_path - query = unquote(request.url.query.decode(encoding="ascii")) + query = request.url.query.decode(encoding="ascii") default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme] @@ -395,7 +399,9 @@ if self.portal is not None: yield self.portal else: - with anyio.start_blocking_portal(**self.async_backend) as portal: + with anyio.from_thread.start_blocking_portal( + **self.async_backend + ) as portal: yield portal def _choose_redirect_arg( @@ -426,22 +432,22 @@ method: str, url: httpx._types.URLTypes, *, - content: httpx._types.RequestContent = None, - data: httpx._types.RequestData = None, - files: httpx._types.RequestFiles = None, + content: typing.Optional[httpx._types.RequestContent] = None, + data: typing.Optional[_RequestData] = None, + files: typing.Optional[httpx._types.RequestFiles] = None, json: typing.Any = None, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: url = self.base_url.join(url) redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) @@ -449,7 +455,7 @@ method, url, content=content, - data=data, + data=data, # type: ignore[arg-type] files=files, json=json, params=params, @@ -465,18 +471,18 @@ self, url: httpx._types.URLTypes, *, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().get( @@ -494,18 +500,18 @@ self, url: httpx._types.URLTypes, *, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().options( @@ -523,18 +529,18 @@ self, url: httpx._types.URLTypes, *, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().head( @@ -552,28 +558,28 @@ self, url: httpx._types.URLTypes, *, - content: httpx._types.RequestContent = None, - data: httpx._types.RequestData = None, - files: httpx._types.RequestFiles = None, + content: typing.Optional[httpx._types.RequestContent] = None, + data: typing.Optional[_RequestData] = None, + files: typing.Optional[httpx._types.RequestFiles] = None, json: typing.Any = None, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().post( url, content=content, - data=data, + data=data, # type: ignore[arg-type] files=files, json=json, params=params, @@ -589,28 +595,28 @@ self, url: httpx._types.URLTypes, *, - content: httpx._types.RequestContent = None, - data: httpx._types.RequestData = None, - files: httpx._types.RequestFiles = None, + content: typing.Optional[httpx._types.RequestContent] = None, + data: typing.Optional[_RequestData] = None, + files: typing.Optional[httpx._types.RequestFiles] = None, json: typing.Any = None, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().put( url, content=content, - data=data, + data=data, # type: ignore[arg-type] files=files, json=json, params=params, @@ -626,28 +632,28 @@ self, url: httpx._types.URLTypes, *, - content: httpx._types.RequestContent = None, - data: httpx._types.RequestData = None, - files: httpx._types.RequestFiles = None, + content: typing.Optional[httpx._types.RequestContent] = None, + data: typing.Optional[_RequestData] = None, + files: typing.Optional[httpx._types.RequestFiles] = None, json: typing.Any = None, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().patch( url, content=content, - data=data, + data=data, # type: ignore[arg-type] files=files, json=json, params=params, @@ -663,18 +669,18 @@ self, url: httpx._types.URLTypes, *, - params: httpx._types.QueryParamTypes = None, - headers: httpx._types.HeaderTypes = None, - cookies: httpx._types.CookieTypes = None, + params: typing.Optional[httpx._types.QueryParamTypes] = None, + headers: typing.Optional[httpx._types.HeaderTypes] = None, + cookies: typing.Optional[httpx._types.CookieTypes] = None, auth: typing.Union[ httpx._types.AuthTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool = None, - allow_redirects: bool = None, + follow_redirects: typing.Optional[bool] = None, + allow_redirects: typing.Optional[bool] = None, timeout: typing.Union[ httpx._client.TimeoutTypes, httpx._client.UseClientDefault ] = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict = None, + extensions: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> httpx.Response: redirect = self._choose_redirect_arg(follow_redirects, allow_redirects) return super().delete( @@ -711,7 +717,7 @@ def __enter__(self) -> "TestClient": with contextlib.ExitStack() as stack: self.portal = portal = stack.enter_context( - anyio.start_blocking_portal(**self.async_backend) + anyio.from_thread.start_blocking_portal(**self.async_backend) ) @stack.callback diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/tests/conftest.py new/starlette-0.22.0/tests/conftest.py --- old/starlette-0.21.0/tests/conftest.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/tests/conftest.py 2022-11-17 07:24:34.000000000 +0100 @@ -6,12 +6,6 @@ @pytest.fixture -def no_trio_support(anyio_backend_name): - if anyio_backend_name == "trio": - pytest.skip("Trio not supported (yet!)") - - -@pytest.fixture def test_client_factory(anyio_backend_name, anyio_backend_options): # anyio_backend_name defined by: # https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/tests/middleware/test_gzip.py new/starlette-0.22.0/tests/middleware/test_gzip.py --- old/starlette-0.21.0/tests/middleware/test_gzip.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/tests/middleware/test_gzip.py 2022-11-17 07:24:34.000000000 +0100 @@ -76,3 +76,27 @@ assert response.text == "x" * 4000 assert response.headers["Content-Encoding"] == "gzip" assert "Content-Length" not in response.headers + + +def test_gzip_ignored_for_responses_with_encoding_set(test_client_factory): + def homepage(request): + async def generator(bytes, count): + for index in range(count): + yield bytes + + streaming = generator(bytes=b"x" * 400, count=10) + return StreamingResponse( + streaming, status_code=200, headers={"Content-Encoding": "br"} + ) + + app = Starlette( + routes=[Route("/", endpoint=homepage)], + middleware=[Middleware(GZipMiddleware)], + ) + + client = test_client_factory(app) + response = client.get("/", headers={"accept-encoding": "gzip, br"}) + assert response.status_code == 200 + assert response.text == "x" * 4000 + assert response.headers["Content-Encoding"] == "br" + assert "Content-Length" not in response.headers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/tests/test_database.py new/starlette-0.22.0/tests/test_database.py --- old/starlette-0.21.0/tests/test_database.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/tests/test_database.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,175 +0,0 @@ -import databases -import pytest -import sqlalchemy - -from starlette.applications import Starlette -from starlette.requests import Request -from starlette.responses import JSONResponse -from starlette.routing import Route - -DATABASE_URL = "sqlite:///test.db" - -metadata = sqlalchemy.MetaData() - -notes = sqlalchemy.Table( - "notes", - metadata, - sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True), - sqlalchemy.Column("text", sqlalchemy.String(length=100)), - sqlalchemy.Column("completed", sqlalchemy.Boolean), -) - - -pytestmark = pytest.mark.usefixtures("no_trio_support") - - -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - -database = databases.Database(DATABASE_URL, force_rollback=True) - - -async def startup(): - await database.connect() - - -async def shutdown(): - await database.disconnect() - - -async def list_notes(request: Request): - query = notes.select() - results = await database.fetch_all(query) - content = [ - {"text": result["text"], "completed": result["completed"]} for result in results - ] - return JSONResponse(content) - - -@database.transaction() -async def add_note(request: Request): - data = await request.json() - query = notes.insert().values(text=data["text"], completed=data["completed"]) - await database.execute(query) - if "raise_exc" in request.query_params: - raise RuntimeError() - return JSONResponse({"text": data["text"], "completed": data["completed"]}) - - -async def bulk_create_notes(request: Request): - data = await request.json() - query = notes.insert() - await database.execute_many(query, data) - return JSONResponse({"notes": data}) - - -async def read_note(request: Request): - note_id = request.path_params["note_id"] - query = notes.select().where(notes.c.id == note_id) - result = await database.fetch_one(query) - assert result is not None - content = {"text": result["text"], "completed": result["completed"]} - return JSONResponse(content) - - -async def read_note_text(request: Request): - note_id = request.path_params["note_id"] - query = sqlalchemy.select([notes.c.text]).where(notes.c.id == note_id) - result = await database.fetch_one(query) - assert result is not None - return JSONResponse(result[0]) - - -app = Starlette( - routes=[ - Route("/notes", endpoint=list_notes, methods=["GET"]), - Route("/notes", endpoint=add_note, methods=["POST"]), - Route("/notes/bulk_create", endpoint=bulk_create_notes, methods=["POST"]), - Route("/notes/{note_id:int}", endpoint=read_note, methods=["GET"]), - Route("/notes/{note_id:int}/text", endpoint=read_note_text, methods=["GET"]), - ], - on_startup=[startup], - on_shutdown=[shutdown], -) - - -def test_database(test_client_factory): - with test_client_factory(app) as client: - response = client.post( - "/notes", json={"text": "buy the milk", "completed": True} - ) - assert response.status_code == 200 - - with pytest.raises(RuntimeError): - response = client.post( - "/notes", - json={"text": "you wont see me", "completed": False}, - params={"raise_exc": "true"}, - ) - - response = client.post( - "/notes", json={"text": "walk the dog", "completed": False} - ) - assert response.status_code == 200 - - response = client.get("/notes") - assert response.status_code == 200 - assert response.json() == [ - {"text": "buy the milk", "completed": True}, - {"text": "walk the dog", "completed": False}, - ] - - response = client.get("/notes/1") - assert response.status_code == 200 - assert response.json() == {"text": "buy the milk", "completed": True} - - response = client.get("/notes/1/text") - assert response.status_code == 200 - assert response.json() == "buy the milk" - - -def test_database_execute_many(test_client_factory): - with test_client_factory(app) as client: - data = [ - {"text": "buy the milk", "completed": True}, - {"text": "walk the dog", "completed": False}, - ] - response = client.post("/notes/bulk_create", json=data) - assert response.status_code == 200 - - response = client.get("/notes") - assert response.status_code == 200 - assert response.json() == [ - {"text": "buy the milk", "completed": True}, - {"text": "walk the dog", "completed": False}, - ] - - -def test_database_isolated_during_test_cases(test_client_factory): - """ - Using `TestClient` as a context manager - """ - with test_client_factory(app) as client: - response = client.post( - "/notes", json={"text": "just one note", "completed": True} - ) - assert response.status_code == 200 - - response = client.get("/notes") - assert response.status_code == 200 - assert response.json() == [{"text": "just one note", "completed": True}] - - with test_client_factory(app) as client: - response = client.post( - "/notes", json={"text": "just one note", "completed": True} - ) - assert response.status_code == 200 - - response = client.get("/notes") - assert response.status_code == 200 - assert response.json() == [{"text": "just one note", "completed": True}] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/tests/test_datastructures.py new/starlette-0.22.0/tests/test_datastructures.py --- old/starlette-0.21.0/tests/test_datastructures.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/tests/test_datastructures.py 2022-11-17 07:24:34.000000000 +0100 @@ -201,9 +201,9 @@ def test_mutable_headers_merge_not_mapping(): h = MutableHeaders() with pytest.raises(TypeError): - h |= {"not_mapping"} # type: ignore + h |= {"not_mapping"} # type: ignore[arg-type] with pytest.raises(TypeError): - h | {"not_mapping"} # type: ignore + h | {"not_mapping"} # type: ignore[operator] def test_headers_mutablecopy(): @@ -214,6 +214,16 @@ assert c.items() == [("a", "abc"), ("b", "789")] +def test_mutable_headers_from_scope(): + # "headers" in scope must not necessarily be a list + h = MutableHeaders(scope={"headers": ((b"a", b"1"),)}) + assert dict(h) == {"a": "1"} + h.update({"b": "2"}) + assert dict(h) == {"a": "1", "b": "2"} + assert list(h.items()) == [("a", "1"), ("b", "2")] + assert list(h.raw) == [(b"a", b"1"), (b"b", b"2")] + + def test_url_blank_params(): q = QueryParams("a=123&abc&def&b=456") assert "a" in q diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/starlette-0.21.0/tests/test_testclient.py new/starlette-0.22.0/tests/test_testclient.py --- old/starlette-0.21.0/tests/test_testclient.py 2022-09-26 19:09:36.000000000 +0200 +++ new/starlette-0.22.0/tests/test_testclient.py 2022-11-17 07:24:34.000000000 +0100 @@ -9,7 +9,7 @@ from starlette.applications import Starlette from starlette.middleware import Middleware -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response from starlette.routing import Route from starlette.websockets import WebSocket, WebSocketDisconnect @@ -240,3 +240,14 @@ client = test_client_factory(app) response = client.get("/") assert response.json() == {"host": "testclient", "port": 50000} + + +@pytest.mark.parametrize("param", ("2020-07-14T00:00:00+00:00", "España", "voilà ")) +def test_query_params(test_client_factory, param: str): + def homepage(request): + return Response(request.query_params["param"]) + + app = Starlette(routes=[Route("/", endpoint=homepage)]) + client = test_client_factory(app) + response = client.get("/", params={"param": param}) + assert response.text == param