Coverage for pyTooling / Filesystem / __init__.py: 48%

484 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-07 17:18 +0000

1# ==================================================================================================================== # 

2# _____ _ _ _____ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | ___(_) | ___ ___ _ _ ___| |_ ___ _ __ ___ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_ | | |/ _ \/ __| | | / __| __/ _ \ '_ ` _ \ # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| _| | | | __/\__ \ |_| \__ \ || __/ | | | | | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|_|\___||___/\__, |___/\__\___|_| |_| |_| # 

7# |_| |___/ |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32An object-oriented file system abstraction for directory, file, symbolic link, ... statistics collection. 

33 

34.. important:: 

35 

36 This isn't a replacement of :mod:`pathlib` introduced with Python 3.4. 

37""" 

38from os import scandir, readlink 

39 

40from enum import Enum 

41from itertools import chain 

42from pathlib import Path 

43from typing import Optional as Nullable, Dict, Generic, Generator, TypeVar, List, Any, Callable, Union 

44 

45from pyTooling.Decorators import readonly, export 

46from pyTooling.Exceptions import ToolingException 

47from pyTooling.MetaClasses import ExtendedType 

48from pyTooling.Common import getFullyQualifiedName, zipdicts 

49from pyTooling.Stopwatch import Stopwatch 

50from pyTooling.Tree import Node 

51 

52 

53__all__ = ["_ParentType"] 

54 

55_ParentType = TypeVar("_ParentType", bound="Element") 

56"""The type variable for a parent reference.""" 

57 

58 

59@export 

60class FilesystemException(ToolingException): 

61 """Base-exception of all exceptions raised by :mod:`pyTooling.Filesystem`.""" 

62 

63 

64@export 

65class NodeKind(Enum): 

66 """ 

67 Node kind for filesystem elements in a :ref:`tree <STRUCT/Tree>`. 

68 

69 This enumeration is used when converting the filesystem statistics tree to an instance of :mod:`pyTooling.Tree`. 

70 """ 

71 Directory = 0 #: Node represents a directory. 

72 File = 1 #: Node represents a regular file. 

73 SymbolicLink = 2 #: Node represents a symbolic link. 

74 

75 

76@export 

77class Base(metaclass=ExtendedType, slots=True): 

78 """ 

79 Base-class for all filesystem elements in :mod:`pyTooling.Filesystem`. 

80 

81 It implements a size and a reference to the root element of the filesystem. 

82 """ 

83 _root: Nullable["Root"] #: Reference to the root of the filesystem statistics scope. 

84 _size: Nullable[int] #: Actual or aggregated size of the filesystem element. 

85 

86 def __init__( 

87 self, 

88 size: Nullable[int], 

89 root: Nullable["Root"] 

90 ) -> None: 

91 """ 

92 Initialize the base-class with filesystem element size and root reference. 

93 

94 :param size: Optional size of the element. 

95 :param root: Optional reference to the filesystem root element. 

96 """ 

97 if size is None: 

98 pass 

99 elif not isinstance(size, int): 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 ex = TypeError("Parameter 'size' is not of type 'int'.") 

101 ex.add_note(f"Got type '{getFullyQualifiedName(size)}'.") 

102 raise ex 

103 

104 self._size = size 

105 self._root = root 

106 

107 @property 

108 def Root(self) -> Nullable["Root"]: 

109 """ 

110 Property to access the root of the filesystem statistics scope. 

111 

112 :returns: Root of the filesystem statistics scope. 

113 """ 

114 return self._root 

115 

116 @Root.setter 

117 def Root(self, value: "Root") -> None: 

118 self._root = value 

119 

120 @readonly 

121 def Size(self) -> int: 

122 """ 

123 Read-only property to access the element's size in Bytes. 

124 

125 :returns: Size in Bytes. 

126 :raises FilesystemException: If size is not computed, yet. 

127 """ 

128 if self._size is None: 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true

129 raise FilesystemException("Size is not computed, yet.") 

130 

131 return self._size 

132 

133 # FIXME: @abstractmethod 

134 def ToTree(self) -> Node: 

135 """ 

136 Convert a filesystem element to a node in :mod:`pyTooling.Tree`. 

137 

138 The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the filesystem element. Additional data 

139 will be stored in the node's key-value store. 

140 

141 :returns: A tree's node referencing this filesystem element. 

142 """ 

143 raise NotImplementedError() 

144 

145 

146@export 

147class Element(Base, Generic[_ParentType]): 

148 """ 

149 Base-class for all named elements within a filesystem. 

150 

151 It adds a name, parent reference and list of symbolic-link sources. 

152 

153 .. hint:: 

154 

