Skip to content

Commit 3bd42e0

Browse files
committed
Version 0.1.0 release
1 parent dc415a4 commit 3bd42e0

File tree

8 files changed

+301
-20
lines changed

8 files changed

+301
-20
lines changed

LICENSE

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
MIT License
1+
Copyright 2016-2019 dry-python organization
22

3-
Copyright (c) 2019 Nikita Sobolev
3+
Redistribution and use in source and binary forms, with or without
4+
modification, are permitted provided that the following conditions are
5+
met:
46

5-
Permission is hereby granted, free of charge, to any person obtaining a copy
6-
of this software and associated documentation files (the "Software"), to deal
7-
in the Software without restriction, including without limitation the rights
8-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9-
copies of the Software, and to permit persons to whom the Software is
10-
furnished to do so, subject to the following conditions:
7+
1. Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
119

12-
The above copyright notice and this permission notice shall be included in all
13-
copies or substantial portions of the Software.
10+
2. Redistributions in binary form must reproduce the above copyright
11+
notice, this list of conditions and the following disclaimer in the
12+
documentation and/or other materials provided with the
13+
distribution.
1414

15-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
15+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,100 @@ We also recommend to use the same `mypy` settings [we use](https://github.com/we
4949

5050
Make sure you know how to get started, [check out our docs](https://classes.readthedocs.io/en/latest/)!
5151

52+
53+
## Example
54+
55+
Imagine, that you want to bound implementation to some particular type.
56+
Like, strings behave like this, numbers behave like that, and so on.
57+
58+
The good realworld example is `djangorestframework`.
59+
It is build around the idea that different
60+
data types should be converted differently to and from `json` format.
61+
62+
What is the "traditional" (or outdated if you will!) approach?
63+
To create tons of classes for different data types and use them.
64+
65+
That's how we end up with classes like so:
66+
67+
```python
68+
class IntField(Field):
69+
def from_json(self, value):
70+
return value
71+
72+
def to_json(self, value):
73+
return value
74+
```
75+
76+
It literally has a lot of problems:
77+
78+
- It is hard to type this code. How can I be sure that my `json` will be parsed by the given schema?
79+
- It contains a lot of boilerplate
80+
- It has complex API: there are usually several methods to override, some fields to adjust. Moreover, we use a class, not a callable
81+
- It is hard to extend the default library for new custom types you will have in your own project
82+
83+
There should be a better way of solving this problem!
84+
And typeclasses are a better way!
85+
86+
How would new API look like with this concept?
87+
88+
```python
89+
>>> from typing import Union
90+
>>> from classes import typeclass
91+
>>> @typeclass
92+
... def to_json(instance) -> str:
93+
... """This is a typeclass definition to covert things to json."""
94+
...
95+
>>> @to_json.instance(int)
96+
... @to_json.instance(float)
97+
... def _to_json_int(instance: Union[int, float]) -> str:
98+
... return str(instance)
99+
...
100+
>>> @to_json.instance(bool)
101+
... def _to_json_bool(instance: bool) -> str:
102+
... return 'true' if instance else 'false'
103+
...
104+
>>> @to_json.instance(list)
105+
... def _to_json_list(instance: list) -> str:
106+
... return '[{0}]'.format(
107+
... ', '.join(to_json(list_item) for list_item in instance),
108+
... )
109+
...
110+
111+
```
112+
113+
See how easy it is to works with types and implementation?
114+
115+
Typeclass is represented as a regular function, so you can use it like one:
116+
117+
```python
118+
>>> to_json(True)
119+
'true'
120+
>>> to_json(1)
121+
'1'
122+
>>> to_json([False, 1, 2])
123+
'[false, 1, 2]'
124+
125+
```
126+
127+
And it easy to extend this typeclass with your own classes as well:
128+
129+
```python
130+
>>> # Pretending to import the existing library from somewhere:
131+
>>> # from to_json import to_json
132+
>>> import datetime as dt
133+
>>> @to_json.instance(dt.datetime)
134+
... def _to_json_datetime(instance: dt.datetime) -> str:
135+
... return instance.isoformat()
136+
...
137+
>>> to_json(dt.datetime(2019, 10, 31, 12, 28, 00))
138+
'2019-10-31T12:28:00'
139+
140+
```
141+
142+
That's how simple, safe, and powerful typeclasses are!
143+
Make sure to [check out our docs](https://github.com/dry-python/classes) to learn more.
144+
145+
146+
## License
147+
148+
BSD 2-Clause

classes/typeclass.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
_TypeClassType = TypeVar('_TypeClassType')
88
_ReturnType = TypeVar('_ReturnType')
9-
_CallbackType = TypeVar('_CallbackType')
9+
_CallbackType = TypeVar('_CallbackType', bound=Callable)
1010
_InstanceType = TypeVar('_InstanceType')
1111

1212

@@ -154,6 +154,8 @@ def instance(
154154
would not match ``Type[_InstanceType]`` type due to ``mypy`` rules.
155155
156156
"""
157+
isinstance(object(), type_argument) # That's how we check for generics
158+
157159
def decorator(implementation):
158160
container = self._protocols if is_protocol else self._instances
159161
container[type_argument] = implementation

docs/pages/concept.rst

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,47 @@ That's it. There's nothing extra about typeclasses. They can be:
107107
- and called
108108

109109

110+
Related concepts
111+
----------------
112+
110113
singledispatch
111-
--------------
114+
~~~~~~~~~~~~~~
112115

113116
One may ask, what is the difference
114117
with `singledispatch <https://docs.python.org/3/library/functools.html#functools.singledispatch>`_
115118
function from the standard library?
116119

117120
The thing about ``singledispatch`` is that it allows almost the same features.
118-
But, it lacks type-safety. For example,
121+
But, it lacks type-safety.
122+
For example, it does not check for the same
123+
function signatures and return types in all cases:
124+
125+
.. code:: python
126+
127+
>>> from functools import singledispatch
128+
>>> @singledispatch
129+
... def example(instance) -> str:
130+
... return 'default'
131+
...
132+
>>> @example.register
133+
... def _example_int(instance: int, other: int) -> int:
134+
... return instance + other
135+
...
136+
>>> @example.register
137+
... def _example_str(instance: str) -> bool:
138+
... return bool(instance)
139+
...
140+
>>> bool(example(1, 0)) == example('a')
141+
True
142+
143+
As you can see: you are able to create
144+
instances with different return types and number of parameters.
145+
146+
Good luck working with that!
147+
148+
149+
Further reading
150+
---------------
151+
152+
- `Wikipedia <https://en.wikipedia.org/wiki/Type_class>`_
153+
- `Typeclasses in Haskell <http://learnyouahaskell.com/types-and-typeclasses>`_

docs/pages/typeclass.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
Typeclass
22
=========
33

4+
Here are the technical docs about ``typeclass`` and how to use it.
5+
46
.. automodule:: classes.typeclass
57
:members:

docs/pages/typesafety.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,62 @@ Here are features that we support:
4848
to be the first (or instance) parameter in a call.
4949
2. We also check that other parameters do exist in the original signature
5050
3. We check the return type: it matches that defined one in the signature
51+
52+
53+
Limitations
54+
-----------
55+
56+
We are limited in generics support.
57+
We support them, but without type parameters.
58+
59+
- We support: ``list``, ``List``, ``Dict``,
60+
``Mapping``, ``Iterable``, ``MyCustomGeneric``
61+
- We don't support ``List[int]``, ``Dict[str, str]``, ``Iterable[Any]``, etc
62+
63+
Why? Because we cannot tell the difference
64+
between ``List[int]`` and ``List[str]`` in runtime.
65+
66+
Python just does not have this information. It requires types to be infered.
67+
And that's currently not possible.
68+
69+
So, this would not work:
70+
71+
.. code:: python
72+
73+
>>> from typing import List
74+
>>> from classes import typeclass
75+
>>> @typeclass
76+
... def generic_typeclass(instance) -> str:
77+
... """We use this example to demonstrate the typing limitation."""
78+
...
79+
>>> @generic_typeclass.instance(List[int])
80+
... def _generic_typeclass_list_int(instance: List[int]):
81+
... ...
82+
...
83+
Traceback (most recent call last):
84+
...
85+
TypeError: Subscripted generics cannot be used with class and instance checks
86+
87+
88+
But, this will (note that we use ``list`` inside ``.instance()`` call):
89+
90+
.. code:: python
91+
92+
>>> from typing import List
93+
>>> from classes import typeclass
94+
>>> @typeclass
95+
... def generic_typeclass(instance) -> str:
96+
... """We use this example to demonstrate the typing limitation."""
97+
...
98+
>>> @generic_typeclass.instance(list)
99+
... def _generic_typeclass_list_int(instance: List):
100+
... return ''.join(str(list_item) for list_item in instance)
101+
...
102+
>>> generic_typeclass([1, 2, 3])
103+
'123'
104+
>>> generic_typeclass(['a', 1, True])
105+
'a1True'
106+
107+
Use primitive generics as they always have ``Any`` inside.
108+
Annotations should also be bound to any parameters.
109+
Do not supply any other values there, we cannot even check for it.

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ norecursedirs = temp *.egg .eggs dist build docs .tox .git __pycache__
5656
# you an overhead. See `docs/template/development-process.rst`.
5757
addopts =
5858
--doctest-modules
59+
--doctest-glob='*.md'
5960
--doctest-glob='*.rst'
6061
--cov=classes
6162
--cov-report=term:skip-covered
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
- case: typeclass_instances_union
2+
disable_cache: true
3+
main: |
4+
from typing import Union
5+
from classes import typeclass
6+
7+
@typeclass
8+
def a(instance) -> str:
9+
...
10+
11+
@a.instance(str)
12+
@a.instance(int)
13+
def _a_int_str(instance: Union[str, int]) -> str:
14+
return str(instance)
15+
16+
reveal_type(a) # N: Revealed type is 'classes.typeclass._TypeClass[Union[builtins.str*, builtins.int*], builtins.str, def (builtins.str*) -> builtins.str]'
17+
18+
19+
- case: typeclass_instance_any
20+
disable_cache: true
21+
main: |
22+
from classes import typeclass
23+
24+
@typeclass
25+
def a(instance):
26+
...
27+
28+
@a.instance(str)
29+
def _a_int_str(instance: str) -> str:
30+
return str(instance)
31+
32+
reveal_type(a) # N: Revealed type is 'classes.typeclass._TypeClass[builtins.str*, Any, def (builtins.str*) -> Any]'
33+
34+
35+
- case: typeclass_instance_missing_first_arg
36+
disable_cache: true
37+
main: |
38+
from classes import typeclass
39+
40+
@typeclass
41+
def a(instance):
42+
...
43+
44+
@a.instance
45+
def some():
46+
...
47+
48+
out: |
49+
main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "Callable[[], Any]"
50+
main:7: note: <1 more non-matching overload not shown>
51+
main:7: note: def [_InstanceType] instance(self, type_argument: Type[_InstanceType], *, is_protocol: Literal[False] = ...) -> Callable[[Callable[[_InstanceType], Any]], NoReturn]
52+
main:7: note: Possible overload variant:
53+
54+
55+
- case: typeclass_wrong_param
56+
disable_cache: true
57+
main: |
58+
from classes import typeclass
59+
60+
typeclass(1)
61+
62+
out: |
63+
main:3: error: Value of type variable "_CallbackType" of "typeclass" cannot be "int"
64+
65+
66+
- case: typeclass_instance_wrong_param
67+
disable_cache: true
68+
main: |
69+
from classes import typeclass
70+
71+
@typeclass
72+
def a(instance):
73+
...
74+
75+
a.instance(1)
76+
77+
out: |
78+
main:7: error: No overload variant of "instance" of "_TypeClass" matches argument type "int"
79+
main:7: note: <1 more non-matching overload not shown>
80+
main:7: note: def [_InstanceType] instance(self, type_argument: Type[_InstanceType], *, is_protocol: Literal[False] = ...) -> Callable[[Callable[[_InstanceType], Any]], NoReturn]
81+
main:7: note: Possible overload variant:

0 commit comments

Comments
 (0)