diskinfo 4.1.2__tar.gz → 4.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.
- {diskinfo-4.1.2/src/diskinfo.egg-info → diskinfo-4.2.0}/PKG-INFO +5 -5
- {diskinfo-4.1.2 → diskinfo-4.2.0}/README.md +1 -1
- {diskinfo-4.1.2 → diskinfo-4.2.0}/pyproject.toml +17 -11
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/demo.py +3 -2
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/disk.py +2 -1
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/filesystem.py +30 -7
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/partition.py +3 -2
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/utils.py +60 -4
- {diskinfo-4.1.2 → diskinfo-4.2.0/src/diskinfo.egg-info}/PKG-INFO +5 -5
- {diskinfo-4.1.2 → diskinfo-4.2.0}/test/test_filesystem.py +68 -1
- {diskinfo-4.1.2 → diskinfo-4.2.0}/test/test_partition.py +35 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/test/test_utils.py +85 -4
- {diskinfo-4.1.2 → diskinfo-4.2.0}/LICENSE +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/setup.cfg +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/__init__.py +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/diskinfo.py +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/disksmart.py +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo/disktype.py +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo.egg-info/SOURCES.txt +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo.egg-info/dependency_links.txt +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo.egg-info/requires.txt +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/src/diskinfo.egg-info/top_level.txt +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/test/test_disk.py +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/test/test_diskinfo.py +0 -0
- {diskinfo-4.1.2 → diskinfo-4.2.0}/test/test_disksmart.py +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diskinfo
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: Disk information Python library for Linux
|
|
5
5
|
Author-email: Peter Sulyok <peter@sulyok.net>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/petersulyok/diskinfo
|
|
8
|
-
Project-URL: Changelog, https://github.com/petersulyok/diskinfo/CHANGELOG.md
|
|
8
|
+
Project-URL: Changelog, https://github.com/petersulyok/diskinfo/blob/main/CHANGELOG.md
|
|
9
9
|
Project-URL: Documentation, https://diskinfo.readthedocs.io/en/latest/index.html
|
|
10
10
|
Project-URL: Issues, https://github.com/petersulyok/diskinfo/issues
|
|
11
11
|
Keywords: disk,linux
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Topic :: System :: Hardware
|
|
19
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
20
|
-
Requires-Python: <4,>=3.
|
|
20
|
+
Requires-Python: <4,>=3.11
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: pySMART
|
|
@@ -25,7 +25,7 @@ Requires-Dist: pyudev
|
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
27
|
# diskinfo
|
|
28
|
-
[](https://github.com/petersulyok/diskinfo/actions/workflows/test.yml)
|
|
29
29
|
[](https://app.codecov.io/gh/petersulyok/diskinfo)
|
|
30
30
|
[](https://diskinfo.readthedocs.io/en/latest/?badge=latest)
|
|
31
31
|
[](https://github.com/petersulyok/diskinfo/issues)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# diskinfo
|
|
2
|
-
[](https://github.com/petersulyok/diskinfo/actions/workflows/test.yml)
|
|
3
3
|
[](https://app.codecov.io/gh/petersulyok/diskinfo)
|
|
4
4
|
[](https://diskinfo.readthedocs.io/en/latest/?badge=latest)
|
|
5
5
|
[](https://github.com/petersulyok/diskinfo/issues)
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "diskinfo"
|
|
3
3
|
description = "Disk information Python library for Linux"
|
|
4
|
-
version = "4.
|
|
4
|
+
version = "4.2.0"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "Peter Sulyok", email = "peter@sulyok.net" }
|
|
8
8
|
]
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.11, <4"
|
|
10
10
|
keywords = ["disk", "linux"]
|
|
11
11
|
license= "MIT"
|
|
12
12
|
license-files= ["LICENSE"]
|
|
13
13
|
classifiers = [
|
|
14
|
-
"Programming Language :: Python :: 3.10",
|
|
15
14
|
"Programming Language :: Python :: 3.11",
|
|
16
15
|
"Programming Language :: Python :: 3.12",
|
|
17
16
|
"Programming Language :: Python :: 3.13",
|
|
18
17
|
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Programming Language :: Python :: 3.15",
|
|
19
19
|
"Operating System :: POSIX :: Linux",
|
|
20
20
|
"Topic :: System :: Hardware",
|
|
21
21
|
"Development Status :: 5 - Production/Stable"
|
|
@@ -29,10 +29,8 @@ dependencies = [
|
|
|
29
29
|
dev = [
|
|
30
30
|
"build",
|
|
31
31
|
"coverage",
|
|
32
|
-
"mock",
|
|
33
32
|
"pytest",
|
|
34
33
|
"pytest-cov",
|
|
35
|
-
"pytest-mock",
|
|
36
34
|
"twine"
|
|
37
35
|
]
|
|
38
36
|
lint = [
|
|
@@ -55,7 +53,7 @@ build-backend = "setuptools.build_meta"
|
|
|
55
53
|
|
|
56
54
|
[project.urls]
|
|
57
55
|
Homepage = "https://github.com/petersulyok/diskinfo"
|
|
58
|
-
Changelog = "https://github.com/petersulyok/diskinfo/CHANGELOG.md"
|
|
56
|
+
Changelog = "https://github.com/petersulyok/diskinfo/blob/main/CHANGELOG.md"
|
|
59
57
|
Documentation = "https://diskinfo.readthedocs.io/en/latest/index.html"
|
|
60
58
|
Issues = "https://github.com/petersulyok/diskinfo/issues"
|
|
61
59
|
|
|
@@ -73,19 +71,27 @@ filterwarnings = [
|
|
|
73
71
|
]
|
|
74
72
|
|
|
75
73
|
# coverage options
|
|
74
|
+
[tool.coverage.run]
|
|
75
|
+
source = ["src/diskinfo"]
|
|
76
|
+
omit = [
|
|
77
|
+
"src/diskinfo/demo.py"
|
|
78
|
+
]
|
|
79
|
+
|
|
76
80
|
[tool.coverage.report]
|
|
81
|
+
fail_under = 100
|
|
82
|
+
show_missing = true
|
|
83
|
+
skip_covered = false
|
|
77
84
|
exclude_lines = [
|
|
78
85
|
"pragma: no cover",
|
|
79
86
|
"if __name__ == .__main__.:",
|
|
80
|
-
"unittest.main()"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"src/diskinfo/demo.py"
|
|
87
|
+
"unittest.main()",
|
|
88
|
+
"raise NotImplementedError",
|
|
89
|
+
"if TYPE_CHECKING:"
|
|
84
90
|
]
|
|
85
91
|
|
|
86
92
|
# ruff options
|
|
87
93
|
[tool.ruff]
|
|
88
|
-
target-version = "
|
|
94
|
+
target-version = "py311"
|
|
89
95
|
line-length = 120
|
|
90
96
|
|
|
91
97
|
[tool.ruff.lint]
|
|
@@ -227,7 +227,8 @@ def partition_demo(name: str):
|
|
|
227
227
|
table.add_column("Type", justify="left", style="bold orchid")
|
|
228
228
|
table.add_column("Start", justify="right", style="bold gray54")
|
|
229
229
|
table.add_column("Size", justify="right", style="bold gray54")
|
|
230
|
-
table.add_column("
|
|
230
|
+
table.add_column("Part label", justify="left", style="bold sky_blue3")
|
|
231
|
+
table.add_column("FS label", justify="left", style="bold purple3")
|
|
231
232
|
table.add_column("Mounting point", justify="left", style="bold green")
|
|
232
233
|
table.add_column("Free", justify="right", style="bold blue")
|
|
233
234
|
for p in plist:
|
|
@@ -237,7 +238,7 @@ def partition_demo(name: str):
|
|
|
237
238
|
else:
|
|
238
239
|
free_size_str = " - %"
|
|
239
240
|
table.add_row(p.get_name(), fs.get_fs_type(), str(p.get_part_offset()), str(p.get_part_size()),
|
|
240
|
-
fs.get_fs_label(), fs.get_fs_mounting_point(), free_size_str)
|
|
241
|
+
p.get_part_label(), fs.get_fs_label(), fs.get_fs_mounting_point(), free_size_str)
|
|
241
242
|
group = Group(panel, table)
|
|
242
243
|
|
|
243
244
|
rprint(Panel(group, title="[markdown.strong]diskinfo demo: partitions[/]", title_align="left",
|
|
@@ -307,7 +307,8 @@ class Disk:
|
|
|
307
307
|
return self.__wwn
|
|
308
308
|
|
|
309
309
|
def get_model(self) -> str:
|
|
310
|
-
"""Returns the disk model string.
|
|
310
|
+
"""Returns the disk model string. The escape sequences of the udev property (e.g. ``\\x20`` for a space
|
|
311
|
+
character) are decoded in the result.
|
|
311
312
|
|
|
312
313
|
Example:
|
|
313
314
|
An example about the use of this function::
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Peter Sulyok (C) 2022-2026.
|
|
4
4
|
#
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
from typing import Tuple
|
|
7
8
|
from pyudev import Device
|
|
8
9
|
from diskinfo.utils import size_in_hrf, _pyudev_getenc
|
|
@@ -10,6 +11,24 @@ from diskinfo.utils import size_in_hrf, _pyudev_getenc
|
|
|
10
11
|
# Path to the kernel mount table.
|
|
11
12
|
_PROC_MOUNTS = "/proc/mounts"
|
|
12
13
|
|
|
14
|
+
# Octal escape sequence used by the kernel in the mount table (e.g. `\\040` for a space character).
|
|
15
|
+
_MOUNT_ESCAPE_RE = re.compile(r"\\([0-7]{3})")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _unmangle_path(path: str) -> str:
|
|
19
|
+
"""Decodes the octal escape sequences of a path in the kernel mount table. The kernel escapes the space,
|
|
20
|
+
tab, newline and backslash characters (i.e. ``\\040``, ``\\011``, ``\\012`` and ``\\134``) in the device node and
|
|
21
|
+
in the mounting point fields of ``/proc/mounts``.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
path (str): path with escape sequences
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: decoded path
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
return _MOUNT_ESCAPE_RE.sub(lambda m: chr(int(m.group(1), 8)), path)
|
|
31
|
+
|
|
13
32
|
|
|
14
33
|
class FileSystem:
|
|
15
34
|
"""This class encapsulates filesystem-related information read from a block device's udev properties.
|
|
@@ -19,7 +38,8 @@ class FileSystem:
|
|
|
19
38
|
|
|
20
39
|
1. The mounting point and available space are determined by reading ``/proc/mounts`` and calling
|
|
21
40
|
:func:`os.statvfs`. If the file system is not mounted, the mounting point will be empty and the
|
|
22
|
-
free size will be ``0``.
|
|
41
|
+
free size will be ``0``. The octal escape sequences of the kernel mount table (e.g. ``\\040`` for a
|
|
42
|
+
space character) are decoded.
|
|
23
43
|
2. Instances are created internally by :class:`~diskinfo.Partition` and :class:`~diskinfo.Disk`.
|
|
24
44
|
|
|
25
45
|
Args:
|
|
@@ -55,10 +75,10 @@ class FileSystem:
|
|
|
55
75
|
parts = line.split()
|
|
56
76
|
if len(parts) < 2:
|
|
57
77
|
continue
|
|
58
|
-
if parts[0] == path:
|
|
59
|
-
self.__fs_mounting_point = parts[1]
|
|
78
|
+
if _unmangle_path(parts[0]) == path:
|
|
79
|
+
self.__fs_mounting_point = _unmangle_path(parts[1])
|
|
60
80
|
try:
|
|
61
|
-
st = os.statvfs(
|
|
81
|
+
st = os.statvfs(self.__fs_mounting_point)
|
|
62
82
|
self.__fs_size = (st.f_blocks * st.f_frsize) // 512
|
|
63
83
|
self.__fs_free_size = (st.f_bavail * st.f_frsize) // 512
|
|
64
84
|
except OSError:
|
|
@@ -75,13 +95,15 @@ class FileSystem:
|
|
|
75
95
|
pass
|
|
76
96
|
|
|
77
97
|
def get_fs_label(self) -> str:
|
|
78
|
-
"""Returns the label of the file system. The result could be empty if the file system does not have a
|
|
98
|
+
"""Returns the label of the file system. The result could be empty if the file system does not have a
|
|
99
|
+
label. The escape sequences of the udev property (e.g. ``\\xc3\\x89`` for the ``É`` character) are decoded
|
|
100
|
+
in the result.
|
|
79
101
|
"""
|
|
80
102
|
return self.__fs_label
|
|
81
103
|
|
|
82
104
|
def get_fs_uuid(self) -> str:
|
|
83
105
|
"""Returns the UUID of the file system. The result could be empty if the device does not contain a
|
|
84
|
-
file system.
|
|
106
|
+
file system. The escape sequences of the udev property are decoded in the result.
|
|
85
107
|
"""
|
|
86
108
|
return self.__fs_uuid
|
|
87
109
|
|
|
@@ -155,7 +177,8 @@ class FileSystem:
|
|
|
155
177
|
|
|
156
178
|
def get_fs_mounting_point(self) -> str:
|
|
157
179
|
"""Returns the mounting point of the file system. The result could be empty if the device does not
|
|
158
|
-
contain any file system, or it is not mounted.
|
|
180
|
+
contain any file system, or it is not mounted. The octal escape sequences of the kernel mount table
|
|
181
|
+
(e.g. ``\\040`` for a space character) are decoded in the result.
|
|
159
182
|
"""
|
|
160
183
|
return self.__fs_mounting_point
|
|
161
184
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#
|
|
5
5
|
from typing import List, Tuple
|
|
6
6
|
from pyudev import Device
|
|
7
|
-
from diskinfo.utils import size_in_hrf, _pyudev_getint
|
|
7
|
+
from diskinfo.utils import size_in_hrf, _pyudev_getint, _pyudev_getesc
|
|
8
8
|
from diskinfo.filesystem import FileSystem
|
|
9
9
|
|
|
10
10
|
|
|
@@ -96,7 +96,7 @@ class Partition:
|
|
|
96
96
|
|
|
97
97
|
# Save further partition attributes.
|
|
98
98
|
self.__part_scheme = _device.get("ID_PART_ENTRY_SCHEME")
|
|
99
|
-
self.__part_label = _device
|
|
99
|
+
self.__part_label = _pyudev_getesc(_device, "ID_PART_ENTRY_NAME")
|
|
100
100
|
self.__part_uuid = _device.get("ID_PART_ENTRY_UUID")
|
|
101
101
|
self.__part_type = _device.get("ID_PART_ENTRY_TYPE")
|
|
102
102
|
self.__part_number = _pyudev_getint(_device, "ID_PART_ENTRY_NUMBER", 0)
|
|
@@ -347,6 +347,7 @@ class Partition:
|
|
|
347
347
|
|
|
348
348
|
def get_part_label(self) -> str:
|
|
349
349
|
"""Returns the label of the partition. The result could be empty if the partition does not have a label.
|
|
350
|
+
The escape sequences of the udev property (e.g. ``\\x20`` for a space character) are decoded in the result.
|
|
350
351
|
|
|
351
352
|
.. note::
|
|
352
353
|
This is a GPT partition-specific value.
|
|
@@ -2,9 +2,36 @@
|
|
|
2
2
|
# Module `utils`: implements utility functions.
|
|
3
3
|
# Peter Sulyok (C) 2022-2026.
|
|
4
4
|
#
|
|
5
|
+
import re
|
|
5
6
|
from typing import List, Tuple, Union
|
|
6
7
|
from pyudev import Device
|
|
7
8
|
|
|
9
|
+
# Escape sequences of an udev `_ENC` property value (e.g. `\\x20` for a space character). One non-ASCII
|
|
10
|
+
# character is stored as several consecutive sequences, one per UTF-8 byte, so they are matched in groups.
|
|
11
|
+
_UDEV_ESCAPE_RE = re.compile(r"(?:\\x[0-9a-fA-F]{2})+")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _unescape_udev_value(value: str) -> str:
|
|
15
|
+
"""Decodes the escape sequences of an udev ``_ENC`` property value. The udev escapes all bytes outside its
|
|
16
|
+
safe character set in the form of ``\\xNN``, where ``NN`` is the hexadecimal value of the byte. The escaped
|
|
17
|
+
bytes of a group are decoded together as an UTF-8 character.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
value (str): property value with escape sequences
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str: decoded property value
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
An example about the use of the function::
|
|
27
|
+
|
|
28
|
+
>>> from diskinfo import *
|
|
29
|
+
>>> _unescape_udev_value("CL\\xc3\\x89\\x20USB")
|
|
30
|
+
"CLÉ USB"
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
return _UDEV_ESCAPE_RE.sub(lambda m: bytes.fromhex(m.group(0).replace("\\x", "")).decode("utf-8", "replace"), value)
|
|
34
|
+
|
|
8
35
|
|
|
9
36
|
def _read_file(path, encoding: str = "utf-8") -> str:
|
|
10
37
|
"""Reads the text content of the specified file. The function will hide :py:obj:`IOError` and
|
|
@@ -215,10 +242,40 @@ def _pyudev_getint(device: Device, key: str, default_value: int = None) -> Union
|
|
|
215
242
|
return default_value
|
|
216
243
|
|
|
217
244
|
|
|
245
|
+
def _pyudev_getesc(dev: Device, key: str) -> Union[str, None]:
|
|
246
|
+
"""Returns a device attribute value where udev stores the escaped value in the property itself, so there is
|
|
247
|
+
no `_ENC` key pair (e.g. `ID_PART_ENTRY_NAME`). The escape sequences of the value will be decoded and the
|
|
248
|
+
result will be stripped.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
device (pyudev.Device): pyudev.Device() class
|
|
252
|
+
key (str): attribute key
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Union[str, None]: str attribute value or None
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
An example about the use of the function::
|
|
259
|
+
|
|
260
|
+
>>> from diskinfo import *
|
|
261
|
+
>>> from pyudev import *
|
|
262
|
+
>>> c = Context()
|
|
263
|
+
>>> d = Devices.from_name(c, "block", "sda1")
|
|
264
|
+
>>> print(_pyudev_getesc(d, "ID_PART_ENTRY_NAME"))
|
|
265
|
+
Basic data partition
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
value = dev.get(key)
|
|
269
|
+
if value:
|
|
270
|
+
return _unescape_udev_value(value).strip()
|
|
271
|
+
return value
|
|
272
|
+
|
|
273
|
+
|
|
218
274
|
def _pyudev_getenc(dev: Device, key: str) -> Union[str, None]:
|
|
219
275
|
"""Returns one of the device attribute key pairs. There are udev attributes key pairs (e.g. `ID_MODEL` and
|
|
220
|
-
`ID_MODEL_ENC`), where this function
|
|
221
|
-
key
|
|
276
|
+
`ID_MODEL_ENC`), where this function reads and decodes the `_ENC` version of the key if it exists, otherwise
|
|
277
|
+
the simple key will be read. The `_ENC` version is always preferred, because the simple key contains the
|
|
278
|
+
sanitized value of udev (e.g. the space characters are replaced there).
|
|
222
279
|
|
|
223
280
|
Args:
|
|
224
281
|
device (pyudev.Device): pyudev.Device() class
|
|
@@ -243,8 +300,7 @@ def _pyudev_getenc(dev: Device, key: str) -> Union[str, None]:
|
|
|
243
300
|
# Read and decode the `_ENC` key.
|
|
244
301
|
value = dev.get(key + "_ENC")
|
|
245
302
|
if value:
|
|
246
|
-
|
|
247
|
-
return value.replace("\\x20", " ").strip()
|
|
303
|
+
return _unescape_udev_value(value).strip()
|
|
248
304
|
# Read the simple key.
|
|
249
305
|
return dev.get(key)
|
|
250
306
|
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diskinfo
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: Disk information Python library for Linux
|
|
5
5
|
Author-email: Peter Sulyok <peter@sulyok.net>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/petersulyok/diskinfo
|
|
8
|
-
Project-URL: Changelog, https://github.com/petersulyok/diskinfo/CHANGELOG.md
|
|
8
|
+
Project-URL: Changelog, https://github.com/petersulyok/diskinfo/blob/main/CHANGELOG.md
|
|
9
9
|
Project-URL: Documentation, https://diskinfo.readthedocs.io/en/latest/index.html
|
|
10
10
|
Project-URL: Issues, https://github.com/petersulyok/diskinfo/issues
|
|
11
11
|
Keywords: disk,linux
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
18
|
Classifier: Topic :: System :: Hardware
|
|
19
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
20
|
-
Requires-Python: <4,>=3.
|
|
20
|
+
Requires-Python: <4,>=3.11
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: pySMART
|
|
@@ -25,7 +25,7 @@ Requires-Dist: pyudev
|
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
27
|
# diskinfo
|
|
28
|
-
[](https://github.com/petersulyok/diskinfo/actions/workflows/test.yml)
|
|
29
29
|
[](https://app.codecov.io/gh/petersulyok/diskinfo)
|
|
30
30
|
[](https://diskinfo.readthedocs.io/en/latest/?badge=latest)
|
|
31
31
|
[](https://github.com/petersulyok/diskinfo/issues)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import os
|
|
6
6
|
from unittest.mock import MagicMock, patch, mock_open
|
|
7
7
|
import pytest
|
|
8
|
-
from diskinfo.filesystem import FileSystem
|
|
8
|
+
from diskinfo.filesystem import FileSystem, _unmangle_path
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# ── mock helpers ──────────────────────────────────────────────────────────────
|
|
@@ -270,4 +270,71 @@ def test_filesystem_repr():
|
|
|
270
270
|
assert "/mnt" in result
|
|
271
271
|
|
|
272
272
|
|
|
273
|
+
# ── _unmangle_path ────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@pytest.mark.parametrize(
|
|
277
|
+
"mangled, expected",
|
|
278
|
+
[
|
|
279
|
+
("/mnt/data", "/mnt/data"),
|
|
280
|
+
("", ""),
|
|
281
|
+
("/run/media/user/Laptop\\040Files", "/run/media/user/Laptop Files"),
|
|
282
|
+
("/mnt/tab\\011here", "/mnt/tab\there"),
|
|
283
|
+
("/mnt/new\\012line", "/mnt/new\nline"),
|
|
284
|
+
("/mnt/back\\134slash", "/mnt/back\\slash"),
|
|
285
|
+
("/mnt/a\\040b\\040c", "/mnt/a b c"),
|
|
286
|
+
("/mnt/mixed\\040and\\134escaped", "/mnt/mixed and\\escaped"),
|
|
287
|
+
],
|
|
288
|
+
)
|
|
289
|
+
def test_unmangle_path_decodes_octal_escapes(mangled, expected):
|
|
290
|
+
"""_unmangle_path() decodes the octal escape sequences of the kernel mount table."""
|
|
291
|
+
assert _unmangle_path(mangled) == expected
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_unmangle_path_does_not_double_decode():
|
|
295
|
+
"""_unmangle_path() decodes an escaped backslash without decoding the text that follows it."""
|
|
296
|
+
# `\134040` is an escaped backslash followed by the literal text `040`.
|
|
297
|
+
assert _unmangle_path("/mnt/x\\134040") == "/mnt/x\\040"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ── FileSystem.__init__ – mangled paths in /proc/mounts ───────────────────────
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_filesystem_unmangles_mount_point_with_space():
|
|
304
|
+
"""FileSystem decodes \\040 in the mounting point and calls statvfs() with the decoded path."""
|
|
305
|
+
dev = _make_fs_device(path="/dev/sdc4")
|
|
306
|
+
m = _proc_mounts(("/dev/sdc4", "/run/media/pchauhan/Laptop\\040Files"))
|
|
307
|
+
with (patch("builtins.open", m),
|
|
308
|
+
patch("os.statvfs", return_value=_statvfs_result(f_bavail=2048, f_frsize=512)) as mock_statvfs):
|
|
309
|
+
fs = FileSystem(dev)
|
|
310
|
+
|
|
311
|
+
assert fs.get_fs_mounting_point() == "/run/media/pchauhan/Laptop Files"
|
|
312
|
+
mock_statvfs.assert_called_once_with("/run/media/pchauhan/Laptop Files")
|
|
313
|
+
assert fs.get_fs_free_size() == 2048
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_filesystem_unmangles_mount_point_with_tab_and_backslash():
|
|
317
|
+
"""FileSystem decodes the tab and backslash escape sequences in the mounting point."""
|
|
318
|
+
dev = _make_fs_device(path="/dev/sdb1")
|
|
319
|
+
m = _proc_mounts(("/dev/sdb1", "/mnt/a\\011b\\134c"))
|
|
320
|
+
with (patch("builtins.open", m),
|
|
321
|
+
patch("os.statvfs", return_value=_statvfs_result(f_bavail=100, f_frsize=512)) as mock_statvfs):
|
|
322
|
+
fs = FileSystem(dev)
|
|
323
|
+
|
|
324
|
+
assert fs.get_fs_mounting_point() == "/mnt/a\tb\\c"
|
|
325
|
+
mock_statvfs.assert_called_once_with("/mnt/a\tb\\c")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_filesystem_unmangles_device_node():
|
|
329
|
+
"""FileSystem matches a device node that is escaped in /proc/mounts."""
|
|
330
|
+
dev = _make_fs_device(path="/dev/disk/by-label/My Disk")
|
|
331
|
+
m = _proc_mounts(("/dev/disk/by-label/My\\040Disk", "/mnt/data"))
|
|
332
|
+
with (patch("builtins.open", m),
|
|
333
|
+
patch("os.statvfs", return_value=_statvfs_result(f_bavail=64, f_frsize=512))):
|
|
334
|
+
fs = FileSystem(dev)
|
|
335
|
+
|
|
336
|
+
assert fs.get_fs_mounting_point() == "/mnt/data"
|
|
337
|
+
assert fs.get_fs_free_size() == 64
|
|
338
|
+
|
|
339
|
+
|
|
273
340
|
# End
|
|
@@ -373,4 +373,39 @@ def test_partition_repr():
|
|
|
373
373
|
assert "filesystem=" in result
|
|
374
374
|
|
|
375
375
|
|
|
376
|
+
# ── Partition – udev escape sequences in the partition label ─────────────────
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@pytest.mark.parametrize(
|
|
380
|
+
"escaped, expected",
|
|
381
|
+
[
|
|
382
|
+
("EFI\\x20system\\x20partition", "EFI system partition"),
|
|
383
|
+
("Basic\\x20data\\x20partition", "Basic data partition"),
|
|
384
|
+
("Microsoft\\x20reserved\\x20partition", "Microsoft reserved partition"),
|
|
385
|
+
("Boot", "Boot"),
|
|
386
|
+
("Donn\\xc3\\xa9es", "Données"),
|
|
387
|
+
],
|
|
388
|
+
)
|
|
389
|
+
def test_partition_decodes_part_label(escaped, expected):
|
|
390
|
+
"""Partition decodes the udev escape sequences of the partition label (issue #10)."""
|
|
391
|
+
dev = _make_part_device(part_label=escaped)
|
|
392
|
+
m = _proc_mounts(("/dev/sda1", "/boot/efi"))
|
|
393
|
+
with (patch("builtins.open", m),
|
|
394
|
+
patch("os.statvfs", return_value=_statvfs_result(f_bavail=512000))):
|
|
395
|
+
p = Partition(dev)
|
|
396
|
+
|
|
397
|
+
assert p.get_part_label() == expected
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_partition_missing_part_label_stays_none():
|
|
401
|
+
"""Partition keeps the missing partition label as None instead of raising an exception."""
|
|
402
|
+
dev = _make_part_device(part_label=None)
|
|
403
|
+
m = _proc_mounts(("/dev/sda1", "/boot/efi"))
|
|
404
|
+
with (patch("builtins.open", m),
|
|
405
|
+
patch("os.statvfs", return_value=_statvfs_result(f_bavail=512000))):
|
|
406
|
+
p = Partition(dev)
|
|
407
|
+
|
|
408
|
+
assert p.get_part_label() is None
|
|
409
|
+
|
|
410
|
+
|
|
376
411
|
# End
|
|
@@ -12,6 +12,8 @@ from diskinfo.utils import (
|
|
|
12
12
|
time_in_hrf,
|
|
13
13
|
_pyudev_getint,
|
|
14
14
|
_pyudev_getenc,
|
|
15
|
+
_unescape_udev_value,
|
|
16
|
+
_pyudev_getesc,
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
|
|
@@ -263,13 +265,29 @@ def test_pyudev_getenc_falls_back_when_enc_is_none():
|
|
|
263
265
|
assert _pyudev_getenc(dev, "ID_MODEL") == "Samsung_SSD_850"
|
|
264
266
|
|
|
265
267
|
|
|
266
|
-
def
|
|
267
|
-
"""_pyudev_getenc() uses the
|
|
268
|
+
def test_pyudev_getenc_prefers_enc_key_without_escapes():
|
|
269
|
+
"""_pyudev_getenc() uses the _ENC key even if it does not contain any escape sequence."""
|
|
268
270
|
dev = MagicMock()
|
|
269
271
|
props = {"ID_MODEL_ENC": "SamsungSSD850", "ID_MODEL": "SamsungSSD850_plain"}
|
|
270
272
|
dev.get.side_effect = props.get
|
|
271
|
-
#
|
|
272
|
-
assert _pyudev_getenc(dev, "ID_MODEL") == "
|
|
273
|
+
# The _ENC key is the unsanitized value, so it wins over the plain key (issue #9).
|
|
274
|
+
assert _pyudev_getenc(dev, "ID_MODEL") == "SamsungSSD850"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_pyudev_getenc_decodes_non_ascii_with_space():
|
|
278
|
+
"""_pyudev_getenc() decodes a non-ASCII character and a space in the same value (issue #9)."""
|
|
279
|
+
dev = MagicMock()
|
|
280
|
+
props = {"ID_FS_LABEL_ENC": "CL\\xc3\\x89\\x20USB", "ID_FS_LABEL": "CLÉ_USB"}
|
|
281
|
+
dev.get.side_effect = props.get
|
|
282
|
+
assert _pyudev_getenc(dev, "ID_FS_LABEL") == "CLÉ USB"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_pyudev_getenc_decodes_non_ascii_without_space():
|
|
286
|
+
"""_pyudev_getenc() decodes a non-ASCII character when the value has no space (issue #9)."""
|
|
287
|
+
dev = MagicMock()
|
|
288
|
+
props = {"ID_FS_LABEL_ENC": "CL\\xc3\\x89USB", "ID_FS_LABEL": "CL_USB"}
|
|
289
|
+
dev.get.side_effect = props.get
|
|
290
|
+
assert _pyudev_getenc(dev, "ID_FS_LABEL") == "CLÉUSB"
|
|
273
291
|
|
|
274
292
|
|
|
275
293
|
def test_pyudev_getenc_returns_none_when_both_keys_absent():
|
|
@@ -287,4 +305,67 @@ def test_pyudev_getenc_strips_whitespace_after_decode():
|
|
|
287
305
|
assert _pyudev_getenc(dev, "ID_FS_LABEL") == "My Label"
|
|
288
306
|
|
|
289
307
|
|
|
308
|
+
# ── _unescape_udev_value ──────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@pytest.mark.parametrize(
|
|
312
|
+
"escaped, expected",
|
|
313
|
+
[
|
|
314
|
+
("SYSTEM", "SYSTEM"),
|
|
315
|
+
("", ""),
|
|
316
|
+
("Recovery\\x20tools", "Recovery tools"),
|
|
317
|
+
("Samsung\\x20SSD\\x20850", "Samsung SSD 850"),
|
|
318
|
+
("CL\\xc3\\x89\\x20USB", "CLÉ USB"),
|
|
319
|
+
("CL\\xc3\\x89USB", "CLÉUSB"),
|
|
320
|
+
("caf\\xc3\\xa9\\x20dr\\xc3\\xafve", "café drïve"),
|
|
321
|
+
("\\xe2\\x82\\xac", "€"),
|
|
322
|
+
("\\xC3\\x89", "É"),
|
|
323
|
+
],
|
|
324
|
+
)
|
|
325
|
+
def test_unescape_udev_value_decodes_escapes(escaped, expected):
|
|
326
|
+
"""_unescape_udev_value() decodes the udev escape sequences into UTF-8 characters."""
|
|
327
|
+
assert _unescape_udev_value(escaped) == expected
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_unescape_udev_value_invalid_utf8_is_replaced():
|
|
331
|
+
"""_unescape_udev_value() replaces an invalid UTF-8 byte instead of raising an exception."""
|
|
332
|
+
assert _unescape_udev_value("bad\\xffend") == "bad\ufffdend"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_unescape_udev_value_keeps_non_escape_text():
|
|
336
|
+
"""_unescape_udev_value() keeps a backslash that is not part of an escape sequence."""
|
|
337
|
+
assert _unescape_udev_value("a\\xzz\\x41") == "a\\xzzA"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ── _pyudev_getesc ────────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_pyudev_getesc_decodes_escapes():
|
|
344
|
+
"""_pyudev_getesc() decodes the escape sequences stored in the property value itself."""
|
|
345
|
+
dev = MagicMock()
|
|
346
|
+
dev.get.side_effect = {"ID_PART_ENTRY_NAME": "Basic\\x20data\\x20partition"}.get
|
|
347
|
+
assert _pyudev_getesc(dev, "ID_PART_ENTRY_NAME") == "Basic data partition"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_pyudev_getesc_strips_whitespace_after_decode():
|
|
351
|
+
"""_pyudev_getesc() strips the leading and trailing whitespaces after the decoding."""
|
|
352
|
+
dev = MagicMock()
|
|
353
|
+
dev.get.side_effect = {"ID_PART_ENTRY_NAME": "\\x20Boot\\x20"}.get
|
|
354
|
+
assert _pyudev_getesc(dev, "ID_PART_ENTRY_NAME") == "Boot"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_pyudev_getesc_missing_key_returns_none():
|
|
358
|
+
"""_pyudev_getesc() returns None if the key does not exist."""
|
|
359
|
+
dev = MagicMock()
|
|
360
|
+
dev.get.return_value = None
|
|
361
|
+
assert _pyudev_getesc(dev, "ID_PART_ENTRY_NAME") is None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def test_pyudev_getesc_empty_value_is_kept():
|
|
365
|
+
"""_pyudev_getesc() returns the empty value without decoding."""
|
|
366
|
+
dev = MagicMock()
|
|
367
|
+
dev.get.return_value = ""
|
|
368
|
+
assert _pyudev_getesc(dev, "ID_PART_ENTRY_NAME") == ""
|
|
369
|
+
|
|
370
|
+
|
|
290
371
|
# End
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|