155 Symbolic link sources are reverse references describing which symbolic links point to this element. 

156 """ 

157 _name: str #: Name of the filesystem element. 

158 _parent: _ParentType #: Reference to the filesystem element's parent (:class:`Directory`) 

159 _linkSources: List["SymbolicLink"] #: A list of symbolic links pointing to this filesystem element. 

160 

161 def __init__( 

162 self, 

163 name: str, 

164 size: Nullable[int] = None, 

165 parent: Nullable[_ParentType] = None 

166 ) -> None: 

167 """ 

168 Initialize the element base-class with name, size and parent reference. 

169 

170 :param name: Name of the element. 

171 :param size: Optional size of the element. 

172 :param parent: Optional parent reference. 

173 """ 

174 root = None # FIXME: if parent is None else parent._root 

175 

176 super().__init__(size, root) 

177 

178 self._parent = parent 

179 self._name = name 

180 self._linkSources = [] 

181 

182 @property 

183 def Parent(self) -> _ParentType: 

184 """ 

185 Property to access the element's parent. 

186 

187 :returns: Parent element. 

188 """ 

189 return self._parent 

190 

191 @Parent.setter 

192 def Parent(self, value: _ParentType) -> None: 

193 self._parent = value 

194 

195 if value._root is not None: 

196 self._root = value._root 

197 

198 @readonly 

199 def Name(self) -> str: 

200 """ 

201 Read-only property to access the element's name. 

202 

203 :returns: Element name. 

204 """ 

205 return self._name 

206 

207 @readonly 

208 def Path(self) -> Path: 

209 raise NotImplemented(f"Property 'Path' is abstract.") 

210 

211 def AddLinkSources(self, source: "SymbolicLink") -> None: 

212 """ 

213 Add a link source of a symbolic link to the named element (reverse reference). 

214 

215 :param source: The referenced symbolic link. 

216 """ 

217 if not isinstance(source, SymbolicLink): 

218 ex = TypeError("Parameter 'source' is not of type 'SymbolicLink'.") 

219 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.") 

220 raise ex 

221 

222 self._linkSources.append(source) 

223 

224 

225@export 

226class Directory(Element["Directory"]): 

227 """ 

228 A **directory** represents a directory in the filesystem, which contains subdirectories, regular files and symbolic links. 

229 

230 While scanning for subelements, the directory is populated with elements. Every file object added, gets registered in 

231 the filesystems :class:`Root` for deduplication. In case a file identifier already exists, the found filename will 

232 reference the same file objects. In turn, the file objects has then references to multiple filenames (parents). This 

233 allows to detect :term:`hardlinks <hardlink>`. 

234 

235 The time needed for scanning the directory and its subelements is provided via :data:`ScanDuration`. 

236 

237 After scnaning the directory for subelements, certain directory properties get aggregated. The time needed for 

238 aggregation is provided via :data:`AggregateDuration`. 

239 """ 

240 

241 _path: Nullable[Path] #: Cached :class:`~pathlib.Path` object of this directory. 

242 _subdirectories: Dict[str, "Directory"] #: Dictionary containing name-:class:`Directory` pairs. 

243 _files: Dict[str, "Filename"] #: Dictionary containing name-:class:`Filename` pairs. 

244 _symbolicLinks: Dict[str, "SymbolicLink"] #: Dictionary containing name-:class:`SymbolicLink` pairs. 

245 _collapsed: bool #: True, if this directory was collapsed. It contains no subelements. 

246 _scanDuration: Nullable[float] #: Duration for scanning the directory and all its subelements. 

247 _aggregateDuration: Nullable[float] #: Duration for aggregating all subelements. 

248 

249 def __init__( 

250 self, 

251 name: str, 

252 collectSubdirectories: bool = False, 

253 parent: Nullable["Directory"] = None 

254 ) -> None: 

255 """ 

256 Initialize the directory with name and parent reference. 

257 

258 :param name: Name of the element. 

259 :param collectSubdirectories: If true, collect subdirectory statistics. 

260 :param parent: Optional parent reference. 

261 """ 

262 super().__init__(name, None, parent) 

263 

264 self._path = None 

265 self._subdirectories = {} 

266 self._files = {} 

267 self._symbolicLinks = {} 

268 self._collapsed = False 

269 self._scanDuration = None 

270 self._aggregateDuration = None 

271 

272 if parent is not None: 

273 parent._subdirectories[name] = self 

274 

275 if parent._root is not None: 275 ↛ 278line 275 didn't jump to line 278 because the condition on line 275 was always true

276 self._root = parent._root 

277 

278 if collectSubdirectories: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true

279 self._collectSubdirectories() 

280 

281 def _collectSubdirectories(self) -> None: 

282 """ 

