Coverage for pyTooling/Filesystem/__init__.py: 55%
606 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-11 23:59 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-11 23:59 +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.
34.. important::
36 This isn't a replacement of :mod:`pathlib` introduced with Python 3.4.
38.. seealso::
40 :mod:`pyTooling.Filesystem.Docker`
41 |rarr| Slicing a scanned filesystem into Docker image layers.
42 :mod:`pyTooling.Tree`
43 |rarr| The tree data structure a filesystem scope is converted to.
44 :mod:`pyTooling.Stopwatch`
45 |rarr| The stopwatch measuring how long a scan took.
46"""
47from __future__ import annotations
49from enum import Enum
50from itertools import chain
51from os import scandir, readlink
52from pathlib import Path
53from typing import Optional as Nullable, Generic, Generator, TypeVar, Any, Callable, Union
54from typing import Iterator, cast
56from pyTooling.Decorators import readonly, export
57from pyTooling.Exceptions import ToolingException
58from pyTooling.MetaClasses import ExtendedType
59from pyTooling.Common import getFullyQualifiedName, zipdicts
60from pyTooling.Warning import WarningCollector, Warning
61from pyTooling.Stopwatch import Stopwatch
62from pyTooling.Tree import Node
65__all__ = ["_ParentType"]
68_ParentType = TypeVar("_ParentType", bound="Element")
69"""The type variable for a parent reference."""
72@export
73class FilesystemException(ToolingException):
74 """Base-exception of all exceptions raised by :mod:`pyTooling.Filesystem`."""
77@export
78class PermissionWarning(Warning):
79 """
80 Warning emitted when a directory or file couldn't be read while scanning a filesystem.
82 The scan continues, so the collected statistics are incomplete by exactly the path this warning carries.
83 """
84 _path: Path #: Path that couldn't be read.
86 def __init__(self, path: Path, *args: Any) -> None:
87 """
88 Initialize a permission warning for the path that couldn't be read.
90 :param path: The path that raised a :exc:`PermissionError`.
91 :param args: Positional parameters forwarded to the base-class.
92 """
93 super().__init__(*args)
94 self._path = path
96 @readonly
97 def Path(self) -> Path:
98 """
99 Read-only property to access the path that couldn't be read (:attr:`_path`).
101 :returns: The path that raised a :exc:`PermissionError`.
102 """
103 return self._path
106@export
107class NodeKind(Enum):
108 """
109 Node kind for filesystem elements in a :ref:`tree <STRUCT/Tree>`.
111 This enumeration is used when converting the filesystem statistics tree to an instance of :mod:`pyTooling.Tree`.
112 """
113 Directory = 0 #: Node represents a directory.
114 File = 1 #: Node represents a regular file.
115 SymbolicLink = 2 #: Node represents a symbolic link.
118@export
119class Base(metaclass=ExtendedType, slots=True):
120 """
121 Base-class for all filesystem elements in :mod:`pyTooling.Filesystem`.
123 It implements a size and a reference to the root element of the filesystem.
124 """
125 _root: Nullable[Root] #: Reference to the root of the filesystem statistics scope.
126 _size: Nullable[int] #: Actual or aggregated size of the filesystem element.
128 def __init__(
129 self,
130 size: Nullable[int],
131 root: Nullable[Root]
132 ) -> None:
133 """
134 Initialize the base-class with filesystem element size and root reference.
136 :param size: Optional, size of the element.
137 :param root: Optional reference to the filesystem root element.
138 :raises TypeError: If parameter 'size' is not of type integer.
139 :raises TypeError: If parameter 'root' is not of type :class:`Root`.
140 """
141 if size is not None and not isinstance(size, int): 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true
142 ex = TypeError("Parameter 'size' is not of type 'int'.")
143 ex.add_note(f"Got type '{getFullyQualifiedName(size)}'.")
144 raise ex
146 if root is not None and not isinstance(root, Root): 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true
147 ex = TypeError("Parameter 'root' is not of type 'Root'.")
148 ex.add_note(f"Got type '{getFullyQualifiedName(root)}'.")
149 raise ex
151 self._size = size
152 self._root = root
154 @property
155 def Root(self) -> Nullable[Root]:
156 """
157 Property to access the root of the filesystem statistics scope.
159 :returns: Root of the filesystem statistics scope.
160 :raises ValueError: If ``None`` is assigned.
161 :raises TypeError: If an assigned value is not of type :class:`Root`.
162 """
163 return self._root
165 @Root.setter
166 def Root(self, value: Root) -> None:
167 if value is None: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true
168 raise ValueError(f"Parameter 'value' is None.")
169 elif not isinstance(value, Root): 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true
170 ex = TypeError("Parameter 'value' is not of type 'Root'.")
171 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
172 raise ex
174 self._root = value
176 @readonly
177 def Size(self) -> int:
178 """
179 Read-only property to access the element's size in Bytes.
181 :returns: Size in Bytes.
182 :raises FilesystemException: If size is not computed, yet.
183 """
184 if self._size is None: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true
185 raise FilesystemException("Size is not computed, yet.")
187 return self._size
189 # FIXME: @abstractmethod
190 def ToTree(self) -> Node:
191 """
192 Convert a filesystem element to a node in :mod:`pyTooling.Tree`.
194 The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the filesystem element. Additional data
195 will be stored in the node's key-value store.
197 :returns: A tree's node referencing this filesystem element.
198 """
199 raise NotImplementedError()
202@export
203class Element(Base, Generic[_ParentType]):
204 """
205 Base-class for all named elements within a filesystem.
207 It adds a name, parent reference and list of symbolic-link sources.
209 .. hint::
211 Symbolic link sources are reverse references describing which symbolic links point to this element.
212 """
213 _name: str #: Name of the filesystem element.
214 _parent: _ParentType #: Reference to the filesystem element's parent (:class:`Directory`)
215 _linkSources: list[SymbolicLink] #: A list of symbolic links pointing to this filesystem element.
217 def __init__(
218 self,
219 name: str,
220 size: Nullable[int] = None,
221 parent: Nullable[_ParentType] = None
222 ) -> None:
223 """
224 Initialize the element base-class with name, size and parent reference.
226 :param name: Name of the element.
227 :param size: Optional, size of the element.
228 :param parent: Optional, parent reference.
229 :raises ValueError: If parameter 'name' is None.
230 :raises TypeError: If parameter 'parent' is not of type :class:`Directory`.
231 """
232 if name is None: 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true
233 raise ValueError(f"Parameter 'name' is None.")
234 elif not isinstance(name, str): 234 ↛ 235line 234 didn't jump to line 235 because the condition on line 234 was never true
235 ex = TypeError("Parameter 'name' is not of type 'str'.")
236 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.")
237 raise ex
239 self._name = name
241 if parent is None:
242 super().__init__(size, None)
243 self._parent = None
244 else:
245 if not isinstance(parent, Directory): 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 ex = TypeError("Parameter 'parent' is not of type 'Directory'.")
247 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
248 raise ex
250 super().__init__(size, parent._root)
251 self._parent = parent
253 self._linkSources = []
255 @property
256 def Parent(self) -> _ParentType:
257 """
258 Property to access the element's parent.
260 :returns: Parent element.
261 :raises ValueError: If ``None`` is assigned.
262 :raises TypeError: If an assigned value is not of type :class:`Directory`.
263 """
264 return self._parent
266 @Parent.setter
267 def Parent(self, value: _ParentType) -> None:
268 if value is None: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true
269 raise ValueError(f"Parameter 'value' is None.")
270 elif not isinstance(value, Directory): 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true
271 ex = TypeError("Parameter 'value' is not of type 'Directory'.")
272 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
273 raise ex
275 self._parent = value
277 if value._root is not None:
278 self._root = value._root
280 @readonly
281 def Name(self) -> str:
282 """
283 Read-only property to access the element's name.
285 :returns: Element name.
286 """
287 return self._name
289 @readonly
290 def Path(self) -> Path:
291 """
292 Read-only property to access the element's path.
294 :returns: Path of the element.
295 """
296 raise NotImplementedError(f"Property 'Path' is abstract.")
298 @readonly
299 def LinkSources(self) -> list[SymbolicLink]:
300 """
301 Read-only property to access the symbolic links pointing to this element (:attr:`_linkSources`).
303 :returns: List of symbolic links targeting this element.
304 """
305 return self._linkSources
307 def AddLinkSources(self, source: SymbolicLink) -> None:
308 """
309 Add a link source of a symbolic link to the named element (reverse reference).
311 :param source: The referenced symbolic link.
312 :raises TypeError: If parameter 'source' is not of type :class:`SymbolicLink`.
313 """
314 if not isinstance(source, SymbolicLink):
315 ex = TypeError("Parameter 'source' is not of type 'SymbolicLink'.")
316 ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.")
317 raise ex
319 source._isConnected = True
320 source._isBroken = False
321 source._isOutOfRange = False
322 self._linkSources.append(source)
325@export
326class Directory(Element["Directory"]):
327 """
328 A **directory** represents a directory in the filesystem, which contains subdirectories, regular files and symbolic links.
330 While scanning for subelements, the directory is populated with elements. Every file object added, gets registered in
331 the filesystems :class:`Root` for deduplication. In case a file identifier already exists, the found filename will
332 reference the same file objects. In turn, the file objects has then references to multiple filenames (parents). This
333 allows to detect :term:`hardlinks <hardlink>`.
335 The time needed for scanning the directory and its subelements is provided via :data:`ScanDuration`.
337 After scnaning the directory for subelements, certain directory properties get aggregated. The time needed for
338 aggregation is provided via :data:`AggregateDuration`.
339 """
341 _path: Nullable[Path] #: Cached :class:`~pathlib.Path` object of this directory.
342 _subdirectories: dict[str, Directory] #: Dictionary containing name-:class:`Directory` pairs.
343 _files: dict[str, Filename] #: Dictionary containing name-:class:`Filename` pairs.
344 _symbolicLinks: dict[str, SymbolicLink] #: Dictionary containing name-:class:`SymbolicLink` pairs.
345 _filesSize: int #: Aggregated size of all direct files.
346 _collapsed: bool #: True, if this directory was collapsed. It contains no subelements.
347 _scanDuration: Nullable[float] #: Duration for scanning the directory and all its subelements.
348 _aggregateDuration: Nullable[float] #: Duration for aggregating all subelements.
350 def __init__(
351 self,
352 name: str,
353 collectSubdirectories: bool = False,
354 parent: Nullable[Directory] = None
355 ) -> None:
356 """
357 Initialize the directory with name and parent reference.
359 :param name: Name of the element.
360 :param collectSubdirectories: Optional, if ``True``, collect subdirectory statistics.
361 :param parent: Optional, parent reference.
362 """
363 super().__init__(name, None, parent)
365 self._path = None
366 self._subdirectories = {}
367 self._files = {}
368 self._symbolicLinks = {}
369 self._filesSize = 0
370 self._collapsed = False
371 self._scanDuration = None
372 self._aggregateDuration = None
374 if parent is not None:
375 parent._subdirectories[name] = self
377 if parent._root is not None: 377 ↛ 380line 377 didn't jump to line 380 because the condition on line 377 was always true
378 self._root = parent._root
380 if collectSubdirectories:
381 self.CollectSubdirectories()
383 def CollectSubdirectories(self) -> None:
384 """
385 Helper method for scanning subdirectories and aggregating found element sizes therein.
386 """
387 self.ScanSubdirectories()
388 self.AggregateSizes()
390 def ScanSubdirectories(self) -> None:
391 """
392 Helper method for scanning subdirectories (recursively) and building a
393 :class:`Directory`-:class:`Filename`-:class:`File` object tree.
395 If a file refers to the same filesystem internal unique ID, a hardlink (two or more filenames) to the same file
396 storage object is assumed.
398 A directory that can't be read is reported as a :class:`PermissionWarning` and skipped, so the scan continues and
399 the collected statistics are incomplete by exactly that path.
401 :raises FilesystemException: If this directory isn't attached to a :class:`Root`, which owns the ID table.
402 :raises FilesystemException: If the directory contains an element that is neither a directory, a file nor a
403 symbolic link.
404 """
405 if (root := self._root) is None:
406 raise FilesystemException(f"Directory '{self._name}' is not attached to a filesystem root.")
408 with Stopwatch() as sw1:
409 try:
410 items = scandir(directoryPath := self.Path)
411 except PermissionError as ex:
412 return WarningCollector.Raise(PermissionWarning(self.Path), ex)
414 for dirEntry in items:
415 if dirEntry.is_dir(follow_symlinks=False):
416 _ = Directory(dirEntry.name, collectSubdirectories=True, parent=self)
417 elif dirEntry.is_file(follow_symlinks=False): 417 ↛ 429line 417 didn't jump to line 429 because the condition on line 417 was always true
418 id = dirEntry.inode()
419 if id in root._ids: 419 ↛ 420line 419 didn't jump to line 420 because the condition on line 419 was never true
420 file = root._ids[id]
422 _ = Filename(dirEntry.name, file=file, parent=self)
423 else:
424 s = dirEntry.stat(follow_symlinks=False)
425 filename = Filename(dirEntry.name, parent=self)
426 file = File(id, s.st_size, parent=filename)
428 root._ids[id] = file
429 elif dirEntry.is_symlink():
430 target = Path(readlink(directoryPath / dirEntry.name))
431 _ = SymbolicLink(dirEntry.name, target, parent=self)
432 else:
433 raise FilesystemException(f"Unknown directory element.")
435 self._scanDuration = sw1.Duration
437 def ResolveSymbolicLinks(self) -> None:
438 """
439 Resolve the symbolic links of this directory and of every directory below it.
441 A link whose target lies inside the scanned tree is connected to that element; a target that doesn't exist
442 registers the link as broken, and a target outside the scanned tree registers it as unconnected.
443 """
444 for dir in self._subdirectories.values():
445 dir.ResolveSymbolicLinks()
447 for link in self._symbolicLinks.values(): 447 ↛ 448line 447 didn't jump to line 448 because the loop on line 447 never started
448 if link._target.is_absolute():
449 # todo: resolve path and check if target is in range, otherwise add to out-of-range list
450 pass
451 else:
452 target = self
453 for elem in link._target.parts:
454 if elem == ".":
455 continue
456 elif elem == "..":
457 if (target := target._parent) is None:
458 self._root.RegisterUnconnectedSymbolicLink(link)
459 break
461 continue
463 try:
464 target = target._subdirectories[elem]
465 continue
466 except KeyError:
467 pass
469 try:
470 target = target._files[elem]
471 continue
472 except KeyError:
473 pass
475 try:
476 target = target._symbolicLinks[elem]
477 continue
478 except KeyError:
479 self._root.RegisterBrokenSymbolicLink(link)
480 break
481 else:
482 target.AddLinkSources(link)
484 def AggregateSizes(self) -> set[File]:
485 """
486 Compute the aggregated size of this directory and of every directory below it.
488 A file is counted once, even when several filenames (hardlinks) refer to it, which is why the already counted
489 files are returned and handed up the recursion.
491 :returns: The set of file objects counted in this subtree.
492 """
493 with Stopwatch() as sw2:
494 aggregatedFiles = set()
496 self._size = 0
497 self._filesSize = 0
498 for dir in self._subdirectories.values():
499 aggregatedFiles |= dir.AggregateSizes()
500 self._size += dir._size
502 for filename in self._files.values():
503 if (file := filename._file) not in aggregatedFiles: 503 ↛ 502line 503 didn't jump to line 502 because the condition on line 503 was always true
504 self._filesSize += file._size
505 aggregatedFiles.add(file)
507 self._size += self._filesSize
509 self._aggregateDuration = sw2.Duration
511 return aggregatedFiles
513 @Element.Root.setter
514 def Root(self, value: Root) -> None:
515 Element.Root.fset(self, value)
517 for subdir in self._subdirectories.values(): 517 ↛ 518line 517 didn't jump to line 518 because the loop on line 517 never started
518 subdir.Root = value
520 for file in self._files.values():
521 file.Root = value
523 for link in self._symbolicLinks.values(): 523 ↛ 524line 523 didn't jump to line 524 because the loop on line 523 never started
524 link.Root = value
526 @Element.Parent.setter
527 def Parent(self, value: _ParentType) -> None:
528 Element.Parent.fset(self, value)
530 value._subdirectories[self._name] = self
532 if isinstance(value, Root): 532 ↛ exitline 532 didn't return from function 'Parent' because the condition on line 532 was always true
533 self.Root = value
535 @readonly
536 def Count(self) -> int:
537 """
538 Read-only property to return the number of elements in a directory.
540 :returns: Number of files plus subdirectories.
541 """
542 return len(self._subdirectories) + len(self._files) + len(self._symbolicLinks)
544 @readonly
545 def FileCount(self) -> int:
546 """
547 Read-only property to return the number of files in a directory.
549 .. hint::
551 Files include regular files and symbolic links.
553 :returns: Number of files.
554 """
555 return len(self._files) + len(self._symbolicLinks)
557 @readonly
558 def RegularFileCount(self) -> int:
559 """
560 Read-only property to return the number of regular files in a directory.
562 :returns: Number of regular files.
563 """
564 return len(self._files)
566 @readonly
567 def SymbolicLinkCount(self) -> int:
568 """
569 Read-only property to return the number of symbolic links in a directory.
571 :returns: Number of symbolic links.
572 """
573 return len(self._symbolicLinks)
575 @readonly
576 def SubdirectoryCount(self) -> int:
577 """
578 Read-only property to return the number of subdirectories in a directory.
580 :returns: Number of subdirectories.
581 """
582 return len(self._subdirectories)
584 @readonly
585 def TotalFileCount(self) -> int:
586 """
587 Read-only property to return the total number of files in all child hierarchy levels (recursively).
589 .. hint::
591 Files include regular files and symbolic links.
593 :returns: Total number of files.
594 """
595 return sum(d.TotalFileCount for d in self._subdirectories.values()) + len(self._files) + len(self._symbolicLinks)
597 @readonly
598 def TotalRegularFileCount(self) -> int:
599 """
600 Read-only property to return the total number of regular files in all child hierarchy levels (recursively).
602 :returns: Total number of regular files.
603 """
604 return sum(d.TotalRegularFileCount for d in self._subdirectories.values()) + len(self._files)
606 @readonly
607 def TotalSymbolicLinkCount(self) -> int:
608 """
609 Read-only property to return the total number of symbolic links in all child hierarchy levels (recursively).
611 :returns: Total number of symbolic links.
612 """
613 return sum(d.TotalSymbolicLinkCount for d in self._subdirectories.values()) + len(self._symbolicLinks)
615 @readonly
616 def TotalSubdirectoryCount(self) -> int:
617 """
618 Read-only property to return the total number of subdirectories in all child hierarchy levels (recursively).
620 :returns: Total number of subdirectories.
621 """
622 return len(self._subdirectories) + sum(d.TotalSubdirectoryCount for d in self._subdirectories.values())
624 @readonly
625 def Subdirectories(self) -> Generator[Directory, None, None]:
626 """
627 Iterate all direct subdirectories of the directory.
629 :returns: A generator to iterate all direct subdirectories.
630 """
631 return (d for d in self._subdirectories.values())
633 @readonly
634 def Files(self) -> Generator[Filename | SymbolicLink, None, None]:
635 """
636 Iterate all direct files of the directory.
638 .. hint::
640 Files include regular files and symbolic links.
642 :returns: A generator to iterate all direct files.
643 """
644 return (f for f in chain(self._files.values(), self._symbolicLinks.values()))
646 @readonly
647 def RegularFiles(self) -> Generator[Filename, None, None]:
648 """
649 Iterate all direct regular files of the directory.
651 :returns: A generator to iterate all direct regular files.
652 """
653 return (f for f in self._files.values())
655 @readonly
656 def SymbolicLinks(self) -> Generator[SymbolicLink, None, None]:
657 """
658 Iterate all direct symbolic links of the directory.
660 :returns: A generator to iterate all direct symbolic links.
661 """
662 return (l for l in self._symbolicLinks.values())
664 @readonly
665 def Path(self) -> Path:
666 """
667 Read-only property to access the equivalent Path instance for accessing the represented directory.
669 :returns: Path to the directory.
670 :raises FilesystemException: If no parent is set.
671 """
672 if self._path is not None:
673 return self._path
675 if self._parent is None: 675 ↛ 676line 675 didn't jump to line 676 because the condition on line 675 was never true
676 raise FilesystemException(f"No parent or root set for directory.")
678 self._path = self._parent.Path / self._name
679 return self._path
681 @readonly
682 def ScanDuration(self) -> float:
683 """
684 Read-only property to access the time needed to scan a directory structure including all subelements (recursively).
686 :returns: The scan duration in seconds.
687 :raises FilesystemException: If the directory was not scanned.
688 """
689 if self._scanDuration is None:
690 raise FilesystemException(f"Directory was not scanned, yet.")
692 return self._scanDuration
694 @readonly
695 def AggregateDuration(self) -> float:
696 """
697 Read-only property to access the time needed to aggregate the directory's and subelement's properties (recursively).
699 :returns: The aggregation duration in seconds.
700 :raises FilesystemException: If the directory properties were not aggregated.
701 """
702 if self._scanDuration is None:
703 raise FilesystemException(f"Directory properties were not aggregated, yet.")
705 return self._aggregateDuration
707 def __hash__(self) -> int:
708 """
709 Compute a hash for this filesystem element based on its identity.
711 Two elements with the same name in different directories are different elements, so the hash is derived from the
712 object's identity and not from its name.
714 :returns: Hash of this filesystem element.
715 """
716 return hash(id(self))
718 def IterateDirectories(self) -> Generator[Directory, None, None]:
719 """
720 A generator to iterate all subdirectories below this directory in pre-order.
722 A parent directory is yielded before its children.
724 :returns: A generator to iterate all subdirectories below this directory.
725 """
726 # pre-order
727 for directory in self._subdirectories.values():
728 yield directory
729 yield from directory.IterateDirectories()
731 def IterateFiles(self) -> Generator[Element, None, None]:
732 """
733 A generator to iterate all files and symbolic links below this directory in post-order.
735 The elements of the subdirectories are yielded before this directory's own.
737 :returns: A generator to iterate all files and symbolic links below this directory.
738 """
739 # post-order
740 for directory in self._subdirectories.values():
741 yield from directory.IterateFiles()
743 yield from self._files.values()
744 yield from self._symbolicLinks.values()
746 def Copy(self, parent: Nullable[Directory] = None) -> Directory:
747 """
748 Copy the directory structure including all subelements and link it to the given parent.
750 .. hint::
752 Statistics like aggregated directory size are copied too. |br|
753 There is no rescan or repeated aggregation needed.
755 :param parent: Optional, the parent element of the copied directory.
756 :returns: A deep copy of the directory structure.
757 """
758 dir = Directory(self._name, parent=parent)
759 dir._size = self._size
761 for subdir in self._subdirectories.values():
762 subdir.Copy(dir)
764 for file in self._files.values():
765 file.Copy(dir)
767 for link in self._symbolicLinks.values():
768 link.Copy(dir)
770 return dir
772 def Collapse(self, func: Callable[[Directory], bool]) -> bool:
773 """
774 Collapse this directory's subtree where the given predicate accepts it.
776 A directory is collapsed when it has no subdirectories left - or all of them collapsed - and the predicate
777 accepts it. Collapsing discards the directory's elements, so only its aggregated numbers remain.
779 :param func: Predicate deciding whether a directory may be collapsed.
780 :returns: ``True``, if this directory was collapsed.
781 """
782 # if len(self._subdirectories) == 0 or all(subdir.Collapse(func) for subdir in self._subdirectories.values()):
783 if len(self._subdirectories) == 0:
784 if func(self):
785 # print(f"collapse 1 {self.Path}")
786 self._collapsed = True
787 self._subdirectories.clear()
788 self._files.clear()
789 self._symbolicLinks.clear()
791 return True
792 else:
793 return False
795 # if all(subdir.Collapse(func) for subdir in self._subdirectories.values())
796 collapsible = True
797 for subdir in self._subdirectories.values():
798 result = subdir.Collapse(func)
799 collapsible = collapsible and result
801 if collapsible:
802 # print(f"collapse 2 {self.Path}")
803 self._collapsed = True
804 self._subdirectories.clear()
805 self._files.clear()
806 self._symbolicLinks.clear()
808 return True
809 else:
810 return False
812 def ToTree(self, format: Nullable[Callable[[Node], str]] = None) -> Node:
813 """
814 Convert the directory to a :class:`~pyTooling.Tree.Node`.
816 The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the directory. Additional data is
817 attached to the node's key-value store:
819 ``kind``
820 The node's kind. See :class:`NodeKind`.
821 ``size``
822 The directory's aggregated size.
824 :param format: Optional, a user defined formatting function for tree nodes.
825 :returns: A tree node representing this directory.
826 """
827 if format is None:
828 def format(node: Node) -> str:
829 """
830 Nested function rendering a tree node as one line.
832 :param node: The tree node to render.
833 :returns: The node's size in MiB, followed by its name.
834 """
835 element = cast(Element[Any], node._value) # the node was created with this element as its value
836 return f"{node['size'] * 1e-6:7.1f} MiB {element.Name}"
838 directoryNode: Node[Any, Any, Any, Any] = Node(
839 value=self,
840 keyValuePairs={
841 "kind": NodeKind.File,
842 "size": self._size
843 },
844 format=format
845 )
846 directoryNode.AddChildren(
847 e.ToTree(format) for e in chain(self._subdirectories.values()) #, self._files.values(), self._symbolicLinks.values())
848 )
850 return directoryNode
852 def __eq__(self, other: Any) -> bool:
853 """
854 Compare two Directory instances for equality.
856 :param other: Parameter to compare against.
857 :returns: ``True``, if both directories and all its subelements are equal.
858 :raises TypeError: If parameter ``other`` is not of type :class:`Directory`.
859 """
860 if not isinstance(other, Directory):
861 ex = TypeError("Parameter 'other' is not of type Directory.")
862 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
863 raise ex
865 if not all(dir1 == dir2 for _, dir1, dir2 in zipdicts(self._subdirectories, other._subdirectories)):
866 return False
868 if not all(file1 == file2 for _, file1, file2 in zipdicts(self._files, other._files)):
869 return False
871 if not all(link1 == link2 for _, link1, link2 in zipdicts(self._symbolicLinks, other._symbolicLinks)):
872 return False
874 return True
876 def __ne__(self, other: Any) -> bool:
877 """
878 Compare two Directory instances for inequality.
880 :param other: Parameter to compare against.
881 :returns: ``True``, if both directories and all its subelements are unequal.
882 :raises TypeError: If parameter ``other`` is not of type :class:`Directory`.
883 """
884 return not self.__eq__(other)
886 def __repr__(self) -> str:
887 """
888 Return a detailed string representation of this directory.
890 :returns: The directory's full path, prefixed by its kind.
891 """
892 return f"Directory: {self.Path}"
894 def __str__(self) -> str:
895 """
896 Return a string representation of this filesystem element.
898 :returns: The element's name, without any path.
899 """
900 return self._name
903@export
904class Filename(Element[Directory]):
905 """
906 Represents a filename in the filesystem, but not the file storage object (:class:`File`).
908 .. hint::
910 Filename and file storage are represented by two classes, which allows multiple names (hard links) per file storage
911 object.
912 """
913 _file: Nullable[File] #: The file this filename refers to; ``None`` until the filename is linked.
915 def __init__(
916 self,
917 name: str,
918 file: Nullable[File] = None,
919 parent: Nullable[Directory] = None
920 ) -> None:
921 """
922 Initialize the filename with name, file (storage) object and parent reference.
924 :param name: Name of the file.
925 :param file: Optional, file (storage) object.
926 :param parent: Optional, parent reference.
927 :raises TypeError: If parameter 'file' is not of type :class:`File`.
928 """
929 super().__init__(name, None, parent)
931 if file is None: 931 ↛ 934line 931 didn't jump to line 934 because the condition on line 931 was always true
932 self._file = None
933 else:
934 if not isinstance(file, File):
935 ex = TypeError("Parameter 'file' is not of type 'File'.")
936 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
937 raise ex
939 self._file = file
940 file._parents.append(self)
942 if parent is not None:
943 parent._files[name] = self
945 if parent._root is not None:
946 self._root = parent._root
948 @Element.Root.setter
949 def Root(self, value: Root) -> None:
950 Element.Root.fset(self, value)
952 if self._file is not None: 952 ↛ exitline 952 didn't return from function 'Root' because the condition on line 952 was always true
953 self._file.Root = value
955 @Element.Parent.setter
956 def Parent(self, value: _ParentType) -> None:
957 Element.Parent.fset(self, value)
959 value._files[self._name] = self
961 if isinstance(value, Root): 961 ↛ 962line 961 didn't jump to line 962 because the condition on line 961 was never true
962 self.Root = value
964 @readonly
965 def File(self) -> Nullable[File]:
966 """
967 Read-only property to access the file this filename is linked to (:attr:`_file`).
969 :returns: The linked file, or ``None`` if the filename isn't linked yet.
970 """
971 return self._file
973 @readonly
974 def Size(self) -> int:
975 """
976 Read-only property to access the size of the linked file.
978 :returns: Size of the linked file in bytes.
979 :raises ToolingException: If the filename isn't linked to a file object.
980 """
981 if self._file is None:
982 raise ToolingException(f"Filename isn't linked to a File object.")
984 return self._file._size
986 @readonly
987 def Path(self) -> Path:
988 """
989 Read-only property to return the filename's absolute path.
991 The path is computed from the parent directory's path and the filename.
993 :returns: Absolute path of the file.
994 :raises ToolingException: If the filename has no parent object.
995 """
996 if self._parent is None:
997 raise ToolingException(f"Filename has no parent object.")
999 return self._parent.Path / self._name
1001 def __hash__(self) -> int:
1002 """
1003 Compute a hash for this filesystem element based on its identity.
1005 Two elements with the same name in different directories are different elements, so the hash is derived from the
1006 object's identity and not from its name.
1008 :returns: Hash of this filesystem element.
1009 """
1010 return hash(id(self))
1012 def Copy(self, parent: Directory) -> Filename:
1013 """
1014 Copy this filename into another filesystem statistics scope.
1016 The file object behind the filename is copied only once per scope: a filename referring to a file that was
1017 already copied - a hardlink - is connected to the existing copy.
1019 :param parent: Optional, the directory in the target scope the copy is registered at.
1020 :returns: The copied filename.
1021 """
1022 fileID = self._file._id
1024 if fileID in parent._root._ids:
1025 file = parent._root._ids[fileID]
1026 else:
1027 fileSize = self._file._size
1028 file = File(fileID, fileSize)
1030 parent._root._ids[fileID] = file
1032 return Filename(self._name, file, parent=parent)
1034 def ToTree(self) -> Node:
1035 """
1036 Convert this filename to a node of a :mod:`pyTooling.Tree`.
1038 :returns: A tree node carrying this filename, its kind and its size.
1039 """
1040 def format(node: Node) -> str:
1041 """
1042 Nested function rendering a tree node as one line.
1044 :param node: The tree node to render.
1045 :returns: The node's size in MiB, followed by its name.
1046 """
1047 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"
1049 fileNode: Node[Any, Any, Any, Any] = Node(
1050 value=self,
1051 keyValuePairs={
1052 "kind": NodeKind.File,
1053 "size": self._size
1054 },
1055 format=format
1056 )
1058 return fileNode
1060 def __eq__(self, other: Any) -> bool:
1061 """
1062 Compare two Filename instances for equality.
1064 :param other: Parameter to compare against.
1065 :returns: ``True``, if both filenames are equal.
1066 :raises TypeError: If parameter ``other`` is not of type :class:`Filename`.
1067 """
1068 if not isinstance(other, Filename):
1069 ex = TypeError("Parameter 'other' is not of type 'Filename'.")
1070 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1071 raise ex
1073 return self._name == other._name and self.Size == other.Size
1075 def __ne__(self, other: Any) -> bool:
1076 """
1077 Compare two Filename instances for inequality.
1079 :param other: Parameter to compare against.
1080 :returns: ``True``, if both filenames are unequal.
1081 :raises TypeError: If parameter ``other`` is not of type :class:`Filename`.
1082 """
1083 if not isinstance(other, Filename):
1084 ex = TypeError("Parameter 'other' is not of type 'Filename'.")
1085 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1086 raise ex
1088 return self._name != other._name or self.Size != other.Size
1090 def __repr__(self) -> str:
1091 """
1092 Return a detailed string representation of this filename.
1094 :returns: The file's full path, prefixed by its kind.
1095 """
1096 return f"File: {self.Path}"
1098 def __str__(self) -> str:
1099 """
1100 Return a string representation of this filesystem element.
1102 :returns: The element's name, without any path.
1103 """
1104 return self._name
1107@export
1108class SymbolicLink(Element[Directory]):
1109 """
1110 A symbolic link in the filesystem statistics scope.
1112 After the scan, the link is resolved: it is either connected to an element of the scanned tree, broken (the target
1113 doesn't exist), or out of range (the target lies outside the scanned tree).
1114 """
1115 _target: Path #: Path the symbolic link points to.
1116 _isConnected: bool #: ``True``, if the link target was resolved to an element of the scanned tree.
1117 _isBroken: Nullable[bool] #: ``True``, if the link target doesn't exist; ``None`` until resolved.
1118 _isOutOfRange: Nullable[bool] #: ``True``, if the link target lies outside the scanned tree; ``None`` until resolved.
1120 def __init__(
1121 self,
1122 name: str,
1123 target: Path,
1124 parent: Nullable[Directory]
1125 ) -> None:
1126 """
1127 Initialize a symbolic link, which is registered at its parent directory.
1129 The link is unresolved at first: :meth:`Root.ResolveSymbolicLinks` decides afterwards whether it is connected,
1130 broken or out of range.
1132 :param name: Name of the symbolic link.
1133 :param target: Path the symbolic link points to.
1134 :param parent: Optional, parent directory of the symbolic link.
1135 :raises ValueError: If parameter 'target' is None.
1136 :raises TypeError: If parameter 'target' is not of type :class:`~pathlib.Path`.
1137 """
1138 super().__init__(name, None, parent)
1140 if target is None:
1141 raise ValueError(f"Parameter 'target' is None.")
1142 elif not isinstance(target, Path):
1143 ex = TypeError("Parameter 'target' is not of type 'Path'.")
1144 ex.add_note(f"Got type '{getFullyQualifiedName(target)}'.")
1145 raise ex
1147 self._target = target
1148 self._isConnected = False
1149 self._isBroken = None
1150 self._isOutOfRange = None
1152 if parent is not None:
1153 parent._symbolicLinks[name] = self
1155 if parent._root is not None:
1156 self._root = parent._root
1158 @readonly
1159 def Path(self) -> Path:
1160 """
1161 Read-only property to return the symbolic link's path.
1163 The path is computed from the parent directory's path and the link's name.
1165 :returns: Path of the symbolic link.
1166 """
1167 return self._parent.Path / self._name
1169 @readonly
1170 def Target(self) -> Path:
1171 """
1172 Read-only property to access the path this symbolic link points to (:attr:`_target`).
1174 :returns: Target path of the symbolic link.
1175 """
1176 return self._target
1178 @readonly
1179 def IsConnected(self) -> bool:
1180 """
1181 Check if the symbolic link was resolved to an element within the scanned filesystem (:attr:`_isConnected`).
1183 :returns: ``True``, if the link's target was found and connected.
1184 """
1185 return self._isConnected
1187 @readonly
1188 def IsBroken(self) -> Nullable[bool]:
1189 """
1190 Check if the symbolic link points to a non-existing target (:attr:`_isBroken`).
1192 :returns: ``True``, if the target doesn't exist. ``None``, if the link wasn't resolved yet.
1193 """
1194 return self._isBroken
1196 @readonly
1197 def IsOutOfRange(self) -> Nullable[bool]:
1198 """
1199 Check if the symbolic link points outside the scanned filesystem (:attr:`_isOutOfRange`).
1201 :returns: ``True``, if the target lies outside the scanned root. ``None``, if the link wasn't resolved yet.
1202 """
1203 return self._isOutOfRange
1205 def __hash__(self) -> int:
1206 """
1207 Compute a hash for this filesystem element based on its identity.
1209 Two elements with the same name in different directories are different elements, so the hash is derived from the
1210 object's identity and not from its name.
1212 :returns: Hash of this filesystem element.
1213 """
1214 return hash(id(self))
1216 def Copy(self, parent: Directory) -> SymbolicLink:
1217 """
1218 Copy this symbolic link into another filesystem statistics scope.
1220 :param parent: Optional, the directory in the target scope the copy is registered at.
1221 :returns: The copied symbolic link, unresolved.
1222 """
1223 return SymbolicLink(self._name, self._target, parent=parent)
1225 def ToTree(self) -> Node:
1226 """
1227 Convert this symbolic link to a node of a :mod:`pyTooling.Tree`.
1229 :returns: A tree node carrying this symbolic link, its kind and its size.
1230 """
1231 def format(node: Node) -> str:
1232 """
1233 Nested function rendering a tree node as one line.
1235 :param node: The tree node to render.
1236 :returns: The node's size in MiB, followed by its name.
1237 """
1238 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"
1240 symbolicLinkNode: Node[Any, Any, Any, Any] = Node(
1241 value=self,
1242 keyValuePairs={
1243 "kind": NodeKind.SymbolicLink,
1244 "size": self._size
1245 },
1246 format=format
1247 )
1249 return symbolicLinkNode
1251 def __eq__(self, other: Any) -> bool:
1252 """
1253 Compare two SymbolicLink instances for equality.
1255 :param other: Parameter to compare against.
1256 :returns: ``True``, if both symbolic links are equal.
1257 :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`.
1258 """
1259 if not isinstance(other, SymbolicLink):
1260 ex = TypeError("Parameter 'other' is not of type 'SymbolicLink'.")
1261 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1262 raise ex
1264 return self._name == other._name and self._target == other._target
1266 def __ne__(self, other: Any) -> bool:
1267 """
1268 Compare two SymbolicLink instances for inequality.
1270 :param other: Parameter to compare against.
1271 :returns: ``True``, if both symbolic links are unequal.
1272 :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`.
1273 """
1274 if not isinstance(other, SymbolicLink):
1275 ex = TypeError("Parameter 'other' is not of type 'SymbolicLink'.")
1276 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1277 raise ex
1279 return self._name != other._name or self._target != other._target
1281 def __repr__(self) -> str:
1282 """
1283 Return a detailed string representation of this symbolic link.
1285 :returns: The link's full path and the path it points to.
1286 """
1287 return f"SymLink: {self.Path} -> {self._target}"
1289 def __str__(self) -> str:
1290 """
1291 Return a string representation of this filesystem element.
1293 :returns: The element's name, without any path.
1294 """
1295 return self._name
1298@export
1299class Root(Directory):
1300 """
1301 A **Root** represents the root-directory in the filesystem, which contains subdirectories, regular files and symbolic links.
1302 """
1303 _ids: dict[int, File] #: Dictionary of file identifier - file objects pairs found while scanning the directory structure.
1304 _brokenSymbolicLinks: list[SymbolicLink] #: Broken symbolic links (target doesn't exist).
1305 _unconnectedSymbolicLinks: list[SymbolicLink] #: Symbolic links which couldn't be connected to their target (out of scope).
1307 def __init__(
1308 self,
1309 rootDirectory: Path,
1310 collectSubdirectories: bool = True
1311 ) -> None:
1312 """
1313 Initialize a filesystem statistics scope for the given directory.
1315 Unless ``collectSubdirectories`` is disabled, the whole tree is scanned and its symbolic links are resolved right
1316 away, so the root is usable as soon as it exists.
1318 :param rootDirectory: Directory to collect the statistics for.
1319 :param collectSubdirectories: Optional, if ``True``, scan the tree and resolve its symbolic links immediately.
1320 :raises ValueError: If parameter 'rootDirectory' is None.
1321 :raises TypeError: If parameter 'rootDirectory' is not of type :class:`~pathlib.Path`.
1322 :raises ToolingException: If the given path doesn't exist.
1323 """
1324 if rootDirectory is None: 1324 ↛ 1325line 1324 didn't jump to line 1325 because the condition on line 1324 was never true
1325 raise ValueError(f"Parameter 'rootDirectory' is None.")
1326 elif not isinstance(rootDirectory, Path): 1326 ↛ 1327line 1326 didn't jump to line 1327 because the condition on line 1326 was never true
1327 raise TypeError(f"Parameter 'rootDirectory' is not of type 'Path'.")
1328 elif not rootDirectory.exists(): 1328 ↛ 1329line 1328 didn't jump to line 1329 because the condition on line 1328 was never true
1329 raise ToolingException(f"Path '{rootDirectory}' doesn't exist.") from FileNotFoundError(rootDirectory)
1331 self._ids = {}
1332 self._brokenSymbolicLinks = []
1333 self._unconnectedSymbolicLinks = []
1335 super().__init__(rootDirectory.name)
1336 self._root = self
1337 self._path = rootDirectory
1339 if collectSubdirectories:
1340 self.CollectSubdirectories()
1341 self.ResolveSymbolicLinks()
1343 @readonly
1344 def Path(self) -> Path:
1345 """
1346 Read-only property to access the path of the filesystem statistics root.
1348 :returns: Path to the root of the filesystem statistics root directory.
1349 """
1350 return self._path
1352 @readonly
1353 def BrokenSymbolicLinks(self) -> list[SymbolicLink]:
1354 """
1355 Read-only property to access all symbolic links with a non-existing target (:attr:`_brokenSymbolicLinks`).
1357 :returns: List of broken symbolic links.
1358 """
1359 return self._brokenSymbolicLinks
1361 @readonly
1362 def UnconnectedSymbolicLinks(self) -> list[SymbolicLink]:
1363 """
1364 Read-only property to access all symbolic links that couldn't be resolved within the scanned filesystem (:attr:`_unconnectedSymbolicLinks`).
1366 :returns: List of unconnected symbolic links.
1367 """
1368 return self._unconnectedSymbolicLinks
1370 @readonly
1371 def TotalHardLinkCount(self) -> int:
1372 """
1373 Read-only property to return the accumulated number of hardlinks to multiply-linked files.
1375 Every file storage object referenced by more than one directory entry contributes its number of
1376 directory entries.
1378 :returns: Sum of directory entries over all hardlinked files.
1379 """
1380 return sum(l for f in self._ids.values() if (l := len(f._parents)) > 1)
1382 @readonly
1383 def TotalHardLinkCount2(self) -> int:
1384 """
1385 Read-only property to return the number of file storage objects that are hardlinked.
1387 In contrast to :attr:`TotalHardLinkCount`, every hardlinked file contributes ``1``, regardless of how
1388 many directory entries reference it.
1390 :returns: Number of files referenced by more than one directory entry.
1391 """
1392 return sum(1 for f in self._ids.values() if len(f._parents) > 1)
1394 @readonly
1395 def TotalHardLinkCount3(self) -> int:
1396 """
1397 Read-only property to return the number of file storage objects that are **not** hardlinked.
1399 .. attention::
1401 Despite the name, this counts files referenced by exactly *one* directory entry.
1403 :returns: Number of files referenced by exactly one directory entry.
1404 """
1405 return sum(1 for f in self._ids.values() if len(f._parents) == 1)
1407 @readonly
1408 def Size2(self) -> int:
1409 """
1410 Read-only property to return the accumulated size of all hardlinked files, counted once each.
1412 :returns: Sum of sizes over all files referenced by more than one directory entry.
1413 """
1414 return sum(f._size for f in self._ids.values() if len(f._parents) > 1)
1416 @readonly
1417 def Size3(self) -> int:
1418 """
1419 Read-only property to return the accumulated size of all hardlinked files, counted per directory entry.
1421 In contrast to :attr:`Size2`, a file's size is multiplied by the number of directory entries
1422 referencing it, so it reflects the size a filesystem without hardlink support would need.
1424 :returns: Sum of sizes over all hardlinked files, weighted by their number of directory entries.
1425 """
1426 return sum(f._size * len(f._parents) for f in self._ids.values() if len(f._parents) > 1)
1428 @readonly
1429 def TotalUniqueFileCount(self) -> int:
1430 """
1431 Read-only property to return the number of distinct file storage objects, counting hardlinks to the same content once.
1433 :returns: Number of unique files.
1434 """
1435 return len(self._ids)
1437 def RegisterBrokenSymbolicLink(self, symLink: SymbolicLink) -> None:
1438 """
1439 Mark a symbolic link as broken and collect it at the root.
1441 :param symLink: The symbolic link whose target doesn't exist.
1442 """
1443 symLink._isBroken = True
1444 self._brokenSymbolicLinks.append(symLink)
1446 def RegisterUnconnectedSymbolicLink(self, symLink: SymbolicLink) -> None:
1447 """
1448 Mark a symbolic link as out of range and collect it at the root.
1450 :param symLink: The symbolic link whose target lies outside the scanned tree.
1451 """
1452 symLink._isOutOfRange = True
1453 self._unconnectedSymbolicLinks.append(symLink)
1455 def Copy(self) -> Root:
1456 """
1457 Copy the directory structure including all subelements and link it to the given parent.
1459 The duration for the deep copy process is provided in :attr:`ScanDuration`
1461 .. hint::
1463 Statistics like aggregated directory size are copied too. |br|
1464 There is no rescan or repeated aggregation needed.
1466 :returns: A deep copy of the directory structure.
1467 """
1468 with Stopwatch() as sw:
1469 root = Root(self._path, False)
1470 root._size = self._size
1472 for subdir in self._subdirectories.values():
1473 subdir.Copy(root)
1475 for file in self._files.values():
1476 file.Copy(root)
1478 for link in self._symbolicLinks.values():
1479 link.Copy(root)
1481 root._scanDuration = sw.Duration
1482 root._aggregateDuration = 0.0
1484 return root
1486 def __repr__(self) -> str:
1487 """
1488 Return a detailed string representation of this filesystem root.
1490 :returns: The root's path and the number of directories, regular files and symbolic links below it.
1491 """
1492 return f"Root: {self.Path} (dirs: {self.TotalSubdirectoryCount}, files: {self.TotalRegularFileCount}, symlinks: {self.TotalSymbolicLinkCount})"
1494 def __str__(self) -> str:
1495 """
1496 Return a string representation of this filesystem element.
1498 :returns: The element's name, without any path.
1499 """
1500 return self._name
1503@export
1504class File(Base):
1505 """
1506 A **File** represents a file storage object in the filesystem, which is accessible by one or more :class:`Filename` objects.
1508 Each file has an internal id, which is associated to a unique ID within the host's filesystem.
1509 """
1510 _id: int #: Unique (host internal) file object ID)
1511 _parents: list[Filename] #: List of reverse references to :class:`Filename` objects.
1513 def __init__(
1514 self,
1515 id: int,
1516 size: int,
1517 parent: Nullable[Filename] = None
1518 ) -> None:
1519 """
1520 Initialize the File storage object with an ID, size and parent reference.
1522 :param id: Unique ID of the file object.
1523 :param size: Optional, size of the file object.
1524 :param parent: Optional, parent reference.
1525 :raises ValueError: If parameter 'id' is None.
1526 :raises TypeError: If parameter 'parent' is not of type :class:`Filename`.
1527 """
1528 if id is None: 1528 ↛ 1529line 1528 didn't jump to line 1529 because the condition on line 1528 was never true
1529 raise ValueError(f"Parameter 'id' is None.")
1530 elif not isinstance(id, int): 1530 ↛ 1531line 1530 didn't jump to line 1531 because the condition on line 1530 was never true
1531 ex = TypeError("Parameter 'id' is not of type 'int'.")
1532 ex.add_note(f"Got type '{getFullyQualifiedName(id)}'.")
1533 raise ex
1535 self._id = id
1537 if parent is None:
1538 super().__init__(size, None)
1539 self._parents = []
1540 elif isinstance(parent, Filename): 1540 ↛ 1545line 1540 didn't jump to line 1545 because the condition on line 1540 was always true
1541 super().__init__(size, parent._root)
1542 self._parents = [parent]
1543 parent._file = self
1544 else:
1545 ex = TypeError("Parameter 'parent' is not of type 'Filename'.")
1546 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
1547 raise ex
1549 @readonly
1550 def ID(self) -> int:
1551 """
1552 Read-only property to access the file object's unique identifier.
1554 :returns: Unique file object identifier.
1555 """
1556 return self._id
1558 @readonly
1559 def Parents(self) -> list[Filename]:
1560 """
1561 Read-only property to access the list of filenames using the same file storage object.
1563 .. hint::
1565 This allows to check if a file object has multiple filenames a.k.a hardlinks.
1567 :returns: List of filenames for the file storage object.
1568 """
1569 return self._parents
1571 def AddParent(self, filename: Filename) -> None:
1572 """
1573 Add another parent reference to a :class:`Filename`.
1575 :param filename: Reference to a filename object.
1576 :raises ValueError: If parameter 'filename' is None.
1577 :raises TypeError: If parameter 'filename' is not of type :class:`Filename`.
1578 :raises ToolingException: If the filename already references another file object.
1579 """
1580 if filename is None: 1580 ↛ 1581line 1580 didn't jump to line 1581 because the condition on line 1580 was never true
1581 raise ValueError(f"Parameter 'filename' is None.")
1582 elif not isinstance(filename, Filename): 1582 ↛ 1583line 1582 didn't jump to line 1583 because the condition on line 1582 was never true
1583 ex = TypeError("Parameter 'filename' is not of type 'Filename'.")
1584 ex.add_note(f"Got type '{getFullyQualifiedName(filename)}'.")
1585 raise ex
1586 elif filename._file is not None: 1586 ↛ 1587line 1586 didn't jump to line 1587 because the condition on line 1586 was never true
1587 raise ToolingException(f"Filename is already referencing an other file object ({filename._file._id}).")
1589 self._parents.append(filename)
1590 filename._file = self
1592 if filename._root is not None:
1593 self._root = filename._root