From 875f6c6a4d11407de0d46dc04f058eb8f6a244dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=BChn?= Date: Sat, 20 Jul 2024 09:41:01 +0200 Subject: [PATCH 1/5] Suppress `KeyboardInterrupt` from CLI and programmatic usage (#2384) Co-authored-by: Marcelo Trylesinski --- uvicorn/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uvicorn/main.py b/uvicorn/main.py index 41e9ec84e..6fab04b5f 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -575,6 +575,8 @@ def run( Multiprocess(config, target=server.run, sockets=[sock]).run() else: server.run() + except KeyboardInterrupt: + pass finally: if config.uds and os.path.exists(config.uds): os.remove(config.uds) # pragma: py-win32 From 92798254173f9c34a7cdc7817a06a99d0394af2a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jul 2024 09:59:22 +0200 Subject: [PATCH 2/5] `ClientDisconnect` inherits from `OSError` instead of `IOError` (#2393) --- uvicorn/protocols/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py index 4845e1f7b..e1d6f01d5 100644 --- a/uvicorn/protocols/utils.py +++ b/uvicorn/protocols/utils.py @@ -6,7 +6,7 @@ from uvicorn._types import WWWScope -class ClientDisconnected(IOError): ... +class ClientDisconnected(OSError): ... def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None: From 9baded3dcf1a59b2956a00e875be4bbb6bea162c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2024 08:11:13 +0000 Subject: [PATCH 3/5] Bump the python-packages group with 9 updates (#2376) * Bump the python-packages group with 9 updates Bumps the python-packages group with 9 updates: | Package | From | To | | --- | --- | --- | | [a2wsgi](https://github.com/abersheeran/a2wsgi) | `1.10.4` | `1.10.6` | | [twine](https://github.com/pypa/twine) | `5.0.0` | `5.1.1` | | [ruff](https://github.com/astral-sh/ruff) | `0.3.7` | `0.5.0` | | [pytest](https://github.com/pytest-dev/pytest) | `8.1.1` | `8.2.2` | | [mypy](https://github.com/python/mypy) | `1.9.0` | `1.10.1` | | [cryptography](https://github.com/pyca/cryptography) | `42.0.5` | `42.0.8` | | [coverage](https://github.com/nedbat/coveragepy) | `7.4.4` | `7.5.4` | | [mkdocs](https://github.com/mkdocs/mkdocs) | `1.5.3` | `1.6.0` | | [mkdocs-material](https://github.com/squidfunk/mkdocs-material) | `9.5.17` | `9.5.27` | Updates `a2wsgi` from 1.10.4 to 1.10.6 - [Commits](https://github.com/abersheeran/a2wsgi/compare/v1.10.4...v1.10.6) Updates `twine` from 5.0.0 to 5.1.1 - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/5.0.0...v5.1.1) Updates `ruff` from 0.3.7 to 0.5.0 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.3.7...0.5.0) Updates `pytest` from 8.1.1 to 8.2.2 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.2) Updates `mypy` from 1.9.0 to 1.10.1 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/1.9.0...v1.10.1) Updates `cryptography` from 42.0.5 to 42.0.8 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.5...42.0.8) Updates `coverage` from 7.4.4 to 7.5.4 - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/7.4.4...7.5.4) Updates `mkdocs` from 1.5.3 to 1.6.0 - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.5.3...1.6.0) Updates `mkdocs-material` from 9.5.17 to 9.5.27 - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.17...9.5.27) --- updated-dependencies: - dependency-name: a2wsgi dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-packages - dependency-name: twine dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-packages - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: mkdocs dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-packages - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-packages ... Signed-off-by: dependabot[bot] * Ignore UP031 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marcelo Trylesinski --- pyproject.toml | 11 ++++------- requirements.txt | 18 +++++++++--------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9fb3e3315..67c21d402 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ line-length = 120 [tool.ruff.lint] select = ["E", "F", "I", "FA", "UP"] -ignore = ["B904", "B028"] +ignore = ["B904", "B028", "UP031"] [tool.ruff.lint.isort] combine-as-imports = true @@ -90,16 +90,13 @@ filterwarnings = [ "error", 'ignore: \"watchgod\" is deprecated\, you should switch to watchfiles \(`pip install watchfiles`\)\.:DeprecationWarning', "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning", - "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning" + "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning", ] [tool.coverage.run] source_pkgs = ["uvicorn", "tests"] plugins = ["coverage_conditional_plugin"] -omit = [ - "uvicorn/workers.py", - "uvicorn/__main__.py", -] +omit = ["uvicorn/workers.py", "uvicorn/__main__.py"] [tool.coverage.report] precision = 2 @@ -128,7 +125,7 @@ py-not-win32 = "sys_platform != 'win32'" py-linux = "sys_platform == 'linux'" py-darwin = "sys_platform == 'darwin'" py-gte-38 = "sys_version_info >= (3, 8)" -py-lt-38 = "sys_version_info < (3, 8)" +py-lt-38 = "sys_version_info < (3, 8)" py-gte-39 = "sys_version_info >= (3, 9)" py-lt-39 = "sys_version_info < (3, 9)" py-gte-310 = "sys_version_info >= (3, 10)" diff --git a/requirements.txt b/requirements.txt index d35fd7f30..b3aba41c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,28 +5,28 @@ h11 @ git+https://github.com/python-hyper/h11.git@master # Explicit optionals -a2wsgi==1.10.4 +a2wsgi==1.10.6 wsproto==1.2.0 websockets==12.0 # Packaging build==1.2.1 -twine==5.0.0 +twine==5.1.1 # Testing -ruff==0.3.7 -pytest==8.1.1 +ruff==0.5.0 +pytest==8.2.2 pytest-mock==3.14.0 -mypy==1.9.0 +mypy==1.10.1 types-click==7.1.8 types-pyyaml==6.0.12.20240311 trustme==1.1.0 -cryptography==42.0.5 -coverage==7.4.4 +cryptography==42.0.8 +coverage==7.5.4 coverage-conditional-plugin==0.9.0 httpx==0.27.0 watchgod==0.8.2 # Documentation -mkdocs==1.5.3 -mkdocs-material==9.5.17 +mkdocs==1.6.0 +mkdocs-material==9.5.27 From 8f4c8a7f34914c16650ebd026127b96560425fde Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jul 2024 10:50:38 +0200 Subject: [PATCH 4/5] Add 100% clean coverage (#2394) * Add 100% clean coverage * Add 100% clean coverage * Add 100% clean coverage * Add 100% clean coverage --- docs/contributing.md | 4 ++-- pyproject.toml | 7 +++---- tests/supervisors/test_multiprocess.py | 2 +- tests/supervisors/test_signal.py | 4 ++-- tests/test_server.py | 4 ++-- uvicorn/_subprocess.py | 2 +- uvicorn/config.py | 18 +++++++++--------- uvicorn/loops/asyncio.py | 2 +- uvicorn/main.py | 2 +- uvicorn/middleware/proxy_headers.py | 2 +- uvicorn/protocols/http/flow_control.py | 6 +++--- uvicorn/protocols/http/h11_impl.py | 10 +++++----- uvicorn/protocols/http/httptools_impl.py | 14 +++++++------- .../protocols/websockets/websockets_impl.py | 2 +- uvicorn/protocols/websockets/wsproto_impl.py | 8 ++++---- uvicorn/server.py | 12 ++++++------ uvicorn/supervisors/basereload.py | 6 +++--- uvicorn/supervisors/multiprocess.py | 2 +- uvicorn/supervisors/watchfilesreload.py | 6 +++--- 19 files changed, 56 insertions(+), 57 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index 62cf40252..194ce8347 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -144,10 +144,10 @@ If tests are failing you will see this message under the coverage report: `=== 1 failed, 354 passed, 1 skipped, 1 xfailed in 37.08s ===` -If tests succeed but coverage doesn't reach our current threshold, you will see this +If tests succeed but coverage doesn't reach 100%, you will see this message under the coverage report: -`Coverage failure: total of 88 is less than fail-under=95` +`Coverage failure: total of 98 is less than fail-under=100` ## Releasing diff --git a/pyproject.toml b/pyproject.toml index 67c21d402..ca34a1e00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,7 @@ description = "The lightning-fast ASGI server." readme = "README.md" license = "BSD-3-Clause" requires-python = ">=3.8" -authors = [ - { name = "Tom Christie", email = "tom@tomchristie.com" }, -] +authors = [{ name = "Tom Christie", email = "tom@tomchristie.com" }] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", @@ -100,12 +98,13 @@ omit = ["uvicorn/workers.py", "uvicorn/__main__.py"] [tool.coverage.report] precision = 2 -fail_under = 98.35 +fail_under = 100 show_missing = true skip_covered = true exclude_lines = [ "pragma: no cover", "pragma: nocover", + "pragma: full coverage", "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", "raise NotImplementedError", diff --git a/tests/supervisors/test_multiprocess.py b/tests/supervisors/test_multiprocess.py index 5365907aa..e1f594efe 100644 --- a/tests/supervisors/test_multiprocess.py +++ b/tests/supervisors/test_multiprocess.py @@ -45,7 +45,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable def run(sockets: list[socket.socket] | None) -> None: - while True: + while True: # pragma: no cover time.sleep(1) diff --git a/tests/supervisors/test_signal.py b/tests/supervisors/test_signal.py index 95c4675d6..50bc5fca8 100644 --- a/tests/supervisors/test_signal.py +++ b/tests/supervisors/test_signal.py @@ -60,7 +60,7 @@ async def forever_app(scope, receive, send): await send({"type": "http.response.body", "body": b"start", "more_body": True}) # we never continue this one, so this request will time out await server_event.wait() - await send({"type": "http.response.body", "body": b"end", "more_body": False}) + await send({"type": "http.response.body", "body": b"end", "more_body": False}) # pragma: full coverage config = Config(app=forever_app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1) server: Server @@ -90,7 +90,7 @@ async def test_sigint_deny_request_after_triggered(unused_tcp_port: int, caplog) async def app(scope, receive, send): await send({"type": "http.response.start", "status": 200, "headers": []}) - await asyncio.sleep(1) + await asyncio.sleep(1) # pragma: full coverage config = Config(app=app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1) server: Server diff --git a/tests/test_server.py b/tests/test_server.py index dac8fb026..5b9d9ba2d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -37,10 +37,10 @@ async def dummy_app(scope, receive, send): # pragma: py-win32 pass -if sys.platform == "win32": +if sys.platform == "win32": # pragma: py-not-win32 signals = [signal.SIGBREAK] signal_captures = [capture_signal_sync] -else: +else: # pragma: py-win32 signals = [signal.SIGTERM, signal.SIGINT] signal_captures = [capture_signal_sync, capture_signal_async] diff --git a/uvicorn/_subprocess.py b/uvicorn/_subprocess.py index 36b369256..1c06844de 100644 --- a/uvicorn/_subprocess.py +++ b/uvicorn/_subprocess.py @@ -70,7 +70,7 @@ def subprocess_started( """ # Re-open stdin. if stdin_fileno is not None: - sys.stdin = os.fdopen(stdin_fileno) + sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage # Logging needs to be setup again for each child. config.configure_logging() diff --git a/uvicorn/config.py b/uvicorn/config.py index fca8d5fd2..9aff8c968 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -124,7 +124,7 @@ def is_dir(path: Path) -> bool: if not path.is_absolute(): path = path.resolve() return path.is_dir() - except OSError: + except OSError: # pragma: full coverage return False @@ -153,9 +153,9 @@ def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str children = [] for j in range(len(directories)): - for k in range(j + 1, len(directories)): + for k in range(j + 1, len(directories)): # pragma: full coverage if directories[j] in directories[k].parents: - children.append(directories[k]) # pragma: py-darwin + children.append(directories[k]) elif directories[k] in directories[j].parents: children.append(directories[j]) @@ -298,12 +298,12 @@ def __init__( if directory == reload_directory or directory in reload_directory.parents: try: self.reload_dirs.remove(reload_directory) - except ValueError: + except ValueError: # pragma: full coverage pass for pattern in self.reload_excludes: if pattern in self.reload_includes: - self.reload_includes.remove(pattern) + self.reload_includes.remove(pattern) # pragma: full coverage if not self.reload_dirs: if reload_dirs: @@ -332,7 +332,7 @@ def __init__( if forwarded_allow_ips is None: self.forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1") else: - self.forwarded_allow_ips = forwarded_allow_ips + self.forwarded_allow_ips = forwarded_allow_ips # pragma: full coverage if self.reload and self.workers > 1: logger.warning('"workers" flag is ignored when reloading is enabled.') @@ -485,7 +485,7 @@ def bind_socket(self) -> socket.socket: sock.bind(path) uds_perms = 0o666 os.chmod(self.uds, uds_perms) - except OSError as exc: + except OSError as exc: # pragma: full coverage logger.error(exc) sys.exit(1) @@ -503,7 +503,7 @@ def bind_socket(self) -> socket.socket: family = socket.AF_INET addr_format = "%s://%s:%d" - if self.host and ":" in self.host: # pragma: py-win32 + if self.host and ":" in self.host: # pragma: full coverage # It's an IPv6 address. family = socket.AF_INET6 addr_format = "%s://[%s]:%d" @@ -512,7 +512,7 @@ def bind_socket(self) -> socket.socket: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((self.host, self.port)) - except OSError as exc: + except OSError as exc: # pragma: full coverage logger.error(exc) sys.exit(1) diff --git a/uvicorn/loops/asyncio.py b/uvicorn/loops/asyncio.py index b24f4fe0d..1bead4a06 100644 --- a/uvicorn/loops/asyncio.py +++ b/uvicorn/loops/asyncio.py @@ -7,4 +7,4 @@ def asyncio_setup(use_subprocess: bool = False) -> None: if sys.platform == "win32" and use_subprocess: - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # pragma: full coverage diff --git a/uvicorn/main.py b/uvicorn/main.py index 6fab04b5f..4352efbca 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -576,7 +576,7 @@ def run( else: server.run() except KeyboardInterrupt: - pass + pass # pragma: full coverage finally: if config.uds and os.path.exists(config.uds): os.remove(config.uds) # pragma: py-win32 diff --git a/uvicorn/middleware/proxy_headers.py b/uvicorn/middleware/proxy_headers.py index 45d5518ce..fad2b0198 100644 --- a/uvicorn/middleware/proxy_headers.py +++ b/uvicorn/middleware/proxy_headers.py @@ -37,7 +37,7 @@ def get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str | Non if host not in self.trusted_hosts: return host - return None + return None # pragma: full coverage async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: if scope["type"] in ("http", "websocket"): diff --git a/uvicorn/protocols/http/flow_control.py b/uvicorn/protocols/http/flow_control.py index 42ecade71..2d1b5fa2d 100644 --- a/uvicorn/protocols/http/flow_control.py +++ b/uvicorn/protocols/http/flow_control.py @@ -16,7 +16,7 @@ def __init__(self, transport: asyncio.Transport) -> None: self._is_writable_event.set() async def drain(self) -> None: - await self._is_writable_event.wait() + await self._is_writable_event.wait() # pragma: full coverage def pause_reading(self) -> None: if not self.read_paused: @@ -29,12 +29,12 @@ def resume_reading(self) -> None: self._transport.resume_reading() def pause_writing(self) -> None: - if not self.write_paused: + if not self.write_paused: # pragma: full coverage self.write_paused = True self._is_writable_event.clear() def resume_writing(self) -> None: - if self.write_paused: + if self.write_paused: # pragma: full coverage self.write_paused = False self._is_writable_event.set() diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py index a2735fc78..9d2a2dabd 100644 --- a/uvicorn/protocols/http/h11_impl.py +++ b/uvicorn/protocols/http/h11_impl.py @@ -263,7 +263,7 @@ def handle_events(self) -> None: self.cycle.message_event.set() def handle_websocket_upgrade(self, event: h11.Request) -> None: - if self.logger.level <= TRACE_LOG_LEVEL: + if self.logger.level <= TRACE_LOG_LEVEL: # pragma: full coverage prefix = "%s:%d - " % self.client if self.client else "" self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix) @@ -333,13 +333,13 @@ def pause_writing(self) -> None: """ Called by the transport when the write buffer exceeds the high water mark. """ - self.flow.pause_writing() + self.flow.pause_writing() # pragma: full coverage def resume_writing(self) -> None: """ Called by the transport when the write buffer drops below the low water mark. """ - self.flow.resume_writing() + self.flow.resume_writing() # pragma: full coverage def timeout_keep_alive_handler(self) -> None: """ @@ -441,10 +441,10 @@ async def send(self, message: ASGISendEvent) -> None: message_type = message["type"] if self.flow.write_paused and not self.disconnected: - await self.flow.drain() + await self.flow.drain() # pragma: full coverage if self.disconnected: - return + return # pragma: full coverage if not self.response_started: # Sending response status line and headers diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py index 6dff0d631..98e0fe2bd 100644 --- a/uvicorn/protocols/http/httptools_impl.py +++ b/uvicorn/protocols/http/httptools_impl.py @@ -139,7 +139,7 @@ def _get_upgrade(self) -> bytes | None: upgrade = value.lower() if b"upgrade" in connection: return upgrade - return None + return None # pragma: full coverage def _should_upgrade_to_ws(self, upgrade: bytes | None) -> bool: if upgrade == b"websocket" and self.ws_protocol_class is not None: @@ -193,7 +193,7 @@ def handle_websocket_upgrade(self) -> None: def send_400_response(self, msg: str) -> None: content = [STATUS_LINE[400]] for name, value in self.server_state.default_headers: - content.extend([name, b": ", value, b"\r\n"]) + content.extend([name, b": ", value, b"\r\n"]) # pragma: full coverage content.extend( [ b"content-type: text/plain; charset=utf-8\r\n", @@ -336,13 +336,13 @@ def pause_writing(self) -> None: """ Called by the transport when the write buffer exceeds the high water mark. """ - self.flow.pause_writing() + self.flow.pause_writing() # pragma: full coverage def resume_writing(self) -> None: """ Called by the transport when the write buffer drops below the low water mark. """ - self.flow.resume_writing() + self.flow.resume_writing() # pragma: full coverage def timeout_keep_alive_handler(self) -> None: """ @@ -441,10 +441,10 @@ async def send(self, message: ASGISendEvent) -> None: message_type = message["type"] if self.flow.write_paused and not self.disconnected: - await self.flow.drain() + await self.flow.drain() # pragma: full coverage if self.disconnected: - return + return # pragma: full coverage if not self.response_started: # Sending response status line and headers @@ -477,7 +477,7 @@ async def send(self, message: ASGISendEvent) -> None: for name, value in headers: if HEADER_RE.search(name): - raise RuntimeError("Invalid HTTP header name.") + raise RuntimeError("Invalid HTTP header name.") # pragma: full coverage if HEADER_VALUE_RE.search(value): raise RuntimeError("Invalid HTTP header value.") diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py index 6897643c9..c0700d4d3 100644 --- a/uvicorn/protocols/websockets/websockets_impl.py +++ b/uvicorn/protocols/websockets/websockets_impl.py @@ -242,7 +242,7 @@ async def run_asgi(self) -> None: """ try: result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value] - except ClientDisconnected: + except ClientDisconnected: # pragma: full coverage self.closed_event.set() self.transport.close() except BaseException: diff --git a/uvicorn/protocols/websockets/wsproto_impl.py b/uvicorn/protocols/websockets/wsproto_impl.py index 3025a91b7..072dec942 100644 --- a/uvicorn/protocols/websockets/wsproto_impl.py +++ b/uvicorn/protocols/websockets/wsproto_impl.py @@ -44,7 +44,7 @@ def __init__( _loop: asyncio.AbstractEventLoop | None = None, ) -> None: if not config.loaded: - config.load() + config.load() # pragma: full coverage self.config = config self.app = cast(ASGI3Application, config.loaded_app) @@ -140,13 +140,13 @@ def pause_writing(self) -> None: """ Called by the transport when the write buffer exceeds the high water mark. """ - self.writable.clear() + self.writable.clear() # pragma: full coverage def resume_writing(self) -> None: """ Called by the transport when the write buffer drops below the low water mark. """ - self.writable.set() + self.writable.set() # pragma: full coverage def shutdown(self) -> None: if self.handshake_complete: @@ -233,7 +233,7 @@ async def run_asgi(self) -> None: try: result = await self.app(self.scope, self.receive, self.send) # type: ignore[func-returns-value] except ClientDisconnected: - self.transport.close() + self.transport.close() # pragma: full coverage except BaseException: self.logger.exception("Exception in ASGI application\n") self.send_500_response() diff --git a/uvicorn/server.py b/uvicorn/server.py index bfce1b1b1..fa7638b7d 100644 --- a/uvicorn/server.py +++ b/uvicorn/server.py @@ -112,7 +112,7 @@ def create_protocol( loop = asyncio.get_running_loop() listeners: Sequence[socket.SocketType] - if sockets is not None: + if sockets is not None: # pragma: full coverage # Explicitly passed a list of open sockets. # We use this when the server is run from a Gunicorn worker. @@ -147,7 +147,7 @@ def _share_socket( # Create a socket using UNIX domain socket. uds_perms = 0o666 if os.path.exists(config.uds): - uds_perms = os.stat(config.uds).st_mode + uds_perms = os.stat(config.uds).st_mode # pragma: full coverage server = await loop.create_unix_server( create_protocol, path=config.uds, ssl=config.ssl, backlog=config.backlog ) @@ -180,7 +180,7 @@ def _share_socket( else: # We're most likely running multiple workers, so a message has already been # logged by `config.bind_socket()`. - pass + pass # pragma: full coverage self.started = True @@ -243,7 +243,7 @@ async def on_tick(self, counter: int) -> bool: # Callback to `callback_notify` once every `timeout_notify` seconds. if self.config.callback_notify is not None: - if current_time - self.last_notified > self.config.timeout_notify: + if current_time - self.last_notified > self.config.timeout_notify: # pragma: full coverage self.last_notified = current_time await self.config.callback_notify() @@ -261,7 +261,7 @@ async def shutdown(self, sockets: list[socket.socket] | None = None) -> None: for server in self.servers: server.close() for sock in sockets or []: - sock.close() + sock.close() # pragma: full coverage # Request shutdown on all existing connections. for connection in list(self.server_state.connections): @@ -330,6 +330,6 @@ def capture_signals(self) -> Generator[None, None, None]: def handle_exit(self, sig: int, frame: FrameType | None) -> None: self._captured_signals.append(sig) if self.should_exit and sig == signal.SIGINT: - self.force_exit = True + self.force_exit = True # pragma: full coverage else: self.should_exit = True diff --git a/uvicorn/supervisors/basereload.py b/uvicorn/supervisors/basereload.py index 1c791a8fb..f07ca3912 100644 --- a/uvicorn/supervisors/basereload.py +++ b/uvicorn/supervisors/basereload.py @@ -38,14 +38,14 @@ def __init__( self.is_restarting = False self.reloader_name: str | None = None - def signal_handler(self, sig: int, frame: FrameType | None) -> None: + def signal_handler(self, sig: int, frame: FrameType | None) -> None: # pragma: full coverage """ A signal handler that is registered with the parent process. """ if sys.platform == "win32" and self.is_restarting: - self.is_restarting = False # pragma: py-not-win32 + self.is_restarting = False else: - self.should_exit.set() # pragma: py-win32 + self.should_exit.set() def run(self) -> None: self.startup() diff --git a/uvicorn/supervisors/multiprocess.py b/uvicorn/supervisors/multiprocess.py index fff6871fc..93c34d0d3 100644 --- a/uvicorn/supervisors/multiprocess.py +++ b/uvicorn/supervisors/multiprocess.py @@ -171,7 +171,7 @@ def keep_subprocess_alive(self) -> None: process.join() if self.should_exit.is_set(): - return + return # pragma: full coverage logger.info(f"Child process [{process.pid}] died") process = Process(self.config, self.target, self.sockets) diff --git a/uvicorn/supervisors/watchfilesreload.py b/uvicorn/supervisors/watchfilesreload.py index 292a7bab8..0d3b9b77e 100644 --- a/uvicorn/supervisors/watchfilesreload.py +++ b/uvicorn/supervisors/watchfilesreload.py @@ -31,14 +31,14 @@ def __init__(self, config: Config): if is_dir: self.exclude_dirs.append(p) else: - self.excludes.append(e) + self.excludes.append(e) # pragma: full coverage self.excludes = list(set(self.excludes)) def __call__(self, path: Path) -> bool: for include_pattern in self.includes: if path.match(include_pattern): if str(path).endswith(include_pattern): - return True + return True # pragma: full coverage for exclude_dir in self.exclude_dirs: if exclude_dir in path.parents: @@ -46,7 +46,7 @@ def __call__(self, path: Path) -> bool: for exclude_pattern in self.excludes: if path.match(exclude_pattern): - return False + return False # pragma: full coverage return True return False From 5bf788f0eb0fc771f5c4eed8f282c7ec256565d2 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 20 Jul 2024 10:58:34 +0200 Subject: [PATCH 5/5] Version 0.30.3 (#2395) --- CHANGELOG.md | 7 +++++++ uvicorn/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40aace5a6..be1a09080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 0.30.3 (2024-07-20) + +### Fixed + +- Suppress `KeyboardInterrupt` from CLI and programmatic usage (#2384) +- `ClientDisconnect` inherits from `OSError` instead of `IOError` (#2393) + ## 0.30.2 (2024-07-20) ### Added diff --git a/uvicorn/__init__.py b/uvicorn/__init__.py index 17b80e8c8..fdcd74a14 100644 --- a/uvicorn/__init__.py +++ b/uvicorn/__init__.py @@ -1,5 +1,5 @@ from uvicorn.config import Config from uvicorn.main import Server, main, run -__version__ = "0.30.2" +__version__ = "0.30.3" __all__ = ["main", "run", "Config", "Server"]