283 Helper method for scanning subdirectories and aggregating found element sizes therein. 

284 """ 

285 with Stopwatch() as sw1: 

286 self._scanSubdirectories() 

287 

288 with Stopwatch() as sw2: 

289 self._aggregateSizes() 

290 

291 self._scanDuration = sw1.Duration 

292 self._aggregateDuration = sw2.Duration 

293 

294 def _scanSubdirectories(self) -> None: 

295 """ 

296 Helper method for scanning subdirectories (recursively) and building a 

297 :class:`Directory`-:class:`Filename`-:class:`File` object tree. 

298 

299 If a file refers to the same filesystem internal unique ID, a hardlink (two or more filenames) to the same file 

300 storage object is assumed. 

301 """ 

302 try: 

303 items = scandir(directoryPath := self.Path) 

304 except PermissionError as ex: 

305 return 

306 

307 for dirEntry in items: 

308 if dirEntry.is_dir(follow_symlinks=False): 

309 subdirectory = Directory(dirEntry.name, collectSubdirectories=True, parent=self) 

310 elif dirEntry.is_file(follow_symlinks=False): 

311 id = dirEntry.inode() 

312 if id in self._root._ids: 

313 file = self._root._ids[id] 

314 

315 hardLink = Filename(dirEntry.name, file=file, parent=self) 

316 else: 

317 s = dirEntry.stat(follow_symlinks=False) 

318 filename = Filename(dirEntry.name, parent=self) 

319 file = File(id, s.st_size, parent=filename) 

320 

321 self._root._ids[id] = file 

322 elif dirEntry.is_symlink(): 

323 target = Path(readlink(directoryPath / dirEntry.name)) 

324 symlink = SymbolicLink(dirEntry.name, target, parent=self) 

325 else: 

326 raise FilesystemException(f"Unknown directory element.") 

327 

328 def _connectSymbolicLinks(self) -> None: 

329 for dir in self._subdirectories.values(): 

330 dir._connectSymbolicLinks() 

331 

332 for link in self._symbolicLinks.values(): 

333 if link._target.is_absolute(): 

334 pass 

335 else: 

336 target = self 

337 for elem in link._target.parts: 

338 if elem == ".": 

339 continue 

340 elif elem == "..": 

341 target = target._parent 

342 continue 

343 

344 try: 

345 target = target._subdirectories[elem] 

346 continue 

347 except KeyError: 

348 pass 

349 

350 try: 

351 target = target._files[elem] 

352 continue 

353 except KeyError: 

354 pass 

355 

356 try: 

357 target = target._symbolicLinks[elem] 

358 continue 

359 except KeyError: 

360 pass 

361 

362 target.AddLinkSources(link) 

363 

364 def _aggregateSizes(self) -> None: 

365 self._size = ( 

366 sum(dir._size for dir in self._subdirectories.values()) + 

367 sum(file._file._size for file in self._files.values()) 

368 ) 

369 

370 @Element.Root.setter 

371 def Root(self, value: "Root") -> None: 

372 Element.Root.fset(self, value) 

373 

374 for subdir in self._subdirectories.values(): 374 ↛ 375line 374 didn't jump to line 375 because the loop on line 374 never started

375 subdir.Root = value 

376 

377 for file in self._files.values(): 

378 file.Root = value 

379 

380 for link in self._symbolicLinks.values(): 380 ↛ 381line 380 didn't jump to line 381 because the loop on line 380 never started

381 link.Root = value 

382 

383 @Element.Parent.setter 

384 def Parent(self, value: _ParentType) -> None: 

385 Element.Parent.fset(self, value) 

386 

387 value._subdirectories[self._name] = self 

388 

389 if isinstance(value, Root): 389 ↛ exitline 389 didn't return from function 'Parent' because the condition on line 389 was always true

390 self.Root = value 

391 

392 @readonly 

393 def Count(self) -> int: 

394 """ 

395 Read-only property to access the number of elements in a directory. 

396 

397 :returns: Number of files plus subdirectories. 

398 """ 

399 return len(self._subdirectories) + len(self._files) + len(self._symbolicLinks) 

400 

401 @readonly 

402 def FileCount(self) -> int: 

403 """ 

404 Read-only property to access the number of files in a directory. 

405 

406 .. hint:: 

407 

408 Files include regular files and symbolic links. 

409 

410 :returns: Number of files. 

411 """ 

412 return len(self._files) + len(self._symbolicLinks) 

413 

414 @readonly 

415 def RegularFileCount(self) -> int: 

416 """ 

