adding sentinel metaclass example
This commit is contained in:
@@ -41,7 +41,7 @@ T_contra = TypeVar('T_contra', contravariant=True)
|
||||
|
||||
class TrashCan(Generic[T_contra]):
|
||||
def put(self, trash: T_contra) -> None:
|
||||
"""Store trash until dumped..."""
|
||||
"""Store trash until dumped."""
|
||||
|
||||
|
||||
class Cafeteria:
|
||||
45
15-more-types/cafeteria/contravariant.py
Normal file
45
15-more-types/cafeteria/contravariant.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# tag::TRASH_TYPES[]
|
||||
from typing import TypeVar, Generic
|
||||
|
||||
class Refuse: # <1>
|
||||
"""Any refuse."""
|
||||
|
||||
class Biodegradable(Refuse):
|
||||
"""Biodegradable refuse."""
|
||||
|
||||
class Compostable(Biodegradable):
|
||||
"""Compostable refuse."""
|
||||
|
||||
T_contra = TypeVar('T_contra', contravariant=True) # <2>
|
||||
|
||||
class TrashCan(Generic[T_contra]): # <3>
|
||||
def put(self, refuse: T_contra) -> None:
|
||||
"""Store trash until dumped."""
|
||||
|
||||
def deploy(trash_can: TrashCan[Biodegradable]):
|
||||
"""Deploy a trash can for biodegradable refuse."""
|
||||
# end::TRASH_TYPES[]
|
||||
|
||||
|
||||
################################################ contravariant trash can
|
||||
|
||||
|
||||
# tag::DEPLOY_TRASH_CANS[]
|
||||
bio_can: TrashCan[Biodegradable] = TrashCan()
|
||||
deploy(bio_can)
|
||||
|
||||
trash_can: TrashCan[Refuse] = TrashCan()
|
||||
deploy(trash_can)
|
||||
# end::DEPLOY_TRASH_CANS[]
|
||||
|
||||
|
||||
################################################ more specific trash can
|
||||
|
||||
# tag::DEPLOY_NOT_VALID[]
|
||||
compost_can: TrashCan[Compostable] = TrashCan()
|
||||
deploy(compost_can)
|
||||
# end::DEPLOY_NOT_VALID[]
|
||||
|
||||
## Argument 1 to "deploy" has
|
||||
## incompatible type "TrashCan[Compostable]"
|
||||
## expected "TrashCan[Biodegradable]"
|
||||
48
15-more-types/cafeteria/covariant.py
Normal file
48
15-more-types/cafeteria/covariant.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from typing import TypeVar, Generic
|
||||
|
||||
|
||||
class Beverage:
|
||||
"""Any beverage."""
|
||||
|
||||
|
||||
class Juice(Beverage):
|
||||
"""Any fruit juice."""
|
||||
|
||||
|
||||
class OrangeJuice(Juice):
|
||||
"""Delicious juice from Brazilian oranges."""
|
||||
|
||||
|
||||
# tag::BEVERAGE_TYPES[]
|
||||
T_co = TypeVar('T_co', covariant=True) # <1>
|
||||
|
||||
|
||||
class BeverageDispenser(Generic[T_co]): # <2>
|
||||
def __init__(self, beverage: T_co) -> None:
|
||||
self.beverage = beverage
|
||||
|
||||
def dispense(self) -> T_co:
|
||||
return self.beverage
|
||||
|
||||
def install(dispenser: BeverageDispenser[Juice]) -> None: # <3>
|
||||
"""Install a fruit juice dispenser."""
|
||||
# end::BEVERAGE_TYPES[]
|
||||
|
||||
################################################ covariant dispenser
|
||||
|
||||
# tag::INSTALL_JUICE_DISPENSERS[]
|
||||
juice_dispenser = BeverageDispenser(Juice())
|
||||
install(juice_dispenser)
|
||||
|
||||
orange_juice_dispenser = BeverageDispenser(OrangeJuice())
|
||||
install(orange_juice_dispenser)
|
||||
# end::INSTALL_JUICE_DISPENSERS[]
|
||||
|
||||
################################################ not a juice dispenser
|
||||
|
||||
beverage_dispenser = BeverageDispenser(Beverage())
|
||||
|
||||
## Argument 1 to "install" has
|
||||
## incompatible type "BeverageDispenser[Beverage]"
|
||||
## expected "BeverageDispenser[Juice]"
|
||||
install(beverage_dispenser)
|
||||
54
15-more-types/cafeteria/invariant.py
Normal file
54
15-more-types/cafeteria/invariant.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# tag::BEVERAGE_TYPES[]
|
||||
from typing import TypeVar, Generic
|
||||
|
||||
class Beverage: # <1>
|
||||
"""Any beverage."""
|
||||
|
||||
class Juice(Beverage):
|
||||
"""Any fruit juice."""
|
||||
|
||||
class OrangeJuice(Juice):
|
||||
"""Delicious juice from Brazilian oranges."""
|
||||
|
||||
T = TypeVar('T') # <2>
|
||||
|
||||
class BeverageDispenser(Generic[T]): # <3>
|
||||
"""A dispenser parameterized on the beverage type."""
|
||||
def __init__(self, beverage: T) -> None:
|
||||
self.beverage = beverage
|
||||
|
||||
def dispense(self) -> T:
|
||||
return self.beverage
|
||||
|
||||
def install(dispenser: BeverageDispenser[Juice]) -> None: # <4>
|
||||
"""Install a fruit juice dispenser."""
|
||||
# end::BEVERAGE_TYPES[]
|
||||
|
||||
################################################ exact type
|
||||
|
||||
# tag::INSTALL_JUICE_DISPENSER[]
|
||||
juice_dispenser = BeverageDispenser(Juice())
|
||||
install(juice_dispenser)
|
||||
# end::INSTALL_JUICE_DISPENSER[]
|
||||
|
||||
|
||||
################################################ variant dispenser
|
||||
|
||||
# tag::INSTALL_BEVERAGE_DISPENSER[]
|
||||
beverage_dispenser = BeverageDispenser(Beverage())
|
||||
install(beverage_dispenser)
|
||||
## Argument 1 to "install" has
|
||||
## incompatible type "BeverageDispenser[Beverage]"
|
||||
## expected "BeverageDispenser[Juice]"
|
||||
# end::INSTALL_BEVERAGE_DISPENSER[]
|
||||
|
||||
|
||||
################################################ variant dispenser
|
||||
|
||||
# tag::INSTALL_ORANGE_JUICE_DISPENSER[]
|
||||
orange_juice_dispenser = BeverageDispenser(OrangeJuice())
|
||||
install(orange_juice_dispenser)
|
||||
# end::INSTALL_ORANGE_JUICE_DISPENSER[]
|
||||
## Argument 1 to "install" has
|
||||
## incompatible type "BeverageDispenser[OrangeJuice]"
|
||||
## expected "BeverageDispenser[Juice]"
|
||||
38
15-more-types/clip_annot.py
Normal file
38
15-more-types/clip_annot.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
>>> clip('banana ', 6)
|
||||
'banana'
|
||||
>>> clip('banana ', 7)
|
||||
'banana'
|
||||
>>> clip('banana ', 5)
|
||||
'banana'
|
||||
>>> clip('banana split', 6)
|
||||
'banana'
|
||||
>>> clip('banana split', 7)
|
||||
'banana'
|
||||
>>> clip('banana split', 10)
|
||||
'banana'
|
||||
>>> clip('banana split', 11)
|
||||
'banana'
|
||||
>>> clip('banana split', 12)
|
||||
'banana split'
|
||||
"""
|
||||
|
||||
# tag::CLIP_ANNOT[]
|
||||
def clip(text: str, max_len: int = 80) -> str:
|
||||
"""Return new ``str`` clipped at last space before or after ``max_len``.
|
||||
Return full ``text`` if no space found.
|
||||
"""
|
||||
end = None
|
||||
if len(text) > max_len:
|
||||
space_before = text.rfind(' ', 0, max_len)
|
||||
if space_before >= 0:
|
||||
end = space_before
|
||||
else:
|
||||
space_after = text.rfind(' ', max_len)
|
||||
if space_after >= 0:
|
||||
end = space_after
|
||||
if end is None:
|
||||
end = len(text)
|
||||
return text[:end].rstrip()
|
||||
|
||||
# end::CLIP_ANNOT[]
|
||||
38
15-more-types/clip_annot_1ed.py
Normal file
38
15-more-types/clip_annot_1ed.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
>>> clip('banana ', 6)
|
||||
'banana'
|
||||
>>> clip('banana ', 7)
|
||||
'banana'
|
||||
>>> clip('banana ', 5)
|
||||
'banana'
|
||||
>>> clip('banana split', 6)
|
||||
'banana'
|
||||
>>> clip('banana split', 7)
|
||||
'banana'
|
||||
>>> clip('banana split', 10)
|
||||
'banana'
|
||||
>>> clip('banana split', 11)
|
||||
'banana'
|
||||
>>> clip('banana split', 12)
|
||||
'banana split'
|
||||
"""
|
||||
|
||||
# tag::CLIP_ANNOT[]
|
||||
|
||||
def clip(text: str, max_len: 'int > 0' = 80) -> str: # <1>
|
||||
"""Return text clipped at the last space before or after max_len
|
||||
"""
|
||||
end = None
|
||||
if len(text) > max_len:
|
||||
space_before = text.rfind(' ', 0, max_len)
|
||||
if space_before >= 0:
|
||||
end = space_before
|
||||
else:
|
||||
space_after = text.rfind(' ', max_len)
|
||||
if space_after >= 0:
|
||||
end = space_after
|
||||
if end is None: # no spaces were found
|
||||
end = len(text)
|
||||
return text[:end].rstrip()
|
||||
|
||||
# end::CLIP_ANNOT[]
|
||||
10
15-more-types/clip_annot_signature.rst
Normal file
10
15-more-types/clip_annot_signature.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
>>> from clip_annot import clip
|
||||
>>> from inspect import signature
|
||||
>>> sig = signature(clip)
|
||||
>>> sig.return_annotation
|
||||
<class 'str'>
|
||||
>>> for param in sig.parameters.values():
|
||||
... note = repr(param.annotation).ljust(13)
|
||||
... print(note, ':', param.name, '=', param.default)
|
||||
<class 'str'> : text = <class 'inspect._empty'>
|
||||
'int > 0' : max_len = 80
|
||||
60
15-more-types/mymax/mymax.py
Normal file
60
15-more-types/mymax/mymax.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# tag::MYMAX_TYPES[]
|
||||
from typing import Protocol, Any, TypeVar, overload, Callable, Iterable, Union
|
||||
|
||||
class SupportsLessThan(Protocol):
|
||||
def __lt__(self, other: Any) -> bool: ...
|
||||
|
||||
T = TypeVar('T')
|
||||
LT = TypeVar('LT', bound=SupportsLessThan)
|
||||
DT = TypeVar('DT')
|
||||
|
||||
MISSING = object()
|
||||
EMPTY_MSG = 'max() arg is an empty sequence'
|
||||
|
||||
@overload
|
||||
def max(__arg1: LT, __arg2: LT, *_args: LT, key: None = ...) -> LT:
|
||||
...
|
||||
@overload
|
||||
def max(__arg1: T, __arg2: T, *_args: T, key: Callable[[T], LT]) -> T:
|
||||
...
|
||||
@overload
|
||||
def max(__iterable: Iterable[LT], *, key: None = ...) -> LT:
|
||||
...
|
||||
@overload
|
||||
def max(__iterable: Iterable[T], *, key: Callable[[T], LT]) -> T:
|
||||
...
|
||||
@overload
|
||||
def max(__iterable: Iterable[LT], *, key: None = ...,
|
||||
default: DT) -> Union[LT, DT]:
|
||||
...
|
||||
@overload
|
||||
def max(__iterable: Iterable[T], *, key: Callable[[T], LT],
|
||||
default: DT) -> Union[T, DT]:
|
||||
...
|
||||
# end::MYMAX_TYPES[]
|
||||
# tag::MYMAX[]
|
||||
def max(first, *args, key=None, default=MISSING):
|
||||
if args:
|
||||
series = args
|
||||
candidate = first
|
||||
else:
|
||||
series = iter(first)
|
||||
try:
|
||||
candidate = next(series)
|
||||
except StopIteration:
|
||||
if default is not MISSING:
|
||||
return default
|
||||
raise ValueError(EMPTY_MSG) from None
|
||||
if key is None:
|
||||
for current in series:
|
||||
if candidate < current:
|
||||
candidate = current
|
||||
else:
|
||||
candidate_key = key(candidate)
|
||||
for current in series:
|
||||
current_key = key(current)
|
||||
if candidate_key < current_key:
|
||||
candidate = current
|
||||
candidate_key = current_key
|
||||
return candidate
|
||||
# end::MYMAX[]
|
||||
128
15-more-types/mymax/mymax_demo.py
Normal file
128
15-more-types/mymax/mymax_demo.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
import mymax as my
|
||||
|
||||
def demo_args_list_float() -> None:
|
||||
args = [2.5, 3.5, 1.5]
|
||||
expected = 3.5
|
||||
result = my.max(*args)
|
||||
print(args, expected, result, sep='\n')
|
||||
assert result == expected
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(args)
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
|
||||
def demo_args_iter_int() -> None:
|
||||
args = [30, 10, 20]
|
||||
expected = 30
|
||||
result = my.max(args)
|
||||
print(args, expected, result, sep='\n')
|
||||
assert result == expected
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(args)
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
|
||||
|
||||
def demo_args_iter_str() -> None:
|
||||
args = iter('banana kiwi mango apple'.split())
|
||||
expected = 'mango'
|
||||
result = my.max(args)
|
||||
print(args, expected, result, sep='\n')
|
||||
assert result == expected
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(args)
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
|
||||
|
||||
def demo_args_iter_not_comparable_with_key() -> None:
|
||||
args = [object(), object(), object()]
|
||||
key = id
|
||||
expected = max(args, key=id)
|
||||
result = my.max(args, key=key)
|
||||
print(args, key, expected, result, sep='\n')
|
||||
assert result == expected
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(args)
|
||||
reveal_type(key)
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
|
||||
|
||||
def demo_empty_iterable_with_default() -> None:
|
||||
args: List[float] = []
|
||||
default = None
|
||||
expected = None
|
||||
result = my.max(args, default=default)
|
||||
print(args, default, expected, result, sep='\n')
|
||||
assert result == expected
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(args)
|
||||
reveal_type(default)
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
|
||||
|
||||
def demo_different_key_return_type() -> None:
|
||||
args = iter('banana kiwi mango apple'.split())
|
||||
key = len
|
||||
expected = 'banana'
|
||||
result = my.max(args, key=key)
|
||||
print(args, key, expected, result, sep='\n')
|
||||
assert result == expected
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(args)
|
||||
reveal_type(key)
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
|
||||
|
||||
def demo_different_key_none() -> None:
|
||||
args = iter('banana kiwi mango apple'.split())
|
||||
key = None
|
||||
expected = 'mango'
|
||||
result = my.max(args, key=key)
|
||||
print(args, key, expected, result, sep='\n')
|
||||
assert result == expected
|
||||
if TYPE_CHECKING:
|
||||
reveal_type(args)
|
||||
reveal_type(key)
|
||||
reveal_type(expected)
|
||||
reveal_type(result)
|
||||
|
||||
###################################### intentional type errors
|
||||
|
||||
def error_reported_bug() -> None:
|
||||
# example from https://github.com/python/typeshed/issues/4051
|
||||
top: Optional[int] = None
|
||||
try:
|
||||
my.max(5, top)
|
||||
except TypeError as exc:
|
||||
print(exc)
|
||||
|
||||
|
||||
def error_args_iter_not_comparable() -> None:
|
||||
try:
|
||||
my.max([None, None])
|
||||
except TypeError as exc:
|
||||
print(exc)
|
||||
|
||||
|
||||
def error_single_arg_not_iterable() -> None:
|
||||
try:
|
||||
my.max(1)
|
||||
except TypeError as exc:
|
||||
print(exc)
|
||||
|
||||
###################################### run demo and error functions
|
||||
|
||||
def main():
|
||||
for name, val in globals().items():
|
||||
if name.startswith('demo') or name.startswith('error'):
|
||||
print('_' * 20, name)
|
||||
val()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
69
15-more-types/mymax/mymax_test.py
Normal file
69
15-more-types/mymax/mymax_test.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from typing import List, Callable
|
||||
|
||||
import pytest # type: ignore
|
||||
|
||||
import mymax as my
|
||||
|
||||
@pytest.fixture
|
||||
def fruits():
|
||||
return 'banana kiwi mango apple'.split()
|
||||
|
||||
@pytest.mark.parametrize('args, expected', [
|
||||
([1, 3], 3),
|
||||
([3, 1], 3),
|
||||
([30, 10, 20], 30),
|
||||
])
|
||||
def test_max_args(args, expected):
|
||||
result = my.max(*args)
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('iterable, expected', [
|
||||
([7], 7),
|
||||
([1, 3], 3),
|
||||
([3, 1], 3),
|
||||
([30, 10, 20], 30),
|
||||
])
|
||||
def test_max_iterable(iterable, expected):
|
||||
result = my.max(iterable)
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_max_single_arg_not_iterable():
|
||||
msg = "'int' object is not iterable"
|
||||
with pytest.raises(TypeError) as exc:
|
||||
my.max(1)
|
||||
assert exc.value.args[0] == msg
|
||||
|
||||
|
||||
def test_max_empty_iterable_no_default():
|
||||
with pytest.raises(ValueError) as exc:
|
||||
my.max([])
|
||||
assert exc.value.args[0] == my.EMPTY_MSG
|
||||
|
||||
|
||||
@pytest.mark.parametrize('iterable, default, expected', [
|
||||
([7], -1, 7),
|
||||
([], -1, -1),
|
||||
([], None, None),
|
||||
])
|
||||
def test_max_empty_iterable_with_default(iterable, default, expected):
|
||||
result = my.max(iterable, default=default)
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key, expected', [
|
||||
(None, 'mango'),
|
||||
(lambda x: x, 'mango'),
|
||||
(len, 'banana'),
|
||||
(lambda s: -len(s), 'kiwi'),
|
||||
(lambda s: -ord(s[0]), 'apple'),
|
||||
(lambda s: ord(s[-1]), 'mango'),
|
||||
])
|
||||
def test_max_iterable_with_key(
|
||||
fruits: List[str],
|
||||
key: Callable[[str], str],
|
||||
expected: str
|
||||
) -> None:
|
||||
result = my.max(fruits, key=key)
|
||||
assert result == expected
|
||||
13
15-more-types/mysum.py
Normal file
13
15-more-types/mysum.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from functools import reduce # <1>
|
||||
from operator import add
|
||||
from typing import overload, Iterable, Union, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
S = TypeVar('S') # <2>
|
||||
|
||||
@overload
|
||||
def sum(it: Iterable[T]) -> Union[T, int]: ... # <3>
|
||||
@overload
|
||||
def sum(it: Iterable[T], /, start: S) -> Union[T, S]: ... # <4>
|
||||
def sum(it, /, start=0): # <5>
|
||||
return reduce(add, it, start)
|
||||
Reference in New Issue
Block a user