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 |