417 Read-only property to access the number of regular files in a directory. 

418 

419 :returns: Number of regular files. 

420 """ 

421 return len(self._files) 

422 

423 @readonly 

424 def SymbolicLinkCount(self) -> int: 

425 """ 

426 Read-only property to access the number of symbolic links in a directory. 

427 

428 :returns: Number of symbolic links. 

429 """ 

430 return len(self._symbolicLinks) 

431 

432 @readonly 

433 def SubdirectoryCount(self) -> int: 

434 """ 

435 Read-only property to access the number of subdirectories in a directory. 

436 

437 :returns: Number of subdirectories. 

438 """ 

439 return len(self._subdirectories) 

440 

441 @readonly 

442 def TotalFileCount(self) -> int: 

443 """ 

444 Read-only property to access the total number of files in all child hierarchy levels (recursively). 

445 

446 .. hint:: 

447 

448 Files include regular files and symbolic links. 

449 

450 :returns: Total number of files. 

451 """ 

452 return sum(d.TotalFileCount for d in self._subdirectories.values()) + len(self._files) + len(self._symbolicLinks) 

453 

454 @readonly 

455 def TotalRegularFileCount(self) -> int: 

456 """ 

457 Read-only property to access the total number of regular files in all child hierarchy levels (recursively). 

458 

459 :returns: Total number of regular files. 

460 """ 

461 return sum(d.TotalRegularFileCount for d in self._subdirectories.values()) + len(self._files) 

462 

463 @readonly 

464 def TotalSymbolicLinkCount(self) -> int: 

465 """ 

466 Read-only property to access the total number of symbolic links in all child hierarchy levels (recursively). 

467 

468 :returns: Total number of symbolic links. 

469 """ 

470 return sum(d.TotalSymbolicLinkCount for d in self._subdirectories.values()) + len(self._symbolicLinks) 

471 

472 @readonly 

473 def TotalSubdirectoryCount(self) -> int: 

474 """ 

475 Read-only property to access the total number of subdirectories in all child hierarchy levels (recursively). 

476 

477 :returns: Total number of subdirectories. 

478 """ 

479 return len(self._subdirectories) + sum(d.TotalSubdirectoryCount for d in self._subdirectories.values()) 

480 

481 @readonly 

482 def Subdirectories(self) -> Generator["Directory", None, None]: 

483 """ 

484 Iterate all direct subdirectories of the directory. 

485 

486 :returns: A generator to iterate all direct subdirectories. 

487 """ 

488 return (d for d in self._subdirectories.values()) 

489 

490 @readonly 

491 def Files(self) -> Generator[Union["Filename", "SymbolicLink"], None, None]: 

492 """ 

493 Iterate all direct files of the directory. 

494 

495 .. hint:: 

496 

497 Files include regular files and symbolic links. 

498 

499 :returns: A generator to iterate all direct files. 

500 """ 

501 return (f for f in chain(self._files.values(), self._symbolicLinks.values())) 

502 

503 @readonly 

504 def RegularFiles(self) -> Generator["Filename", None, None]: 

505 """ 

506 Iterate all direct regular files of the directory. 

507 

508 :returns: A generator to iterate all direct regular files. 

509 """ 

510 return (f for f in self._files.values()) 

511 

512 @readonly 

513 def SymbolicLinks(self) -> Generator["SymbolicLink", None, None]: 

514 """ 

515 Iterate all direct symbolic links of the directory. 

516 

517 :returns: A generator to iterate all direct symbolic links. 

518 """ 

519 return (l for l in self._symbolicLinks.values()) 

520 

521 @readonly 

522 def Path(self) -> Path: 

523 """ 

524 Read-only property to access the equivalent Path instance for accessing the represented directory. 

525 

526 :returns: Path to the directory. 

527 :raises FilesystemException: If no parent is set. 

528 """ 

529 if self._path is not None: 

530 return self._path 

531 

532 if self._parent is None: 

533 raise FilesystemException(f"No parent or root set for directory.") 

534 

535 self._path = self._parent.Path / self._name 

536 return self._path 

537 

538 @readonly 

539 def ScanDuration(self) -> float: 

540 """ 

541 Read-only property to access the time needed to scan a directory structure including all subelements (recursively). 

542 

543 :returns: The scan duration in seconds. 

544 :raises FilesystemException: If the directory was not scanned. 

545 """ 

546 if self._scanDuration is None: 

547 raise FilesystemException(f"Directory was not scanned, yet.") 

548 

549 return self._scanDuration 

550 

551 @readonly 

552 def AggregateDuration(self) -> float: 

553 """ 

554 Read-only property to access the time needed to aggregate the directory's and subelement's properties (recursively). 

