-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from curegit/develop
Update to 0.2.0
- Loading branch information
Showing
16 changed files
with
618 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,31 @@ | ||
.PHONY: build install preview publish clean test | ||
.PHONY: build install devinstall preview publish clean format check test | ||
|
||
build: clean | ||
python3 -m build | ||
|
||
install: build | ||
pip3 install . | ||
|
||
devinstall: build | ||
pip3 install -e .[dev] | ||
|
||
preview: build | ||
python3 -m twine upload --repository-url "https://test.pypi.org/legacy/" dist/* | ||
|
||
publish: build | ||
python3 -m twine upload --repository "https://upload.pypi.org/legacy/" dist/* | ||
python3 -m twine upload --repository-url "https://upload.pypi.org/legacy/" dist/* | ||
|
||
clean: | ||
python3 -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)' | ||
python3 -c 'import shutil; shutil.rmtree("build", ignore_errors=True)' | ||
python3 -c 'import shutil; shutil.rmtree("reification.egg-info", ignore_errors=True)' | ||
python3 -c 'import shutil; shutil.rmtree(".mypy_cache", ignore_errors=True)' | ||
|
||
format: | ||
python3 -m black -l 200 reification tests | ||
|
||
check: | ||
python3 -m mypy reification tests | ||
|
||
test: | ||
python3 -m unittest discover -v tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,187 @@ | ||
# Reification (Python library) | ||
|
||
Reified generics in Python to get type parameters at runtime | ||
|
||
```py | ||
from reification import Reified | ||
|
||
class ReifiedList[T](Reified): pass | ||
|
||
l = ReifiedList[int]([1, 2, 3]) | ||
class ReifiedList[T](Reified, list[T]): | ||
pass | ||
|
||
print(l.types) # int | ||
|
||
xs = ReifiedList[int](range(10)) | ||
print(xs) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] | ||
print(xs.targ) # <class 'int'> | ||
``` | ||
|
||
## Requirements | ||
|
||
Python >= 3.12 | ||
- Python >= 3.12 | ||
|
||
Any non-builtin modules are NOT required. | ||
This library is written in pure Python and does not require any non-builtin modules. | ||
|
||
## Install | ||
|
||
```sh | ||
pip3 install reification | ||
``` | ||
|
||
## Usage | ||
## API | ||
|
||
### `Reified` (class) | ||
|
||
`Reified` is a Mixin class designed to facilitate the creation of new types based on reified type parameters. | ||
|
||
This class is threadsafe so that inheriting classes can be used in multiple threads. | ||
|
||
You cannot instantiate this class directly. | ||
|
||
#### `targ: type | tuple[type | Any, ...] | Any` (class property) | ||
|
||
This class property represents the type argument(s) specified for the reified generic class. | ||
If there's more than one type argument, `targ` will be a tuple containing each given type or type-like values. | ||
If type argument is not specified, it may return 'Any'. | ||
|
||
#### `type_args: tuple[type | Any, ...]` (class property) | ||
|
||
This is another class property that carries the type argument(s) provided for the reified generic class. | ||
Unlike `targ`, `type_args` always returns a tuple of the specified type arguments, even when there's only one type argument. | ||
If no type arguments are given, it may contain single 'Any'. | ||
|
||
#### `__class_getitem__(cls, params: type | tuple[type | Any, ...] | Any) -> type` (special class method, for Mixin) | ||
|
||
This method, which the class overrides, is used for creating new types each time it is called with distinct type arguments. | ||
It serves a key role in handling parameterized generic classes, enabling the different identities on different type arguments of the same base class. | ||
|
||
## Example Usage: Type Checked Generic Stack | ||
|
||
```py | ||
from reification import Reified | ||
|
||
|
||
class ReifiedStack[T](Reified): | ||
def __init__(self) -> None: | ||
super().__init__() | ||
self.items: list[T] = [] | ||
|
||
def push(self, item: T) -> None: | ||
# We can do runtime check | ||
if isinstance(item, self.targ): | ||
self.items.append(item) | ||
else: | ||
raise TypeError() | ||
|
||
def pop(self) -> T: | ||
if self.items: | ||
return self.items.pop() | ||
else: | ||
raise IndexError("pop from empty stack") | ||
|
||
|
||
stack = ReifiedStack[str]() | ||
stack.push("spam") # OK | ||
stack.push(42) # raise TypeError | ||
``` | ||
|
||
The `ReifiedStack` class created here is generic and derived from the `Reified` base class, and implements a simple stack with `push` and `pop` methods. | ||
|
||
In the `push` method, we are checking at runtime if the item being pushed is of the specified generic type (this type is accessible via the `targ` attribute inherited from `Reified`). | ||
If the type of the item does not match, a `TypeError` is raised. | ||
|
||
In the example usage, we create an instance of the ReifiedStack class with a type argument as string. When we try to push a string `"spam"`, the item is accepted since it matches with the stack's specified type argument. | ||
However, when we try to push an integer `42`, a `TypeError` is raised because the type of item does not match with the stack's type argument. | ||
|
||
This demonstrates the use of reified generics in Python where we can have runtime access to the type parameters, enabling us to type check dynamically at runtime. | ||
This is useful in situations where we need to enforce type safety in our code or use type information at runtime. | ||
|
||
## Typing | ||
|
||
With `Reified` generic types, type parameters are considered for understanding and respecting the typing semantics as much as possible. | ||
|
||
Python's native `isinstance` function works seamlessly with reified generic types. | ||
|
||
In context of reified generics: | ||
|
||
```py | ||
>>> from reification import reify | ||
>>> a = reify[list[int]]([1, 2, 3]) | ||
>>> a.type | ||
list[int] | ||
>>> isinstance(ReifiedList[int](), ReifiedList[int]) | ||
True | ||
``` | ||
|
||
The above expression returns `True` as a `ReifiedList` object of integer type is indeed an instance of a `ReifiedList` of integer type. | ||
|
||
On the other hand: | ||
|
||
```py | ||
T = TypeVar("T") | ||
>>> isinstance(ReifiedList[str](), ReifiedList[int]) | ||
False | ||
``` | ||
|
||
class CheckedStack(Reific[T]): | ||
This returns `False` because, while both the objects are instances of the `ReifiedList` class, their type parameters are different (string vs integer). | ||
|
||
### Type equivalence | ||
|
||
It treats two instances of the `Reified` derived same class as equivalent only if the type parameters provided in their instantiation are exactly the same. | ||
That is, `ReifiedClass[T, ...] == ReifiedClass[S, ...]` if and only if `(T, ...) == (S, ...)`. | ||
|
||
```py | ||
>>> ReifiedList[float] == ReifiedList[float] | ||
True | ||
>>> ReifiedList[float] == ReifiedList[int] | ||
False | ||
>>> ReifiedList[tuple[int, str]] == ReifiedList[tuple[int, str]] | ||
True | ||
>>> ReifiedList[tuple[int, str]] == ReifiedList[tuple[int, float]] | ||
False | ||
>>> ReifiedList[ReifiedList[int]] == ReifiedList[ReifiedList[int]] | ||
True | ||
>>> ReifiedList[ReifiedList[int]] == ReifiedList[ReifiedList[str]] | ||
False | ||
``` | ||
|
||
### Subtyping | ||
|
||
The `Reified` Mixin supports nominal subtyping. | ||
|
||
Type `A` is a subtype of type `B` if `A == B` or `A` is directly derived from `B`. | ||
|
||
A `Reified` derived class with type parameters is considered a subtype of the same class without type parameters. | ||
This means that `ReifiedClass[T, ...]` is a subtype of `ReifiedClass`. | ||
|
||
```py | ||
>>> int_stack = CheckedStack[int]() | ||
>>> isinstance(int_stack, CheckedStack[int]) | ||
>>> issubclass(ReifiedList[int], ReifiedList[int]) | ||
True | ||
>>> issubclass(ReifiedList, ReifiedList[int]) | ||
False | ||
>>> issubclass(ReifiedList[int], ReifiedList) | ||
True | ||
>>> isinstance(int_stack, CheckedStack[str]) | ||
>>> issubclass(ReifiedList[str], ReifiedList[int]) | ||
False | ||
>>> class ReifiedListSub(ReifiedList[int]): | ||
... pass | ||
... | ||
>>> issubclass(ReifiedListSub, ReifiedList[int]) | ||
True | ||
``` | ||
|
||
subclass works as you expected. | ||
#### Type Variance | ||
|
||
`Reified` Mixin only considers direct equivalence of type parameters for subtyping and does not cater for type variance. | ||
|
||
```py | ||
>>> issubclass(Reified[int], Reified[int]) | ||
>>> issubclass(Reified, Reified[int]) | ||
>>> issubclass(Reified[int], Reified) | ||
>>> issubclass(Reified[str], Reified[int]) | ||
>>> issubclass(bool, int) | ||
True | ||
>>> class ReifiedTuple[T](Reified, tuple[T]): | ||
... pass | ||
... | ||
>>> issubclass(ReifiedTuple[bool], ReifiedTuple[int]) | ||
False | ||
``` | ||
|
||
## Tips | ||
|
||
`typing_inspect` | ||
|
||
## License | ||
|
||
[WTFPL](LICENSE) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
__version__ = "0.1.0" | ||
__version__ = "0.2.0" | ||
|
||
from reification.reific import Reific | ||
from .core import Reified | ||
|
||
__all__ = ["Reified"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from typing import Any | ||
from .utils import get_reified_type, tuplize_class_getitem_params | ||
|
||
|
||
class Reified: | ||
targ: Any = type | Any | ||
|
||
type_args: tuple[type | Any, ...] = (Any,) | ||
|
||
def __new__(cls, *args, **kwargs): | ||
# Prohibit from instantiating directly | ||
if cls is Reified: | ||
raise RuntimeError("Cannot instantiate 'Reified' class directly.") | ||
return super().__new__(cls, *args, **kwargs) | ||
|
||
# Return type should be inferred | ||
def __class_getitem__(cls, params: type | tuple[type | Any, ...] | Any): | ||
# Prohibit from instantiating directly | ||
if cls is Reified: | ||
raise RuntimeError("Cannot instantiate 'Reified' class directly.") | ||
# Returns a separated reified type | ||
param_tuple = tuplize_class_getitem_params(params) | ||
rt = get_reified_type(cls, param_tuple) | ||
rt.targ = params | ||
rt.type_args = param_tuple | ||
return rt |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import types | ||
from typing import Any | ||
from threading import RLock | ||
|
||
|
||
_type_dict: dict[tuple[type, tuple[type | Any, ...]], type] = dict() | ||
|
||
|
||
_lock = RLock() | ||
|
||
|
||
def get_reified_type[T](base_cls: type[T], type_args: tuple[type | Any, ...]) -> type[T]: | ||
key = (base_cls, type_args) | ||
with _lock: | ||
if key in _type_dict: | ||
return _type_dict[key] | ||
else: | ||
new_type = clone_type(base_cls, type_args) | ||
_type_dict[key] = new_type | ||
return new_type | ||
|
||
|
||
def clone_type[T](cls: type[T], type_args: tuple[type | Any, ...]) -> type[T]: | ||
reified2 = types.new_class(name=cls.__name__ + str(type_args), bases=(cls,)) | ||
return reified2 | ||
|
||
|
||
def tuplize_class_getitem_params(params: type | tuple[type | Any, ...] | Any) -> tuple[type | Any, ...]: | ||
if isinstance(params, tuple): | ||
return params | ||
else: | ||
return (params,) |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.