dwipe 3.0.2__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {dwipe-3.0.2 → dwipe-3.2.0}/PKG-INFO +25 -5
  2. {dwipe-3.0.2 → dwipe-3.2.0}/README.md +8 -1
  3. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/DeviceChangeMonitor.py +2 -3
  4. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/DeviceInfo.py +17 -19
  5. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/DeviceWorker.py +8 -8
  6. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/DiskWipe.py +33 -19
  7. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/DrivePreChecker.py +3 -1
  8. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/FirmwareWipeTask.py +6 -7
  9. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/NvmeTool.py +9 -5
  10. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/PersistentState.py +4 -3
  11. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/Prereqs.py +3 -4
  12. dwipe-3.2.0/dwipe/README-DEV.md +186 -0
  13. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/SataTool.py +12 -8
  14. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/StructuredLogger.py +22 -22
  15. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/Tunables.py +2 -0
  16. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/Utils.py +7 -6
  17. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/VerifyTask.py +2 -3
  18. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/WipeJob.py +9 -10
  19. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/WriteTask.py +3 -4
  20. dwipe-3.2.0/dwipe/__init__.py +10 -0
  21. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/main.py +10 -4
  22. dwipe-3.2.0/pyproject.toml +150 -0
  23. dwipe-3.0.2/.github/workflows/tests.yml +0 -47
  24. dwipe-3.0.2/.gitignore +0 -10
  25. dwipe-3.0.2/dwipe/__init__.py +0 -0
  26. dwipe-3.0.2/images/dwipe-2025-12-31-09-37.gif +0 -0
  27. dwipe-3.0.2/images/dwipe-2026-02-05.07-58.gif +0 -0
  28. dwipe-3.0.2/linter +0 -6
  29. dwipe-3.0.2/pyproject.toml +0 -49
  30. dwipe-3.0.2/resources/dwipe-help-screen.png +0 -0
  31. dwipe-3.0.2/resources/dwipe-main-screen.png +0 -0
  32. dwipe-3.0.2/runner +0 -19
  33. dwipe-3.0.2/tests/conftest.py +0 -50
  34. dwipe-3.0.2/tests/device_double.py +0 -135
  35. dwipe-3.0.2/tests/test_cli_options.py +0 -25
  36. dwipe-3.0.2/tests/test_device_change_monitor.py +0 -87
  37. dwipe-3.0.2/tests/test_device_io.py +0 -165
  38. dwipe-3.0.2/tests/test_device_worker.py +0 -228
  39. dwipe-3.0.2/tests/test_deviceinfo_logic.py +0 -314
  40. dwipe-3.0.2/tests/test_diskwipe_terminal.py +0 -41
  41. dwipe-3.0.2/tests/test_drive_prechecker.py +0 -157
  42. dwipe-3.0.2/tests/test_exception_observability.py +0 -92
  43. dwipe-3.0.2/tests/test_firmware_wipe_task.py +0 -126
  44. dwipe-3.0.2/tests/test_nvme_tool.py +0 -182
  45. dwipe-3.0.2/tests/test_persistent_state.py +0 -203
  46. dwipe-3.0.2/tests/test_safe_write.py +0 -90
  47. dwipe-3.0.2/tests/test_satatool_parse.py +0 -118
  48. dwipe-3.0.2/tests/test_structured_logger.py +0 -167
  49. dwipe-3.0.2/tests/test_utils.py +0 -172
  50. dwipe-3.0.2/tests/test_verification.py +0 -97
  51. dwipe-3.0.2/tests/test_verify_analysis.py +0 -80
  52. dwipe-3.0.2/tests/test_wipejob_logic.py +0 -208
  53. dwipe-3.0.2/tests/test_wipejob_resume.py +0 -214
  54. {dwipe-3.0.2 → dwipe-3.2.0}/LICENSE +0 -0
  55. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/WipeTask.py +0 -0
  56. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/device_io.py +0 -0
  57. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/markers.py +1 -1
  58. {dwipe-3.0.2 → dwipe-3.2.0}/dwipe/verification.py +1 -1
@@ -1,21 +1,34 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: dwipe
3
- Version: 3.0.2
3
+ Version: 3.2.0
4
4
  Summary: A tool to wipe disks and partitions for Linux