555 

556 :returns: The aggregation duration in seconds. 

557 :raises FilesystemException: If the directory properties were not aggregated. 

558 """ 

559 if self._scanDuration is None: 

560 raise FilesystemException(f"Directory properties were not aggregated, yet.") 

561 

562 return self._aggregateDuration 

563 

564 def Copy(self, parent: Nullable["Directory"] = None) -> "Directory": 

565 """ 

566 Copy the directory structure including all subelements and link it to the given parent. 

567 

568 .. hint:: 

569 

570 Statistics like aggregated directory size are copied too. |br| 

571 There is no rescan or repeated aggregation needed. 

572 

573 :param parent: The parent element of the copied directory. 

574 :returns: A deep copy of the directory structure. 

575 """ 

576 dir = Directory(self._name, parent=parent) 

577 dir._size = self._size 

578 

579 for subdir in self._subdirectories.values(): 

580 subdir.Copy(dir) 

581 

582 for file in self._files.values(): 

583 file.Copy(dir) 

584 

585 for link in self._symbolicLinks.values(): 

586 link.Copy(dir) 

587 

588 return dir 

589 

590 def Collapse(self, func: Callable[["Directory"], bool]) -> bool: 

591 # if len(self._subdirectories) == 0 or all(subdir.Collapse(func) for subdir in self._subdirectories.values()): 

592 if len(self._subdirectories) == 0: 

593 if func(self): 

594 # print(f"collapse 1 {self.Path}") 

595 self._collapsed = True 

596 self._subdirectories.clear() 

597 self._files.clear() 

598 self._symbolicLinks.clear() 

599 

600 return True 

601 else: 

602 return False 

603 

604 # if all(subdir.Collapse(func) for subdir in self._subdirectories.values()) 

605 collapsible = True 

606 for subdir in self._subdirectories.values(): 

607 result = subdir.Collapse(func) 

608 collapsible = collapsible and result 

609 

610 if collapsible: 

611 # print(f"collapse 2 {self.Path}") 

612 self._collapsed = True 

613 self._subdirectories.clear() 

614 self._files.clear() 

615 self._symbolicLinks.clear() 

616 

617 return True 

618 else: 

619 return False 

620 

621 def ToTree(self, format: Nullable[Callable[[Node], str]] = None) -> Node: 

622 """ 

623 Convert the directory to a :class:`~pyTooling.Tree.Node`. 

624 

625 The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the directory. Additional data is 

626 attached to the node's key-value store: 

627 

628 ``kind`` 

629 The node's kind. See :class:`NodeKind`. 

630 ``size`` 

631 The directory's aggregated size. 

632 

633 :param format: A user defined formatting function for tree nodes. 

634 :returns: A tree node representing this directory. 

635 """ 

636 if format is None: 

637 def format(node: Node) -> str: 

638 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}" 

639 

640 directoryNode = Node( 

641 value=self, 

642 keyValuePairs={ 

643 "kind": NodeKind.File, 

644 "size": self._size 

645 }, 

646 format=format 

647 ) 

648 directoryNode.AddChildren( 

649 e.ToTree(format) for e in chain(self._subdirectories.values()) #, self._files.values(), self._symbolicLinks.values()) 

650 ) 

651 

652 return directoryNode 

653 

654 def __eq__(self, other) -> bool: 

655 """ 

656 Compare two Directory instances for equality. 

657 

658 :param other: Parameter to compare against. 

659 :returns: ``True``, if both directories and all its subelements are equal. 

660 :raises TypeError: If parameter ``other`` is not of type :class:`Directory`. 

661 """ 

662 if not isinstance(other, Directory): 

663 ex = TypeError("Parameter 'other' is not of type Directory.") 

664 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

665 raise ex 

666 

667 if not all(dir1 == dir2 for _, dir1, dir2 in zipdicts(self._subdirectories, other._subdirectories)): 

668 return False 

669 

670 if not all(file1 == file2 for _, file1, file2 in zipdicts(self._files, other._files)): 

671 return False 

672 

673 if not all(link1 == link2 for _, link1, link2 in zipdicts(self._symbolicLinks, other._symbolicLinks)): 

674 return False 

675 

676 return True 

677 

678 def __ne__(self, other: Any) -> bool: 

679 """ 

680 Compare two Directory instances for inequality. 

681 

682 :param other: Parameter to compare against. 

683 :returns: ``True``, if both directories and all its subelements are unequal. 

684 :raises TypeError: If parameter ``other`` is not of type :class:`Directory`. 

