Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

馃摑 Rename Websocket by WebSocket #272

Merged
merged 6 commits into from
Jun 27, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 59 additions & 0 deletions asgiref/_pep562.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Backport of PEP 562.
https://pypi.org/search/?q=pep562
Licensed under MIT
Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
import sys
from typing import Any, Callable, List, Optional


class Pep562:
"""
Backport of PEP 562 <https://pypi.org/search/?q=pep562>.
Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`.
The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed.
"""

def __init__(self, name: str) -> None:
"""Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7."""

self._module = sys.modules[name]
self._get_attr = getattr(self._module, "__getattr__", None)
self._get_dir: Optional[Callable[..., List[str]]] = getattr(
self._module, "__dir__", None
)
sys.modules[name] = self # type: ignore[assignment]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sys.modules expects a Module and self is a Pep562, jfyk.


def __dir__(self) -> List[str]:
"""Return the overridden `dir` if one was provided, else apply `dir` to the module."""

return self._get_dir() if self._get_dir else dir(self._module)

def __getattr__(self, name: str) -> Any:
"""Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present."""

try:
return getattr(self._module, name)
except AttributeError:
if self._get_attr:
return self._get_attr(name)
raise


def pep562(module_name: str) -> None:
"""Helper function to apply PEP 562."""

if sys.version_info < (3, 7):
Pep562(module_name)
121 changes: 101 additions & 20 deletions asgiref/typing.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
import sys
from typing import Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union
import warnings
from typing import (
Any,
Awaitable,
Callable,
Dict,
Iterable,
List,
Optional,
Tuple,
Type,
Union,
)

from asgiref._pep562 import pep562

if sys.version_info >= (3, 8):
from typing import Literal, Protocol, TypedDict
else:
from typing_extensions import Literal, Protocol, TypedDict

__all__ = (
"ASGIVersions",
"HTTPScope",
"WebSocketScope",
"LifespanScope",
"WWWScope",
"Scope",
"HTTPRequestEvent",
"HTTPResponseStartEvent",
"HTTPResponseBodyEvent",
"HTTPServerPushEvent",
"HTTPDisconnectEvent",
"WebSocketConnectEvent",
"WebSocketAcceptEvent",
"WebSocketReceiveEvent",
"WebSocketSendEvent",
"WebSocketResponseStartEvent",
"WebSocketResponseBodyEvent",
"WebSocketDisconnectEvent",
"WebSocketCloseEvent",
"LifespanStartupEvent",
"LifespanShutdownEvent",
"LifespanStartupCompleteEvent",
"LifespanStartupFailedEvent",
"LifespanShutdownCompleteEvent",
"LifespanShutdownFailedEvent",
"ASGIReceiveEvent",
"ASGISendEvent",
"ASGIReceiveCallable",
"ASGISendCallable",
"ASGI2Protocol",
"ASGI2Application",
"ASGI3Application",
"ASGIApplication",
)


class ASGIVersions(TypedDict):
spec_version: str
Expand All @@ -28,7 +78,7 @@ class HTTPScope(TypedDict):
extensions: Dict[str, Dict[object, object]]


class WebsocketScope(TypedDict):
class WebSocketScope(TypedDict):
type: Literal["websocket"]
asgi: ASGIVersions
http_version: str
Expand All @@ -49,8 +99,8 @@ class LifespanScope(TypedDict):
asgi: ASGIVersions


WWWScope = Union[HTTPScope, WebsocketScope]
Scope = Union[HTTPScope, WebsocketScope, LifespanScope]
WWWScope = Union[HTTPScope, WebSocketScope]
Scope = Union[HTTPScope, WebSocketScope, LifespanScope]


class HTTPRequestEvent(TypedDict):
Expand Down Expand Up @@ -81,46 +131,46 @@ class HTTPDisconnectEvent(TypedDict):
type: Literal["http.disconnect"]


class WebsocketConnectEvent(TypedDict):
class WebSocketConnectEvent(TypedDict):
type: Literal["websocket.connect"]


class WebsocketAcceptEvent(TypedDict):
class WebSocketAcceptEvent(TypedDict):
type: Literal["websocket.accept"]
subprotocol: Optional[str]
headers: Iterable[Tuple[bytes, bytes]]


class WebsocketReceiveEvent(TypedDict):
class WebSocketReceiveEvent(TypedDict):
type: Literal["websocket.receive"]
bytes: Optional[bytes]
text: Optional[str]


