Marco Ricci commited on 2024-07-20 23:46:05
Zeige 14 geänderte Dateien mit 1049 Einfügungen und 247 Löschungen.
Because `ruff` only shows online documentation for the latest version, it is inconvenient to work with outdated rulesets. So, manually update the `hatch` configuration from `ruff` v0.4.5 to v0.5.0, and reimplement all rule changes by hand.
... | ... |
@@ -118,9 +118,34 @@ exclude_also = [ |
118 | 118 |
[tool.ruff] |
119 | 119 |
line-length = 79 |
120 | 120 |
src = ["src"] |
121 |
+extend = "ruff_defaults_v0.5.0.toml" |
|
121 | 122 |
|
122 | 123 |
[tool.ruff.format] |
123 | 124 |
quote-style = 'single' |
125 |
+preview = true |
|
126 |
+ |
|
127 |
+[tool.ruff.lint] |
|
128 |
+preview = true |
|
129 |
+extend-ignore = [ |
|
130 |
+ 'S101', |
|
131 |
+] |
|
132 |
+extend-select = [ |
|
133 |
+ 'E501', |
|
134 |
+] |
|
124 | 135 |
|
125 | 136 |
[tool.ruff.lint.pydocstyle] |
126 | 137 |
convention = 'google' |
138 |
+ |
|
139 |
+[tool.ruff.lint.flake8-pytest-style] |
|
140 |
+parametrize-names-type = 'list' |
|
141 |
+ |
|
142 |
+[tool.ruff.lint.extend-per-file-ignores] |
|
143 |
+"**/tests/**/*" = [ |
|
144 |
+ 'SLF001', |
|
145 |
+ 'A002', |
|
146 |
+ 'FBT001', |
|
147 |
+] |
|
148 |
+ |
|
149 |
+[tool.hatch.envs.hatch-static-analysis] |
|
150 |
+config-path = "ruff_defaults_v0.5.0.toml" |
|
151 |
+dependencies = ["ruff==0.5.0"] |
... | ... |
@@ -0,0 +1,686 @@ |
1 |
+line-length = 120 |
|
2 |
+ |
|
3 |
+[format] |
|
4 |
+docstring-code-format = true |
|
5 |
+docstring-code-line-length = 80 |
|
6 |
+ |
|
7 |
+[lint] |
|
8 |
+select = [ |
|
9 |
+ "A001", |
|
10 |
+ "A002", |
|
11 |
+ "A003", |
|
12 |
+ "ARG001", |
|
13 |
+ "ARG002", |
|
14 |
+ "ARG003", |
|
15 |
+ "ARG004", |
|
16 |
+ "ARG005", |
|
17 |
+ "ASYNC100", |
|
18 |
+ "ASYNC105", |
|
19 |
+ "ASYNC109", |
|
20 |
+ "ASYNC110", |
|
21 |
+ "ASYNC115", |
|
22 |
+ "ASYNC210", |
|
23 |
+ "ASYNC220", |
|
24 |
+ "ASYNC221", |
|
25 |
+ "ASYNC230", |
|
26 |
+ "ASYNC251", |
|
27 |
+ "B002", |
|
28 |
+ "B003", |
|
29 |
+ "B004", |
|
30 |
+ "B005", |
|
31 |
+ "B006", |
|
32 |
+ "B007", |
|
33 |
+ "B008", |
|
34 |
+ "B009", |
|
35 |
+ "B010", |
|
36 |
+ "B011", |
|
37 |
+ "B012", |
|
38 |
+ "B013", |
|
39 |
+ "B014", |
|
40 |
+ "B015", |
|
41 |
+ "B016", |
|
42 |
+ "B017", |
|
43 |
+ "B018", |
|
44 |
+ "B019", |
|
45 |
+ "B020", |
|
46 |
+ "B021", |
|
47 |
+ "B022", |
|
48 |
+ "B023", |
|
49 |
+ "B024", |
|
50 |
+ "B025", |
|
51 |
+ "B026", |
|
52 |
+ "B028", |
|
53 |
+ "B029", |
|
54 |
+ "B030", |
|
55 |
+ "B031", |
|
56 |
+ "B032", |
|
57 |
+ "B033", |
|
58 |
+ "B034", |
|
59 |
+ "B035", |
|
60 |
+ "B904", |
|
61 |
+ "B905", |
|
62 |
+ "B909", |
|
63 |
+ "BLE001", |
|
64 |
+ "C400", |
|
65 |
+ "C401", |
|
66 |
+ "C402", |
|
67 |
+ "C403", |
|
68 |
+ "C404", |
|
69 |
+ "C405", |
|
70 |
+ "C406", |
|
71 |
+ "C408", |
|
72 |
+ "C409", |
|
73 |
+ "C410", |
|
74 |
+ "C411", |
|
75 |
+ "C413", |
|
76 |
+ "C414", |
|
77 |
+ "C415", |
|
78 |
+ "C416", |
|
79 |
+ "C417", |
|
80 |
+ "C418", |
|
81 |
+ "C419", |
|
82 |
+ "COM818", |
|
83 |
+ "DTZ001", |
|
84 |
+ "DTZ002", |
|
85 |
+ "DTZ003", |
|
86 |
+ "DTZ004", |
|
87 |
+ "DTZ005", |
|
88 |
+ "DTZ006", |
|
89 |
+ "DTZ007", |
|
90 |
+ "DTZ011", |
|
91 |
+ "DTZ012", |
|
92 |
+ "E101", |
|
93 |
+ "E112", |
|
94 |
+ "E113", |
|
95 |
+ "E115", |
|
96 |
+ "E116", |
|
97 |
+ "E201", |
|
98 |
+ "E202", |
|
99 |
+ "E203", |
|
100 |
+ "E211", |
|
101 |
+ "E221", |
|
102 |
+ "E222", |
|
103 |
+ "E223", |
|
104 |
+ "E224", |
|
105 |
+ "E225", |
|
106 |
+ "E226", |
|
107 |
+ "E227", |
|
108 |
+ "E228", |
|
109 |
+ "E231", |
|
110 |
+ "E241", |
|
111 |
+ "E242", |
|
112 |
+ "E251", |
|
113 |
+ "E252", |
|
114 |
+ "E261", |
|
115 |
+ "E262", |
|
116 |
+ "E265", |
|
117 |
+ "E266", |
|
118 |
+ "E271", |
|
119 |
+ "E272", |
|
120 |
+ "E273", |
|
121 |
+ "E274", |
|
122 |
+ "E275", |
|
123 |
+ "E401", |
|
124 |
+ "E402", |
|
125 |
+ "E502", |
|
126 |
+ "E701", |
|
127 |
+ "E702", |
|
128 |
+ "E703", |
|
129 |
+ "E711", |
|
130 |
+ "E712", |
|
131 |
+ "E713", |
|
132 |
+ "E714", |
|
133 |
+ "E721", |
|
134 |
+ "E722", |
|
135 |
+ "E731", |
|
136 |
+ "E741", |
|
137 |
+ "E742", |
|
138 |
+ "E743", |
|
139 |
+ "E902", |
|
140 |
+ "EM101", |
|
141 |
+ "EM102", |
|
142 |
+ "EM103", |
|
143 |
+ "EXE001", |
|
144 |
+ "EXE002", |
|
145 |
+ "EXE003", |
|
146 |
+ "EXE004", |
|
147 |
+ "EXE005", |
|
148 |
+ "F401", |
|
149 |
+ "F402", |
|
150 |
+ "F403", |
|
151 |
+ "F404", |
|
152 |
+ "F405", |
|
153 |
+ "F406", |
|
154 |
+ "F407", |
|
155 |
+ "F501", |
|
156 |
+ "F502", |
|
157 |
+ "F503", |
|
158 |
+ "F504", |
|
159 |
+ "F505", |
|
160 |
+ "F506", |
|
161 |
+ "F507", |
|
162 |
+ "F508", |
|
163 |
+ "F509", |
|
164 |
+ "F521", |
|
165 |
+ "F522", |
|
166 |
+ "F523", |
|
167 |
+ "F524", |
|
168 |
+ "F525", |
|
169 |
+ "F541", |
|
170 |
+ "F601", |
|
171 |
+ "F602", |
|
172 |
+ "F621", |
|
173 |
+ "F622", |
|
174 |
+ "F631", |
|
175 |
+ "F632", |
|
176 |
+ "F633", |
|
177 |
+ "F634", |
|
178 |
+ "F701", |
|
179 |
+ "F702", |
|
180 |
+ "F704", |
|
181 |
+ "F706", |
|
182 |
+ "F707", |
|
183 |
+ "F722", |
|
184 |
+ "F811", |
|
185 |
+ "F821", |
|
186 |
+ "F822", |
|
187 |
+ "F823", |
|
188 |
+ "F841", |
|
189 |
+ "F842", |
|
190 |
+ "F901", |
|
191 |
+ "FA100", |
|
192 |
+ "FA102", |
|
193 |
+ "FBT001", |
|
194 |
+ "FBT002", |
|
195 |
+ "FLY002", |
|
196 |
+ "FURB105", |
|
197 |
+ "FURB110", |
|
198 |
+ "FURB113", |
|
199 |
+ "FURB116", |
|
200 |
+ "FURB118", |
|
201 |
+ "FURB129", |
|
202 |
+ "FURB131", |
|
203 |
+ "FURB132", |
|
204 |
+ "FURB136", |
|
205 |
+ "FURB142", |
|
206 |
+ "FURB145", |
|
207 |
+ "FURB148", |
|
208 |
+ "FURB152", |
|
209 |
+ "FURB157", |
|
210 |
+ "FURB161", |
|
211 |
+ "FURB163", |
|
212 |
+ "FURB164", |
|
213 |
+ "FURB166", |
|
214 |
+ "FURB167", |
|
215 |
+ "FURB168", |
|
216 |
+ "FURB169", |
|
217 |
+ "FURB171", |
|
218 |
+ "FURB177", |
|
219 |
+ "FURB180", |
|
220 |
+ "FURB181", |
|
221 |
+ "FURB187", |
|
222 |
+ "FURB192", |
|
223 |
+ "G001", |
|
224 |
+ "G002", |
|
225 |
+ "G003", |
|
226 |
+ "G004", |
|
227 |
+ "G010", |
|
228 |
+ "G101", |
|
229 |
+ "G201", |
|
230 |
+ "G202", |
|
231 |
+ "I001", |
|
232 |
+ "I002", |
|
233 |
+ "ICN001", |
|
234 |
+ "ICN002", |
|
235 |
+ "ICN003", |
|
236 |
+ "INP001", |
|
237 |
+ "INT001", |
|
238 |
+ "INT002", |
|
239 |
+ "INT003", |
|
240 |
+ "ISC003", |
|
241 |
+ "LOG001", |
|
242 |
+ "LOG002", |
|
243 |
+ "LOG007", |
|
244 |
+ "LOG009", |
|
245 |
+ "N801", |
|
246 |
+ "N802", |
|
247 |
+ "N803", |
|
248 |
+ "N804", |
|
249 |
+ "N805", |
|
250 |
+ "N806", |
|
251 |
+ "N807", |
|
252 |
+ "N811", |
|
253 |
+ "N812", |
|
254 |
+ "N813", |
|
255 |
+ "N814", |
|
256 |
+ "N815", |
|
257 |
+ "N816", |
|
258 |
+ "N817", |
|
259 |
+ "N818", |
|
260 |
+ "N999", |
|
261 |
+ "PERF101", |
|
262 |
+ "PERF102", |
|
263 |
+ "PERF401", |
|
264 |
+ "PERF402", |
|
265 |
+ "PERF403", |
|
266 |
+ "PGH005", |
|
267 |
+ "PIE790", |
|
268 |
+ "PIE794", |
|
269 |
+ "PIE796", |
|
270 |
+ "PIE800", |
|
271 |
+ "PIE804", |
|
272 |
+ "PIE807", |
|
273 |
+ "PIE808", |
|
274 |
+ "PIE810", |
|
275 |
+ "PLC0105", |
|
276 |
+ "PLC0131", |
|
277 |
+ "PLC0132", |
|
278 |
+ "PLC0205", |
|
279 |
+ "PLC0208", |
|
280 |
+ "PLC0414", |
|
281 |
+ "PLC0415", |
|
282 |
+ "PLC1901", |
|
283 |
+ "PLC2401", |
|
284 |
+ "PLC2403", |
|
285 |
+ "PLC2701", |
|
286 |
+ "PLC2801", |
|
287 |
+ "PLC3002", |
|
288 |
+ "PLE0100", |
|
289 |
+ "PLE0101", |
|
290 |
+ "PLE0115", |
|
291 |
+ "PLE0116", |
|
292 |
+ "PLE0117", |
|
293 |
+ "PLE0118", |
|
294 |
+ "PLE0237", |
|
295 |
+ "PLE0241", |
|
296 |
+ "PLE0302", |
|
297 |
+ "PLE0303", |
|
298 |
+ "PLE0304", |
|
299 |
+ "PLE0305", |
|
300 |
+ "PLE0307", |
|
301 |
+ "PLE0308", |
|
302 |
+ "PLE0309", |
|
303 |
+ "PLE0604", |
|
304 |
+ "PLE0605", |
|
305 |
+ "PLE0643", |
|
306 |
+ "PLE0704", |
|
307 |
+ "PLE1132", |
|
308 |
+ "PLE1141", |
|
309 |
+ "PLE1142", |
|
310 |
+ "PLE1205", |
|
311 |
+ "PLE1206", |
|
312 |
+ "PLE1300", |
|
313 |
+ "PLE1307", |
|
314 |
+ "PLE1310", |
|
315 |
+ "PLE1507", |
|
316 |
+ "PLE1519", |
|
317 |
+ "PLE1520", |
|
318 |
+ "PLE1700", |
|
319 |
+ "PLE2502", |
|
320 |
+ "PLE2510", |
|
321 |
+ "PLE2512", |
|
322 |
+ "PLE2513", |
|
323 |
+ "PLE2514", |
|
324 |
+ "PLE2515", |
|
325 |
+ "PLE4703", |
|
326 |
+ "PLR0124", |
|
327 |
+ "PLR0133", |
|
328 |
+ "PLR0202", |
|
329 |
+ "PLR0203", |
|
330 |
+ "PLR0206", |
|
331 |
+ "PLR0402", |
|
332 |
+ "PLR1704", |
|
333 |
+ "PLR1711", |
|
334 |
+ "PLR1714", |
|
335 |
+ "PLR1722", |
|
336 |
+ "PLR1730", |
|
337 |
+ "PLR1733", |
|
338 |
+ "PLR1736", |
|
339 |
+ "PLR2004", |
|
340 |
+ "PLR2044", |
|
341 |
+ "PLR5501", |
|
342 |
+ "PLR6104", |
|
343 |
+ "PLR6201", |
|
344 |
+ "PLR6301", |
|
345 |
+ "PLW0108", |
|
346 |
+ "PLW0120", |
|
347 |
+ "PLW0127", |
|
348 |
+ "PLW0128", |
|
349 |
+ "PLW0129", |
|
350 |
+ "PLW0131", |
|
351 |
+ "PLW0133", |
|
352 |
+ "PLW0177", |
|
353 |
+ "PLW0211", |
|
354 |
+ "PLW0245", |
|
355 |
+ "PLW0406", |
|
356 |
+ "PLW0602", |
|
357 |
+ "PLW0603", |
|
358 |
+ "PLW0604", |
|
359 |
+ "PLW0642", |
|
360 |
+ "PLW0711", |
|
361 |
+ "PLW1501", |
|
362 |
+ "PLW1508", |
|
363 |
+ "PLW1509", |
|
364 |
+ "PLW1510", |
|
365 |
+ "PLW1514", |
|
366 |
+ "PLW1641", |
|
367 |
+ "PLW2101", |
|
368 |
+ "PLW2901", |
|
369 |
+ "PLW3201", |
|
370 |
+ "PLW3301", |
|
371 |
+ "PT001", |
|
372 |
+ "PT002", |
|
373 |
+ "PT003", |
|
374 |
+ "PT006", |
|
375 |
+ "PT007", |
|
376 |
+ "PT008", |
|
377 |
+ "PT009", |
|
378 |
+ "PT010", |
|
379 |
+ "PT011", |
|
380 |
+ "PT012", |
|
381 |
+ "PT013", |
|
382 |
+ "PT014", |
|
383 |
+ "PT015", |
|
384 |
+ "PT016", |
|
385 |
+ "PT017", |
|
386 |
+ "PT018", |
|
387 |
+ "PT019", |
|
388 |
+ "PT020", |
|
389 |
+ "PT021", |
|
390 |
+ "PT022", |
|
391 |
+ "PT023", |
|
392 |
+ "PT024", |
|
393 |
+ "PT025", |
|
394 |
+ "PT026", |
|
395 |
+ "PT027", |
|
396 |
+ "PYI001", |
|
397 |
+ "PYI002", |
|
398 |
+ "PYI003", |
|
399 |
+ "PYI004", |
|
400 |
+ "PYI005", |
|
401 |
+ "PYI006", |
|
402 |
+ "PYI007", |
|
403 |
+ "PYI008", |
|
404 |
+ "PYI009", |
|
405 |
+ "PYI010", |
|
406 |
+ "PYI011", |
|
407 |
+ "PYI012", |
|
408 |
+ "PYI013", |
|
409 |
+ "PYI014", |
|
410 |
+ "PYI015", |
|
411 |
+ "PYI016", |
|
412 |
+ "PYI017", |
|
413 |
+ "PYI018", |
|
414 |
+ "PYI019", |
|
415 |
+ "PYI020", |
|
416 |
+ "PYI021", |
|
417 |
+ "PYI024", |
|
418 |
+ "PYI025", |
|
419 |
+ "PYI026", |
|
420 |
+ "PYI029", |
|
421 |
+ "PYI030", |
|
422 |
+ "PYI032", |
|
423 |
+ "PYI033", |
|
424 |
+ "PYI034", |
|
425 |
+ "PYI035", |
|
426 |
+ "PYI036", |
|
427 |
+ "PYI041", |
|
428 |
+ "PYI042", |
|
429 |
+ "PYI043", |
|
430 |
+ "PYI044", |
|
431 |
+ "PYI045", |
|
432 |
+ "PYI046", |
|
433 |
+ "PYI047", |
|
434 |
+ "PYI048", |
|
435 |
+ "PYI049", |
|
436 |
+ "PYI050", |
|
437 |
+ "PYI051", |
|
438 |
+ "PYI052", |
|
439 |
+ "PYI053", |
|
440 |
+ "PYI054", |
|
441 |
+ "PYI055", |
|
442 |
+ "PYI056", |
|
443 |
+ "PYI058", |
|
444 |
+ "PYI059", |
|
445 |
+ "PYI062", |
|
446 |
+ "RET503", |
|
447 |
+ "RET504", |
|
448 |
+ "RET505", |
|
449 |
+ "RET506", |
|
450 |
+ "RET507", |
|
451 |
+ "RET508", |
|
452 |
+ "RSE102", |
|
453 |
+ "RUF001", |
|
454 |
+ "RUF002", |
|
455 |
+ "RUF003", |
|
456 |
+ "RUF005", |
|
457 |
+ "RUF006", |
|
458 |
+ "RUF007", |
|
459 |
+ "RUF008", |
|
460 |
+ "RUF009", |
|
461 |
+ "RUF010", |
|
462 |
+ "RUF012", |
|
463 |
+ "RUF013", |
|
464 |
+ "RUF015", |
|
465 |
+ "RUF016", |
|
466 |
+ "RUF017", |
|
467 |
+ "RUF018", |
|
468 |
+ "RUF019", |
|
469 |
+ "RUF020", |
|
470 |
+ "RUF021", |
|
471 |
+ "RUF022", |
|
472 |
+ "RUF023", |
|
473 |
+ "RUF024", |
|
474 |
+ "RUF025", |
|
475 |
+ "RUF026", |
|
476 |
+ "RUF027", |
|
477 |
+ "RUF028", |
|
478 |
+ "RUF029", |
|
479 |
+ "RUF100", |
|
480 |
+ "RUF101", |
|
481 |
+ "S101", |
|
482 |
+ "S102", |
|
483 |
+ "S103", |
|
484 |
+ "S104", |
|
485 |
+ "S105", |
|
486 |
+ "S106", |
|
487 |
+ "S107", |
|
488 |
+ "S108", |
|
489 |
+ "S110", |
|
490 |
+ "S112", |
|
491 |
+ "S113", |
|
492 |
+ "S201", |
|
493 |
+ "S202", |
|
494 |
+ "S301", |
|
495 |
+ "S302", |
|
496 |
+ "S303", |
|
497 |
+ "S304", |
|
498 |
+ "S305", |
|
499 |
+ "S306", |
|
500 |
+ "S307", |
|
501 |
+ "S308", |
|
502 |
+ "S310", |
|
503 |
+ "S311", |
|
504 |
+ "S312", |
|
505 |
+ "S313", |
|
506 |
+ "S314", |
|
507 |
+ "S315", |
|
508 |
+ "S316", |
|
509 |
+ "S317", |
|
510 |
+ "S318", |
|
511 |
+ "S319", |
|
512 |
+ "S320", |
|
513 |
+ "S321", |
|
514 |
+ "S323", |
|
515 |
+ "S324", |
|
516 |
+ "S401", |
|
517 |
+ "S402", |
|
518 |
+ "S403", |
|
519 |
+ "S405", |
|
520 |
+ "S406", |
|
521 |
+ "S407", |
|
522 |
+ "S408", |
|
523 |
+ "S409", |
|
524 |
+ "S411", |
|
525 |
+ "S412", |
|
526 |
+ "S413", |
|
527 |
+ "S415", |
|
528 |
+ "S501", |
|
529 |
+ "S502", |
|
530 |
+ "S503", |
|
531 |
+ "S504", |
|
532 |
+ "S505", |
|
533 |
+ "S506", |
|
534 |
+ "S507", |
|
535 |
+ "S508", |
|
536 |
+ "S509", |
|
537 |
+ "S601", |
|
538 |
+ "S602", |
|
539 |
+ "S604", |
|
540 |
+ "S605", |
|
541 |
+ "S606", |
|
542 |
+ "S607", |
|
543 |
+ "S608", |
|
544 |
+ "S609", |
|
545 |
+ "S610", |
|
546 |
+ "S611", |
|
547 |
+ "S612", |
|
548 |
+ "S701", |
|
549 |
+ "S702", |
|
550 |
+ "SIM101", |
|
551 |
+ "SIM102", |
|
552 |
+ "SIM103", |
|
553 |
+ "SIM105", |
|
554 |
+ "SIM107", |
|
555 |
+ "SIM108", |
|
556 |
+ "SIM109", |
|
557 |
+ "SIM110", |
|
558 |
+ "SIM112", |
|
559 |
+ "SIM113", |
|
560 |
+ "SIM114", |
|
561 |
+ "SIM115", |
|
562 |
+ "SIM116", |
|
563 |
+ "SIM117", |
|
564 |
+ "SIM118", |
|
565 |
+ "SIM201", |
|
566 |
+ "SIM202", |
|
567 |
+ "SIM208", |
|
568 |
+ "SIM210", |
|
569 |
+ "SIM211", |
|
570 |
+ "SIM212", |
|
571 |
+ "SIM220", |
|
572 |
+ "SIM221", |
|
573 |
+ "SIM222", |
|
574 |
+ "SIM223", |
|
575 |
+ "SIM300", |
|
576 |
+ "SIM910", |
|
577 |
+ "SIM911", |
|
578 |
+ "SLF001", |
|
579 |
+ "SLOT000", |
|
580 |
+ "SLOT001", |
|
581 |
+ "SLOT002", |
|
582 |
+ "T100", |
|
583 |
+ "T201", |
|
584 |
+ "T203", |
|
585 |
+ "TCH001", |
|
586 |
+ "TCH002", |
|
587 |
+ "TCH003", |
|
588 |
+ "TCH004", |
|
589 |
+ "TCH005", |
|
590 |
+ "TCH010", |
|
591 |
+ "TD004", |
|
592 |
+ "TD005", |
|
593 |
+ "TD006", |
|
594 |
+ "TD007", |
|
595 |
+ "TID251", |
|
596 |
+ "TID252", |
|
597 |
+ "TID253", |
|
598 |
+ "TRY002", |
|
599 |
+ "TRY003", |
|
600 |
+ "TRY004", |
|
601 |
+ "TRY201", |
|
602 |
+ "TRY300", |
|
603 |
+ "TRY301", |
|
604 |
+ "TRY302", |
|
605 |
+ "TRY400", |
|
606 |
+ "TRY401", |
|
607 |
+ "UP001", |
|
608 |
+ "UP003", |
|
609 |
+ "UP004", |
|
610 |
+ "UP005", |
|
611 |
+ "UP006", |
|
612 |
+ "UP007", |
|
613 |
+ "UP008", |
|
614 |
+ "UP009", |
|
615 |
+ "UP010", |
|
616 |
+ "UP011", |
|
617 |
+ "UP012", |
|
618 |
+ "UP013", |
|
619 |
+ "UP014", |
|
620 |
+ "UP015", |
|
621 |
+ "UP017", |
|
622 |
+ "UP018", |
|
623 |
+ "UP019", |
|
624 |
+ "UP020", |
|
625 |
+ "UP021", |
|
626 |
+ "UP022", |
|
627 |
+ "UP023", |
|
628 |
+ "UP024", |
|
629 |
+ "UP025", |
|
630 |
+ "UP026", |
|
631 |
+ "UP027", |
|
632 |
+ "UP028", |
|
633 |
+ "UP029", |
|
634 |
+ "UP030", |
|
635 |
+ "UP031", |
|
636 |
+ "UP032", |
|
637 |
+ "UP033", |
|
638 |
+ "UP034", |
|
639 |
+ "UP035", |
|
640 |
+ "UP036", |
|
641 |
+ "UP037", |
|
642 |
+ "UP038", |
|
643 |
+ "UP039", |
|
644 |
+ "UP040", |
|
645 |
+ "UP041", |
|
646 |
+ "UP042", |
|
647 |
+ "W291", |
|
648 |
+ "W292", |
|
649 |
+ "W293", |
|
650 |
+ "W391", |
|
651 |
+ "W505", |
|
652 |
+ "W605", |
|
653 |
+ "YTT101", |
|
654 |
+ "YTT102", |
|
655 |
+ "YTT103", |
|
656 |
+ "YTT201", |
|
657 |
+ "YTT202", |
|
658 |
+ "YTT203", |
|
659 |
+ "YTT204", |
|
660 |
+ "YTT301", |
|
661 |
+ "YTT302", |
|
662 |
+ "YTT303", |
|
663 |
+] |
|
664 |
+ |
|
665 |
+[lint.per-file-ignores] |
|
666 |
+"**/scripts/*" = [ |
|
667 |
+ "INP001", |
|
668 |
+ "T201", |
|
669 |
+] |
|
670 |
+"**/tests/**/*" = [ |
|
671 |
+ "PLC1901", |
|
672 |
+ "PLR2004", |
|
673 |
+ "PLR6301", |
|
674 |
+ "S", |
|
675 |
+ "TID252", |
|
676 |
+] |
|
677 |
+ |
|
678 |
+[lint.flake8-tidy-imports] |
|
679 |
+ban-relative-imports = "all" |
|
680 |
+ |
|
681 |
+[lint.isort] |
|
682 |
+known-first-party = ["derivepassphrase"] |
|
683 |
+ |
|
684 |
+[lint.flake8-pytest-style] |
|
685 |
+fixture-parentheses = false |
|
686 |
+mark-parentheses = false |
... | ... |
@@ -4,7 +4,7 @@ |
4 | 4 |
|
5 | 5 |
"""Work-alike of vault(1) – a deterministic, stateless password manager |
6 | 6 |
|
7 |
-""" |
|
7 |
+""" # noqa: RUF002 |
|
8 | 8 |
|
9 | 9 |
from __future__ import annotations |
10 | 10 |
|
... | ... |
@@ -24,6 +24,24 @@ __version__ = "0.1.1" |
24 | 24 |
|
25 | 25 |
class AmbiguousByteRepresentationError(ValueError): |
26 | 26 |
"""The object has an ambiguous byte representation.""" |
27 |
+ def __init__(self): |
|
28 |
+ super().__init__('text string has ambiguous byte representation') |
|
29 |
+ |
|
30 |
+_CHARSETS = collections.OrderedDict([ |
|
31 |
+ ('lower', b'abcdefghijklmnopqrstuvwxyz'), |
|
32 |
+ ('upper', b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'), |
|
33 |
+ ('alpha', b''), # Placeholder. |
|
34 |
+ ('number', b'0123456789'), |
|
35 |
+ ('alphanum', b''), # Placeholder. |
|
36 |
+ ('space', b' '), |
|
37 |
+ ('dash', b'-_'), |
|
38 |
+ ('symbol', b'!"#$%&\'()*+,./:;<=>?@[\\]^{|}~-_'), |
|
39 |
+ ('all', b''), # Placeholder. |
|
40 |
+]) |
|
41 |
+_CHARSETS['alpha'] = _CHARSETS['lower'] + _CHARSETS['upper'] |
|
42 |
+_CHARSETS['alphanum'] = _CHARSETS['alpha'] + _CHARSETS['number'] |
|
43 |
+_CHARSETS['all'] = (_CHARSETS['alphanum'] + _CHARSETS['space'] |
|
44 |
+ + _CHARSETS['symbol']) |
|
27 | 45 |
|
28 | 46 |
class Vault: |
29 | 47 |
"""A work-alike of James Coglan's vault. |
... | ... |
@@ -48,28 +66,13 @@ class Vault: |
48 | 66 |
""" |
49 | 67 |
_UUID = b'e87eb0f4-34cb-46b9-93ad-766c5ab063e7' |
50 | 68 |
"""A tag used by vault in the bit stream generation.""" |
51 |
- _CHARSETS: collections.OrderedDict[str, bytes] |
|
69 |
+ _CHARSETS = _CHARSETS |
|
52 | 70 |
""" |
53 | 71 |
Known character sets from which to draw passphrase characters. |
54 | 72 |
Relies on a certain, fixed order for their definition and their |
55 | 73 |
contents. |
56 | 74 |
|
57 | 75 |
""" |
58 |
- _CHARSETS = collections.OrderedDict([ |
|
59 |
- ('lower', b'abcdefghijklmnopqrstuvwxyz'), |
|
60 |
- ('upper', b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'), |
|
61 |
- ('alpha', b''), # Placeholder. |
|
62 |
- ('number', b'0123456789'), |
|
63 |
- ('alphanum', b''), # Placeholder. |
|
64 |
- ('space', b' '), |
|
65 |
- ('dash', b'-_'), |
|
66 |
- ('symbol', b'!"#$%&\'()*+,./:;<=>?@[\\]^{|}~-_'), |
|
67 |
- ('all', b''), # Placeholder. |
|
68 |
- ]) |
|
69 |
- _CHARSETS['alpha'] = _CHARSETS['lower'] + _CHARSETS['upper'] |
|
70 |
- _CHARSETS['alphanum'] = _CHARSETS['alpha'] + _CHARSETS['number'] |
|
71 |
- _CHARSETS['all'] = (_CHARSETS['alphanum'] + _CHARSETS['space'] |
|
72 |
- + _CHARSETS['symbol']) |
|
73 | 76 |
|
74 | 77 |
def __init__( |
75 | 78 |
self, *, phrase: bytes | bytearray | str = b'', |
... | ... |
@@ -124,7 +127,7 @@ class Vault: |
124 | 127 |
) -> None: |
125 | 128 |
if not isinstance(count, int): |
126 | 129 |
return |
127 |
- elif count <= 0: |
|
130 |
+ if count <= 0: |
|
128 | 131 |
self._allowed = self._subtract(characters, self._allowed) |
129 | 132 |
else: |
130 | 133 |
for _ in range(count): |
... | ... |
@@ -136,9 +139,11 @@ class Vault: |
136 | 139 |
subtract_or_require(dash, self._CHARSETS['dash']) |
137 | 140 |
subtract_or_require(symbol, self._CHARSETS['symbol']) |
138 | 141 |
if len(self._required) > self._length: |
139 |
- raise ValueError('requested passphrase length too short') |
|
142 |
+ msg = 'requested passphrase length too short' |
|
143 |
+ raise ValueError(msg) |
|
140 | 144 |
if not self._allowed: |
141 |
- raise ValueError('no allowed characters left') |
|
145 |
+ msg = 'no allowed characters left' |
|
146 |
+ raise ValueError(msg) |
|
142 | 147 |
for _ in range(len(self._required), self._length): |
143 | 148 |
self._required.append(bytes(self._allowed)) |
144 | 149 |
|
... | ... |
@@ -167,8 +172,7 @@ class Vault: |
167 | 172 |
if not self._required or any(not x for x in self._required): |
168 | 173 |
return float('-inf') |
169 | 174 |
for i, charset in enumerate(self._required): |
170 |
- factors.append(i + 1) |
|
171 |
- factors.append(len(charset)) |
|
175 |
+ factors.extend([i + 1, len(charset)]) |
|
172 | 176 |
factors.sort() |
173 | 177 |
return math.fsum(math.log2(f) for f in factors) |
174 | 178 |
|
... | ... |
@@ -199,10 +203,11 @@ class Vault: |
199 | 203 |
try: |
200 | 204 |
safety_factor = float(safety_factor) |
201 | 205 |
except TypeError as e: |
202 |
- raise TypeError(f'invalid safety factor: not a float: ' |
|
203 |
- f'{safety_factor!r}') from e |
|
206 |
+ msg = f'invalid safety factor: not a float: {safety_factor!r}' |
|
207 |
+ raise TypeError(msg) from e |
|
204 | 208 |
if not math.isfinite(safety_factor) or safety_factor < 1.0: |
205 |
- raise ValueError(f'invalid safety factor {safety_factor!r}') |
|
209 |
+ msg = f'invalid safety factor {safety_factor!r}' |
|
210 |
+ raise ValueError(msg) |
|
206 | 211 |
# Ensure the bound is strictly positive. |
207 | 212 |
entropy_bound = max(1, self._entropy()) |
208 | 213 |
return int(math.ceil(safety_factor * entropy_bound / 8)) |
... | ... |
@@ -230,8 +235,7 @@ class Vault: |
230 | 235 |
if isinstance(s, str): |
231 | 236 |
norm = unicodedata.normalize |
232 | 237 |
if norm('NFC', s) != norm('NFD', s): |
233 |
- raise AmbiguousByteRepresentationError( |
|
234 |
- 'text string has ambiguous byte representation') |
|
238 |
+ raise AmbiguousByteRepresentationError |
|
235 | 239 |
return s.encode('UTF-8') |
236 | 240 |
return bytes(s) |
237 | 241 |
|
... | ... |
@@ -452,11 +456,12 @@ class Vault: |
452 | 456 |
|
453 | 457 |
""" |
454 | 458 |
if not cls._is_suitable_ssh_key(key): |
455 |
- raise ValueError( |
|
456 |
- 'unsuitable SSH key: bad key, or signature not deterministic') |
|
459 |
+ msg = ('unsuitable SSH key: bad key, or ' |
|
460 |
+ 'signature not deterministic') |
|
461 |
+ raise ValueError(msg) |
|
457 | 462 |
with ssh_agent_client.SSHAgentClient() as client: |
458 | 463 |
raw_sig = client.sign(key, cls._UUID) |
459 |
- keytype, trailer = client.unstring_prefix(raw_sig) |
|
464 |
+ _keytype, trailer = client.unstring_prefix(raw_sig) |
|
460 | 465 |
signature_blob = client.unstring(trailer) |
461 | 466 |
return bytes(base64.standard_b64encode(signature_blob)) |
462 | 467 |
|
... | ... |
@@ -487,10 +492,11 @@ class Vault: |
487 | 492 |
allowed = (allowed if isinstance(allowed, bytearray) |
488 | 493 |
else bytearray(allowed)) |
489 | 494 |
assert_type(allowed, bytearray) |
495 |
+ msg_dup_characters = 'duplicate characters in set' |
|
490 | 496 |
if len(frozenset(allowed)) != len(allowed): |
491 |
- raise ValueError('duplicate characters in set') |
|
497 |
+ raise ValueError(msg_dup_characters) |
|
492 | 498 |
if len(frozenset(charset)) != len(charset): |
493 |
- raise ValueError('duplicate characters in set') |
|
499 |
+ raise ValueError(msg_dup_characters) |
|
494 | 500 |
for c in charset: |
495 | 501 |
try: |
496 | 502 |
pos = allowed.index(c) |
... | ... |
@@ -11,26 +11,47 @@ from __future__ import annotations |
11 | 11 |
import base64 |
12 | 12 |
import collections |
13 | 13 |
import contextlib |
14 |
+import copy |
|
14 | 15 |
import inspect |
15 | 16 |
import json |
16 | 17 |
import os |
17 |
-import pathlib |
|
18 | 18 |
import socket |
19 |
-from typing_extensions import ( |
|
20 |
- Any, assert_never, cast, Iterator, Sequence, TextIO, |
|
19 |
+from typing import ( |
|
20 |
+ TYPE_CHECKING, |
|
21 |
+ TextIO, |
|
22 |
+ cast, |
|
21 | 23 |
) |
22 | 24 |
|
23 | 25 |
import click |
26 |
+from typing_extensions import ( |
|
27 |
+ Any, |
|
28 |
+ assert_never, |
|
29 |
+) |
|
30 |
+ |
|
24 | 31 |
import derivepassphrase as dpp |
25 |
-from derivepassphrase import types as dpp_types |
|
26 | 32 |
import ssh_agent_client |
33 |
+from derivepassphrase import types as dpp_types |
|
34 |
+ |
|
35 |
+if TYPE_CHECKING: |
|
36 |
+ import pathlib |
|
37 |
+ from collections.abc import ( |
|
38 |
+ Iterator, |
|
39 |
+ Sequence, |
|
40 |
+ ) |
|
27 | 41 |
|
28 | 42 |
__author__ = dpp.__author__ |
29 | 43 |
__version__ = dpp.__version__ |
30 | 44 |
|
31 | 45 |
__all__ = ('derivepassphrase',) |
32 | 46 |
|
33 |
-prog_name = 'derivepassphrase' |
|
47 |
+PROG_NAME = 'derivepassphrase' |
|
48 |
+KEY_DISPLAY_LENGTH = 30 |
|
49 |
+ |
|
50 |
+# Error messages |
|
51 |
+_INVALID_VAULT_CONFIG = 'Invalid vault config' |
|
52 |
+_AGENT_COMMUNICATION_ERROR = 'Error communicating with the SSH agent' |
|
53 |
+_NO_USABLE_KEYS = 'No usable SSH keys were found' |
|
54 |
+_EMPTY_SELECTION = 'Empty selection' |
|
34 | 55 |
|
35 | 56 |
|
36 | 57 |
def _config_filename() -> str | bytes | pathlib.Path: |
... | ... |
@@ -43,8 +64,8 @@ def _config_filename() -> str | bytes | pathlib.Path: |
43 | 64 |
|
44 | 65 |
""" |
45 | 66 |
path: str | bytes | pathlib.Path |
46 |
- path = (os.getenv(prog_name.upper() + '_PATH') |
|
47 |
- or click.get_app_dir(prog_name, force_posix=True)) |
|
67 |
+ path = (os.getenv(PROG_NAME.upper() + '_PATH') |
|
68 |
+ or click.get_app_dir(PROG_NAME, force_posix=True)) |
|
48 | 69 |
return os.path.join(path, 'settings.json') |
49 | 70 |
|
50 | 71 |
|
... | ... |
@@ -71,7 +92,7 @@ def _load_config() -> dpp_types.VaultConfig: |
71 | 92 |
with open(filename, 'rb') as fileobj: |
72 | 93 |
data = json.load(fileobj) |
73 | 94 |
if not dpp_types.is_vault_config(data): |
74 |
- raise ValueError('Invalid vault config') |
|
95 |
+ raise ValueError(_INVALID_VAULT_CONFIG) |
|
75 | 96 |
return data |
76 | 97 |
|
77 | 98 |
|
... | ... |
@@ -94,9 +115,9 @@ def _save_config(config: dpp_types.VaultConfig, /) -> None: |
94 | 115 |
|
95 | 116 |
""" |
96 | 117 |
if not dpp_types.is_vault_config(config): |
97 |
- raise ValueError('Invalid vault config') |
|
118 |
+ raise ValueError(_INVALID_VAULT_CONFIG) |
|
98 | 119 |
filename = _config_filename() |
99 |
- with open(filename, 'wt', encoding='UTF-8') as fileobj: |
|
120 |
+ with open(filename, 'w', encoding='UTF-8') as fileobj: |
|
100 | 121 |
json.dump(config, fileobj) |
101 | 122 |
|
102 | 123 |
|
... | ... |
@@ -150,21 +171,20 @@ def _get_suitable_ssh_keys( |
150 | 171 |
client_context = client |
151 | 172 |
case _: # pragma: no cover |
152 | 173 |
assert_never(conn) |
153 |
- raise TypeError(f'invalid connection hint: {conn!r}') |
|
174 |
+ msg = f'invalid connection hint: {conn!r}' |
|
175 |
+ raise TypeError(msg) |
|
154 | 176 |
with client_context: |
155 | 177 |
try: |
156 | 178 |
all_key_comment_pairs = list(client.list_keys()) |
157 | 179 |
except EOFError as e: # pragma: no cover |
158 |
- raise RuntimeError( |
|
159 |
- 'error communicating with the SSH agent' |
|
160 |
- ) from e |
|
161 |
- suitable_keys = all_key_comment_pairs[:] |
|
180 |
+ raise RuntimeError(_AGENT_COMMUNICATION_ERROR) from e |
|
181 |
+ suitable_keys = copy.copy(all_key_comment_pairs) |
|
162 | 182 |
for pair in all_key_comment_pairs: |
163 |
- key, comment = pair |
|
164 |
- if dpp.Vault._is_suitable_ssh_key(key): |
|
183 |
+ key, _comment = pair |
|
184 |
+ if dpp.Vault._is_suitable_ssh_key(key): # noqa: SLF001 |
|
165 | 185 |
yield pair |
166 | 186 |
if not suitable_keys: # pragma: no cover |
167 |
- raise IndexError('No usable SSH keys were found') |
|
187 |
+ raise IndexError(_NO_USABLE_KEYS) |
|
168 | 188 |
|
169 | 189 |
|
170 | 190 |
def _prompt_for_selection( |
... | ... |
@@ -212,9 +232,8 @@ def _prompt_for_selection( |
212 | 232 |
err=True, type=choices, show_choices=False, |
213 | 233 |
show_default=False, default='') |
214 | 234 |
if not choice: |
215 |
- raise IndexError('empty selection') |
|
235 |
+ raise IndexError(_EMPTY_SELECTION) |
|
216 | 236 |
return int(choice) - 1 |
217 |
- else: |
|
218 | 237 |
prompt_suffix = (' ' |
219 | 238 |
if single_choice_prompt.endswith(tuple('?.!')) |
220 | 239 |
else ': ') |
... | ... |
@@ -223,7 +242,7 @@ def _prompt_for_selection( |
223 | 242 |
prompt_suffix=prompt_suffix, err=True, |
224 | 243 |
abort=True, default=False, show_default=False) |
225 | 244 |
except click.Abort: |
226 |
- raise IndexError('empty selection') from None |
|
245 |
+ raise IndexError(_EMPTY_SELECTION) from None |
|
227 | 246 |
return 0 |
228 | 247 |
|
229 | 248 |
|
... | ... |
@@ -273,7 +292,9 @@ def _select_ssh_key( |
273 | 292 |
for key, comment in suitable_keys: |
274 | 293 |
keytype = unstring_prefix(key)[0].decode('ASCII') |
275 | 294 |
key_str = base64.standard_b64encode(key).decode('ASCII') |
276 |
- key_prefix = key_str if len(key_str) < 30 else key_str[:27] + '...' |
|
295 |
+ key_prefix = (key_str |
|
296 |
+ if len(key_str) < KEY_DISPLAY_LENGTH + len('...') |
|
297 |
+ else key_str[:KEY_DISPLAY_LENGTH] + '...') |
|
277 | 298 |
comment_str = comment.decode('UTF-8', errors='replace') |
278 | 299 |
key_listing.append(f'{keytype} {key_prefix} {comment_str}') |
279 | 300 |
choice = _prompt_for_selection( |
... | ... |
@@ -317,8 +338,8 @@ class OptionGroupOption(click.Option): |
317 | 338 |
|
318 | 339 |
def __init__(self, *args, **kwargs): # type: ignore |
319 | 340 |
if self.__class__ == __class__: |
320 |
- raise NotImplementedError() |
|
321 |
- return super().__init__(*args, **kwargs) |
|
341 |
+ raise NotImplementedError |
|
342 |
+ super().__init__(*args, **kwargs) |
|
322 | 343 |
|
323 | 344 |
|
324 | 345 |
class CommandWithHelpGroups(click.Command): |
... | ... |
@@ -420,6 +441,8 @@ def _validate_occurrence_constraint( |
420 | 441 |
ctx: click.Context, param: click.Parameter, value: Any, |
421 | 442 |
) -> int | None: |
422 | 443 |
"""Check that the occurrence constraint is valid (int, 0 or larger).""" |
444 |
+ del ctx # Unused. |
|
445 |
+ del param # Unused. |
|
423 | 446 |
if value is None: |
424 | 447 |
return value |
425 | 448 |
if isinstance(value, int): |
... | ... |
@@ -428,9 +451,11 @@ def _validate_occurrence_constraint( |
428 | 451 |
try: |
429 | 452 |
int_value = int(value, 10) |
430 | 453 |
except ValueError as e: |
431 |
- raise click.BadParameter('not an integer') from e |
|
454 |
+ msg = 'not an integer' |
|
455 |
+ raise click.BadParameter(msg) from e |
|
432 | 456 |
if int_value < 0: |
433 |
- raise click.BadParameter('not a non-negative integer') |
|
457 |
+ msg = 'not a non-negative integer' |
|
458 |
+ raise click.BadParameter(msg) |
|
434 | 459 |
return int_value |
435 | 460 |
|
436 | 461 |
|
... | ... |
@@ -438,6 +463,8 @@ def _validate_length( |
438 | 463 |
ctx: click.Context, param: click.Parameter, value: Any, |
439 | 464 |
) -> int | None: |
440 | 465 |
"""Check that the length is valid (int, 1 or larger).""" |
466 |
+ del ctx # Unused. |
|
467 |
+ del param # Unused. |
|
441 | 468 |
if value is None: |
442 | 469 |
return value |
443 | 470 |
if isinstance(value, int): |
... | ... |
@@ -446,9 +473,11 @@ def _validate_length( |
446 | 473 |
try: |
447 | 474 |
int_value = int(value, 10) |
448 | 475 |
except ValueError as e: |
449 |
- raise click.BadParameter('not an integer') from e |
|
476 |
+ msg = 'not an integer' |
|
477 |
+ raise click.BadParameter(msg) from e |
|
450 | 478 |
if int_value < 1: |
451 |
- raise click.BadParameter('not a positive integer') |
|
479 |
+ msg = 'not a positive integer' |
|
480 |
+ raise click.BadParameter(msg) |
|
452 | 481 |
return int_value |
453 | 482 |
|
454 | 483 |
DEFAULT_NOTES_TEMPLATE = '''\ |
... | ... |
@@ -544,7 +573,7 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -' |
544 | 573 |
type=click.Path(file_okay=True, allow_dash=True, exists=False), |
545 | 574 |
help='import saved settings from file PATH', |
546 | 575 |
cls=StorageManagementOption) |
547 |
-@click.version_option(version=dpp.__version__, prog_name=prog_name) |
|
576 |
+@click.version_option(version=dpp.__version__, prog_name=PROG_NAME) |
|
548 | 577 |
@click.argument('service', required=False) |
549 | 578 |
@click.pass_context |
550 | 579 |
def derivepassphrase( |
... | ... |
@@ -674,8 +703,8 @@ def derivepassphrase( |
674 | 703 |
case StorageManagementOption(): |
675 | 704 |
group = StorageManagementOption |
676 | 705 |
case OptionGroupOption(): |
677 |
- raise AssertionError( |
|
678 |
- f'Unknown option group for {param!r}') |
|
706 |
+ raise AssertionError( # noqa: TRY003 |
|
707 |
+ f'Unknown option group for {param!r}') # noqa: EM102 |
|
679 | 708 |
case _: |
680 | 709 |
group = click.Option |
681 | 710 |
options_in_group.setdefault(group, []).append(param) |
... | ... |
@@ -696,7 +725,7 @@ def derivepassphrase( |
696 | 725 |
return |
697 | 726 |
for other in incompatible: |
698 | 727 |
if isinstance(other, str): |
699 |
- other = params_by_str[other] |
|
728 |
+ other = params_by_str[other] # noqa: PLW2901 |
|
700 | 729 |
assert isinstance(other, click.Parameter) |
701 | 730 |
if other != param and is_param_set(other): |
702 | 731 |
opt_str = param.opts[0] |
... | ... |
@@ -709,7 +738,7 @@ def derivepassphrase( |
709 | 738 |
return _load_config() |
710 | 739 |
except FileNotFoundError: |
711 | 740 |
return {'services': {}} |
712 |
- except Exception as e: |
|
741 |
+ except Exception as e: # noqa: BLE001 |
|
713 | 742 |
ctx.fail(f'cannot load config: {e}') |
714 | 743 |
|
715 | 744 |
configuration: dpp_types.VaultConfig |
... | ... |
@@ -733,22 +762,24 @@ def derivepassphrase( |
733 | 762 |
for param in sv_options: |
734 | 763 |
if is_param_set(param) and not service: |
735 | 764 |
opt_str = param.opts[0] |
736 |
- raise click.UsageError(f'{opt_str} requires a SERVICE') |
|
765 |
+ msg = f'{opt_str} requires a SERVICE' |
|
766 |
+ raise click.UsageError(msg) |
|
737 | 767 |
for param in [params_by_str['--key'], params_by_str['--phrase']]: |
738 | 768 |
if ( |
739 | 769 |
is_param_set(param) |
740 | 770 |
and not (service or is_param_set(params_by_str['--config'])) |
741 | 771 |
): |
742 | 772 |
opt_str = param.opts[0] |
743 |
- raise click.UsageError(f'{opt_str} requires a SERVICE or --config') |
|
773 |
+ msg = f'{opt_str} requires a SERVICE or --config' |
|
774 |
+ raise click.UsageError(msg) |
|
744 | 775 |
no_sv_options = [params_by_str['--delete-globals'], |
745 | 776 |
params_by_str['--clear'], |
746 | 777 |
*options_in_group[StorageManagementOption]] |
747 | 778 |
for param in no_sv_options: |
748 | 779 |
if is_param_set(param) and service: |
749 | 780 |
opt_str = param.opts[0] |
750 |
- raise click.UsageError( |
|
751 |
- f'{opt_str} does not take a SERVICE argument') |
|
781 |
+ msg = f'{opt_str} does not take a SERVICE argument' |
|
782 |
+ raise click.UsageError(msg) |
|
752 | 783 |
|
753 | 784 |
if edit_notes: |
754 | 785 |
assert service is not None |
... | ... |
@@ -854,17 +885,14 @@ def derivepassphrase( |
854 | 885 |
view['phrase'] = phrase |
855 | 886 |
for m in view.maps: |
856 | 887 |
m.pop('key', '') |
857 |
- if service: |
|
858 | 888 |
if not view.maps[0]: |
859 |
- raise click.UsageError('cannot update service settings ' |
|
860 |
- 'without actual settings') |
|
861 |
- else: |
|
889 |
+ settings_type = 'service' if service else 'global' |
|
890 |
+ msg = (f'cannot update {settings_type} settings without ' |
|
891 |
+ f'actual settings') |
|
892 |
+ raise click.UsageError(msg) |
|
893 |
+ if service: |
|
862 | 894 |
configuration['services'].setdefault( |
863 | 895 |
service, {}).update(view) # type: ignore[typeddict-item] |
864 |
- else: |
|
865 |
- if not view.maps[0]: |
|
866 |
- raise click.UsageError('cannot update global settings ' |
|
867 |
- 'without actual settings') |
|
868 | 896 |
else: |
869 | 897 |
configuration.setdefault( |
870 | 898 |
'global', {}).update(view) # type: ignore[typeddict-item] |
... | ... |
@@ -874,7 +902,8 @@ def derivepassphrase( |
874 | 902 |
_save_config(configuration) |
875 | 903 |
else: |
876 | 904 |
if not service: |
877 |
- raise click.UsageError('SERVICE is required') |
|
905 |
+ msg = 'SERVICE is required' |
|
906 |
+ raise click.UsageError(msg) |
|
878 | 907 |
kwargs: dict[str, Any] = {k: v for k, v in settings.items() |
879 | 908 |
if k in service_keys and v is not None} |
880 | 909 |
# If either --key or --phrase are given, use that setting. |
... | ... |
@@ -906,9 +935,9 @@ def derivepassphrase( |
906 | 935 |
elif kwargs.get('phrase'): |
907 | 936 |
pass |
908 | 937 |
else: |
909 |
- raise click.UsageError( |
|
910 |
- 'no passphrase or key given on command-line ' |
|
938 |
+ msg = ('no passphrase or key given on command-line ' |
|
911 | 939 |
'or in configuration') |
940 |
+ raise click.UsageError(msg) |
|
912 | 941 |
vault = dpp.Vault(**kwargs) |
913 | 942 |
result = vault.generate(service) |
914 | 943 |
click.echo(result.decode('ASCII')) |
... | ... |
@@ -8,8 +8,13 @@ |
8 | 8 |
|
9 | 9 |
from __future__ import annotations |
10 | 10 |
|
11 |
+from typing import TypeGuard |
|
12 |
+ |
|
11 | 13 |
from typing_extensions import ( |
12 |
- Any, NotRequired, Required, TypedDict, TypeGuard, |
|
14 |
+ Any, |
|
15 |
+ NotRequired, |
|
16 |
+ Required, |
|
17 |
+ TypedDict, |
|
13 | 18 |
) |
14 | 19 |
|
15 | 20 |
import derivepassphrase |
... | ... |
@@ -19,13 +19,18 @@ thoroughly documented. |
19 | 19 |
|
20 | 20 |
""" |
21 | 21 |
|
22 |
+# ruff: noqa: RUF002,RUF003 |
|
23 |
+ |
|
22 | 24 |
from __future__ import annotations |
23 | 25 |
|
24 | 26 |
import collections |
27 |
+from typing import TYPE_CHECKING |
|
25 | 28 |
|
26 |
-from collections.abc import Iterator, Sequence |
|
27 | 29 |
from typing_extensions import assert_type |
28 | 30 |
|
31 |
+if TYPE_CHECKING: |
|
32 |
+ from collections.abc import Iterator, Sequence |
|
33 |
+ |
|
29 | 34 |
__all__ = ('Sequin', 'SequinExhaustedError') |
30 | 35 |
__author__ = "Marco Ricci <m@the13thletter.info>" |
31 | 36 |
__version__ = "0.1.1" |
... | ... |
@@ -78,6 +83,7 @@ class Sequin: |
78 | 83 |
range. |
79 | 84 |
|
80 | 85 |
""" |
86 |
+ msg = 'sequence item out of range' |
|
81 | 87 |
def uint8_to_bits(value): |
82 | 88 |
"""Yield individual bits of an 8-bit number, MSB first.""" |
83 | 89 |
for i in (0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01): |
... | ... |
@@ -86,7 +92,7 @@ class Sequin: |
86 | 92 |
try: |
87 | 93 |
sequence = tuple(sequence.encode('iso-8859-1')) |
88 | 94 |
except UnicodeError as e: |
89 |
- raise ValueError('sequence item out of range') from e |
|
95 |
+ raise ValueError(msg) from e |
|
90 | 96 |
else: |
91 | 97 |
sequence = tuple(sequence) |
92 | 98 |
assert_type(sequence, tuple[int, ...]) |
... | ... |
@@ -94,7 +100,7 @@ class Sequin: |
94 | 100 |
def gen() -> Iterator[int]: |
95 | 101 |
for num in sequence: |
96 | 102 |
if num not in range(2 if is_bitstring else 256): |
97 |
- raise ValueError('sequence item out of range') |
|
103 |
+ raise ValueError(msg) |
|
98 | 104 |
if is_bitstring: |
99 | 105 |
yield num |
100 | 106 |
else: |
... | ... |
@@ -146,7 +152,7 @@ class Sequin: |
146 | 152 |
return () |
147 | 153 |
stash: collections.deque[int] = collections.deque() |
148 | 154 |
try: |
149 |
- for i in range(count): |
|
155 |
+ for _ in range(count): |
|
150 | 156 |
stash.append(seq.popleft()) |
151 | 157 |
except IndexError: |
152 | 158 |
seq.extendleft(reversed(stash)) |
... | ... |
@@ -183,8 +189,9 @@ class Sequin: |
183 | 189 |
True |
184 | 190 |
|
185 | 191 |
""" |
186 |
- if base < 2: |
|
187 |
- raise ValueError(f'invalid base: {base!r}') |
|
192 |
+ if base < 2: # noqa: PLR2004 |
|
193 |
+ msg = f'invalid base: {base!r}' |
|
194 |
+ raise ValueError(msg) |
|
188 | 195 |
ret = 0 |
189 | 196 |
allowed_range = range(base) |
190 | 197 |
n = len(digits) |
... | ... |
@@ -192,9 +199,11 @@ class Sequin: |
192 | 199 |
i2 = (n - 1) - i |
193 | 200 |
x = digits[i] |
194 | 201 |
if not isinstance(x, int): |
195 |
- raise TypeError(f'not an integer: {x!r}') |
|
202 |
+ msg = f'not an integer: {x!r}' |
|
203 |
+ raise TypeError(msg) |
|
196 | 204 |
if x not in allowed_range: |
197 |
- raise ValueError(f'invalid base {base!r} digit: {x!r}') |
|
205 |
+ msg = f'invalid base {base!r} digit: {x!r}' |
|
206 |
+ raise ValueError(msg) |
|
198 | 207 |
ret += (base ** i2) * x |
199 | 208 |
return ret |
200 | 209 |
|
... | ... |
@@ -250,11 +259,11 @@ class Sequin: |
250 | 259 |
SequinExhaustedError: Sequin is exhausted |
251 | 260 |
|
252 | 261 |
""" |
253 |
- if 2 not in self.bases: |
|
254 |
- raise SequinExhaustedError('Sequin is exhausted') |
|
262 |
+ if 2 not in self.bases: # noqa: PLR2004 |
|
263 |
+ raise SequinExhaustedError |
|
255 | 264 |
value = self._generate_inner(n, base=2) |
256 | 265 |
if value == n: |
257 |
- raise SequinExhaustedError('Sequin is exhausted') |
|
266 |
+ raise SequinExhaustedError |
|
258 | 267 |
return value |
259 | 268 |
|
260 | 269 |
def _generate_inner( |
... | ... |
@@ -319,9 +328,11 @@ class Sequin: |
319 | 328 |
|
320 | 329 |
""" |
321 | 330 |
if n < 1: |
322 |
- raise ValueError('invalid target range') |
|
323 |
- if base < 2: |
|
324 |
- raise ValueError(f'invalid base: {base!r}') |
|
331 |
+ msg = 'invalid target range' |
|
332 |
+ raise ValueError(msg) |
|
333 |
+ if base < 2: # noqa: PLR2004 |
|
334 |
+ msg = f'invalid base: {base!r}' |
|
335 |
+ raise ValueError(msg) |
|
325 | 336 |
# p = base ** k, where k is the smallest integer such that |
326 | 337 |
# p >= n. We determine p and k inductively. |
327 | 338 |
p = 1 |
... | ... |
@@ -340,7 +351,6 @@ class Sequin: |
340 | 351 |
if not list_slice: |
341 | 352 |
if n != 1: |
342 | 353 |
return n |
343 |
- else: |
|
344 | 354 |
v = 0 |
345 | 355 |
v = self._big_endian_number(list_slice, base=base) |
346 | 356 |
if v > n - 1: |
... | ... |
@@ -365,3 +375,5 @@ class SequinExhaustedError(Exception): |
365 | 375 |
No more values can be generated from this sequin. |
366 | 376 |
|
367 | 377 |
""" |
378 |
+ def __init__(self): |
|
379 |
+ super().__init__('Sequin is exhausted') |
... | ... |
@@ -10,20 +10,30 @@ import collections |
10 | 10 |
import errno |
11 | 11 |
import os |
12 | 12 |
import socket |
13 |
+from typing import TYPE_CHECKING |
|
13 | 14 |
|
14 |
-from collections.abc import Sequence |
|
15 |
-from typing_extensions import Any, Self |
|
15 |
+from typing_extensions import Self |
|
16 |
+ |
|
17 |
+from ssh_agent_client import types as ssh_types |
|
16 | 18 |
|
17 |
-from ssh_agent_client import types |
|
19 |
+if TYPE_CHECKING: |
|
20 |
+ import types |
|
21 |
+ from collections.abc import Sequence |
|
18 | 22 |
|
19 | 23 |
__all__ = ('SSHAgentClient',) |
20 | 24 |
__author__ = "Marco Ricci <m@the13thletter.info>" |
21 | 25 |
__version__ = "0.1.1" |
22 | 26 |
|
27 |
+# In SSH bytestrings, the "length" of the byte string is stored as |
|
28 |
+# a 4-byte/32-bit unsigned integer at the beginning. |
|
29 |
+HEAD_LEN = 4 |
|
30 |
+ |
|
23 | 31 |
_socket = socket |
24 | 32 |
|
25 | 33 |
class TrailingDataError(RuntimeError): |
26 | 34 |
"""The result contained trailing data.""" |
35 |
+ def __init__(self): |
|
36 |
+ super().__init__('Overlong response from SSH agent') |
|
27 | 37 |
|
28 | 38 |
class SSHAgentClient: |
29 | 39 |
"""A bare-bones SSH agent client supporting signing and key listing. |
... | ... |
@@ -76,7 +86,8 @@ class SSHAgentClient: |
76 | 86 |
if e.errno != errno.ENOTCONN: # pragma: no cover |
77 | 87 |
raise |
78 | 88 |
if 'SSH_AUTH_SOCK' not in os.environ: |
79 |
- raise KeyError('SSH_AUTH_SOCK environment variable') |
|
89 |
+ msg = 'SSH_AUTH_SOCK environment variable' |
|
90 |
+ raise KeyError(msg) from None |
|
80 | 91 |
ssh_auth_sock = os.environ['SSH_AUTH_SOCK'] |
81 | 92 |
self._connection.settimeout(timeout) |
82 | 93 |
self._connection.connect(ssh_auth_sock) |
... | ... |
@@ -87,7 +98,10 @@ class SSHAgentClient: |
87 | 98 |
return self |
88 | 99 |
|
89 | 100 |
def __exit__( |
90 |
- self, exc_type: Any, exc_val: Any, exc_tb: Any |
|
101 |
+ self, |
|
102 |
+ exc_type: type[BaseException] | None, |
|
103 |
+ exc_val: BaseException | None, |
|
104 |
+ exc_tb: types.TracebackType | None, |
|
91 | 105 |
) -> bool: |
92 | 106 |
"""Close socket connection upon context manager completion.""" |
93 | 107 |
return bool( |
... | ... |
@@ -136,9 +150,10 @@ class SSHAgentClient: |
136 | 150 |
ret = bytearray() |
137 | 151 |
ret.extend(cls.uint32(len(payload))) |
138 | 152 |
ret.extend(payload) |
139 |
- return ret |
|
140 | 153 |
except Exception as e: |
141 |
- raise TypeError('invalid payload type') from e |
|
154 |
+ msg = 'invalid payload type' |
|
155 |
+ raise TypeError(msg) from e |
|
156 |
+ return ret |
|
142 | 157 |
|
143 | 158 |
@classmethod |
144 | 159 |
def unstring(cls, bytestring: bytes | bytearray, /) -> bytes | bytearray: |
... | ... |
@@ -163,11 +178,14 @@ class SSHAgentClient: |
163 | 178 |
|
164 | 179 |
""" |
165 | 180 |
n = len(bytestring) |
166 |
- if n < 4: |
|
167 |
- raise ValueError('malformed SSH byte string') |
|
168 |
- elif n != 4 + int.from_bytes(bytestring[:4], 'big', signed=False): |
|
169 |
- raise ValueError('malformed SSH byte string') |
|
170 |
- return bytestring[4:] |
|
181 |
+ msg = 'malformed SSH byte string' |
|
182 |
+ if ( |
|
183 |
+ n < HEAD_LEN |
|
184 |
+ or n != HEAD_LEN + int.from_bytes(bytestring[:HEAD_LEN], 'big', |
|
185 |
+ signed=False) |
|
186 |
+ ): |
|
187 |
+ raise ValueError(msg) |
|
188 |
+ return bytestring[HEAD_LEN:] |
|
171 | 189 |
|
172 | 190 |
@classmethod |
173 | 191 |
def unstring_prefix( |
... | ... |
@@ -201,12 +219,13 @@ class SSHAgentClient: |
201 | 219 |
|
202 | 220 |
""" |
203 | 221 |
n = len(bytestring) |
204 |
- if n < 4: |
|
205 |
- raise ValueError('malformed SSH byte string') |
|
206 |
- m = int.from_bytes(bytestring[:4], 'big', signed=False) |
|
207 |
- if m + 4 > n: |
|
208 |
- raise ValueError('malformed SSH byte string') |
|
209 |
- return (bytestring[4:m + 4], bytestring[m + 4:]) |
|
222 |
+ msg = 'malformed SSH byte string' |
|
223 |
+ if n < HEAD_LEN: |
|
224 |
+ raise ValueError(msg) |
|
225 |
+ m = int.from_bytes(bytestring[:HEAD_LEN], 'big', signed=False) |
|
226 |
+ if m + HEAD_LEN > n: |
|
227 |
+ raise ValueError(msg) |
|
228 |
+ return (bytestring[HEAD_LEN:m + HEAD_LEN], bytestring[m + HEAD_LEN:]) |
|
210 | 229 |
|
211 | 230 |
def request( |
212 | 231 |
self, code: int, payload: bytes | bytearray, / |
... | ... |
@@ -236,16 +255,18 @@ class SSHAgentClient: |
236 | 255 |
request_message = bytearray([code]) |
237 | 256 |
request_message.extend(payload) |
238 | 257 |
self._connection.sendall(self.string(request_message)) |
239 |
- chunk = self._connection.recv(4) |
|
240 |
- if len(chunk) < 4: |
|
241 |
- raise EOFError('cannot read response length') |
|
258 |
+ chunk = self._connection.recv(HEAD_LEN) |
|
259 |
+ if len(chunk) < HEAD_LEN: |
|
260 |
+ msg = 'cannot read response length' |
|
261 |
+ raise EOFError(msg) |
|
242 | 262 |
response_length = int.from_bytes(chunk, 'big', signed=False) |
243 | 263 |
response = self._connection.recv(response_length) |
244 | 264 |
if len(response) < response_length: |
245 |
- raise EOFError('truncated response from SSH agent') |
|
265 |
+ msg = 'truncated response from SSH agent' |
|
266 |
+ raise EOFError(msg) |
|
246 | 267 |
return response[0], response[1:] |
247 | 268 |
|
248 |
- def list_keys(self) -> Sequence[types.KeyCommentPair]: |
|
269 |
+ def list_keys(self) -> Sequence[ssh_types.KeyCommentPair]: |
|
249 | 270 |
"""Request a list of keys known to the SSH agent. |
250 | 271 |
|
251 | 272 |
Returns: |
... | ... |
@@ -261,37 +282,35 @@ class SSHAgentClient: |
261 | 282 |
|
262 | 283 |
""" |
263 | 284 |
response_code, response = self.request( |
264 |
- types.SSH_AGENTC.REQUEST_IDENTITIES.value, b'') |
|
265 |
- if response_code != types.SSH_AGENT.IDENTITIES_ANSWER.value: |
|
266 |
- raise RuntimeError( |
|
267 |
- f'error return from SSH agent: ' |
|
268 |
- f'{response_code = }, {response = }' |
|
269 |
- ) |
|
285 |
+ ssh_types.SSH_AGENTC.REQUEST_IDENTITIES.value, b'') |
|
286 |
+ if response_code != ssh_types.SSH_AGENT.IDENTITIES_ANSWER.value: |
|
287 |
+ msg = (f'error return from SSH agent: ' |
|
288 |
+ f'{response_code = }, {response = }') |
|
289 |
+ raise RuntimeError(msg) |
|
270 | 290 |
response_stream = collections.deque(response) |
271 | 291 |
def shift(num: int) -> bytes: |
272 |
- buf = collections.deque(bytes()) |
|
273 |
- for i in range(num): |
|
292 |
+ buf = collections.deque(b'') |
|
293 |
+ for _ in range(num): |
|
274 | 294 |
try: |
275 | 295 |
val = response_stream.popleft() |
276 | 296 |
except IndexError: |
277 | 297 |
response_stream.extendleft(reversed(buf)) |
278 |
- raise EOFError( |
|
279 |
- 'truncated response from SSH agent' |
|
280 |
- ) from None |
|
298 |
+ msg = 'truncated response from SSH agent' |
|
299 |
+ raise EOFError(msg) from None |
|
281 | 300 |
buf.append(val) |
282 | 301 |
return bytes(buf) |
283 | 302 |
key_count = int.from_bytes(shift(4), 'big') |
284 |
- keys: collections.deque[types.KeyCommentPair] |
|
303 |
+ keys: collections.deque[ssh_types.KeyCommentPair] |
|
285 | 304 |
keys = collections.deque() |
286 |
- for i in range(key_count): |
|
305 |
+ for _ in range(key_count): |
|
287 | 306 |
key_size = int.from_bytes(shift(4), 'big') |
288 | 307 |
key = shift(key_size) |
289 | 308 |
comment_size = int.from_bytes(shift(4), 'big') |
290 | 309 |
comment = shift(comment_size) |
291 | 310 |
# Both `key` and `comment` are not wrapped as SSH strings. |
292 |
- keys.append(types.KeyCommentPair(key, comment)) |
|
311 |
+ keys.append(ssh_types.KeyCommentPair(key, comment)) |
|
293 | 312 |
if response_stream: |
294 |
- raise TrailingDataError('overlong response from SSH agent') |
|
313 |
+ raise TrailingDataError |
|
295 | 314 |
return keys |
296 | 315 |
|
297 | 316 |
def sign( |
... | ... |
@@ -336,14 +355,14 @@ class SSHAgentClient: |
336 | 355 |
if check_if_key_loaded: |
337 | 356 |
loaded_keys = frozenset({pair.key for pair in self.list_keys()}) |
338 | 357 |
if bytes(key) not in loaded_keys: |
339 |
- raise KeyError('target SSH key not loaded into agent') |
|
358 |
+ msg = 'target SSH key not loaded into agent' |
|
359 |
+ raise KeyError(msg) |
|
340 | 360 |
request_data = bytearray(self.string(key)) |
341 | 361 |
request_data.extend(self.string(payload)) |
342 | 362 |
request_data.extend(self.uint32(flags)) |
343 | 363 |
response_code, response = self.request( |
344 |
- types.SSH_AGENTC.SIGN_REQUEST.value, request_data) |
|
345 |
- if response_code != types.SSH_AGENT.SIGN_RESPONSE.value: |
|
346 |
- raise RuntimeError( |
|
347 |
- f'signing data failed: {response_code = }, {response = }' |
|
348 |
- ) |
|
364 |
+ ssh_types.SSH_AGENTC.SIGN_REQUEST.value, request_data) |
|
365 |
+ if response_code != ssh_types.SSH_AGENT.SIGN_RESPONSE.value: |
|
366 |
+ msg = f'signing data failed: {response_code = }, {response = }' |
|
367 |
+ raise RuntimeError(msg) |
|
349 | 368 |
return self.unstring(response) |
... | ... |
@@ -7,10 +7,9 @@ |
7 | 7 |
from __future__ import annotations |
8 | 8 |
|
9 | 9 |
import enum |
10 |
- |
|
11 | 10 |
from typing import NamedTuple |
12 | 11 |
|
13 |
-__all__ = ('KeyCommentPair', 'SSH_AGENT', 'SSH_AGENTC') |
|
12 |
+__all__ = ('SSH_AGENT', 'SSH_AGENTC', 'KeyCommentPair') |
|
14 | 13 |
|
15 | 14 |
class KeyCommentPair(NamedTuple): |
16 | 15 |
"""SSH key plus comment pair. For typing purposes. |
... | ... |
@@ -23,7 +22,7 @@ class KeyCommentPair(NamedTuple): |
23 | 22 |
key: bytes | bytearray |
24 | 23 |
comment: bytes | bytearray |
25 | 24 |
|
26 |
-class SSH_AGENTC(enum.Enum): |
|
25 |
+class SSH_AGENTC(enum.Enum): # noqa: N801 |
|
27 | 26 |
"""SSH agent protocol numbers: client requests. |
28 | 27 |
|
29 | 28 |
Attributes: |
... | ... |
@@ -36,7 +35,7 @@ class SSH_AGENTC(enum.Enum): |
36 | 35 |
REQUEST_IDENTITIES: int = 11 |
37 | 36 |
SIGN_REQUEST: int = 13 |
38 | 37 |
|
39 |
-class SSH_AGENT(enum.Enum): |
|
38 |
+class SSH_AGENT(enum.Enum): # noqa: N801 |
|
40 | 39 |
"""SSH agent protocol numbers: server replies. |
41 | 40 |
|
42 | 41 |
Attributes: |
... | ... |
@@ -5,24 +5,27 @@ |
5 | 5 |
from __future__ import annotations |
6 | 6 |
|
7 | 7 |
import base64 |
8 |
-from collections.abc import Iterator, Mapping |
|
9 | 8 |
import contextlib |
10 | 9 |
import json |
11 | 10 |
import os |
12 | 11 |
from typing import TYPE_CHECKING |
13 |
-from typing_extensions import Any, TypedDict |
|
14 | 12 |
|
15 |
-import click.testing |
|
13 |
+import pytest |
|
14 |
+ |
|
16 | 15 |
import derivepassphrase |
17 | 16 |
import derivepassphrase.cli |
18 | 17 |
import derivepassphrase.types |
19 |
-import pytest |
|
20 | 18 |
import ssh_agent_client |
21 | 19 |
import ssh_agent_client.types |
22 | 20 |
|
23 | 21 |
__all__ = () |
24 | 22 |
|
25 | 23 |
if TYPE_CHECKING: |
24 |
+ from collections.abc import Iterator, Mapping |
|
25 |
+ |
|
26 |
+ import click.testing |
|
27 |
+ from typing_extensions import Any, TypedDict |
|
28 |
+ |
|
26 | 29 |
class SSHTestKey(TypedDict): |
27 | 30 |
private_key: bytes |
28 | 31 |
public_key: bytes | str |
... | ... |
@@ -343,7 +346,8 @@ skip_if_no_agent = pytest.mark.skipif( |
343 | 346 |
def list_keys( |
344 | 347 |
self: Any = None, |
345 | 348 |
) -> list[ssh_agent_client.types.KeyCommentPair]: |
346 |
- Pair = ssh_agent_client.types.KeyCommentPair |
|
349 |
+ del self # Unused. |
|
350 |
+ Pair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
|
347 | 351 |
list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
348 | 352 |
for key, value in SUPPORTED_KEYS.items()] |
349 | 353 |
list2 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
... | ... |
@@ -353,7 +357,8 @@ def list_keys( |
353 | 357 |
def list_keys_singleton( |
354 | 358 |
self: Any = None, |
355 | 359 |
) -> list[ssh_agent_client.types.KeyCommentPair]: |
356 |
- Pair = ssh_agent_client.types.KeyCommentPair |
|
360 |
+ del self # Unused. |
|
361 |
+ Pair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
|
357 | 362 |
list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII')) |
358 | 363 |
for key, value in SUPPORTED_KEYS.items()] |
359 | 364 |
return list1[:1] |
... | ... |
@@ -361,6 +366,7 @@ def list_keys_singleton( |
361 | 366 |
def suitable_ssh_keys( |
362 | 367 |
conn: Any |
363 | 368 |
) -> Iterator[ssh_agent_client.types.KeyCommentPair]: |
369 |
+ del conn # Unused. |
|
364 | 370 |
yield from [ |
365 | 371 |
ssh_agent_client.types.KeyCommentPair(DUMMY_KEY1, b'no comment'), |
366 | 372 |
ssh_agent_client.types.KeyCommentPair(DUMMY_KEY2, b'a comment'), |
... | ... |
@@ -375,17 +381,23 @@ def phrase_from_key(key: bytes) -> bytes: |
375 | 381 |
def isolated_config( |
376 | 382 |
monkeypatch: Any, runner: click.testing.CliRunner, config: Any, |
377 | 383 |
): |
378 |
- prog_name = derivepassphrase.cli.prog_name |
|
384 |
+ prog_name = derivepassphrase.cli.PROG_NAME |
|
379 | 385 |
env_name = prog_name.replace(' ', '_').upper() + '_PATH' |
380 | 386 |
with runner.isolated_filesystem(): |
381 | 387 |
monkeypatch.setenv('HOME', os.getcwd()) |
382 | 388 |
monkeypatch.setenv('USERPROFILE', os.getcwd()) |
383 | 389 |
monkeypatch.delenv(env_name, raising=False) |
384 |
- os.makedirs(os.path.dirname(derivepassphrase.cli._config_filename()), |
|
385 |
- exist_ok=True) |
|
386 |
- with open(derivepassphrase.cli._config_filename(), 'wt') as outfile: |
|
390 |
+ os.makedirs( |
|
391 |
+ os.path.dirname(derivepassphrase.cli._config_filename()), |
|
392 |
+ exist_ok=True, |
|
393 |
+ ) |
|
394 |
+ with open( |
|
395 |
+ derivepassphrase.cli._config_filename(), |
|
396 |
+ 'w', encoding='UTF-8', |
|
397 |
+ ) as outfile: |
|
387 | 398 |
json.dump(config, outfile) |
388 | 399 |
yield |
389 | 400 |
|
390 | 401 |
def auto_prompt(*args: Any, **kwargs: Any) -> str: |
402 |
+ del args, kwargs # Unused. |
|
391 | 403 |
return DUMMY_PASSPHRASE.decode('UTF-8') |
... | ... |
@@ -9,9 +9,10 @@ from __future__ import annotations |
9 | 9 |
import math |
10 | 10 |
from typing import Any |
11 | 11 |
|
12 |
-import derivepassphrase |
|
13 | 12 |
import pytest |
14 | 13 |
|
14 |
+import derivepassphrase |
|
15 |
+ |
|
15 | 16 |
Vault = derivepassphrase.Vault |
16 | 17 |
|
17 | 18 |
class TestVault: |
... | ... |
@@ -106,7 +107,7 @@ class TestVault: |
106 | 107 |
def test_219_very_limited_character_set(self): |
107 | 108 |
generated = Vault(phrase=b'', length=24, lower=0, upper=0, |
108 | 109 |
space=0, symbol=0).generate('testing') |
109 |
- assert b'763252593304946694588866' == generated |
|
110 |
+ assert generated == b'763252593304946694588866' |
|
110 | 111 |
|
111 | 112 |
def test_220_character_set_subtraction(self): |
112 | 113 |
assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf') |
... | ... |
@@ -152,21 +153,21 @@ class TestVault: |
152 | 153 |
v = Vault(phrase=self.phrase) |
153 | 154 |
monkeypatch.setattr(v, |
154 | 155 |
'_estimate_sufficient_hash_length', |
155 |
- lambda *args, **kwargs: 1) |
|
156 |
+ lambda *args, **kwargs: 1) # noqa: ARG005 |
|
156 | 157 |
assert v._estimate_sufficient_hash_length() < len(self.phrase) |
157 | 158 |
assert v.generate(service) == expected |
158 | 159 |
|
159 | 160 |
@pytest.mark.parametrize(['s', 'raises'], [ |
160 | 161 |
('ñ', True), ('Düsseldorf', True), |
161 | 162 |
('liberté, egalité, fraternité', True), ('ASCII', False), |
162 |
- ('Düsseldorf'.encode('UTF-8'), False), |
|
163 |
+ (b'D\xc3\xbcsseldorf', False), |
|
163 | 164 |
(bytearray([2, 3, 5, 7, 11, 13]), False), |
164 | 165 |
]) |
165 | 166 |
def test_224_binary_strings( |
166 | 167 |
self, s: str | bytes | bytearray, raises: bool |
167 | 168 |
) -> None: |
168 | 169 |
binstr = derivepassphrase.Vault._get_binary_string |
169 |
- AmbiguousByteRepresentationError = ( |
|
170 |
+ AmbiguousByteRepresentationError = ( # noqa: N806 |
|
170 | 171 |
derivepassphrase.AmbiguousByteRepresentationError |
171 | 172 |
) |
172 | 173 |
if raises: |
... | ... |
@@ -4,18 +4,25 @@ |
4 | 4 |
|
5 | 5 |
from __future__ import annotations |
6 | 6 |
|
7 |
-from collections.abc import Callable |
|
7 |
+import contextlib |
|
8 | 8 |
import json |
9 | 9 |
import os |
10 | 10 |
import socket |
11 |
-from typing_extensions import Any, cast, NamedTuple |
|
11 |
+from typing import TYPE_CHECKING, cast |
|
12 | 12 |
|
13 | 13 |
import click.testing |
14 |
-import derivepassphrase as dpp |
|
15 |
-import derivepassphrase.cli as cli |
|
16 |
-import ssh_agent_client.types |
|
17 | 14 |
import pytest |
15 |
+from typing_extensions import NamedTuple |
|
16 |
+ |
|
17 |
+import derivepassphrase as dpp |
|
18 |
+import ssh_agent_client |
|
18 | 19 |
import tests |
20 |
+from derivepassphrase import cli |
|
21 |
+ |
|
22 |
+if TYPE_CHECKING: |
|
23 |
+ from collections.abc import Callable |
|
24 |
+ |
|
25 |
+ from typing_extensions import Any |
|
19 | 26 |
|
20 | 27 |
DUMMY_SERVICE = tests.DUMMY_SERVICE |
21 | 28 |
DUMMY_PASSPHRASE = tests.DUMMY_PASSPHRASE |
... | ... |
@@ -64,7 +72,7 @@ STORAGE_OPTIONS: list[tuple[str, ...]] = [ |
64 | 72 |
] |
65 | 73 |
INCOMPATIBLE: dict[tuple[str, ...], IncompatibleConfiguration] = { |
66 | 74 |
('--phrase',): IncompatibleConfiguration( |
67 |
- [('--key',)] + CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
|
75 |
+ [('--key',), *CONFIGURATION_COMMANDS, *STORAGE_OPTIONS], |
|
68 | 76 |
True, DUMMY_PASSPHRASE), |
69 | 77 |
('--key',): IncompatibleConfiguration( |
70 | 78 |
CONFIGURATION_COMMANDS + STORAGE_OPTIONS, |
... | ... |
@@ -95,16 +103,16 @@ INCOMPATIBLE: dict[tuple[str, ...], IncompatibleConfiguration] = { |
95 | 103 |
True, DUMMY_PASSPHRASE), |
96 | 104 |
('--notes',): IncompatibleConfiguration( |
97 | 105 |
[('--config',), ('--delete',), ('--delete-globals',), |
98 |
- ('--clear',)] + STORAGE_OPTIONS, |
|
106 |
+ ('--clear',), *STORAGE_OPTIONS], |
|
99 | 107 |
True, None), |
100 | 108 |
('--config', '-p'): IncompatibleConfiguration( |
101 | 109 |
[('--delete',), ('--delete-globals',), |
102 |
- ('--clear',)] + STORAGE_OPTIONS, |
|
110 |
+ ('--clear',), *STORAGE_OPTIONS], |
|
103 | 111 |
None, DUMMY_PASSPHRASE), |
104 | 112 |
('--delete',): IncompatibleConfiguration( |
105 |
- [('--delete-globals',), ('--clear',)] + STORAGE_OPTIONS, True, None), |
|
113 |
+ [('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], True, None), |
|
106 | 114 |
('--delete-globals',): IncompatibleConfiguration( |
107 |
- [('--clear',)] + STORAGE_OPTIONS, False, None), |
|
115 |
+ [('--clear',), *STORAGE_OPTIONS], False, None), |
|
108 | 116 |
('--clear',): IncompatibleConfiguration(STORAGE_OPTIONS, False, None), |
109 | 117 |
('--export', '-'): IncompatibleConfiguration( |
110 | 118 |
[('--import', '-')], False, None), |
... | ... |
@@ -163,9 +171,9 @@ class TestCLI: |
163 | 171 |
'Option group epilog not printed.' |
164 | 172 |
) |
165 | 173 |
|
166 |
- @pytest.mark.parametrize(['charset_name'], |
|
167 |
- [('lower',), ('upper',), ('number',), ('space',), |
|
168 |
- ('dash',), ('symbol',)]) |
|
174 |
+ @pytest.mark.parametrize('charset_name', |
|
175 |
+ ['lower', 'upper', 'number', 'space', |
|
176 |
+ 'dash', 'symbol']) |
|
169 | 177 |
def test_201_disable_character_set( |
170 | 178 |
self, monkeypatch: Any, charset_name: str |
171 | 179 |
) -> None: |
... | ... |
@@ -207,7 +215,7 @@ class TestCLI: |
207 | 215 |
f'at position {i}: {result.stdout!r}' |
208 | 216 |
) |
209 | 217 |
|
210 |
- @pytest.mark.parametrize(['config'], [ |
|
218 |
+ @pytest.mark.parametrize('config', [ |
|
211 | 219 |
pytest.param({'global': {'key': DUMMY_KEY1_B64}, |
212 | 220 |
'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}}, |
213 | 221 |
id='global'), |
... | ... |
@@ -287,10 +295,10 @@ class TestCLI: |
287 | 295 |
'program generated unexpected result (wrong settings?)' |
288 | 296 |
) |
289 | 297 |
|
290 |
- @pytest.mark.parametrize(['option'], |
|
291 |
- [('--lower',), ('--upper',), ('--number',), |
|
292 |
- ('--space',), ('--dash',), ('--symbol',), |
|
293 |
- ('--repeat',), ('--length',)]) |
|
298 |
+ @pytest.mark.parametrize('option', |
|
299 |
+ ['--lower', '--upper', '--number', |
|
300 |
+ '--space', '--dash', '--symbol', |
|
301 |
+ '--repeat', '--length']) |
|
294 | 302 |
def test_210_invalid_argument_range(self, option: str) -> None: |
295 | 303 |
runner = click.testing.CliRunner(mix_stderr=False) |
296 | 304 |
value: str | int |
... | ... |
@@ -326,7 +334,7 @@ class TestCLI: |
326 | 334 |
'services': {}}): |
327 | 335 |
result = runner.invoke(cli.derivepassphrase, |
328 | 336 |
options if service |
329 |
- else options + [DUMMY_SERVICE], |
|
337 |
+ else [*options, DUMMY_SERVICE], |
|
330 | 338 |
input=input, catch_exceptions=False) |
331 | 339 |
if service is not None: |
332 | 340 |
assert result.exit_code > 0, ( |
... | ... |
@@ -351,7 +359,7 @@ class TestCLI: |
351 | 359 |
monkeypatch.setattr(cli, '_prompt_for_passphrase', |
352 | 360 |
tests.auto_prompt) |
353 | 361 |
result = runner.invoke(cli.derivepassphrase, |
354 |
- options + [DUMMY_SERVICE] |
|
362 |
+ [*options, DUMMY_SERVICE] |
|
355 | 363 |
if service else options, |
356 | 364 |
input=input, catch_exceptions=False) |
357 | 365 |
assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
... | ... |
@@ -359,16 +367,16 @@ class TestCLI: |
359 | 367 |
) |
360 | 368 |
|
361 | 369 |
@pytest.mark.parametrize( |
362 |
- ['options', 'service', 'input'], |
|
363 |
- [(o.options, o.needs_service, o.input) |
|
370 |
+ ['options', 'service'], |
|
371 |
+ [(o.options, o.needs_service) |
|
364 | 372 |
for o in INTERESTING_OPTION_COMBINATIONS if o.incompatible], |
365 | 373 |
) |
366 | 374 |
def test_212_incompatible_options( |
367 |
- self, options: list[str], service: bool | None, input: bytes | None, |
|
375 |
+ self, options: list[str], service: bool | None, |
|
368 | 376 |
) -> None: |
369 | 377 |
runner = click.testing.CliRunner(mix_stderr=False) |
370 | 378 |
result = runner.invoke(cli.derivepassphrase, |
371 |
- options + [DUMMY_SERVICE] if service |
|
379 |
+ [*options, DUMMY_SERVICE] if service |
|
372 | 380 |
else options, |
373 | 381 |
input=DUMMY_PASSPHRASE, catch_exceptions=False) |
374 | 382 |
assert result.exit_code > 0, ( |
... | ... |
@@ -427,7 +435,8 @@ class TestCLI: |
427 | 435 |
# ourselves afterwards, inside the context. |
428 | 436 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
429 | 437 |
config={'services': {}}): |
430 |
- with open(cli._config_filename(), 'wt') as outfile: |
|
438 |
+ with open(cli._config_filename(), 'w', |
|
439 |
+ encoding='UTF-8') as outfile: |
|
431 | 440 |
print('This string is not valid JSON.', file=outfile) |
432 | 441 |
dname = os.path.dirname(cli._config_filename()) |
433 | 442 |
result = runner.invoke( |
... | ... |
@@ -449,10 +458,8 @@ class TestCLI: |
449 | 458 |
runner = click.testing.CliRunner(mix_stderr=False) |
450 | 459 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
451 | 460 |
config={'services': {}}): |
452 |
- try: |
|
461 |
+ with contextlib.suppress(FileNotFoundError): |
|
453 | 462 |
os.remove(cli._config_filename()) |
454 |
- except FileNotFoundError: # pragma: no cover |
|
455 |
- pass |
|
456 | 463 |
result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
457 | 464 |
catch_exceptions=False) |
458 | 465 |
assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
... | ... |
@@ -483,10 +490,8 @@ class TestCLI: |
483 | 490 |
runner = click.testing.CliRunner(mix_stderr=False) |
484 | 491 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
485 | 492 |
config={'services': {}}): |
486 |
- try: |
|
493 |
+ with contextlib.suppress(FileNotFoundError): |
|
487 | 494 |
os.remove(cli._config_filename()) |
488 |
- except FileNotFoundError: # pragma: no cover |
|
489 |
- pass |
|
490 | 495 |
os.makedirs(cli._config_filename()) |
491 | 496 |
result = runner.invoke(cli.derivepassphrase, ['--export', '-'], |
492 | 497 |
input=b'null', catch_exceptions=False) |
... | ... |
@@ -531,13 +536,13 @@ contents go here |
531 | 536 |
config={'global': {'phrase': 'abc'}, |
532 | 537 |
'services': {}}): |
533 | 538 |
monkeypatch.setattr(click, 'edit', |
534 |
- lambda *a, **kw: edit_result) |
|
539 |
+ lambda *a, **kw: edit_result) # noqa: ARG005 |
|
535 | 540 |
result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
536 | 541 |
catch_exceptions=False) |
537 | 542 |
assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
538 | 543 |
'program exited with failure' |
539 | 544 |
) |
540 |
- with open(cli._config_filename(), 'rt') as infile: |
|
545 |
+ with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
541 | 546 |
config = json.load(infile) |
542 | 547 |
assert config == {'global': {'phrase': 'abc'}, |
543 | 548 |
'services': {'sv': {'notes': |
... | ... |
@@ -548,13 +553,14 @@ contents go here |
548 | 553 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
549 | 554 |
config={'global': {'phrase': 'abc'}, |
550 | 555 |
'services': {}}): |
551 |
- monkeypatch.setattr(click, 'edit', lambda *a, **kw: None) |
|
556 |
+ monkeypatch.setattr(click, 'edit', |
|
557 |
+ lambda *a, **kw: None) # noqa: ARG005 |
|
552 | 558 |
result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
553 | 559 |
catch_exceptions=False) |
554 | 560 |
assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
555 | 561 |
'program exited with failure' |
556 | 562 |
) |
557 |
- with open(cli._config_filename(), 'rt') as infile: |
|
563 |
+ with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
558 | 564 |
config = json.load(infile) |
559 | 565 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}} |
560 | 566 |
|
... | ... |
@@ -563,13 +569,14 @@ contents go here |
563 | 569 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
564 | 570 |
config={'global': {'phrase': 'abc'}, |
565 | 571 |
'services': {}}): |
566 |
- monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext') |
|
572 |
+ monkeypatch.setattr(click, 'edit', |
|
573 |
+ lambda *a, **kw: 'long\ntext') # noqa: ARG005 |
|
567 | 574 |
result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
568 | 575 |
catch_exceptions=False) |
569 | 576 |
assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
570 | 577 |
'program exited with failure' |
571 | 578 |
) |
572 |
- with open(cli._config_filename(), 'rt') as infile: |
|
579 |
+ with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
573 | 580 |
config = json.load(infile) |
574 | 581 |
assert config == {'global': {'phrase': 'abc'}, |
575 | 582 |
'services': {'sv': {'notes': 'long\ntext'}}} |
... | ... |
@@ -579,7 +586,8 @@ contents go here |
579 | 586 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
580 | 587 |
config={'global': {'phrase': 'abc'}, |
581 | 588 |
'services': {}}): |
582 |
- monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n') |
|
589 |
+ monkeypatch.setattr(click, 'edit', |
|
590 |
+ lambda *a, **kw: '\n\n') # noqa: ARG005 |
|
583 | 591 |
result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'], |
584 | 592 |
catch_exceptions=False) |
585 | 593 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
... | ... |
@@ -587,7 +595,7 @@ contents go here |
587 | 595 |
assert b'user aborted request' in result.stderr_bytes, ( |
588 | 596 |
'expected error message missing' |
589 | 597 |
) |
590 |
- with open(cli._config_filename(), 'rt') as infile: |
|
598 |
+ with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
591 | 599 |
config = json.load(infile) |
592 | 600 |
assert config == {'global': {'phrase': 'abc'}, 'services': {}} |
593 | 601 |
|
... | ... |
@@ -632,10 +640,10 @@ contents go here |
632 | 640 |
monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
633 | 641 |
tests.suitable_ssh_keys) |
634 | 642 |
result = runner.invoke(cli.derivepassphrase, |
635 |
- ['--config'] + command_line, |
|
643 |
+ ['--config', *command_line], |
|
636 | 644 |
catch_exceptions=False, input=input) |
637 | 645 |
assert result.exit_code == 0, 'program exited with failure' |
638 |
- with open(cli._config_filename(), 'rt') as infile: |
|
646 |
+ with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
639 | 647 |
config = json.load(infile) |
640 | 648 |
assert config == result_config, ( |
641 | 649 |
'stored config does not match expectation' |
... | ... |
@@ -662,7 +670,7 @@ contents go here |
662 | 670 |
monkeypatch.setattr(cli, '_get_suitable_ssh_keys', |
663 | 671 |
tests.suitable_ssh_keys) |
664 | 672 |
result = runner.invoke(cli.derivepassphrase, |
665 |
- ['--config'] + command_line, |
|
673 |
+ ['--config', *command_line], |
|
666 | 674 |
catch_exceptions=False, input=input) |
667 | 675 |
assert result.exit_code != 0, 'program unexpectedly succeeded?!' |
668 | 676 |
assert result.stderr_bytes is not None |
... | ... |
@@ -677,15 +685,16 @@ contents go here |
677 | 685 |
with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
678 | 686 |
config={'global': {'phrase': 'abc'}, |
679 | 687 |
'services': {}}): |
688 |
+ custom_error = 'custom error message' |
|
680 | 689 |
def raiser(): |
681 |
- raise RuntimeError('custom error message') |
|
690 |
+ raise RuntimeError(custom_error) |
|
682 | 691 |
monkeypatch.setattr(cli, '_select_ssh_key', raiser) |
683 | 692 |
result = runner.invoke(cli.derivepassphrase, |
684 | 693 |
['--key', '--config'], |
685 | 694 |
catch_exceptions=False) |
686 | 695 |
assert result.exit_code != 0, 'program unexpectedly succeeded' |
687 | 696 |
assert result.stderr_bytes is not None |
688 |
- assert b'custom error message' in result.stderr_bytes, ( |
|
697 |
+ assert custom_error.encode() in result.stderr_bytes, ( |
|
689 | 698 |
'expected error message missing' |
690 | 699 |
) |
691 | 700 |
|
... | ... |
@@ -714,13 +723,15 @@ class TestCLIUtils: |
714 | 723 |
|
715 | 724 |
def test_100_save_bad_config(self, monkeypatch: Any) -> None: |
716 | 725 |
runner = click.testing.CliRunner() |
717 |
- with tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
718 |
- config={}): |
|
719 |
- with pytest.raises(ValueError, match='Invalid vault config'): |
|
726 |
+ with ( |
|
727 |
+ tests.isolated_config(monkeypatch=monkeypatch, runner=runner, |
|
728 |
+ config={}), |
|
729 |
+ pytest.raises(ValueError, match='Invalid vault config') |
|
730 |
+ ): |
|
720 | 731 |
cli._save_config(None) # type: ignore |
721 | 732 |
|
722 | 733 |
|
723 |
- def test_101_prompt_for_selection_multiple(self, monkeypatch: Any) -> None: |
|
734 |
+ def test_101_prompt_for_selection_multiple(self) -> None: |
|
724 | 735 |
@click.command() |
725 | 736 |
@click.option('--heading', default='Our menu:') |
726 | 737 |
@click.argument('items', nargs=-1) |
... | ... |
@@ -786,7 +797,7 @@ Your selection? (1-10, leave empty to abort): \n''', ( # noqa: E501 |
786 | 797 |
) |
787 | 798 |
|
788 | 799 |
|
789 |
- def test_102_prompt_for_selection_single(self, monkeypatch: Any) -> None: |
|
800 |
+ def test_102_prompt_for_selection_single(self) -> None: |
|
790 | 801 |
@click.command() |
791 | 802 |
@click.option('--item', default='baked beans') |
792 | 803 |
@click.argument('prompt') |
... | ... |
@@ -794,9 +805,9 @@ Your selection? (1-10, leave empty to abort): \n''', ( # noqa: E501 |
794 | 805 |
try: |
795 | 806 |
cli._prompt_for_selection([item], heading='', |
796 | 807 |
single_choice_prompt=prompt) |
797 |
- except IndexError as e: |
|
808 |
+ except IndexError: |
|
798 | 809 |
click.echo('Boo.') |
799 |
- raise e |
|
810 |
+ raise |
|
800 | 811 |
else: |
801 | 812 |
click.echo('Great!') |
802 | 813 |
runner = click.testing.CliRunner(mix_stderr=True) |
... | ... |
@@ -810,13 +821,13 @@ Will replace with spam. Confirm, y/n? y |
810 | 821 |
Great! |
811 | 822 |
''', 'driver program produced unexpected output' |
812 | 823 |
result = runner.invoke(driver, |
813 |
- ['Will replace with spam, okay? ' + |
|
824 |
+ ['Will replace with spam, okay? ' |
|
814 | 825 |
'(Please say "y" or "n".)'], |
815 | 826 |
input='') |
816 | 827 |
assert result.exit_code > 0, 'driver program succeeded?!' |
817 | 828 |
assert result.stdout == '''\ |
818 | 829 |
[1] baked beans |
819 |
-Will replace with spam, okay? (Please say "y" or "n".): |
|
830 |
+Will replace with spam, okay? (Please say "y" or "n".):\x20 |
|
820 | 831 |
Boo. |
821 | 832 |
''', 'driver program produced unexpected output' |
822 | 833 |
assert isinstance(result.exception, IndexError), ( |
... | ... |
@@ -829,19 +840,14 @@ Boo. |
829 | 840 |
lambda *a, **kw: |
830 | 841 |
json.dumps({'args': a, 'kwargs': kw})) |
831 | 842 |
res = json.loads(cli._prompt_for_passphrase()) |
832 |
- assert 'args' in res and 'kwargs' in res, ( |
|
833 |
- 'missing arguments to passphrase prompt' |
|
834 |
- ) |
|
835 |
- assert res['args'][:1] == ['Passphrase'], ( |
|
836 |
- 'missing arguments to passphrase prompt' |
|
837 |
- ) |
|
838 |
- assert (res['kwargs'].get('default') == '' |
|
839 |
- and not res['kwargs'].get('show_default', True)), ( |
|
840 |
- 'missing arguments to passphrase prompt' |
|
841 |
- ) |
|
842 |
- assert res['kwargs'].get('err') and res['kwargs'].get('hide_input'), ( |
|
843 |
- 'missing arguments to passphrase prompt' |
|
844 |
- ) |
|
843 |
+ err_msg = 'missing arguments to passphrase prompt' |
|
844 |
+ assert 'args' in res, err_msg |
|
845 |
+ assert 'kwargs' in res, err_msg |
|
846 |
+ assert res['args'][:1] == ['Passphrase'], err_msg |
|
847 |
+ assert res['kwargs'].get('default') == '', err_msg |
|
848 |
+ assert not res['kwargs'].get('show_default', True), err_msg |
|
849 |
+ assert res['kwargs'].get('err'), err_msg |
|
850 |
+ assert res['kwargs'].get('hide_input'), err_msg |
|
845 | 851 |
|
846 | 852 |
|
847 | 853 |
@pytest.mark.parametrize(['command_line', 'config', 'result_config'], [ |
... | ... |
@@ -869,7 +875,7 @@ Boo. |
869 | 875 |
assert (result.exit_code, result.stderr_bytes) == (0, b''), ( |
870 | 876 |
'program exited with failure' |
871 | 877 |
) |
872 |
- with open(cli._config_filename(), 'rt') as infile: |
|
878 |
+ with open(cli._config_filename(), encoding='UTF-8') as infile: |
|
873 | 879 |
config_readback = json.load(infile) |
874 | 880 |
assert config_readback == result_config |
875 | 881 |
|
... | ... |
@@ -890,14 +896,13 @@ Boo. |
890 | 896 |
vfunc: Callable[[click.Context, click.Parameter, Any], int | None], |
891 | 897 |
input: int, |
892 | 898 |
) -> None: |
893 |
- ctx = cli.derivepassphrase.make_context(cli.prog_name, []) |
|
899 |
+ ctx = cli.derivepassphrase.make_context(cli.PROG_NAME, []) |
|
894 | 900 |
param = cli.derivepassphrase.params[0] |
895 | 901 |
assert vfunc(ctx, param, input) == input |
896 | 902 |
|
897 | 903 |
|
898 | 904 |
@tests.skip_if_no_agent |
899 |
- @pytest.mark.parametrize(['conn_hint'], |
|
900 |
- [('none',), ('socket',), ('client',)]) |
|
905 |
+ @pytest.mark.parametrize('conn_hint', ['none', 'socket', 'client']) |
|
901 | 906 |
def test_227_get_suitable_ssh_keys( |
902 | 907 |
self, monkeypatch: Any, conn_hint: str, |
903 | 908 |
) -> None: |
... | ... |
@@ -918,7 +923,7 @@ Boo. |
918 | 923 |
list(cli._get_suitable_ssh_keys(hint)) |
919 | 924 |
except RuntimeError: # pragma: no cover |
920 | 925 |
pass |
921 |
- except Exception as e: # pragma: no cover |
|
926 |
+ except Exception as e: # noqa: BLE001 # pragma: no cover |
|
922 | 927 |
exception = e |
923 | 928 |
finally: |
924 | 929 |
assert exception is None, 'exception querying suitable SSH keys' |
... | ... |
@@ -90,14 +92,14 @@ class TestSequin: |
90 | 92 |
seq = sequin.Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True) |
91 | 93 |
assert seq.bases == {2: collections.deque([ |
92 | 94 |
1, 0, 1, 0, 0, 1, 0, 0, 0, 1])} |
93 |
- # |
|
95 |
+ |
|
94 | 96 |
assert seq._all_or_nothing_shift(3) == (1, 0, 1) |
95 | 97 |
assert seq._all_or_nothing_shift(3) == (0, 0, 1) |
96 | 98 |
assert seq.bases[2] == collections.deque([0, 0, 0, 1]) |
97 |
- # |
|
99 |
+ |
|
98 | 100 |
assert seq._all_or_nothing_shift(5) == () |
99 | 101 |
assert seq.bases[2] == collections.deque([0, 0, 0, 1]) |
100 |
- # |
|
102 |
+ |
|
101 | 103 |
assert seq._all_or_nothing_shift(4), (0, 0, 0, 1) |
102 | 104 |
assert 2 not in seq.bases |
103 | 105 |
|
... | ... |
@@ -106,7 +108,7 @@ class TestSequin: |
106 | 108 |
[ |
107 | 109 |
([0, 1, 2, 3, 4, 5, 6, 7], True, |
108 | 110 |
ValueError, 'sequence item out of range'), |
109 |
- (u'こんにちは。', False, |
|
111 |
+ ('こんにちは。', False, |
|
110 | 112 |
ValueError, 'sequence item out of range'), |
111 | 113 |
] |
112 | 114 |
) |
... | ... |
@@ -11,13 +11,14 @@ import io |
11 | 11 |
import os |
12 | 12 |
import socket |
13 | 13 |
import subprocess |
14 |
-from typing_extensions import Any |
|
15 | 14 |
|
16 | 15 |
import click |
17 | 16 |
import click.testing |
17 |
+import pytest |
|
18 |
+from typing_extensions import Any |
|
19 |
+ |
|
18 | 20 |
import derivepassphrase |
19 | 21 |
import derivepassphrase.cli |
20 |
-import pytest |
|
21 | 22 |
import ssh_agent_client |
22 | 23 |
import tests |
23 | 24 |
|
... | ... |
@@ -122,6 +124,7 @@ class TestAgentInteraction: |
122 | 124 |
@pytest.mark.parametrize(['keytype', 'data_dict'], |
123 | 125 |
list(tests.SUPPORTED_KEYS.items())) |
124 | 126 |
def test_200_sign_data_via_agent(self, keytype, data_dict): |
127 |
+ del keytype # Unused. |
|
125 | 128 |
private_key = data_dict['private_key'] |
126 | 129 |
try: |
127 | 130 |
_ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
... | ... |
@@ -159,6 +162,7 @@ class TestAgentInteraction: |
159 | 162 |
@pytest.mark.parametrize(['keytype', 'data_dict'], |
160 | 163 |
list(tests.UNSUITABLE_KEYS.items())) |
161 | 164 |
def test_201_sign_data_via_agent_unsupported(self, keytype, data_dict): |
165 |
+ del keytype # Unused. |
|
162 | 166 |
private_key = data_dict['private_key'] |
163 | 167 |
try: |
164 | 168 |
_ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'], |
... | ... |
@@ -246,18 +250,18 @@ class TestAgentInteraction: |
246 | 250 |
monkeypatch.setenv('SSH_AUTH_SOCK', |
247 | 251 |
os.environ['SSH_AUTH_SOCK'] + '~') |
248 | 252 |
sock = socket.socket(family=socket.AF_UNIX) |
249 |
- with pytest.raises(OSError): |
|
253 |
+ with pytest.raises(OSError): # noqa: PT011 |
|
250 | 254 |
ssh_agent_client.SSHAgentClient(socket=sock) |
251 | 255 |
|
252 |
- @pytest.mark.parametrize(['response'], [ |
|
253 |
- (b'\x00\x00',), |
|
254 |
- (b'\x00\x00\x00\x1f some bytes missing',), |
|
256 |
+ @pytest.mark.parametrize('response', [ |
|
257 |
+ b'\x00\x00', |
|
258 |
+ b'\x00\x00\x00\x1f some bytes missing', |
|
255 | 259 |
]) |
256 | 260 |
def test_310_truncated_server_response(self, monkeypatch, response): |
257 | 261 |
client = ssh_agent_client.SSHAgentClient() |
258 | 262 |
response_stream = io.BytesIO(response) |
259 |
- class PseudoSocket(object): |
|
260 |
- def sendall(self, *args: Any, **kwargs: Any) -> Any: |
|
263 |
+ class PseudoSocket: |
|
264 |
+ def sendall(self, *args: Any, **kwargs: Any) -> Any: # noqa: ARG002 |
|
261 | 265 |
return None |
262 | 266 |
def recv(self, *args: Any, **kwargs: Any) -> Any: |
263 | 267 |
return response_stream.read(*args, **kwargs) |
... | ... |
@@ -276,7 +280,7 @@ class TestAgentInteraction: |
276 | 280 |
12, |
277 | 281 |
b'\x00\x00\x00\x00abc', |
278 | 282 |
ssh_agent_client.TrailingDataError, |
279 |
- 'overlong response', |
|
283 |
+ 'Overlong response', |
|
280 | 284 |
), |
281 | 285 |
] |
282 | 286 |
) |
... | ... |
@@ -284,7 +288,7 @@ class TestAgentInteraction: |
284 | 288 |
response, exc_type, exc_pattern): |
285 | 289 |
client = ssh_agent_client.SSHAgentClient() |
286 | 290 |
monkeypatch.setattr(client, 'request', |
287 |
- lambda *a, **kw: (response_code, response)) |
|
291 |
+ lambda *a, **kw: (response_code, response)) # noqa: ARG005 |
|
288 | 292 |
with pytest.raises(exc_type, match=exc_pattern): |
289 | 293 |
client.list_keys() |
290 | 294 |
|
... | ... |
@@ -311,8 +315,8 @@ class TestAgentInteraction: |
311 | 315 |
def test_330_sign_error_responses(self, monkeypatch, key, check, |
312 | 316 |
response, exc_type, exc_pattern): |
313 | 317 |
client = ssh_agent_client.SSHAgentClient() |
314 |
- monkeypatch.setattr(client, 'request', lambda a, b: response) |
|
315 |
- KeyCommentPair = ssh_agent_client.types.KeyCommentPair |
|
318 |
+ monkeypatch.setattr(client, 'request', lambda a, b: response) # noqa: ARG005 |
|
319 |
+ KeyCommentPair = ssh_agent_client.types.KeyCommentPair # noqa: N806 |
|
316 | 320 |
loaded_keys = [KeyCommentPair(v['public_key_data'], b'no comment') |
317 | 321 |
for v in tests.SUPPORTED_KEYS.values()] |
318 | 322 |
monkeypatch.setattr(client, 'list_keys', lambda: loaded_keys) |
319 | 323 |