685 """ 

686 return not self.__eq__(other) 

687 

688 def __repr__(self) -> str: 

689 return f"Directory: {self.Path}" 

690 

691 def __str__(self) -> str: 

692 return self._name 

693 

694 

695@export 

696class Filename(Element[Directory]): 

697 """ 

698 Represents a filename in the filesystem, but not the file storage object (:class:`File`). 

699 

700 .. hint:: 

701 

702 Filename and file storage are represented by two classes, which allows multiple names (hard links) per file storage 

703 object. 

704 """ 

705 _file: Nullable["File"] 

706 

707 def __init__( 

708 self, 

709 name: str, 

710 file: Nullable["File"] = None, 

711 parent: Nullable[Directory] = None 

712 ) -> None: 

713 """ 

714 Initialize the filename with name, file (storage) object and parent reference. 

715 

716 :param name: Name of the file. 

717 :param size: Optional file (storage) object. 

718 :param parent: Optional parent reference. 

719 """ 

720 super().__init__(name, None, parent) 

721 

722 if file is None: 722 ↛ 725line 722 didn't jump to line 725 because the condition on line 722 was always true

723 self._file = None 

724 else: 

725 self._file = file 

726 file._parents.append(self) 

727 

728 if parent is not None: 

729 parent._files[name] = self 

730 

731 if parent._root is not None: 

732 self._root = parent._root 

733 

734 @Element.Root.setter 

735 def Root(self, value: "Root") -> None: 

736 self._root = value 

737 

738 if self._file is not None: 738 ↛ exitline 738 didn't return from function 'Root' because the condition on line 738 was always true

739 self._file._root = value 

740 

741 @Element.Parent.setter 

742 def Parent(self, value: _ParentType) -> None: 

743 Element.Parent.fset(self, value) 

744 

745 value._files[self._name] = self 

746 

747 if isinstance(value, Root): 747 ↛ 748line 747 didn't jump to line 748 because the condition on line 747 was never true

748 self.Root = value 

749 

750 @readonly 

751 def File(self) -> Nullable["File"]: 

752 return self._file 

753 

754 @readonly 

755 def Size(self) -> int: 

756 if self._file is None: 

757 raise ToolingException(f"Filename isn't linked to a File object.") 

758 

759 return self._file._size 

760 

761 @readonly 

762 def Path(self) -> Path: 

763 if self._parent is None: 

764 raise ToolingException(f"Filename has no parent object.") 

765 

766 return self._parent.Path / self._name 

767 

768 def Copy(self, parent: Directory) -> "Filename": 

769 fileID = self._file._id 

770 

771 if fileID in parent._root._ids: 

772 file = parent._root._ids[fileID] 

773 else: 

774 fileSize = self._file._size 

775 file = File(fileID, fileSize) 

776 

777 parent._root._ids[fileID] = file 

778 

779 return Filename(self._name, file, parent=parent) 

780 

781 def ToTree(self) -> Node: 

782 def format(node: Node) -> str: 

783 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}" 

784 

785 fileNode = Node( 

786 value=self, 

787 keyValuePairs={ 

788 "kind": NodeKind.File, 

789 "size": self._size 

790 }, 

791 format=format 

792 ) 

793 

794 return fileNode 

795 

796 def __eq__(self, other) -> bool: 

797 """ 

798 Compare two Filename instances for equality. 

799 

800 :param other: Parameter to compare against. 

801 :returns: ``True``, if both filenames are equal. 

802 :raises TypeError: If parameter ``other`` is not of type :class:`Filename`. 

803 """ 

804 if not isinstance(other, Filename): 

805 ex = TypeError("Parameter 'other' is not of type Filename.") 

806 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

807 raise ex 

808 

809 return self._name == other._name and self.Size == other.Size 

810 

811 def __ne__(self, other: Any) -> bool: 

812 """ 

813 Compare two Filename instances for inequality. 

814 

815 :param other: Parameter to compare against. 

816 :returns: ``True``, if both filenames are unequal. 

817 :raises TypeError: If parameter ``other`` is not of type :class:`Filename`. 

