Coverage for linuxpy/sysfs.py: 71%

94 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-13 08:09 +0200

1# 

2# This file is part of the linuxpy project 

3# 

4# Copyright (c) 2023 Tiago Coutinho 

5# Distributed under the GPLv3 license. See LICENSE for more info. 

6 

7import enum 

8import functools 

9import os 

10import pathlib 

11 

12from linuxpy.types import Callable 

13from linuxpy.util import make_find 

14 

15from . import mounts 

16from .magic import Magic 

17from .statfs import get_fs_type 

18from .types import Iterable, Optional 

19 

20MAGIC = Magic.SYSFS 

21 

22MOUNT_PATH = mounts.sysfs() 

23DEVICE_PATH = MOUNT_PATH / "bus/usb/devices" 

24CLASS_PATH = MOUNT_PATH / "class" 

25THERMAL_PATH = CLASS_PATH / "thermal" 

26LED_PATH = CLASS_PATH / "leds" 

27 

28 

29class Mode(enum.Enum): 

30 enabled = "enabled" 

31 disabled = "disabled" 

32 

33 

34def is_sysfs(path) -> bool: 

35 return get_fs_type(path) == MAGIC 

36 

37 

38def is_available() -> bool: 

39 return is_sysfs(MOUNT_PATH) 

40 

41 

42def iter_read_uevent(path: os.PathLike): 

43 path = pathlib.Path(path) 

44 with path.open() as f: 

45 for line in f: 

46 yield line.strip().split("=", 1) 

47 

48 

49class Attr: 

50 def __init__(self, filename: Optional[str] = None, decode: Callable = str): 

51 self.filename = filename 

52 self.decode = decode 

53 

54 def __set_name__(self, owner, name): 

55 if self.filename is None: 

56 self.filename = name 

57 

58 def _path(self, obj): 

59 return obj.syspath / self.filename 

60 

61 def __get__(self, obj, objtype=None): 

62 with self._path(obj).open() as f: 

63 return self.decode(f.read().strip()) 

64 

65 def __set__(self, obj, value): 

66 with self._path(obj).open("w") as f: 

67 f.write(str(value)) 

68 

69 

70Str = functools.partial(Attr, decode=str) 

71Int = functools.partial(Attr, decode=int) 

72 

73 

74class Device: 

75 syspath: pathlib.Path 

76 _subsystem: Optional[str] = None 

77 _attrs: Optional[dict] = None 

78 

79 def __init__(self, syspath: os.PathLike): 

80 self.syspath = pathlib.Path(syspath) 

81 

82 def __enter__(self): 

83 return self 

84 

85 def __exit__(self, *_): 

86 pass 

87 

88 @classmethod 

89 def from_syspath(cls, syspath): 

90 syspath = pathlib.Path(syspath).resolve() 

91 return cls(syspath) 

92 

93 @property 

94 def subsystem(self): 

95 if self._subsystem is None: 

96 self._subsystem = (self.syspath / "subsystem").resolve().stem 

97 return self._subsystem 

98 

99 @property 

100 def attrs(self) -> dict[str, str]: 

101 if self._attrs is None: 

102 self._attrs = dict(iter_read_uevent(self.syspath / "uevent")) 

103 return self._attrs 

104 

105 @property 

106 def devnum(self) -> int: 

107 return (int(self.attrs["MAJOR"]) << 8) + int(self.attrs["MINOR"]) 

108 

109 @property 

110 def devpath(self) -> pathlib.Path: 

111 return self.syspath 

112 

113 @property 

114 def devname(self) -> str: 

115 return self.attrs.get("DEVNAME", "") 

116 

117 

118def iter_device_paths() -> Iterable[pathlib.Path]: 

119 char = MOUNT_PATH / "dev" / "char" 

120 block = MOUNT_PATH / "dev" / "block" 

121 yield from char.iterdir() 

122 yield from block.iterdir() 

123 

124 

125def iter_devices() -> Iterable[Device]: 

126 return (Device.from_syspath(path) for path in iter_device_paths()) 

127 

128 

129find = make_find(iter_devices) 

130 

131 

132def main(): 

133 devs = find(find_all=True) 

134 devs = sorted(devs, key=lambda dev: dev.subsystem) 

135 for dev in devs: 

136 major_minor = f"{dev.attrs['MAJOR']:>3}:{dev.attrs['MINOR']:<3}" 

137 print(f"{dev.devname:<20} {dev.subsystem:<16} {major_minor:<7} ({dev.devnum:>6}) {dev.syspath}") 

138 

139 

140if __name__ == "__main__": 

141 main()