Skip to content

Commit 8c7e205

Browse files
authored
Merge pull request #1 from TheArtur128/development for 1.2.0 version
Code for 1.2.0 version
2 parents 2b9153d + c348c47 commit 8c7e205

File tree

4 files changed

+182
-37
lines changed

4 files changed

+182
-37
lines changed

README.md

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,75 @@
11
## Pyannotating
2-
Allows you to create similar annotations without copying them all the time.<br>
3-
It is advisable to place annotations created using this library in annotations.py file.
2+
Allows you to structure your annotations and put more information into them.<br>
3+
Templates created with this library should preferably be placed in the annotations.py file.
44

55
### Installation
66
`pip install pyannotating`
77

8-
### Examples
9-
Creating a factory of your annotations
8+
### Features
9+
You can create a template of your annotations
1010
```python
11-
from pyannotating import CustomAnnotationFactory, input_annotation
12-
from typing import Callable
11+
from pyannotating import *
12+
from typing import Callable, Any, Optional, Iterable
1313

14-
handler_of = CustomAnnotationFactory(Callable, [[input_annotation], any])
14+
handler_of = AnnotationTemplate(Callable, [[input_annotation], Any])
1515
```
16-
Now you can create an annotation by this factory
16+
and then create an annotation by this template
1717
```python
1818
handler_of[int | float]
1919
```
2020

21-
What is equivalent
21+
what is equivalent
2222
```python
23-
Callable[[int | float], any]
23+
Callable[[int | float], Any]
2424
```
2525

26-
Also you can use Union with input_annotation
26+
Also you can nest templates inside each other
2727
```python
28-
summator_of = CustomAnnotationFactory(Callable, [[input_annotation | int, input_annotation], int])
29-
summator_of[SomeCustomNumber]
28+
optional_handler_of = AnnotationTemplate(
29+
Callable,
30+
[[input_annotation], AnnotationTemplate(Optional, [input_annotation])]
31+
)
32+
33+
optional_handler_of[int]
34+
```
35+
36+
what results in
37+
```python
38+
Callable[[int], Optional[int]]
39+
```
40+
41+
and use input_annotation in conjunction with something else
42+
```python
43+
summator_of = AnnotationTemplate(Callable, [[input_annotation | int, input_annotation], int])
44+
summator_of[float]
3045
```
3146

32-
What results in
47+
to get
3348
```python
34-
Callable[[SomeCustomNumber | int, SomeCustomNumber], int]
49+
Callable[[float | int, float], int]
3550
```
3651

37-
In addition, you can also annotate something regardless of its type
52+
In addition, you can integrate comments with your annotations
3853
```python
3954
even = FormalAnnotation("Formal annotation of even numbers.")
4055

4156
number: even[int | float] = 42
4257
```
4358

44-
Full example
59+
or annotate downcasts
4560
```python
46-
def some_operation_by(
47-
handler: handler_of[int | float],
48-
number: even[float],
49-
*middleware_handlers: summator_of[SomeCustomNumber]
50-
) -> handler_of[int | float]:
61+
def transform(collection: Special[range, Iterable]) -> Any:
5162
...
5263
```
64+
65+
or just use some pre-made templates
66+
```python
67+
many_or_one[Callable]
68+
method_of[int]
69+
```
70+
71+
for getting
72+
```python
73+
Callable | Iterable[Callable]
74+
Callable[[int, ...], Any]
75+
```

pyannotating.py

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import ABC, abstractmethod
2-
from typing import Optional, Union, Iterable, Self, Mapping, Final, Callable, _UnionGenericAlias
2+
from typing import Optional, Any, Union, Iterable, Self, Mapping, Final, Callable, _UnionGenericAlias
33

44

55
class FormalAnnotation:
@@ -19,7 +19,7 @@ def __init__(self, instance_doc: Optional[str] = None):
1919
def __repr__(self) -> str:
2020
return f"<{self.__class__.__name__}>"
2121

22-
def __getitem__(self, resource: any) -> any:
22+
def __getitem__(self, resource: Any) -> Any:
2323
return resource
2424

2525