818 """ 

819 if not isinstance(other, Filename): 

820 ex = TypeError("Parameter 'other' is not of type Filename.") 

821 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

822 raise ex 

823 

824 return self._name != other._name or self.Size != other.Size 

825 

826 def __repr__(self) -> str: 

827 return f"File: {self.Path}" 

828 

829 def __str__(self) -> str: 

830 return self._name 

831 

832 

833@export 

834class SymbolicLink(Element[Directory]): 

835 _target: Path 

836 

837 def __init__( 

838 self, 

839 name: str, 

840 target: Path, 

841 parent: Nullable[Directory] 

842 ) -> None: 

843 super().__init__(name, None, parent) 

844 

845 self._target = target 

846 

847 if parent is not None: 

848 parent._symbolicLinks[name] = self 

849 

850 if parent._root is not None: 

851 self._root = parent._root 

852 

853 @readonly 

854 def Path(self) -> Path: 

855 return self._parent.Path / self._name 

856 

857 @readonly 

858 def Target(self) -> Path: 

859 return self._target 

860 

861 def Copy(self, parent: Directory) -> "SymbolicLink": 

862 return SymbolicLink(self._name, self._target, parent=parent) 

863 

864 def ToTree(self) -> Node: 

865 def format(node: Node) -> str: 

866 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}" 

867 

868 symbolicLinkNode = Node( 

869 value=self, 

870 keyValuePairs={ 

871 "kind": NodeKind.SymbolicLink, 

872 "size": self._size 

873 }, 

874 format=format 

875 ) 

876 

877 return symbolicLinkNode 

878 

879 def __eq__(self, other) -> bool: 

880 """ 

881 Compare two SymbolicLink instances for equality. 

882 

883 :param other: Parameter to compare against. 

884 :returns: ``True``, if both symbolic links are equal. 

885 :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`. 

886 """ 

887 if not isinstance(other, SymbolicLink): 

888 ex = TypeError("Parameter 'other' is not of type SymbolicLink.") 

889 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

890 raise ex 

891 

892 return self._name == other._name and self._target == other._target 

893 

894 def __ne__(self, other: Any) -> bool: 

895 """ 

896 Compare two SymbolicLink instances for inequality. 

897 

898 :param other: Parameter to compare against. 

899 :returns: ``True``, if both symbolic links are unequal. 

900 :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`. 

901 """ 

902 if not isinstance(other, SymbolicLink): 

903 ex = TypeError("Parameter 'other' is not of type SymbolicLink.") 

904 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

905 raise ex 

906 

907 return self._name != other._name or self._target != other._target 

908 

909 def __repr__(self) -> str: 

910 return f"SymLink: {self.Path} -> {self._target}" 

911 

912 def __str__(self) -> str: 

913 return self._name 

914 

915 

916@export 

917class Root(Directory): 

918 """ 

919 A **Root** represents the root-directory in the filesystem, which contains subdirectories, regular files and symbolic links. 

920 """ 

921 _ids: Dict[int, "File"] #: Dictionary of file identifier - file objects pairs found while scanning the directory structure. 

922 

923 def __init__( 

924 self, 

925 rootDirectory: Path, 

926 collectSubdirectories: bool = True 

927 ) -> None: 

928 if rootDirectory is None: 928 ↛ 929line 928 didn't jump to line 929 because the condition on line 928 was never true

929 raise ValueError(f"Parameter 'rootDirectory' is None.") 

930 elif not isinstance(rootDirectory, Path): 930 ↛ 931line 930 didn't jump to line 931 because the condition on line 930 was never true

931 raise TypeError(f"Parameter 'rootDirectory' is not of type 'Path'.") 

932 elif not rootDirectory.exists(): 932 ↛ 933line 932 didn't jump to line 933 because the condition on line 932 was never true

933 raise ToolingException(f"Path '{rootDirectory}' doesn't exist.") from FileNotFoundError(rootDirectory) 

934 

935 self._ids = {} 

936 

937 super().__init__(rootDirectory.name) 

938 self._root = self 

939 self._path = rootDirectory 

940 

941 if collectSubdirectories: 941 ↛ 942line 941 didn't jump to line 942 because the condition on line 941 was never true

942 self._collectSubdirectories() 

943 self._connectSymbolicLinks() 

944 

945 @readonly 

946 def TotalHardLinkCount(self) -> int: 

947 return sum(l for f in self._ids.values() if (l := len(f._parents)) > 1) 

948 

949 @readonly 

950 def TotalHardLinkCount2(self) -> int: 

951 return sum(1 for f in self._ids.values() if len(f._parents) > 1) 

952 

953 @readonly 

954 def TotalHardLinkCount3(self) -> int: 

955 return sum(1 for f in self._ids.values() if len(f._parents) == 1) 

956 

957 @readonly 

958 def Size2(self) -> int: 

959 return sum(f._size for f in self._ids.values() if len(f._parents) > 1) 

960 

961 @readonly 

962 def Size3(self) -> int: 

963 return sum(f._size * len(f._parents) for f in self._ids.values() if len(f._parents) > 1) 

964 

965 @readonly 

966 def TotalUniqueFileCount(self) -> int: 

967 return len(self._ids) 

968 

969 @readonly 

970 def Path(self) -> Path: 

971 """ 

