tanka 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tanka-0.0.1/.claude/rules/eo.md +387 -0
- tanka-0.0.1/.claude/rules/testing.md +136 -0
- tanka-0.0.1/.claude/rules/workflow.md +31 -0
- tanka-0.0.1/.claude/settings.json +5 -0
- tanka-0.0.1/.claude/skills/test-runner/SKILL.md +56 -0
- tanka-0.0.1/.flake8 +4 -0
- tanka-0.0.1/.github/workflows/linters.yml +25 -0
- tanka-0.0.1/.github/workflows/release.yml +32 -0
- tanka-0.0.1/.github/workflows/tests.yml +27 -0
- tanka-0.0.1/.gitignore +226 -0
- tanka-0.0.1/LICENSE +21 -0
- tanka-0.0.1/Makefile +31 -0
- tanka-0.0.1/PKG-INFO +816 -0
- tanka-0.0.1/README.md +771 -0
- tanka-0.0.1/pyproject.toml +84 -0
- tanka-0.0.1/src/tanka/__init__.py +205 -0
- tanka-0.0.1/src/tanka/abort.py +35 -0
- tanka-0.0.1/src/tanka/application.py +101 -0
- tanka-0.0.1/src/tanka/asgi.py +133 -0
- tanka-0.0.1/src/tanka/auth.py +83 -0
- tanka-0.0.1/src/tanka/body.py +158 -0
- tanka-0.0.1/src/tanka/catch.py +117 -0
- tanka-0.0.1/src/tanka/cookies.py +97 -0
- tanka-0.0.1/src/tanka/cors.py +126 -0
- tanka-0.0.1/src/tanka/endpoint.py +10 -0
- tanka-0.0.1/src/tanka/flash.py +96 -0
- tanka-0.0.1/src/tanka/headers.py +37 -0
- tanka-0.0.1/src/tanka/identity.py +31 -0
- tanka-0.0.1/src/tanka/method.py +58 -0
- tanka-0.0.1/src/tanka/openapi.py +212 -0
- tanka-0.0.1/src/tanka/request.py +74 -0
- tanka-0.0.1/src/tanka/response.py +99 -0
- tanka-0.0.1/src/tanka/routes.py +90 -0
- tanka-0.0.1/src/tanka/server.py +96 -0
- tanka-0.0.1/src/tanka/static.py +40 -0
- tanka-0.0.1/src/tanka/target.py +77 -0
- tanka-0.0.1/tests/fakes.py +62 -0
- tanka-0.0.1/tests/test_abort.py +35 -0
- tanka-0.0.1/tests/test_aborted.py +60 -0
- tanka-0.0.1/tests/test_alert.py +11 -0
- tanka-0.0.1/tests/test_all_of.py +20 -0
- tanka-0.0.1/tests/test_allow_credentials.py +17 -0
- tanka-0.0.1/tests/test_allow_headers.py +46 -0
- tanka-0.0.1/tests/test_allow_methods.py +46 -0
- tanka-0.0.1/tests/test_allow_origins.py +76 -0
- tanka-0.0.1/tests/test_anonymous.py +19 -0
- tanka-0.0.1/tests/test_any_of.py +20 -0
- tanka-0.0.1/tests/test_asgi.py +156 -0
- tanka-0.0.1/tests/test_asgi_body.py +80 -0
- tanka-0.0.1/tests/test_authenticated.py +46 -0
- tanka-0.0.1/tests/test_authorized.py +57 -0
- tanka-0.0.1/tests/test_catch.py +119 -0
- tanka-0.0.1/tests/test_code.py +19 -0
- tanka-0.0.1/tests/test_cookie.py +19 -0
- tanka-0.0.1/tests/test_cookie_path.py +11 -0
- tanka-0.0.1/tests/test_cookies.py +35 -0
- tanka-0.0.1/tests/test_core_request.py +92 -0
- tanka-0.0.1/tests/test_core_response.py +53 -0
- tanka-0.0.1/tests/test_cors.py +118 -0
- tanka-0.0.1/tests/test_delete.py +11 -0
- tanka-0.0.1/tests/test_directory.py +57 -0
- tanka-0.0.1/tests/test_document.py +42 -0
- tanka-0.0.1/tests/test_domain.py +11 -0
- tanka-0.0.1/tests/test_empty.py +19 -0
- tanka-0.0.1/tests/test_error_page.py +28 -0
- tanka-0.0.1/tests/test_every.py +14 -0
- tanka-0.0.1/tests/test_expose_headers.py +39 -0
- tanka-0.0.1/tests/test_failsafe.py +47 -0
- tanka-0.0.1/tests/test_failure.py +11 -0
- tanka-0.0.1/tests/test_file.py +74 -0
- tanka-0.0.1/tests/test_flash.py +19 -0
- tanka-0.0.1/tests/test_flashes.py +52 -0
- tanka-0.0.1/tests/test_forget_cookie.py +19 -0
- tanka-0.0.1/tests/test_get.py +11 -0
- tanka-0.0.1/tests/test_head.py +11 -0
- tanka-0.0.1/tests/test_headers.py +76 -0
- tanka-0.0.1/tests/test_html.py +19 -0
- tanka-0.0.1/tests/test_http_only.py +11 -0
- tanka-0.0.1/tests/test_hypercorn.py +50 -0
- tanka-0.0.1/tests/test_indifferent.py +23 -0
- tanka-0.0.1/tests/test_json.py +21 -0
- tanka-0.0.1/tests/test_lifetime.py +11 -0
- tanka-0.0.1/tests/test_listed.py +19 -0
- tanka-0.0.1/tests/test_logging.py +15 -0
- tanka-0.0.1/tests/test_max_age.py +37 -0
- tanka-0.0.1/tests/test_methods.py +19 -0
- tanka-0.0.1/tests/test_mount.py +58 -0
- tanka-0.0.1/tests/test_named.py +11 -0
- tanka-0.0.1/tests/test_notice.py +11 -0
- tanka-0.0.1/tests/test_on.py +59 -0
- tanka-0.0.1/tests/test_once.py +17 -0
- tanka-0.0.1/tests/test_open_api.py +281 -0
- tanka-0.0.1/tests/test_options.py +11 -0
- tanka-0.0.1/tests/test_package.py +19 -0
- tanka-0.0.1/tests/test_patch.py +11 -0
- tanka-0.0.1/tests/test_path.py +45 -0
- tanka-0.0.1/tests/test_post.py +11 -0
- tanka-0.0.1/tests/test_preflight.py +62 -0
- tanka-0.0.1/tests/test_principal.py +19 -0
- tanka-0.0.1/tests/test_put.py +11 -0
- tanka-0.0.1/tests/test_query.py +43 -0
- tanka-0.0.1/tests/test_range.py +22 -0
- tanka-0.0.1/tests/test_raw.py +27 -0
- tanka-0.0.1/tests/test_redirect.py +35 -0
- tanka-0.0.1/tests/test_reload.py +27 -0
- tanka-0.0.1/tests/test_request.py +110 -0
- tanka-0.0.1/tests/test_response.py +55 -0
- tanka-0.0.1/tests/test_role.py +28 -0
- tanka-0.0.1/tests/test_roles.py +28 -0
- tanka-0.0.1/tests/test_route.py +132 -0
- tanka-0.0.1/tests/test_routes.py +68 -0
- tanka-0.0.1/tests/test_same_site.py +11 -0
- tanka-0.0.1/tests/test_secure.py +11 -0
- tanka-0.0.1/tests/test_silence.py +11 -0
- tanka-0.0.1/tests/test_smart.py +53 -0
- tanka-0.0.1/tests/test_static.py +66 -0
- tanka-0.0.1/tests/test_status.py +29 -0
- tanka-0.0.1/tests/test_stream.py +33 -0
- tanka-0.0.1/tests/test_success.py +11 -0
- tanka-0.0.1/tests/test_tanka.py +99 -0
- tanka-0.0.1/tests/test_target.py +35 -0
- tanka-0.0.1/tests/test_text.py +27 -0
- tanka-0.0.1/tests/test_uvicorn.py +50 -0
- tanka-0.0.1/tests/test_verb.py +11 -0
- tanka-0.0.1/tests/test_with_cookie.py +46 -0
- tanka-0.0.1/tests/test_with_flash.py +46 -0
- tanka-0.0.1/tests/test_with_headers.py +36 -0
- tanka-0.0.1/uv.lock +1625 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Elegant Objects Coding Rules
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
## 1. General
|
|
5
|
+
### 1.1 Language
|
|
6
|
+
- Always use python type hints.
|
|
7
|
+
---
|
|
8
|
+
## 2. Naming
|
|
9
|
+
### 2.1 Class Names
|
|
10
|
+
- Never use class names ending in -er, -or, or -Utils (exceptions: User, Computer).
|
|
11
|
+
- Naming a class based on what its objects do is incorrect.
|
|
12
|
+
- Classes should be named based on what they are, not what they do.
|
|
13
|
+
- Think about what the objects will encapsulate and choose a name that represents that conceptual group.
|
|
14
|
+
### 2.2 Method Names
|
|
15
|
+
- A builder is a method that constructs or returns something.
|
|
16
|
+
- A builder never returns None, and its name must always be a noun (optionally with an adjective).
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
def pow(self, base: int, power: int) -> int:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def speed(self) -> float:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def employee(self, id: int) -> Employee:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parsed_cell(self, x: int, y: int) -> str:
|
|
32
|
+
pass
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- A manipulator is a method that modifies the real-world entity represented by an object.
|
|
36
|
+
- A manipulator always returns None (implicitly), and its name must always be a verb (optionally with an adverb).
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
def save(self, content: str):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def put(self, key: str, value: float):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def remove(self, emp: Employee):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def quickly_print(self, id: int):
|
|
52
|
+
pass
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- If a builder returns a Boolean value, its name should be an adjective.
|
|
56
|
+
- The prefix is_ is redundant and should not be used, but temporarily adding it can help ensure the name sounds correct.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
def empty(self) -> bool:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def readable(self) -> bool:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def negative(self) -> bool:
|
|
68
|
+
pass
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- equals should be renamed to equal_to.
|
|
72
|
+
- exists should be renamed to present.
|
|
73
|
+
### 2.3 Variable Names
|
|
74
|
+
- Do not use compound names anywhere in the code.
|
|
75
|
+
- Every variable must have a name consisting of a single noun.
|
|
76
|
+
- Exceptions are allowed only when a single noun would lose its meaning without an adjective, for example: time_zone, side_effect, MicroService, ChangingRoom, washing_machine, bus_stop, laughing_ga.
|
|
77
|
+
---
|
|
78
|
+
## 3. Constructors
|
|
79
|
+
### 3.1 Constructor Design
|
|
80
|
+
- Prefer having one primary constructor.
|
|
81
|
+
- The more constructors a class has, the more flexibility it gives to clients using the class.
|
|
82
|
+
- Keep constructors code-free: they should only contain assignment statements.
|
|
83
|
+
- Allow only assignments in one primary constructor, delegating from secondaries.
|
|
84
|
+
- Use plum-dispatch python library for method overloading.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from plum import dispatch
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Cash:
|
|
91
|
+
@dispatch
|
|
92
|
+
def __init__(self, value: float):
|
|
93
|
+
self.__init__(int(value))
|
|
94
|
+
|
|
95
|
+
@dispatch
|
|
96
|
+
def __init__(self, value: str):
|
|
97
|
+
self.__init__(int(value))
|
|
98
|
+
|
|
99
|
+
@dispatch
|
|
100
|
+
def __init__(self, value: int):
|
|
101
|
+
self.dollars = value
|
|
102
|
+
```
|
|
103
|
+
---
|
|
104
|
+
## 4. Objects and Encapsulation
|
|
105
|
+
### 4.1 Attributes
|
|
106
|
+
- All encapsulated objects (attributes) are part of an object's identity.
|
|
107
|
+
- An object should encapsulate four or fewer attributes.
|
|
108
|
+
- If more attributes are required, they should be grouped into other objects.
|
|
109
|
+
- Together, these objects should form a structured tree of objects.
|
|
110
|
+
- An object that encapsulates nothing should not exist.
|
|
111
|
+
- Never create getters and setters.
|
|
112
|
+
- Never add an attribute or method to an object at runtime.
|
|
113
|
+
---
|
|
114
|
+
## 5. Interfaces and Methods
|
|
115
|
+
### 5.1 Interfaces
|
|
116
|
+
- Always use interfaces.
|
|
117
|
+
- Ensure that every public method in a class implements an interface.
|
|
118
|
+
- Keep interfaces minimal.
|
|
119
|
+
- Add convenience behavior through smart wrappers.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
# Instead of:
|
|
123
|
+
class Exchange(ABC):
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def rate(
|
|
126
|
+
self,
|
|
127
|
+
target: str,
|
|
128
|
+
source: str = "USD",
|
|
129
|
+
) -> float:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Use:
|
|
134
|
+
class Exchange(ABC):
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def rate(
|
|
137
|
+
self,
|
|
138
|
+
target: str,
|
|
139
|
+
source: str,
|
|
140
|
+
) -> float:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
class Smart:
|
|
144
|
+
def __init__(self, e: "Exchange"):
|
|
145
|
+
self.origin = e
|
|
146
|
+
|
|
147
|
+
def to_usd(
|
|
148
|
+
self,
|
|
149
|
+
source: str,
|
|
150
|
+
) -> float:
|
|
151
|
+
return self.origin.rate(
|
|
152
|
+
source,
|
|
153
|
+
"USD",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Usage:
|
|
158
|
+
rate = Exchange.Smart(NYSE()).to_usd("EUR")
|
|
159
|
+
```
|
|
160
|
+
### 5.2 Methods
|
|
161
|
+
- The more methods a class exposes, the harder it becomes to use correctly.
|
|
162
|
+
- The number of public methods in a class should be five or fewer.
|
|
163
|
+
- Never create static methods.
|
|
164
|
+
- Long methods are forbidden.
|
|
165
|
+
- Methods with multiple return statements are forbidden.
|
|
166
|
+
---
|
|
167
|
+
## 6. Immutability
|
|
168
|
+
### 6.1 Immutable Objects
|
|
169
|
+
- All classes should be immutable.
|
|
170
|
+
- If modification is needed, create a new object instead.
|
|
171
|
+
---
|
|
172
|
+
## 7. Testing
|
|
173
|
+
### 7.1 Unit Tests
|
|
174
|
+
- Write unit tests instead of docstrings.
|
|
175
|
+
- Do not use mocks.
|
|
176
|
+
- Instead mocks, create fake classes that implement interfaces and use them in unit tests.
|
|
177
|
+
---
|
|
178
|
+
## 8. Constants
|
|
179
|
+
### 8.1 Avoiding Constants
|
|
180
|
+
- Do not use public constants.
|
|
181
|
+
- Instead, create a class that encapsulates the semantic meaning of the constant.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# Wrong
|
|
185
|
+
class Constants:
|
|
186
|
+
EOF = "\n"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Correct
|
|
190
|
+
class EOLString:
|
|
191
|
+
def __init__(self, src: str):
|
|
192
|
+
self.origin = src
|
|
193
|
+
|
|
194
|
+
def __str__(self) -> str:
|
|
195
|
+
return self.origin + "\n"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- Stay away from class-level constants.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
# Bad
|
|
202
|
+
class Book:
|
|
203
|
+
BOOK_NOT_FOUND = "book not found"
|
|
204
|
+
|
|
205
|
+
def print(self):
|
|
206
|
+
if {book not found}:
|
|
207
|
+
raise Exception(
|
|
208
|
+
self.BOOK_NOT_FOUND
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def sell(self):
|
|
212
|
+
if {book not found}:
|
|
213
|
+
raise Exception(
|
|
214
|
+
self.BOOK_NOT_FOUND
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Good 1
|
|
218
|
+
class Book:
|
|
219
|
+
def print(self):
|
|
220
|
+
if {book not found}:
|
|
221
|
+
raise Exception(
|
|
222
|
+
"book not found, can’t print it"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def sell(self):
|
|
226
|
+
if {book not found}:
|
|
227
|
+
raise Exception(
|
|
228
|
+
"book not found, can’t sell it"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Good 2
|
|
232
|
+
class Book:
|
|
233
|
+
def print(self):
|
|
234
|
+
x = self._find()
|
|
235
|
+
|
|
236
|
+
def sell(self):
|
|
237
|
+
x = self._find()
|
|
238
|
+
|
|
239
|
+
def _find(self) -> X:
|
|
240
|
+
if {book not found}:
|
|
241
|
+
raise Exception(
|
|
242
|
+
"book not found"
|
|
243
|
+
)
|
|
244
|
+
return "the book found"
|
|
245
|
+
```
|
|
246
|
+
---
|
|
247
|
+
## 9. Exceptions and Fail-Fast
|
|
248
|
+
### 9.1 Fail-Fast
|
|
249
|
+
- Always follow the fail-fast approach.
|
|
250
|
+
- Throw an exception in any suspicious situation.
|
|
251
|
+
- When something is not found, do one of the following: throw an exception; return a collection (possibly empty); return a Null Object.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
class NullUser(User):
|
|
255
|
+
def __init__(self, name: str):
|
|
256
|
+
self.label = name
|
|
257
|
+
|
|
258
|
+
def name(self) -> str:
|
|
259
|
+
return self.label
|
|
260
|
+
|
|
261
|
+
def boost(self, salary: Cash):
|
|
262
|
+
raise IllegalStateException("You can’t raise my salary")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- Never accept None as an argument.
|
|
266
|
+
- Instead of None, create a Null Object implementation.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
class Mask(ABC):
|
|
270
|
+
@abstractmethod
|
|
271
|
+
def matches(self, f: File) -> bool:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class AnyFile(Mask):
|
|
276
|
+
def matches(self, f: File) -> bool:
|
|
277
|
+
return True
|
|
278
|
+
```
|
|
279
|
+
### 9.2 Exception Handling
|
|
280
|
+
- Every except statement must have a very strong reason to exist.
|
|
281
|
+
- Always Chain Exceptions.
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
def length(self, f: File) -> int:
|
|
285
|
+
try:
|
|
286
|
+
return os.path.getsize(PathOf(f))
|
|
287
|
+
except FileNotFoundError as e:
|
|
288
|
+
raise Exception("Can’t calculate file length.") from e
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- Recover only once, at the highest level (the application entry point).
|
|
292
|
+
- All raised exceptions should be of the same type (Exception).
|
|
293
|
+
### 9.3 Validation
|
|
294
|
+
- Input validation and assertions should be implemented through decorators (wrappers), not inside core logic.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
class Day(ABC):
|
|
298
|
+
@abstractmethod
|
|
299
|
+
def distance_to(self, end: "Day") -> int:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class JdkDay(Day):
|
|
304
|
+
def __init__(self, d: Date):
|
|
305
|
+
self.date = d
|
|
306
|
+
|
|
307
|
+
def distance_to(self, end: "Day") -> int:
|
|
308
|
+
# assertion validation happens externally
|
|
309
|
+
return LongAsInteger(end.date - self.date)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Input validation decorator
|
|
313
|
+
class StrictDay(Day):
|
|
314
|
+
def __init__(self, d: Day):
|
|
315
|
+
self.origin = d
|
|
316
|
+
|
|
317
|
+
def distance_to(self, end: Day) -> int:
|
|
318
|
+
if end.compare_to(self) < 0:
|
|
319
|
+
raise Exception(f"Start {self} must be earlier than end {end}")
|
|
320
|
+
return self.origin.distance_to(end)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Usage
|
|
324
|
+
day = StrictDay(JdkDay(Date()))
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- Use Aspect-Oriented Programming to move supplementary mechanisms (like retries, logging, etc.) outside of core classes.
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from tenacity import retry, stop_after_attempt
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class WebPage:
|
|
334
|
+
@retry(stop=stop_after_attempt(3))
|
|
335
|
+
def content(self) -> str:
|
|
336
|
+
# fetch web page content
|
|
337
|
+
pass
|
|
338
|
+
```
|
|
339
|
+
---
|
|
340
|
+
## 10. Composition over Inheritance
|
|
341
|
+
### 10.1 Class Structure
|
|
342
|
+
- A class should be final (all methods implemented) or abstract (no methods implemented, interface only).
|
|
343
|
+
- There is no place for inheritance.
|
|
344
|
+
### 10.2 Composition and Decorators
|
|
345
|
+
- Prefer composable decorators to build behavior step by step.
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
names = Sorted(
|
|
349
|
+
Unique(
|
|
350
|
+
Capitalized(
|
|
351
|
+
FileNames(
|
|
352
|
+
Directory(
|
|
353
|
+
"/var/users/*.xml",
|
|
354
|
+
),
|
|
355
|
+
"([^.]+)\\.xml",
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
- Whenever you need to add new functionality to an existing class, create a new class or decorate an existing one.
|
|
363
|
+
- Modifying the original class should always be the last resort.
|
|
364
|
+
- Never use dependency injection.
|
|
365
|
+
- Use decorators (wrappers) for access control.
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
car = SecureCar(Car("Mercedes-Benz SL63"), "admin")
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- Use object composition instead of the MVC pattern.
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
# Wrong
|
|
375
|
+
class Controller:
|
|
376
|
+
def index(self) -> str:
|
|
377
|
+
title = Model().title
|
|
378
|
+
view = View()
|
|
379
|
+
view.title = title
|
|
380
|
+
return view.render_html()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# Good
|
|
384
|
+
Application(HttpGetBook(HtmlBook(MySQLBook("Elegant Objects")))).run()
|
|
385
|
+
```
|
|
386
|
+
### 10.3 Forbidden Patterns
|
|
387
|
+
- Never use the Singleton pattern.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Testing Rules
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 1. Test Design
|
|
6
|
+
|
|
7
|
+
### 1.1 Logic-Free Tests
|
|
8
|
+
- Unit tests must be logic-free: they must not implement any algorithms.
|
|
9
|
+
- Aim for one-statement tests.
|
|
10
|
+
|
|
11
|
+
### 1.2 Assertions
|
|
12
|
+
- Each test must contain exactly one assertion.
|
|
13
|
+
- Each test must contain at least one assertion.
|
|
14
|
+
- The last statement in every test must be an assertion.
|
|
15
|
+
- Do not assert on the absence of failure.
|
|
16
|
+
- Never assert on side effects.
|
|
17
|
+
- Be creative with assertions.
|
|
18
|
+
- Use the PyHamcrest library to achieve this.
|
|
19
|
+
|
|
20
|
+
### 1.3 Test Inputs
|
|
21
|
+
- Whenever possible, tests should use irregular, boundary, or strange inputs to provoke hidden issues.
|
|
22
|
+
- Every test must use its own literals, numbers, and input values.
|
|
23
|
+
- Do not reuse inputs across tests, as this may falsely suggest shared meaning.
|
|
24
|
+
- Always write descriptive failure messages.
|
|
25
|
+
|
|
26
|
+
### 1.4 Test Isolation
|
|
27
|
+
- Test methods must share absolutely no fixtures with other test methods.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Naming and Documentation
|
|
32
|
+
|
|
33
|
+
### 2.1 Test Naming
|
|
34
|
+
- Test names must be complete English phrases that would start with "it" if written fully. Example: (it) builds_empty_png_image
|
|
35
|
+
- "it" refers to the object under test, not the test itself.
|
|
36
|
+
|
|
37
|
+
### 2.2 Documentation
|
|
38
|
+
- Tests must not contain documentation comments.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Scope and Boundaries
|
|
43
|
+
|
|
44
|
+
### 3.1 What to Test
|
|
45
|
+
- Never write tests for private or protected methods.
|
|
46
|
+
- Avoid overtesting.
|
|
47
|
+
- Tests must never be the reason for expanding an object's public interface.
|
|
48
|
+
- Avoid testing abstract classes directly.
|
|
49
|
+
|
|
50
|
+
### 3.2 Feature Class Correspondence
|
|
51
|
+
- Each feature class must have exactly one corresponding test module.
|
|
52
|
+
- Feature classes should trust each other and only guard against invalid end-user input.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 4. Resources and Cleanup
|
|
57
|
+
|
|
58
|
+
### 4.1 System Resources
|
|
59
|
+
- Tests must close all system resources when they finish.
|
|
60
|
+
- Tests that require resources must begin with cleanup.
|
|
61
|
+
|
|
62
|
+
### 4.2 File Handling
|
|
63
|
+
- If a test works with files, it must create its own temporary directory and store all files there.
|
|
64
|
+
- Do not mock the file system.
|
|
65
|
+
- Place all temporary files inside a top-level tmp/ directory in the repository.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 5. Fakes and Logging
|
|
70
|
+
|
|
71
|
+
### 5.1 Fakes Over Mocks
|
|
72
|
+
- Do not use mocks. Use fake implementations instead.
|
|
73
|
+
|
|
74
|
+
### 5.2 Logging
|
|
75
|
+
- Logging should be disabled during tests whenever possible.
|
|
76
|
+
- If logging is part of the specification, test it using fake classes that capture messages instead of printing them.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 6. Test Patterns
|
|
81
|
+
|
|
82
|
+
### 6.1 Parametrized Tests
|
|
83
|
+
- If multiple inputs share the same scenario, use parametrized tests.
|
|
84
|
+
|
|
85
|
+
### 6.2 Thread Safety
|
|
86
|
+
- If a class uses threads or claims thread safety, there must be tests confirming its thread safety.
|
|
87
|
+
|
|
88
|
+
### 6.3 Flaky Tests
|
|
89
|
+
- Use pytest-repeat to run flaky tests multiple times when needed.
|
|
90
|
+
|
|
91
|
+
### 6.4 Online Tests
|
|
92
|
+
- Mark all tests requiring internet access with: pytest.mark.online
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 7. Test-Driven Development
|
|
97
|
+
|
|
98
|
+
### 7.1 Bug Fixes
|
|
99
|
+
- Before fixing a bug, reproduce it with a failing test case.
|
|
100
|
+
|
|
101
|
+
### 7.2 New Features
|
|
102
|
+
- Before adding a new feature, there must be a failing test first.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 8. Test Execution
|
|
107
|
+
|
|
108
|
+
### 8.1 Performance
|
|
109
|
+
- Use pytest-fail-slow to automatically fail tests that run too slowly.
|
|
110
|
+
|
|
111
|
+
### 8.2 Randomization
|
|
112
|
+
- Execute tests in random order and print the randomization seed to the console.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 9. Test Categories
|
|
117
|
+
|
|
118
|
+
### 9.1 Fast Tests (Unit)
|
|
119
|
+
- Minimal execution time.
|
|
120
|
+
|
|
121
|
+
### 9.2 Deep Tests (Integration)
|
|
122
|
+
- Realistic live scenarios.
|
|
123
|
+
- If deep tests require external dependencies (databases, remote services, etc.), use Testcontainers.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 10. Quality Tools
|
|
128
|
+
|
|
129
|
+
### 10.1 Grammar Checking
|
|
130
|
+
- Use language-tool-python to check English grammar in string literals and exception messages.
|
|
131
|
+
|
|
132
|
+
### 10.2 Property-Based Testing
|
|
133
|
+
- Use Python Hypothesis for property-based testing.
|
|
134
|
+
|
|
135
|
+
### 10.3 Mutation Testing
|
|
136
|
+
- Use mutmut for mutation testing.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Be Quality Dogmatic
|
|
2
|
+
|
|
3
|
+
Keep chat replies and your answers short.
|
|
4
|
+
|
|
5
|
+
## Discipline
|
|
6
|
+
|
|
7
|
+
Write concise, direct, human prose.
|
|
8
|
+
Push back on technical mistakes, deferring to users on vision and architecture.
|
|
9
|
+
Stay on scope, refactoring only what tasks require.
|
|
10
|
+
Design top-down, whole before parts, composition before ingredients.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
Pull from Git before changes, push back only with explicit user permission.
|
|
15
|
+
Name each branch after its GitHub issue integer, asking when unsure.
|
|
16
|
+
Always practice TDD (test-driven development), reproducing bugs before fixing.
|
|
17
|
+
Invest in unit tests until bugs reproduce, trusting intuition over debugging.
|
|
18
|
+
Log extensively when problems stay hard.
|
|
19
|
+
|
|
20
|
+
## Changes
|
|
21
|
+
|
|
22
|
+
Keep changes focused and minimal.
|
|
23
|
+
Flag smells and refactoring, suggesting issues rather than fixing silently.
|
|
24
|
+
Fix style violations in code, trusting checkers rather than suppressing them.
|
|
25
|
+
|
|
26
|
+
## Runtime
|
|
27
|
+
|
|
28
|
+
Disable logging in tests.
|
|
29
|
+
Bound every wait with timeouts.
|
|
30
|
+
Test concurrency, retrying flaky blocks.
|
|
31
|
+
Assume no Internet, using ephemeral ports.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "test-runner"
|
|
3
|
+
description: "Use this skill when application code has been modified (*.py)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test Runner
|
|
7
|
+
|
|
8
|
+
**Tier:** POWERFUL
|
|
9
|
+
**Category:** Testing
|
|
10
|
+
**Domain:** Unit Testing / Quality Assurance
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
This skill provides steps on how to run tests and linters after application code has been changed.
|
|
17
|
+
It shows how unit tests run process is organized in project and how properly work with test results.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
- New application code has been introduced (*.py)
|
|
24
|
+
- Application code has been changed (*.py)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## How to run tests
|
|
29
|
+
|
|
30
|
+
To run unit tests:
|
|
31
|
+
```bash
|
|
32
|
+
make unit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## How to run linters:
|
|
36
|
+
|
|
37
|
+
To run black:
|
|
38
|
+
```bash
|
|
39
|
+
make black
|
|
40
|
+
```
|
|
41
|
+
To run flake8:
|
|
42
|
+
```bash
|
|
43
|
+
make flake8
|
|
44
|
+
```
|
|
45
|
+
To run ruff:
|
|
46
|
+
```bash
|
|
47
|
+
make ruff
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Important rules
|
|
53
|
+
|
|
54
|
+
- Test coverage should be >= 90.
|
|
55
|
+
- If test coverage is not enough, you should write additional tests to achieve desired state.
|
|
56
|
+
- If linters fail and provide changes to implement, then those changes should be implemented.
|
tanka-0.0.1/.flake8
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: linters
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- "**"
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
linters:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v7
|
|
15
|
+
- uses: astral-sh/setup-uv@v10.0.1
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.11"
|
|
18
|
+
- name: Install dependencies
|
|
19
|
+
run: uv sync --locked
|
|
20
|
+
- name: black
|
|
21
|
+
run: make black
|
|
22
|
+
- name: flake8
|
|
23
|
+
run: make flake8
|
|
24
|
+
- name: ruff
|
|
25
|
+
run: make ruff
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
tests:
|
|
10
|
+
uses: ./.github/workflows/tests.yml
|
|
11
|
+
|
|
12
|
+
linters:
|
|
13
|
+
uses: ./.github/workflows/linters.yml
|
|
14
|
+
|
|
15
|
+
publish:
|
|
16
|
+
needs:
|
|
17
|
+
- tests
|
|
18
|
+
- linters
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
environment: pypi
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
contents: read
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v7
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0
|
|
28
|
+
- uses: astral-sh/setup-uv@v10.0.1
|
|
29
|
+
- name: Build distribution
|
|
30
|
+
run: uv build
|
|
31
|
+
- name: Publish to PyPI
|
|
32
|
+
run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- "**"
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
tests:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python: ["3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- uses: astral-sh/setup-uv@v10.0.1
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python }}
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: uv sync --locked
|
|
24
|
+
- name: Unit tests
|
|
25
|
+
run: make unit
|
|
26
|
+
- name: Deep tests
|
|
27
|
+
run: make deep
|