5
5
  Keywords: disk,partition,wipe,clean,scrub
6
6
  Author-email: Joe Defen <joedef@google.com>
7
7
  Requires-Python: >=3.10
8
8
  Description-Content-Type: text/markdown
9
+ License-Expression: MIT
9
10
  Classifier: Programming Language :: Python :: 3
10
- Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
11
16
  Classifier: Operating System :: POSIX :: Linux
12
17
  License-File: LICENSE
13
- Requires-Dist: console-window == 1.4.2
18
+ Requires-Dist: console-window == 2.1.3
19
+ Requires-Dist: pytest >= 7.0 ; extra == "dev"
20
+ Requires-Dist: pytest-cov >= 4.0 ; extra == "dev"
21
+ Requires-Dist: ruff >= 0.9 ; extra == "dev"
22
+ Requires-Dist: build >= 1.2 ; extra == "dev"
23
+ Requires-Dist: twine >= 7 ; extra == "dev"
24
+ Requires-Dist: packaging >= 26 ; extra == "dev"
14
25
  Requires-Dist: pytest >= 7.0 ; extra == "test"
15
26
  Requires-Dist: pytest-cov >= 4.0 ; extra == "test"
16
27
  Project-URL: Bug Tracker, https://github.com/joedefen/dwipe/issues
17
28
  Project-URL: Homepage, https://github.com/joedefen/dwipe
29
+ Provides-Extra: dev
18
30
  Provides-Extra: test
31
+ Import-Name: dwipe
19
32
 
20
33
  # dwipe
21
34
  `dwipe` is a tool to wipe disks and partitions for Linux to help secure your data. `dwipe` aims to reduce mistakes by providing ample information about your devices during selection.
@@ -71,7 +84,7 @@ Provides-Extra: test
71
84
  ## Installation
72
85
 
73
86
  * **Recommended (using pipx):** `pipx install dwipe`
74
- * **Verify installation:** `dwipe --help`
87
+ * **Verify installation:** `dwipe --version` (prints the release number) and `dwipe --help`
75
88
  * **Uninstall:** `pipx uninstall dwipe`
76
89
 
77
90
  ## Quick Start (please READ THIS, at least)
@@ -149,6 +162,7 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
149
162
  - The block is remembered per device; once you unblock, it stays unblocked
150
163
  - Set to `off` when re-provisioning many encrypted disks to avoid unblocking each one
151
164
  - `--dump-lsblk` - Dump parsed device information and exit (for debugging)
165
+ - `--version` - Print the version and exit (works without root, like `--help`)
152
166
  - `--help` - Show help message with all available options
153
167
 
154
168
  ### Color Legend
@@ -598,6 +612,12 @@ sudo hdparm --user-master u --security-disable NULL /dev/sdX
598
612
  ### Contributing
