Skip to content

Commit 1b0e202

Browse files
authored
Closes #98, closes #141 (#147)
* Closes #98, closes #141 * Fixes CI * Fixes CI * Fixes CI * Fixes CI * Fixes CI
1 parent 05e2004 commit 1b0e202

File tree

17 files changed

+114
-177
lines changed

17 files changed

+114
-177
lines changed

.github/workflows/test.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: test
22

3-
on: [push, pull_request]
3+
on: [push, pull_request, workflow_dispatch]
44

55
jobs:
66
build:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
python-version: [3.6, 3.7, 3.8]
10+
python-version: [3.6, 3.7, 3.8, 3.9]
1111

1212

1313
steps:
@@ -16,25 +16,28 @@ jobs:
1616
uses: actions/setup-python@v1
1717
with:
1818
python-version: ${{ matrix.python-version }}
19-
19+
2020
- name: Install poetry
2121
run: |
22+
pip install -U pip
23+
2224
curl -sSL \
2325
"https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py" | python
26+
27+
echo "${HOME}/.poetry/bin" >> $GITHUB_PATH
28+
2429
- name: Set up cache
2530
uses: actions/cache@v1
2631
with:
2732
path: .venv
2833
key: venv-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
2934
- name: Install dependencies
3035
run: |
31-
source "$HOME/.poetry/env"
32-
3336
poetry config virtualenvs.in-project true
3437
poetry install
38+
3539
- name: Run tests
3640
run: |
37-
source "$HOME/.poetry/env"
3841
poetry run flake8 .
3942
poetry run mypy classes ./tests/**/*.py
4043
poetry run pytest . docs/pages

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
We follow Semantic Versions since the `0.1.0` release.
44

55

6+
## Version 0.2.0 WIP
7+
8+
### Features
9+
10+
- **Breaking**: renames mypy `typeclass_plugin` to `classes_plugin`
11+
- Adds `python3.9` support
12+
13+
### Misc
14+
15+
- Updates dependencies
16+
17+
618
## Version 0.1.0
719