class WebsocketSendEvent(TypedDict):
class WebSocketSendEvent(TypedDict):
type: Literal["websocket.send"]
bytes: Optional[bytes]
text: Optional[str]


class WebsocketResponseStartEvent(TypedDict):
class WebSocketResponseStartEvent(TypedDict):
type: Literal["websocket.http.response.start"]
status: int
headers: Iterable[Tuple[bytes, bytes]]


class WebsocketResponseBodyEvent(TypedDict):
class WebSocketResponseBodyEvent(TypedDict):
type: Literal["websocket.http.response.body"]
body: bytes
more_body: bool


class WebsocketDisconnectEvent(TypedDict):
class WebSocketDisconnectEvent(TypedDict):
type: Literal["websocket.disconnect"]
code: int


class WebsocketCloseEvent(TypedDict):
class WebSocketCloseEvent(TypedDict):
type: Literal["websocket.close"]
code: int
reason: Optional[str]
Expand Down Expand Up @@ -155,9 +205,9 @@ class LifespanShutdownFailedEvent(TypedDict):
ASGIReceiveEvent = Union[
HTTPRequestEvent,
HTTPDisconnectEvent,
WebsocketConnectEvent,
WebsocketReceiveEvent,
WebsocketDisconnectEvent,
WebSocketConnectEvent,
WebSocketReceiveEvent,
WebSocketDisconnectEvent,
LifespanStartupEvent,
LifespanShutdownEvent,
]
Expand All @@ -168,11 +218,11 @@ class LifespanShutdownFailedEvent(TypedDict):
HTTPResponseBodyEvent,
HTTPServerPushEvent,
HTTPDisconnectEvent,
WebsocketAcceptEvent,
WebsocketSendEvent,
WebsocketResponseStartEvent,
WebsocketResponseBodyEvent,
WebsocketCloseEvent,
WebSocketAcceptEvent,
WebSocketSendEvent,
WebSocketResponseStartEvent,
WebSocketResponseBodyEvent,
WebSocketCloseEvent,
LifespanStartupCompleteEvent,
LifespanStartupFailedEvent,
LifespanShutdownCompleteEvent,
Expand Down Expand Up @@ -204,3 +254,34 @@ async def __call__(
Awaitable[None],
]
ASGIApplication = Union[ASGI2Application, ASGI3Application]

__deprecated__ = {
"WebsocketConnectEvent": WebSocketConnectEvent,
"WebsocketAcceptEvent": WebSocketAcceptEvent,
"WebsocketReceiveEvent": WebSocketReceiveEvent,
"WebsocketSendEvent": WebSocketSendEvent,
"WebsocketResponseStartEvent": WebSocketResponseStartEvent,
"WebsocketResponseBodyEvent": WebSocketResponseBodyEvent,
"WebsocketDisconnectEvent": WebSocketDisconnectEvent,
"WebsocketCloseEvent": WebSocketCloseEvent,
}


def __getattr__(name: str) -> Any:
deprecated = __deprecated__.get(name)
if deprecated:
stacklevel = 3 if sys.version_info >= (3, 7) else 4
warnings.warn(
"'{}' is deprecated. Use '{}' instead.".format(name, deprecated.__name__),
category=DeprecationWarning,
stacklevel=stacklevel,
)
return deprecated
raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))


def __dir__() -> List[str]:
return sorted(list(__all__) + list(__deprecated__.keys()))


pep562(__name__)
20 changes: 20 additions & 0 deletions tests/test_deprecated_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import importlib

import pytest

from asgiref import typing


@pytest.mark.parametrize("deprecated_type", typing.__deprecated__.keys())
def test_deprecated_types(deprecated_type: str) -> None:
with pytest.warns(DeprecationWarning) as record:
getattr(importlib.import_module("asgiref.typing"), deprecated_type)
assert len(record) == 1
assert deprecated_type in str(record.list[0])


@pytest.mark.parametrize("available_type", typing.__all__)
def test_available_types(available_type: str) -> None:
with pytest.warns(None) as record:
getattr(importlib.import_module("asgiref.typing"), available_type)
assert len(record) == 0
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ commands =
mypy: mypy . {posargs}

[testenv:qa]
skip_install=true
skip_install = true
deps =
pre-commit
commands =
Expand Down