@@ -31,18 +31,18 @@ class AnnotationFactory(ABC):
3131
Can be used via [] (preferred) or by normal call.
3232
"""
3333

34-
def __call__(self, annotation: any) -> any:
34+
def __call__(self, annotation: Any) -> Any:
3535
return self._create_full_annotation_by(annotation)
3636

37-
def __getitem__(self, annotation: any) -> any:
37+
def __getitem__(self, annotation: Any) -> Any:
3838
return self._create_full_annotation_by(
3939
Union[annotation]
4040
if isinstance(annotation, Iterable)
4141
else annotation
4242
)
4343

4444
@abstractmethod
45-
def _create_full_annotation_by(self, annotation: any) -> any:
45+
def _create_full_annotation_by(self, annotation: Any) -> Any:
4646
"""Annotation Creation Method from an input annotation."""
4747

4848

@@ -65,20 +65,25 @@ def __new__(cls, *args, **kwargs):
6565
def __repr__(self) -> str:
6666
return '<input_annotation>'
6767

68-
def __or__(self, other: any) -> Union:
68+
def __or__(self, other: Any) -> Union:
6969
return Union[self, other]
7070

71-
def __ror__(self, other: any) -> Union:
71+
def __ror__(self, other: Any) -> Union:
7272
return Union[other, self]
7373

7474

75-
class CustomAnnotationFactory(AnnotationFactory):
75+
class AnnotationTemplate(AnnotationFactory):
7676
"""
7777
AnnotationFactory class delegating the construction of another factory's
7878
annotation.
7979
8080
When called, replaces the InputAnnotationAnnotation instances from its
8181
arguments and their subcollections with the input annotation.
82+
83+
Templateizes Union.
84+
85+
Delegates responsibilities to other templates when passing them as
86+
annotations.
8287
"""
8388

8489
def __init__(self, original_factory: Mapping, annotations: Iterable):
@@ -103,12 +108,16 @@ def __repr__(self) -> str:
103108
arguments=str(self.__recursively_format(self._annotations)).replace('\'', str())
104109
)
105110

106-
def _create_full_annotation_by(self, annotation: any) -> any:
111+
def _create_full_annotation_by(self, annotation: Any) -> Any:
112+
formatted_annotations = self.__get_formatted_annotations_from(self._annotations, annotation)
113+
107114
return self._original_factory[
108-
*self.__get_formatted_annotations_from(self._annotations, annotation)
115+
formatted_annotations[0]
116+
if len(formatted_annotations) == 1
117+
else formatted_annotations
109118
]
110119

111-
def __get_formatted_annotations_from(self, annotations: Iterable, replacement_annotation: any) -> tuple:
120+
def __get_formatted_annotations_from(self, annotations: Iterable, replacement_annotation: Any) -> tuple:
112121
"""
113122
Recursive function to replace element(s) of the input collection (and
114123
its subcollections) equal to the annotation anonation with the input
@@ -120,11 +129,16 @@ def __get_formatted_annotations_from(self, annotations: Iterable, replacement_an
120129
for annotation in annotations:
121130
if isinstance(annotation, InputAnnotationAnnotation):
122131
annotation = replacement_annotation
132+
123133
elif isinstance(annotation, Iterable) and not isinstance(annotation, str):
124134
annotation = self.__get_formatted_annotations_from(
125135
annotation,
126136
replacement_annotation
127137
)
138+
139+
elif isinstance(annotation, AnnotationTemplate):
140+
annotation = annotation[replacement_annotation]
141+
128142
elif type(annotation) in (Union, _UnionGenericAlias, type(int | float)):
129143
annotation = Union[
130144
*self.__get_formatted_annotations_from(
@@ -156,7 +170,43 @@ def __recursively_format(self, collection: Iterable) -> list:
156170
return formatted_collection
157171

158172

173+
class Special:
174+
"""
175+
Annotation class for formally specifying specific behavior for subclasses.
176+
177+
Returns the second input annotation, or Any if none.
178+
179+
Specifies additional behavior for the first annotation.
180+
181+
Implies use like Special[type_for_special_behavior, generic_type] or
182+
Special[type_for_special_behavior].
183+
"""
184+
185+
def __class_getitem__(cls, annotation_resource: tuple[Any, Any] | Any) -> Any:
186+
if not isinstance(annotation_resource, Iterable):
187+
return Any
188+
189+
elif len(annotation_resource) != 2:
190+
raise TypeError(
191+
"Special must be used as Special[type_for_special_behavior, generic_type] or Special[type_for_special_behavior]"
192+
)
193+
194+
return annotation_resource[1]
195+
196+
197+
number: Final = int | float | complex
198+
159199
# Pre-created instance without permanent formal creation of a new one.
160200
input_annotation: Final[InputAnnotationAnnotation] = InputAnnotationAnnotation()
161201

162-
number: Final = int | float | complex
202+
203+
many_or_one: Final[AnnotationTemplate] = AnnotationTemplate(
204+
Union,
205+
[input_annotation, AnnotationTemplate(Iterable, [input_annotation])]
206+
)
207+
208+
209+
method_of: Final[AnnotationTemplate] = AnnotationTemplate(
210+
Callable,
211+
[[input_annotation, ...], Any]
212+
)

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
PACKAGE_NAME = 'pyannotating'
55

6-
VERSION = '1.1.0'
6+
VERSION = '1.2.0'
77

88
with open('README.md') as readme_file:
99
LONG_DESCRIPTION = readme_file.read()
1010

1111
setup(
1212
name=PACKAGE_NAME,
13-
description="Library for convenient generation of annotations for your code",
13+
description="Library to structure annotations in your code",
1414
long_description=LONG_DESCRIPTION,
1515
long_description_content_type="text/markdown",
1616
license_files = ('LICENSE',),
@@ -22,6 +22,6 @@
2222
author_email="s9339307190@gmail.com",
2323
python_requires='>=3.11',
2424
classifiers=["Programming Language :: Python :: 3.11"],
25-
keywords=['library', 'annotations' 'generation'],
25+
keywords=['library', 'annotations', 'generation', 'templating', 'annotations', 'informing'],
2626
py_modules=[PACKAGE_NAME]
2727
)

tests.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from typing import Any, Union, Optional, Callable, Iterable, Sized
2+
3+
from pytest import mark
4+
5+
from pyannotating import FormalAnnotation, InputAnnotationAnnotation, AnnotationTemplate, input_annotation, Special
6+
7+
8+
@mark.parametrize(
9+
"input_resource, result",
10+
[(42, 42), (Any, Any), (int | float, int | float)]
11+
)
12+
def test_formal_annotation(input_resource: Any, result: Any):
13+
assert FormalAnnotation()[input_resource] == result
14+
15+
16+
@mark.parametrize(
17+
'doc',
18+
["Documentation", "\n\t.!#@$2*-_\n", b"\tHello\n\tWorld\n\t!", str()]
19+
)
20+
def test_formal_annotation_documenting(doc: str):
21+
assert FormalAnnotation(doc).__doc__ == doc
22+
23+
24+
@mark.parametrize('number_of_creation', [8])
25+
def test_input_annotation_annotation_loneliness(number_of_creation: int):
26+
annotations = tuple(InputAnnotationAnnotation() for _ in range(number_of_creation))
27+
28+
for first_annotation_index in range(number_of_creation):
29+
for second_annotation in annotations[first_annotation_index + 1:]:
30+
assert annotations[first_annotation_index] is second_annotation
31+
32+
33+
@mark.parametrize(
34+
"type_to_group",
35+
[int, float, int | float, Union[set, frozenset], Optional[tuple], InputAnnotationAnnotation]
36+
)
37+
def test_input_annotation_annotation_grouping(type_to_group: type):
38+
annotation = InputAnnotationAnnotation()
39+
40+
assert annotation | type_to_group == Union[annotation, type_to_group]
41+
assert type_to_group | annotation == Union[type_to_group | annotation]
42+
43+
44+
@mark.parametrize(
45+
"annotation_template, input_resource, result",
46+
[
47+
(AnnotationTemplate(Optional, [input_annotation]), int, Optional[int]),
48+
(
49+
AnnotationTemplate(Callable, [[input_annotation | int, input_annotation], Any]),
50+
float,
51+
Callable[[float | int, float], Any]
52+
),
53+
(
54+
AnnotationTemplate(
55+
Callable,
56+
[[input_annotation], AnnotationTemplate(Optional, [input_annotation])]
57+
),
58+
str,
59+
Callable[[str], Optional[str]]
60+
),
61+
]
62+
)
63+
def test_annotation_template(annotation_template: AnnotationTemplate, input_resource: Any, result: Any):
64+
assert annotation_template[input_resource] == result
65+
66+
67+
@mark.parametrize(
68+
"input_resource, result",
69+
[(int, Any), ((tuple | list, Iterable), Iterable), ((str, Sized), Sized)]
70+
)
71+
def test_special(input_resource: Any, result: Any):
72+
assert Special[input_resource] == result

0 commit comments

Comments
 (0)