Fix style issues with ruff (hatch default configuration)
Marco Ricci

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'
... ...
@@ -4,10 +4,11 @@
4 4
 
5 5
 from __future__ import annotations
6 6
 
7
+import pytest
7 8
 from typing_extensions import Any
8 9
 
9 10
 import derivepassphrase.types
10
-import pytest
11
+
11 12
 
12 13
 @pytest.mark.parametrize(['obj', 'comment'], [
13 14
     (None, 'not a dict'),
... ...
@@ -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