599
613
  Issues and pull requests welcome at [github.com/joedefen/dwipe](https://github.com/joedefen/dwipe)
600
614
 
615
+ To work on the source tree, see
616
+ [dwipe/README-DEV.md](dwipe/README-DEV.md): it documents the virtualenv + `dev`
617
+ extra, the test gate (`ruff`, `compileall`, `pytest --cov`), and `./bless`, the
618
+ only path to a release -- a single command that runs every gate, rehearses on
619
+ TestPyPI when asked to, and tags, uploads and pushes only when asked to.
620
+
601
621
  ## License
602
622
 
603
623
  MIT License - see [LICENSE](LICENSE) file for details.
@@ -52,7 +52,7 @@
52
52
  ## Installation
53
53
 
54
54
  * **Recommended (using pipx):** `pipx install dwipe`
55
- * **Verify installation:** `dwipe --help`
55
+ * **Verify installation:** `dwipe --version` (prints the release number) and `dwipe --help`
56
56
  * **Uninstall:** `pipx uninstall dwipe`
57
57
 
58
58
  ## Quick Start (please READ THIS, at least)
@@ -130,6 +130,7 @@ Simply run `dwipe` from the command line without arguments: `dwipe`
130
130
  - The block is remembered per device; once you unblock, it stays unblocked
131
131
  - Set to `off` when re-provisioning many encrypted disks to avoid unblocking each one
132
132
  - `--dump-lsblk` - Dump parsed device information and exit (for debugging)
133
+ - `--version` - Print the version and exit (works without root, like `--help`)
133
134
  - `--help` - Show help message with all available options
134
135
 
135
136
  ### Color Legend
@@ -579,6 +580,12 @@ sudo hdparm --user-master u --security-disable NULL /dev/sdX
579
580
  ### Contributing
580
581
  Issues and pull requests welcome at [github.com/joedefen/dwipe](https://github.com/joedefen/dwipe)
581
582
 
583
+ To work on the source tree, see
584
+ [dwipe/README-DEV.md](dwipe/README-DEV.md): it documents the virtualenv + `dev`
585
+ extra, the test gate (`ruff`, `compileall`, `pytest --cov`), and `./bless`, the
586
+ only path to a release -- a single command that runs every gate, rehearses on
587
+ TestPyPI when asked to, and tags, uploads and pushes only when asked to.
588
+
582
589
  ## License
583
590
 
584
591
  MIT License - see [LICENSE](LICENSE) file for details.
@@ -1,14 +1,13 @@
1
1
  """
2
2
  DeviceChangeMonitor - Background thread for monitoring block device changes
3
3
  """
4
+ import ctypes
4
5
  import os
5
6
  import select
6
7
  import threading
7
8
  import traceback
8
- import ctypes
9
9
  from ctypes.util import find_library
10
10
 
11
-
12
11
  # inotify constants (from Linux headers)
13
12
  IN_CREATE = 0x00000100
14
13
  IN_DELETE = 0x00000200
@@ -153,7 +152,7 @@ class DeviceChangeMonitor:
153
152
  current_devs = os.listdir('/sys/class/block')
154
153
 
155
154
  # 2. Secondary check: Do the partition sizes/counts match?
156
- with open('/proc/partitions', 'r', encoding='utf-8') as f:
155
+ with open('/proc/partitions', encoding='utf-8') as f:
157
156
  current_parts = f.read()
158
157
 
159
158
  # Create a combined "Fingerprint"
@@ -6,20 +6,18 @@ DeviceInfo class for device discovery and information management
6
6
  # pylint: disable=too-many-return-statements,too-many-nested-blocks
7
7
  # pylint: disable=too-many-statements
8
8
 
9
+ import curses
9
10
  import os
10
11
  import re
11
- import json
12
- import subprocess
13
- import curses
14
12
  import traceback
15
13
  from fnmatch import fnmatch
16
14
  from types import SimpleNamespace
15
+
17
16
  from console_window import Theme
18
- from dataclasses import asdict
19
17
 
20
- from .Utils import Utils
21
- from .DrivePreChecker import DrivePreChecker
22
18
  from .DeviceWorker import DeviceWorkerManager, ProbeState
19
+ from .DrivePreChecker import DrivePreChecker
20
+ from .Utils import Utils
23
21
 
24
22
 
25
23
  class DeviceInfo:
@@ -209,7 +207,7 @@ class DeviceInfo:
209
207
  try:
210
208
  rv = ''
211
209
  fullpath = f'/sys/class/block/{device_name}/device/{suffix}'
212
- with open(fullpath, 'r', encoding='utf-8') as f: # Read information
210
+ with open(fullpath, encoding='utf-8') as f: # Read information
213
211
  # Sanitize: some USB bridges return strings with embedded nulls
214
212
  rv = f.read().strip().replace('\x00', '')
215
213
  except (FileNotFoundError, Exception):
@@ -240,7 +238,7 @@ class DeviceInfo:
240
238
  parent = device_name
241
239
 
242
240
  path = f'/sys/block/{parent}/queue/rotational'
243
- with open(path, 'r', encoding='utf-8') as f:
241
+ with open(path, encoding='utf-8') as f:
244
242
  return f.read().strip() == '1'
245
243
  except (FileNotFoundError, PermissionError, OSError):
246
244
  return False
@@ -261,7 +259,7 @@ class DeviceInfo:
261
259
  """
262
260
  devices = {}
263
261
  try:
264
- with open('/proc/partitions', 'r', encoding='utf-8') as f:
262
+ with open('/proc/partitions', encoding='utf-8') as f:
265
263
  for line in f:
266
264
  parts = line.split()
267
265
  if len(parts) == 4 and parts[0].isdigit():
@@ -315,7 +313,7 @@ class DeviceInfo:
315
313
  """
316
314
  mounts = {}
317
315
  try:
318
- with open('/proc/mounts', 'r', encoding='utf-8') as f:
316
+ with open('/proc/mounts', encoding='utf-8') as f:
319
317
  for line in f:
320
318
  parts = line.split()
321
319
  if len(parts) >= 3 and parts[0].startswith('/dev/'):
@@ -336,7 +334,7 @@ class DeviceInfo:
336
334
  """Return canonical block names currently used as swap (non-blocking)."""
337
335
  names = set()
338
336
  try:
339
- with open('/proc/swaps', 'r', encoding='utf-8') as f:
337
+ with open('/proc/swaps', encoding='utf-8') as f:
340
338
  next(f, None) # skip header line
341
339
  for line in f:
342
340
  parts = line.split()
@@ -410,7 +408,7 @@ class DeviceInfo:
410
408
  if DeviceInfo._read_sysfs_dir(f'/sys/class/block/{name}/holders'):
411
409
  return True
412
410
  try:
413
- with open('/proc/mounts', 'r', encoding='utf-8') as f:
411
+ with open('/proc/mounts', encoding='utf-8') as f:
414
412
  for line in f:
415
413
  parts = line.split()
416
414
  if parts and DeviceInfo._block_name_from_devpath(parts[0]) == name:
@@ -463,7 +461,7 @@ class DeviceInfo:
463
461
  Reads /sys/class/block/{name}/size which contains sector count.
464
462
  """
465
463
  try:
466
- with open(f'/sys/class/block/{name}/size', 'r', encoding='utf-8') as f:
464
+ with open(f'/sys/class/block/{name}/size', encoding='utf-8') as f:
467
465
  sectors = int(f.read().strip())
468
466
  return sectors * 512 # Convert to bytes
469
467
  except (FileNotFoundError, ValueError, Exception):
@@ -477,7 +475,7 @@ class DeviceInfo:
477
475
  """
478
476
  # Try NVMe-style path first (plain text)
479
477
  try:
480
- with open(f'/sys/class/block/{name}/device/serial', 'r', encoding='utf-8') as f:
478
+ with open(f'/sys/class/block/{name}/device/serial', encoding='utf-8') as f:
481
479
  # Sanitize: some USB bridges return strings with embedded nulls
482
480
  return f.read().strip().replace('\x00', '')
483
481
  except (FileNotFoundError, Exception):
@@ -531,7 +529,7 @@ class DeviceInfo:
531
529
  result['uuid'] = line.split('=', 1)[1].strip()
532
530
  elif line.startswith('E:ID_PART_ENTRY_UUID=') and not result['uuid']:
533
531
  result['uuid'] = line.split('=', 1)[1].strip()
534
- except (FileNotFoundError, IOError, OSError):
532
+ except (FileNotFoundError, OSError):
535
533
  pass
536
534
  return result
537
535
 
@@ -733,7 +731,7 @@ class DeviceInfo:
733
731
 
734
732
  # Phase 4b: Clear marker display and state for disks with actual partitions
735
733
  # If a disk has partitions, the partitions take precedence over the wipe marker
736
- for name, entry in entries.items():
734
+ for entry in entries.values():
737
735
  if entry.minors: # Disk has child partitions
738
736
  entry.marker = '' # Don't show marker when partitions exist
739
737
  # Also clear the state if it was set to a wipe state ('W' or 's')
@@ -875,7 +873,7 @@ class DeviceInfo:
875
873
  # Must be writable (excludes CD-ROMs, eMMC boot partitions, etc.)
876
874
  ro_path = f'/sys/class/block/{name}/ro'
877
875
  try:
878
- with open(ro_path, 'r', encoding='utf-8') as f:
876
+ with open(ro_path, encoding='utf-8') as f:
879
877
  if f.read().strip() != '0':
880
878
  continue # Skip read-only devices
881
879
  except (FileNotFoundError, Exception):
@@ -1262,7 +1260,7 @@ class DeviceInfo:
1262
1260
  if part.parent == disk.name]
1263
1261
 
1264
1262
  if disk_parts:
1265
- for i, (part_name, part) in enumerate(sorted(disk_parts)):
1263
+ for i, (_, part) in enumerate(sorted(disk_parts)):
1266
1264
  is_last = (i == len(disk_parts) - 1)
1267
1265
  branch = '└─' if is_last else '├─'
1268
1266
 
@@ -1281,7 +1279,7 @@ class DeviceInfo:
1281
1279
  mounts_str = ', '.join(part.mounts)
1282
1280
  print(f'│ {" " if is_last else "│ "} Mounted: {mounts_str}')
1283
1281
  else:
1284
- print(f'│ └─ (no partitions)')
1282
+ print('│ └─ (no partitions)')
1285
1283
 
1286
1284
  print('│')
1287
1285
 
@@ -5,9 +5,9 @@ Each device gets its own worker thread that handles potentially-blocking
5
5
  operations (sysfs reads, hdparm, nvme commands) without freezing the UI.
6
6
  """
7
7
  import os
8
- import time
9
- import threading
10
8
  import queue
9
+ import threading
10
+ import time
11
11
  from enum import Enum
12
12
 
13
13
  from .DrivePreChecker import DrivePreChecker
@@ -315,8 +315,8 @@ class DeviceWorker(threading.Thread):
315
315
  """Probe marker once and format the result as a string."""
316
316
  try:
317
317
  # Import here to avoid circular dependency
318
+
318
319
  from .WipeJob import WipeJob
319
- import datetime
320
320
 
321
321
  marker = WipeJob.read_marker_buffer(self.device_name)
322
322
 
@@ -381,10 +381,10 @@ class DeviceWorker(threading.Thread):
381
381
  """
382
382
  path = f"/sys/class/block/{self.device_name}/{attr_path}"
383
383
  try:
384
- with open(path, 'r') as f:
384
+ with open(path) as f:
385
385
  # Sanitize: some USB bridges return strings with embedded nulls
386
386
  return f.read().strip().replace('\x00', '')
387
- except (FileNotFoundError, IOError, OSError):
387
+ except (FileNotFoundError, OSError):
388
388
  return ''
389
389
 
390
390
  def _read_vpd_serial(self):
@@ -399,7 +399,7 @@ class DeviceWorker(threading.Thread):
399
399
  if len(data) > 4:
400
400
  # Sanitize: remove nulls that some USB bridges include
401
401
  return data[4:].decode('ascii', errors='ignore').strip().replace('\x00', '')
402
- except (FileNotFoundError, IOError, OSError):
402
+ except (FileNotFoundError, OSError):
403
403
  pass
404
404
  return ''
405
405
 
@@ -409,7 +409,7 @@ class DeviceWorker(threading.Thread):
409
409
  sysfs_path = f'/sys/class/block/{self.device_name}'
410
410
  real_path = os.path.realpath(sysfs_path).lower()
411
411
  return '/usb' in real_path
412
- except (OSError, IOError):
412
+ except OSError:
413
413
  return False
414
414
 
415
415
  def _is_rotational_device(self):
@@ -424,7 +424,7 @@ class DeviceWorker(threading.Thread):
424
424
  """
425
425
  try:
426
426
  path = f'/sys/block/{self.device_name}/queue/rotational'
427
- with open(path, 'r', encoding='utf-8') as f:
427
+ with open(path, encoding='utf-8') as f:
428
428
  return f.read().strip() == '1'
429
429
  except (FileNotFoundError, PermissionError, OSError):
430
430
  return False
@@ -6,28 +6,38 @@ DiskWipe class - Main application controller/singleton
6
6
  # pylint: disable=too-many-branches,too-many-statements,too-many-locals
7
7
  # pylint: disable=protected-access,too-many-return-statements
8
8
  # pylint: disable=too-few-public-methods
9
+ import curses as cs
10
+
11
+ # import shutil
12
+ import json
9
13
  import os
10
- import sys
11
- import subprocess
12
14
  import re
13
- import time
15
+ import subprocess
16
+ import sys
14
17
  import threading
15
- # import shutil
16
- import json
17
- import curses as cs
18
- from types import SimpleNamespace
18
+ import time
19
19
  from datetime import datetime
20
- from console_window import (ConsoleWindow, ConsoleWindowOpts, OptionSpinner,
21
- IncrementalSearchBar, InlineConfirmation, Theme,
22
- Screen, ScreenStack, Context)
20
+ from types import SimpleNamespace
23
21
 
24
- from .WipeJob import WipeJob
22
+ from console_window import (
23
+ ConsoleWindow,
24
+ ConsoleWindowOpts,
25
+ Context,
26
+ IncrementalSearchBar,
27
+ InlineConfirmation,
28
+ OptionSpinner,
29
+ Screen,
30
+ ScreenStack,
31
+ Theme,
32
+ )
33
+
34
+ from .DeviceChangeMonitor import DeviceChangeMonitor
25
35
  from .DeviceInfo import DeviceInfo
26
36
  from .DeviceWorker import ProbeState
27
- from .Utils import Utils
28
37
  from .PersistentState import PersistentState
29
38
  from .StructuredLogger import StructuredLogger
30
- from .DeviceChangeMonitor import DeviceChangeMonitor
39
+ from .Utils import Utils
40
+ from .WipeJob import WipeJob
31
41
 
32
42
 
33
43
  def _restore_terminal():
@@ -144,9 +154,13 @@ class DiskWipe:
144
154
  command_args = DrivePreChecker.get_wipe_command_args(wipe_type)
145
155
 
146
156
  # Import firmware task classes
147
- from .FirmwareWipeTask import (NvmeWipeTask, SataWipeTask,
148
- StandardPrecheckTask,
149
- FirmwarePreVerifyTask, FirmwarePostVerifyTask)
157
+ from .FirmwareWipeTask import (
158
+ FirmwarePostVerifyTask,
159
+ FirmwarePreVerifyTask,
160
+ NvmeWipeTask,
161
+ SataWipeTask,
162
+ StandardPrecheckTask,
163
+ )
150
164
 
151
165
  # Determine task type based on device name
152
166
  if part.name.startswith('nvme'):
@@ -1033,9 +1047,9 @@ class MainScreen(DiskWipeScreen):
1033
1047
  if app.confirmation.active and app.confirmation.identity == partition.name:
1034
1048
  # Build confirmation message
1035
1049
  if app.confirmation.action_type == 'wipe':
1036
- msg = f'⚠️ WIPE'
1050
+ msg = '⚠️ WIPE'
1037
1051
  else: # verify
1038
- msg = f'⚠️ VERIFY [writes marker]'
1052
+ msg = '⚠️ VERIFY [writes marker]'
1039
1053
 
1040
1054
  # Add mode-specific prompt (base message without input)
1041
1055
  if app.confirmation.mode == 'yes':
@@ -1296,7 +1310,7 @@ class MainScreen(DiskWipeScreen):
1296
1310
  # Check if this is an NVMe controller (class 0x010802)
1297
1311
  class_file = os.path.join(device_path, 'class')
1298
1312
  try:
1299
- with open(class_file, 'r', encoding='utf-8') as f:
1313
+ with open(class_file, encoding='utf-8') as f:
1300
1314
  device_class = f.read().strip()
1301
1315
  if device_class != '0x010802':
1302
1316
  continue
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env python3
2
- import subprocess
3
2
  import json
3
+ import subprocess
4
4
  from dataclasses import dataclass
5
+
5
6
  from .SataTool import SataTool
6
7
  from .Utils import Utils
7
8
 
9
+
8
10
  @dataclass
9
11
  class PreCheckResult:
10
12
  # Dict of capabilities and issues
@@ -11,17 +11,16 @@ Includes:
11
11
  # pylint: disable=too-many-positional-arguments,consider-using-with
12
12
  # pylint: disable=too-many-return-statements
13
13
  import os
14
- import time
15
14
  import random
16
15
  import subprocess
16
+ import time
17
17
  import traceback
18
18
 
19
- from . import device_io
20
- from . import markers
21
- from .WipeTask import WipeTask
22
- from .Utils import Utils
23
- from .SataTool import SataTool
19
+ from . import device_io, markers
24
20
  from .NvmeTool import NvmeTool
21
+ from .SataTool import SataTool
22
+ from .Utils import Utils
23
+ from .WipeTask import WipeTask
25
24
 
26
25
 
27
26
  class FirmwareWipeTask(WipeTask):
@@ -618,7 +617,7 @@ class FirmwarePreVerifyTask(WipeTask):
618
617
 
619
618
  # Generate pseudo-random test data
620
619
  random.seed(int(time.time()))
621
- for i in range(self.NUM_BLOCKS):
620
+ for _ in range(self.NUM_BLOCKS):
622
621
  block = bytes([random.randint(0, 255) for _ in range(self.BLOCK_SIZE)])
623
622
  self.original_blocks.append(block)
624
623
 
@@ -1,10 +1,10 @@
1
- import os
2
- import json
3
- import subprocess
4
1
  import io
2
+ import json
5
3
  import re
4
+ import subprocess
6
5
  import time
7
6
  from types import SimpleNamespace
7
+
8
8
  from .Utils import Utils
9
9
 
10
10
 
@@ -200,12 +200,16 @@ class NvmeTool:
200
200
  return self.job.process
201
201
 
202
202
  except Exception as e:
203
+ # Bind the message before leaving the handler: Python unbinds `e`
204
+ # at the end of the `except` block, so a lambda closing over it
205
+ # would raise NameError when the task manager called it.
206
+ error_text = str(e)
203
207
  self.job.process = SimpleNamespace(
204
208
  poll=lambda: 1,
205
209
  returncode=1,
206
210
  stdout=io.StringIO(""),
207
- stderr=io.StringIO(str(e)),
208
- communicate=lambda: ("", str(e))
211
+ stderr=io.StringIO(error_text),
212
+ communicate=lambda: ("", error_text)
209
213
  )
210
214
  return self.job.process
211
215
 
@@ -5,6 +5,7 @@ PersistentState class for saving user preferences and device states
5
5
  import json
6
6
  import time
7
7
  from pathlib import Path
8
+
8
9
  from .Utils import Utils
9
10
 
10
11
 
@@ -63,7 +64,7 @@ class PersistentState:
63
64
  # Remember valid keys from default state
64
65
  valid_keys = set(self.state.keys())
65
66
 
66
- with open(self.config_path, 'r', encoding='utf-8') as f:
67
+ with open(self.config_path, encoding='utf-8') as f:
67
68
  loaded = json.load(f)
68
69
 
69
70
  # Only load keys that exist in current schema
@@ -87,7 +88,7 @@ class PersistentState:
87
88
  self.state['wipe_mode'] = '-V'
88
89
  self.dirty = True # Save the migration
89
90
 
90
- except (json.JSONDecodeError, IOError) as e:
91
+ except (OSError, json.JSONDecodeError) as e:
91
92
  print(f'Warning: Could not load state from {self.config_path}: {e}')
92
93
 
93
94
  def save(self):
@@ -104,7 +105,7 @@ class PersistentState:
104
105
  # Fix ownership if running with sudo
105
106
  Utils.fix_file_ownership(self.config_path)
106
107
  self.dirty = False
107
- except IOError as e:
108
+ except OSError as e:
108
109
  print(f'Warning: Could not save state to {self.config_path}: {e}')
109
110
 
110
111
  def _cleanup_old_devices(self):
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  import shutil
3
3
  import sys
4
- import os
5
- from typing import Optional, Dict, List
4
+
6
5
 
7
6
  class Prereqs:
8
7
  """Manages tool dependencies and provides actionable install suggestions."""
@@ -29,14 +28,14 @@ class Prereqs:
29
28
  # Track status of required tools
30
29
  self.results = {}
31
30
 
32
- def _detect_package_manager(self) -> Optional[str]:
31
+ def _detect_package_manager(self) -> str | None:
33
32
  managers = ['apt', 'dnf', 'yum', 'pacman', 'zypper', 'apk', 'brew']
34
33
  for cmd in managers:
35
34
  if shutil.which(cmd):
36
35
  return cmd
37
36
  return None
38
37
 
39
- def check_all(self, tools: List[str]):
38
+ def check_all(self, tools: list[str]):
40
39
  """Runs the check for a list of tools."""
41
40
  for tool in tools:
42
41
  path = shutil.which(tool)