Skip to content

Commit

Permalink
Removed the support of writing async __new__
Browse files Browse the repository at this point in the history
  • Loading branch information
francis-clairicia committed Dec 27, 2023
1 parent c2a76c6 commit e30e1a5
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 96 deletions.
30 changes: 6 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ from async_object import AsyncObject


class MyObject(AsyncObject):
async def __new__(cls) -> "MyObject":
self = await super().__new__(cls)

# Do some async stuff

return self

async def __init__(self) -> None:
await super().__init__()

Expand Down Expand Up @@ -84,8 +77,8 @@ The inheritance logic with "normal" constructors is the same here:
from typing_extensions import Self

class MyObjectOnlyNew(AsyncObject):
async def __new__(cls, *args: Any, **kwargs: Any) -> Self:
self = await super().__new__(cls)
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
self = super().__new__(cls)

print(args)
print(kwargs)
Expand All @@ -102,8 +95,8 @@ class MyObjectOnlyInit(AsyncObject):


class MyObjectBothNewAndInit(AsyncObject):
async def __new__(cls, *args: Any, **kwargs: Any) -> Self:
self = await super().__new__(cls)
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
self = super().__new__(cls)

print(args)
print(kwargs)
Expand All @@ -120,7 +113,7 @@ class MyObjectBothNewAndInit(AsyncObject):
### Inheritance
Talking about inheritance, there are a few rules to follow:
- `AsyncObject` or a subclass must appear at least once in the base classes declaration.
- Non-`AsyncObject` classes can be used as base classes if they do not override `__new__` or `__init__` (in order not to break the [MRO](https://docs.python.org/3/glossary.html#term-method-resolution-order)).
- Non-`AsyncObject` classes can be used as base classes if they do not override `__init__` (in order not to break the [MRO](https://docs.python.org/3/glossary.html#term-method-resolution-order)).
- To avoid confusion with [awaitable objects](https://docs.python.org/3/glossary.html#term-awaitable), overriding `__await__` is forbidden.

### Abstract base classes
Expand Down Expand Up @@ -170,7 +163,7 @@ class MyAbstractObject(AsyncABC):
```

## Static type checking: mypy integration
`mypy` does not like having `async def` for `__new__` and `__init__`, and will not understand `await AsyncObject()`.
`mypy` does not like having `async def` for `__init__`, and will not understand `await AsyncObject()`.

`async-object` embeds a plugin which helps `mypy` to understand asynchronous constructors.

Expand Down Expand Up @@ -213,14 +206,3 @@ async def main() -> None:
instance = await coroutine
reveal_type(instance) # Revealed type is "__main__.MyObject"
```

### Caveat/Known issues
The errors triggered by `__new__` cannot be silenced yet. You can use `# type: ignore[misc]` comment to mask these errors.
```py
class MyObject(AsyncObject):
async def __new__(cls) -> Self: # type: ignore[misc]
return await super().__new__(cls)

async def __init__(self) -> None:
await super().__init__()
```
27 changes: 21 additions & 6 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ authors = [{name = "FrankySnow9", email = "clairicia.rcj.francis@gmail.com"}]
requires-python = ">=3.9"
readme = "README.md"
license-files = { paths = ["LICENSE"] }
dependencies = [
"typing-extensions>=4.9.0",
]
dependencies = []
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
Expand Down Expand Up @@ -61,6 +59,7 @@ dev = [
]
tox = [
"tox<5,>=4.0.8",
"tox-pdm~=0.7.0",
]
format = [
"isort!=5.11.0,>=5.10.1",
Expand All @@ -75,6 +74,10 @@ test = [
"pytest-asyncio~=0.20",
"pytest-cov<5,>=4.0",
"coverage[toml]",
"typing-extensions>=4.9.0",
]
coverage = [
"coverage[toml]",
]

[tool.isort]
Expand Down
29 changes: 9 additions & 20 deletions src/async_object/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,8 @@
from functools import partialmethod
from typing import TYPE_CHECKING, Any, Callable, TypeVar

from typing_extensions import Self


def _validate_constructor(func: Any, name: str) -> None:
if isinstance(func, (staticmethod, classmethod)):
return _validate_constructor(func.__func__, name)
if isinstance(func, partialmethod): # pragma: no cover
return _validate_constructor(func.func, name)

Expand All @@ -45,7 +41,7 @@ class AsyncObjectMeta(type):
__Self = TypeVar("__Self", bound="AsyncObjectMeta")

def __new__(mcs: type[__Self], name: str, bases: tuple[type, ...], namespace: dict[str, Any], /, **kwargs: Any) -> __Self:
for attr in {"__new__", "__init__"}:
for attr in {"__init__"}:
try:
func = namespace[attr]
except KeyError:
Expand All @@ -65,49 +61,42 @@ def __new__(mcs: type[__Self], name: str, bases: tuple[type, ...], namespace: di
if not any(issubclass(b, absolute_base_class) for b in bases):
raise TypeError(f"{name} must explicitly derive from {absolute_base_class.__name__}")
if invalid_bases := [
b.__name__
for b in bases
if not issubclass(b, absolute_base_class) and (b.__new__ is not object.__new__ or b.__init__ is not object.__init__)
b.__name__ for b in bases if not issubclass(b, absolute_base_class) and (b.__init__ is not object.__init__)
]:
raise TypeError(
f"These non-async base classes define a custom __new__ or __init__: {', '.join(map(repr, invalid_bases))}"
)
raise TypeError(f"These non-async base classes define a custom __init__: {', '.join(map(repr, invalid_bases))}")
return super().__new__(mcs, name, bases, namespace, **kwargs)

def __setattr__(cls, name: str, value: Any, /) -> None:
if cls is AsyncObject:
raise AttributeError("AsyncObject is immutable")
if name == "__await__":
raise TypeError("AsyncObject subclasses must not have __await__ method")
if name in {"__new__", "__init__"}:
if name == "__init__":
_validate_constructor(value, name)
return super().__setattr__(name, value)

def __delattr__(cls, name: str, /) -> None:
if cls is AsyncObject:
raise AttributeError("AsyncObject is immutable")
if name in {"__await__", "__new__", "__init__"}:
if name in {"__await__", "__init__"}:
raise TypeError(f"{name}() cannot be deleted")
return super().__delattr__(name)

async def __call__(cls, /, *args: Any, **kwargs: Any) -> Any:
cls_new: Callable[..., Any] = cls.__new__
if cls_new is AsyncObject.__new__:
self = await cls_new(cls)
if cls_new is object.__new__:
self = cls_new(cls)
else:
self = await cls_new(cls, *args, **kwargs)
self = cls_new(cls, *args, **kwargs)
cls_init = type(self).__init__
if cls_init is not AsyncObject.__init__ or cls_new is AsyncObject.__new__:
if cls_init is not AsyncObject.__init__ or cls_new is object.__new__:
await cls_init(self, *args, **kwargs)
return self


class AsyncObject(metaclass=AsyncObjectMeta):
__slots__ = ()

async def __new__(cls) -> Self: # type: ignore[misc]
return object.__new__(cls)

async def __init__(self) -> None: # type: ignore[misc]
pass

Expand Down
12 changes: 4 additions & 8 deletions src/async_object/contrib/mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,12 @@ def async_class_instanciation_callback(ctx: FunctionContext) -> Type:
def async_class_def_callback(ctx: ClassDefContext) -> None:
info = ctx.cls.info

for ctor in ("__new__", "__init__"):
for ctor in {"__init__"}:
node = info.names.get(ctor)
if node is None or node.node is None:
continue

new_ctor_name: str | None = None
if ctor in {"__init__"}:
new_ctor_name = f"__async_{ctor[2:-2]}_mypy_placeholder"
new_ctor_name = f"__async_{ctor[2:-2]}_mypy_placeholder"

func_items: Sequence[SymbolNode]
if isinstance(node.node, OverloadedFuncDef):
Expand All @@ -97,11 +95,9 @@ def async_class_def_callback(ctx: ClassDefContext) -> None:
code=errorcodes.OVERRIDE,
)
continue
if new_ctor_name is not None:
__set_func_def_name(defn, new_ctor_name)
__set_func_def_name(defn, new_ctor_name)

if new_ctor_name is not None:
info.names[new_ctor_name] = info.names[ctor]
info.names[new_ctor_name] = info.names[ctor]

if info.get_method("__await__") is not None:
ctx.api.fail('AsyncObject subclasses must not have "__await__" method', ctx.cls, code=errorcodes.OVERRIDE)
Expand Down
8 changes: 4 additions & 4 deletions tests/test_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ async def test_subclass_with_arguments_for_dunder_new() -> None:
class MyObject(AsyncObject):
myattr_from_new: int

async def __new__(cls, value: int) -> Self: # type: ignore[misc]
self = await super().__new__(cls)
def __new__(cls, value: int) -> Self:
self = super().__new__(cls)
self.myattr_from_new = value
return self

Expand All @@ -75,8 +75,8 @@ async def test_subclass_with_custom_dunder_new() -> None:
# Arrange

class MyObject(AsyncObject):
async def __new__(cls, value: int) -> Self: # type: ignore[misc]
self = await super().__new__(cls)
def __new__(cls, value: int) -> Self:
self = super().__new__(cls)
self.myattr_from_new = value * 2
return self

Expand Down
30 changes: 6 additions & 24 deletions tests/test_metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@
from async_object import AsyncABC, AsyncABCMeta, AsyncObject, AsyncObjectMeta


@pytest.mark.asyncio
async def test_explicit_staticmethod_dunder_new() -> None:
class A(AsyncObject):
@staticmethod
async def __new__(cls: type[Any]) -> Any: # type: ignore[misc]
return await super(A, cls).__new__(cls)

assert isinstance(await A(), A)


def test_no_base_class() -> None:
with pytest.raises(TypeError, match=r"^_ must explicitly derive from AsyncObject$"):

Expand All @@ -36,11 +26,11 @@ class _(A, AsyncObject):
pass


def test_non_async_base_class_with_custom_dunder_new_or_dunder_init() -> None:
with pytest.raises(TypeError, match=r"^These non-async base classes define a custom __new__ or __init__: 'A', 'B'$"):
def test_non_async_base_class_with_custom_dunder_init() -> None:
with pytest.raises(TypeError, match=r"^These non-async base classes define a custom __init__: 'A', 'B'$"):

class A:
def __new__(cls) -> Any:
def __init__(self) -> None:
pass

class B:
Expand All @@ -59,14 +49,6 @@ def __init__(self) -> None: # type: ignore[override]
...


def test_dunder_new_not_a_coroutine_function() -> None:
with pytest.raises(TypeError, match=r"^'__new__' must be a coroutine function \(using 'async def'\)$"):

class _(AsyncObject):
def __new__(cls) -> Any: # type: ignore[override]
...


def test_dunder_await_defined() -> None:
with pytest.raises(TypeError, match=r"^AsyncObject subclasses must not have __await__ method$"):

Expand All @@ -85,7 +67,7 @@ def test_AsyncObject_immutable_on_delete() -> None:
delattr(AsyncObject, "something")


@pytest.mark.parametrize("attr", ["__new__", "__init__"])
@pytest.mark.parametrize("attr", ["__init__"])
def test_dunder_init_overwritable_with_another_coroutine(attr: str) -> None:
async def __new_func__(self: Any) -> None:
pass
Expand All @@ -98,7 +80,7 @@ class MyObject(AsyncObject):
assert getattr(MyObject, attr) is __new_func__


@pytest.mark.parametrize("attr", ["__new__", "__init__"])
@pytest.mark.parametrize("attr", ["__init__"])
def test_constructor_overwrite_error(attr: str) -> None:
def __new_func__(self: Any) -> None:
pass
Expand Down Expand Up @@ -127,7 +109,7 @@ class MyObject(AsyncObject):
MyObject.__await__ = None


@pytest.mark.parametrize("attr", ["__await__", "__new__", "__init__"])
@pytest.mark.parametrize("attr", ["__await__", "__init__"])
def test_attribute_cannot_be_deleted(attr: str) -> None:
class MyObject(AsyncObject):
pass
Expand Down
Loading

0 comments on commit e30e1a5

Please sign in to comment.