820
- Initial release

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ to fix [this existing issue](https://github.com/python/mypy/issues/3157):
4040
# In setup.cfg or mypy.ini:
4141
[mypy]
4242
plugins =
43-
classes.contrib.mypy.typeclass_plugin
43+
classes.contrib.mypy.classes_plugin
4444
```
4545

4646
**Without this step**, your project will report type-violations here and there.

classes/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# -*- coding: utf-8 -*-
2-
31
"""
42
Here we define the public API of our module.
53

classes/contrib/mypy/typeclass_plugin.py renamed to classes/contrib/mypy/classes_plugin.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# -*- coding: utf-8 -*-
2-
31
"""
42
Custom mypy plugin to enable typeclass concept to work.
53
@@ -27,7 +25,7 @@
2725

2826
def _adjust_arguments(ctx):
2927
typeclass_def = ctx.default_return_type.args[2]
30-
if not isinstance(typeclass_def, CallableType):
28+
if not isinstance(typeclass_def, CallableType): # TODO: FunctionLike?
3129
return ctx.default_return_type
3230

3331
args = [

classes/typeclass.py

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# -*- coding: utf-8 -*-
2-
31
from typing import Callable, Dict, Generic, NoReturn, Type, TypeVar, overload
42

53
from typing_extensions import Literal
@@ -24,21 +22,21 @@ class _TypeClass(Generic[_TypeClassType, _ReturnType, _CallbackType]):
2422
>>> @typeclass
2523
... def used(instance, other: int) -> int:
2624
... '''Example typeclass to be used later.'''
27-
...
25+
2826
>>> @used.instance(int)
2927
... def _used_int(instance: int, other: int) -> int:
3028
... return instance + other
31-
...
29+
3230
>>> def accepts_typeclass(
3331
... callback: Callable[[int, int], int],
3432
... ) -> int:
3533
... return callback(1, 3)
36-
...
37-
>>> accepts_typeclass(used)
38-
4
34+
35+
>>> assert accepts_typeclass(used) == 4
3936
4037
Take a note, that we structural subtyping here.
41-
And all typeclasses that match this signature will typecheck.
38+
And all typeclasses that match ``Callable[[int, int], int]`` signature
39+
will typecheck.
4240
4341
"""
4442

@@ -123,8 +121,8 @@ def instance(
123121
) -> Callable[
124122
[Callable[[_InstanceType], _ReturnType]],
125123
NoReturn, # We need this type to disallow direct instance calls
126-
]: # pragma: no cover
127-
...
124+
]:
125+
"""Case for regular typeclasses."""
128126

129127
@overload
130128
def instance(
@@ -135,8 +133,8 @@ def instance(
135133
) -> Callable[
136134
[Callable[[_InstanceType], _ReturnType]],
137135
NoReturn, # We need this type to disallow direct instance calls
138-
]: # pragma: no cover
139-
...
136+
]:
137+
"""Case for protocol based typeclasses."""
140138

141139
def instance(
142140
self,
@@ -182,7 +180,7 @@ def typeclass(
182180
>>> @typeclass
183181
... def example(instance) -> str:
184182
... '''Example typeclass.'''
185-
...
183+
186184
>>> example(1)
187185
Traceback (most recent call last):
188186
...
@@ -202,9 +200,8 @@ def typeclass(
202200
>>> @example.instance(int)
203201
... def _example_int(instance: int) -> str:
204202
... return 'int case'
205-
...
206-
>>> example(1)
207-
'int case'
203+
204+
>>> assert example(1) == 'int case'
208205
209206
Now we have a specific instance for ``int``
210207
which does something different from the default implementation.
@@ -227,9 +224,8 @@ def typeclass(
227224
>>> @example.instance(str)
228225
... def _example_str(instance: str) -> str:
229226
... return instance
230-
...
231-
>>> example('a')
232-
'a'
227+
228+
>>> assert example('a') == 'a'
233229
234230
Now it works with ``str`` as well. But differently.
235231
This allows developer to base the implementation on type information.
@@ -252,7 +248,6 @@ def typeclass(
252248
>>> class MyGeneric(Generic[T]):
253249
... def __init__(self, arg: T) -> None:
254250
... self.arg = arg
255-
...
256251
257252
Now, let's define the typeclass instance for this type:
258253
@@ -261,17 +256,15 @@ def typeclass(
261256
>>> @example.instance(MyGeneric)
262257
... def _my_generic_example(instance: MyGeneric) -> str:
263258
... return 'generi' + str(instance.arg)
264-
...
265-
>>> example(MyGeneric('c'))
266-
'generic'
259+
260+
>>> assert example(MyGeneric('c')) == 'generic'
267261
268262
This case will work for all type parameters of ``MyGeneric``,
269263
or in other words it can be assumed as ``MyGeneric[Any]``:
270264
271265
.. code:: python
272266
273-
>>> example(MyGeneric(1))
274-
'generi1'
267+
>>> assert example(MyGeneric(1)) == 'generi1'
275268
276269
In the future, when Python will have new type mechanisms,
277270
we would like to improve our support for specific generic instances
@@ -290,16 +283,14 @@ def typeclass(
290283
>>> @example.instance(Sequence, is_protocol=True)
291284
... def _sequence_example(instance: Sequence) -> str:
292285
... return ','.join(str(item) for item in instance)
293-
...
294-
>>> example([1, 2, 3])
295-
'1,2,3'
286+
287+
>>> assert example([1, 2, 3]) == '1,2,3'
296288
297289
But, ``str`` will still have higher priority over ``Sequence``:
298290
299291
.. code:: python
300292
301-
>>> example('abc')
302-
'abc'
293+
>>> assert example('abc') == 'abc'
303294
304295
We also support user-defined protocols:
305296
@@ -308,21 +299,19 @@ def typeclass(
308299
>>> from typing_extensions import Protocol
309300
>>> class CustomProtocol(Protocol):
310301
... field: str
311-
...
302+
312303
>>> @example.instance(CustomProtocol, is_protocol=True)
313304
... def _custom_protocol_example(instance: CustomProtocol) -> str:
314305
... return instance.field
315-
...
316306
317307
Now, let's build a class that match this protocol and test it:
318308
319309
.. code:: python
320310
321311
>>> class WithField(object):
322312
... field: str = 'with field'
323-
...
324-
>>> example(WithField())
325-
'with field'
313+
314+
>>> assert example(WithField()) == 'with field'
326315
327316
Remember, that generic protocols have the same limitation as generic types.
328317

docs/pages/concept.rst

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ The first on is "Typeclass definition", where we create a new typeclass:
6767
>>> @typeclass
6868
... def json(instance) -> str:
6969
... """That's definition!"""
70-
...
7170
7271
When typeclass is defined it only has a name and a signature
7372
that all instances will share.
@@ -78,12 +77,11 @@ Let's define some instances:
7877
>>> @json.instance(str)
7978
... def _json_str(instance: str) -> str:
8079
... return '"{0}"'.format(instance)
81-
...
80+
8281
>>> @json.instance(int)
8382
... @json.instance(float)
8483
... def _json_int_float(instance) -> str:
8584
... return str(instance)
86-
...
8785
8886
That's how we define instances for our typeclass.
8987
These instances will be executed when the corresponding type will be supplied.
@@ -93,12 +91,9 @@ with different value of different types:
9391

9492
.. code:: python
9593
96-
>>> json('text')
97-
'"text"'
98-
>>> json(1)
99-
'1'
100-
>>> json(1.5)
101-
'1.5'
94+
>>> assert json('text') == '"text"'
95+
>>> assert json(1) == '1'
96+
>>> assert json(1.5) == '1.5'
10297
10398
That's it. There's nothing extra about typeclasses. They can be:
10499

@@ -128,17 +123,16 @@ function signatures and return types in all cases:
128123
>>> @singledispatch
129124
... def example(instance) -> str:
130125
... return 'default'
131-
...
126+
132127
>>> @example.register(int)
133128
... def _example_int(instance: int, other: int) -> int:
134129
... return instance + other
135-
...
130+
136131
>>> @example.register(str)
137132
... def _example_str(instance: str) -> bool:
138133
... return bool(instance)
139-
...
140-
>>> bool(example(1, 0)) == example('a')
141-
True
134+
135+
>>> assert bool(example(1, 0)) == example('a')
142136
143137
As you can see: you are able to create
144138
instances with different return types and number of parameters.

docs/pages/typesafety.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ First of all, we always check that the signature is the same for all cases.
1818
.. code:: python
1919
2020
>>> from classes import typeclass
21+
2122
>>> @typeclass
2223
... def example(instance, arg: int, *, keyword: str) -> bool:
2324
... ...
24-
...
25+
2526
>>> @example.instance(str)
2627
... def _example_str(instance: str, arg: int, *, keyword: str) -> bool:
2728
... return instance * arg + keyword
28-
...
2929
3030
Let's look at this example closely:
3131

@@ -72,10 +72,11 @@ So, this would not work:
7272
7373
>>> from typing import List
7474
>>> from classes import typeclass
75+
7576
>>> @typeclass
7677
... def generic_typeclass(instance) -> str:
7778
... """We use this example to demonstrate the typing limitation."""
78-
...
79+
7980
>>> @generic_typeclass.instance(List[int])
8081
... def _generic_typeclass_list_int(instance: List[int]):
8182
... ...
@@ -91,18 +92,17 @@ But, this will (note that we use ``list`` inside ``.instance()`` call):
9192
9293
>>> from typing import List
9394
>>> from classes import typeclass
95+
9496
>>> @typeclass
9597
... def generic_typeclass(instance) -> str:
9698
... """We use this example to demonstrate the typing limitation."""
97-
...
99+
98100
>>> @generic_typeclass.instance(list)
99101
... def _generic_typeclass_list_int(instance: List):
100102
... 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'
103+
104+
>>> assert generic_typeclass([1, 2, 3]) == '123'
105+
>>> assert generic_typeclass(['a', 1, True]) == 'a1True'
106106
107107
Use primitive generics as they always have ``Any`` inside.
108108
Annotations should also be bound to any parameters.

0 commit comments

Comments
 (0)