972 Read-only property to access the path of the filesystem statistics root. 

973 

974 :returns: Path to the root of the filesystem statistics root directory. 

975 """ 

976 return self._path 

977 

978 def Copy(self) -> "Root": 

979 """ 

980 Copy the directory structure including all subelements and link it to the given parent. 

981 

982 The duration for the deep copy process is provided in :attr:`ScanDuration` 

983 

984 .. hint:: 

985 

986 Statistics like aggregated directory size are copied too. |br| 

987 There is no rescan or repeated aggregation needed. 

988 

989 :returns: A deep copy of the directory structure. 

990 """ 

991 with Stopwatch() as sw: 

992 root = Root(self._path, False) 

993 root._size = self._size 

994 

995 for subdir in self._subdirectories.values(): 

996 subdir.Copy(root) 

997 

998 for file in self._files.values(): 

999 file.Copy(root) 

1000 

1001 for link in self._symbolicLinks.values(): 

1002 link.Copy(root) 

1003 

1004 root._scanDuration = sw.Duration 

1005 root._aggregateDuration = 0.0 

1006 

1007 return root 

1008 

1009 def __repr__(self) -> str: 

1010 return f"Root: {self.Path} (dirs: {self.TotalSubdirectoryCount}, files: {self.TotalRegularFileCount}, symlinks: {self.TotalSymbolicLinkCount})" 

1011 

1012 def __str__(self) -> str: 

1013 return self._name 

1014 

1015 

1016@export 

1017class File(Base): 

1018 """ 

1019 A **File** represents a file storage object in the filesystem, which is accessible by one or more :class:`Filename` objects. 

1020 

1021 Each file has an internal id, which is associated to a unique ID within the host's filesystem. 

1022 """ 

1023 _id: int #: Unique (host internal) file object ID) 

1024 _parents: List[Filename] #: List of reverse references to :class:`Filename` objects. 

1025 

1026 def __init__( 

1027 self, 

1028 id: int, 

1029 size: int, 

1030 parent: Nullable[Filename] = None 

1031 ) -> None: 

1032 """ 

1033 Initialize the File storage object with an ID, size and parent reference. 

1034 

1035 :param id: Unique ID of the file object. 

1036 :param size: Size of the file object. 

1037 :param parent: Optional parent reference. 

1038 """ 

1039 if not isinstance(id, int): 1039 ↛ 1040line 1039 didn't jump to line 1040 because the condition on line 1039 was never true

1040 ex = TypeError("Parameter 'id' is not of type 'int'.") 

1041 ex.add_note(f"Got type '{getFullyQualifiedName(id)}'.") 

1042 raise ex 

1043 

1044 self._id = id 

1045 

1046 if parent is None: 

1047 super().__init__(size, None) 

1048 self._parents = [] 

1049 elif isinstance(parent, Filename): 1049 ↛ 1054line 1049 didn't jump to line 1054 because the condition on line 1049 was always true

1050 super().__init__(size, parent._root) 

1051 self._parents = [parent] 

1052 parent._file = self 

1053 else: 

1054 ex = TypeError("Parameter 'parent' is not of type 'Filename'.") 

1055 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

1056 raise ex 

1057 

1058 @readonly 

1059 def ID(self) -> int: 

1060 """ 

1061 Read-only property to access the file object's unique identifier. 

1062 

1063 :returns: Unique file object identifier. 

1064 """ 

1065 return self._id 

1066 

1067 @readonly 

1068 def Parents(self) -> List[Filename]: 

1069 """ 

1070 Read-only property to access the list of filenames using the same file storage object. 

1071 

1072 .. hint:: 

1073 

1074 This allows to check if a file object has multiple filenames a.k.a hardlinks. 

1075 

1076 :returns: List of filenames for the file storage object. 

1077 """ 

1078 return self._parents 

1079 

1080 def AddParent(self, file: Filename) -> None: 

1081 """ 

1082 Add another parent reference to a :class:`Filename`. 

1083 

1084 :param file: Reference to a filename object. 

1085 """ 

1086 if not isinstance(file, Filename): 1086 ↛ 1087line 1086 didn't jump to line 1087 because the condition on line 1086 was never true

1087 ex = TypeError("Parameter 'file' is not of type 'Filename'.") 

1088 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") 

1089 raise ex 

1090 elif file._file is not None: 1090 ↛ 1091line 1090 didn't jump to line 1091 because the condition on line 1090 was never true

1091 raise ToolingException(f"Filename is already referencing an other file object ({file._file._id}).") 

1092 

1093 self._parents.append(file) 

1094 file._file = self 

1095 

1096 if file._root is not None: 

1097 self._root = file._root