Coverage for pyTooling/Filesystem/__init__.py: 48%
463 statements
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-07 22:21 +0000
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-07 22:21 +0000
1# ==================================================================================================================== #
2# _____ _ _ _____ _ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | ___(_) | ___ ___ _ _ ___| |_ ___ _ __ ___ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_ | | |/ _ \/ __| | | / __| __/ _ \ '_ ` _ \ #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| _| | | | __/\__ \ |_| \__ \ || __/ | | | | | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_|_|\___||___/\__, |___/\__\___|_| |_| |_| #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2025-2025 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.
37"""
38from os import scandir, readlink
39from sys import version_info
41from enum import Enum
42from itertools import chain
43from pathlib import Path
44from typing import Optional as Nullable, Dict, Generic, Generator, TypeVar, List, Any, Callable, Union
46try:
47 from pyTooling.Decorators import readonly, export
48 from pyTooling.Exceptions import ToolingException
49 from pyTooling.MetaClasses import ExtendedType, abstractmethod
50 from pyTooling.Common import getFullyQualifiedName, zipdicts
51 from pyTooling.Stopwatch import Stopwatch
52 from pyTooling.Tree import Node
53except (ImportError, ModuleNotFoundError): # pragma: no cover
54 print("[pyTooling.Filesystem] Could not import from 'pyTooling.*'!")
56 try:
57 from pyTooling.Decorators import readonly, export
58 from pyTooling.Exceptions import ToolingException
59 from pyTooling.MetaClasses import ExtendedType, abstractmethod
60 from pyTooling.Common import getFullyQualifiedName
61 from pyTooling.Stopwatch import Stopwatch
62 from pyTooling.Tree import Node
63 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
64 print("[pyTooling.Filesystem] Could not import directly!")
65 raise ex
68_ParentType = TypeVar("_ParentType", bound="Element")
71@export
72class FilesystemException(ToolingException):
73 """Base-exception of all exceptions raised by :mod:`pyTooling.Filesystem`."""
76@export
77class NodeKind(Enum):
78 """
79 Node kind for filesystem elements in a :ref:`tree <STRUCT/Tree>`.
81 This enumeration is used when converting the filesystem statistics tree to an instance of :mod:`pyTooling.Tree`.
82 """
83 Directory = 0 #: Node represents a directory.
84 File = 1 #: Node represents a regular file.
85 SymbolicLink = 2 #: Node represents a symbolic link.
88@export
89class Base(metaclass=ExtendedType, slots=True):
90 _root: Nullable["Root"] #: Reference to the root of the filesystem statistics scope.
91 _size: Nullable[int] #: Actual or aggregated size of the filesystem element.
93 def __init__(
94 self,
95 root: Nullable["Root"],
96 size: Nullable[int],
97 ) -> None:
98 self._root = root
99 self._size = size
101 @property
102 def Root(self) -> Nullable["Root"]:
103 """
104 Property to access the root of the filesystem statistics scope.
106 :returns: Root of the filesystem statistics scope.
107 """
108 return self._root
110 @Root.setter
111 def Root(self, value: "Root") -> None:
112 self._root = value
114 @readonly
115 def Size(self) -> int:
116 """
117 Read-only property to access the elements size in Bytes.
119 :returns: Size in Bytes.
120 :raises FilesystemException: If size is not computed, yet.
121 """
122 if self._size is None: 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true
123 raise FilesystemException("Size is not computed, yet.")
125 return self._size
127 # @abstractmethod
128 def ToTree(self) -> Node:
129 pass
132@export
133class Element(Base, Generic[_ParentType]):
134 _name: str #: Name of the filesystem element.
135 _parent: _ParentType #: Reference to the filesystem element's parent (:class:`Directory`)
136 _linkSources: List["SymbolicLink"] #: A list of symbolic links pointing to this filesystem element.
138 def __init__(
139 self,
140 name: str,
141 size: Nullable[int] = None,
142 parent: Nullable[_ParentType] = None
143 ) -> None:
144 root = None # if parent is None else parent._root
146 super().__init__(root, size)
148 self._parent = parent
149 self._name = name
150 self._linkSources = []
152 @property
153 def Parent(self) -> _ParentType:
154 return self._parent
156 @Parent.setter
157 def Parent(self, value: _ParentType) -> None:
158 self._parent = value
160 if value._root is not None:
161 self._root = value._root
163 @readonly
164 def Name(self) -> str:
165 """
166 Read-only property to access the elements name.
168 :returns: Element name.
169 """
170 return self._name
172 @readonly
173 def Path(self) -> Path:
174 raise NotImplemented(f"Property 'Path' is abstract.")
176 def AddLinkSources(self, source: "SymbolicLink") -> None:
177 self._linkSources.append(source)
180@export
181class Directory(Element["Directory"]):
182 """
183 A **directory** represents a directory in the filesystem contains subdirectories, regular files and symbolic links.
185 While scanning for subelements, the directory is populated with elements. Every file object added, gets registered in
186 the filesystems :class:`Root` for deduplication. In case a file identifier already exists, the found filename will
187 reference the same file objects. In turn, the file objects has then references to multiple filenames (parents). This
188 allows to detect :term:`hardlinks <hardlink>`.
190 The time needed for scanning the directory and its subelements is provided via :data:`ScanDuration`.
192 After scnaning the directory for subelements, certain directory properties get aggregated. The time needed for
193 aggregation is provided via :data:`AggregateDuration`.
194 """
196 _path: Nullable[Path] #: Cached :class:`~pathlib.Path` object of this directory.
197 _subdirectories: Dict[str, "Directory"] #: Dictionary containing name-:class:`Directory` pairs.
198 _files: Dict[str, "Filename"] #: Dictionary containing name-:class:`Filename` pairs.
199 _symbolicLinks: Dict[str, "SymbolicLink"] #: Dictionary containing name-:class:`SymbolicLink` pairs.
200 _collapsed: bool #: True, if this directory was collapsed. It contains no subelements.
201 _scanDuration: Nullable[float] #: Duration for scanning the directory and all its subelements.
202 _aggregateDuration: Nullable[float] #: Duration for aggregating all subelements.
204 def __init__(
205 self,
206 name: str,
207 collectSubdirectories: bool = False,
208 parent: Nullable["Directory"] = None
209 ) -> None:
210 super().__init__(name, None, parent)
212 self._path = None
213 self._subdirectories = {}
214 self._files = {}
215 self._symbolicLinks = {}
216 self._collapsed = False
217 self._scanDuration = None
218 self._aggregateDuration = None
220 if parent is not None:
221 parent._subdirectories[name] = self
223 if parent._root is not None: 223 ↛ 226line 223 didn't jump to line 226 because the condition on line 223 was always true
224 self._root = parent._root
226 if collectSubdirectories: 226 ↛ 227line 226 didn't jump to line 227 because the condition on line 226 was never true
227 self._collectSubdirectories()
229 def _collectSubdirectories(self) -> None:
230 with Stopwatch() as sw1:
231 self._scanSubdirectories()
233 with Stopwatch() as sw2:
234 self._aggregateSizes()
236 self._scanDuration = sw1.Duration
237 self._aggregateDuration = sw2.Duration
239 def _scanSubdirectories(self) -> None:
240 try:
241 items = scandir(directoryPath := self.Path)
242 except PermissionError as ex:
243 return
245 for dirEntry in items:
246 if dirEntry.is_dir(follow_symlinks=False):
247 subdirectory = Directory(dirEntry.name, collectSubdirectories=True, parent=self)
248 elif dirEntry.is_file(follow_symlinks=False):
249 id = dirEntry.inode()
250 if id in self._root._ids:
251 file = self._root._ids[id]
253 hardLink = Filename(dirEntry.name, file=file, parent=self)
254 else:
255 s = dirEntry.stat(follow_symlinks=False)
256 filename = Filename(dirEntry.name, parent=self)
257 file = File(id, s.st_size, parent=filename)
259 self._root._ids[id] = file
260 elif dirEntry.is_symlink():
261 target = Path(readlink(directoryPath / dirEntry.name))
262 symlink = SymbolicLink(dirEntry.name, target, parent=self)
263 else:
264 raise FilesystemException(f"Unknown directory element.")
266 def _connectSymbolicLinks(self) -> None:
267 for dir in self._subdirectories.values():
268 dir._connectSymbolicLinks()
270 for link in self._symbolicLinks.values():
271 if link._target.is_absolute():
272 pass
273 else:
274 target = self
275 for elem in link._target.parts:
276 if elem == ".":
277 continue
278 elif elem == "..":
279 target = target._parent
280 continue
282 try:
283 target = target._subdirectories[elem]
284 continue
285 except KeyError:
286 pass
288 try:
289 target = target._files[elem]
290 continue
291 except KeyError:
292 pass
294 try:
295 target = target._symbolicLinks[elem]
296 continue
297 except KeyError:
298 pass
300 target.AddLinkSources(link)
302 def _aggregateSizes(self) -> None:
303 self._size = (
304 sum(dir._size for dir in self._subdirectories.values()) +
305 sum(file._file._size for file in self._files.values())
306 )
308 @Element.Root.setter
309 def Root(self, value: "Root") -> None:
310 Element.Root.fset(self, value)
312 for subdir in self._subdirectories.values(): 312 ↛ 313line 312 didn't jump to line 313 because the loop on line 312 never started
313 subdir.Root = value
315 for file in self._files.values():
316 file.Root = value
318 for link in self._symbolicLinks.values(): 318 ↛ 319line 318 didn't jump to line 319 because the loop on line 318 never started
319 link.Root = value
321 @Element.Parent.setter
322 def Parent(self, value: _ParentType) -> None:
323 Element.Parent.fset(self, value)
325 value._subdirectories[self._name] = self
327 if isinstance(value, Root): 327 ↛ exitline 327 didn't return from function 'Parent' because the condition on line 327 was always true
328 self.Root = value
330 @readonly
331 def Count(self) -> int:
332 """
333 Read-only property to access the number of elements in a directory.
335 :returns: Number of files plus subdirectories.
336 """
337 return len(self._subdirectories) + len(self._files) + len(self._symbolicLinks)
339 @readonly
340 def FileCount(self) -> int:
341 """
342 Read-only property to access the number of files in a directory.
344 .. hint::
346 Files include regular files and symbolic links.
348 :returns: Number of files.
349 """
350 return len(self._files) + len(self._symbolicLinks)
352 @readonly
353 def RegularFileCount(self) -> int:
354 """
355 Read-only property to access the number of regular files in a directory.
357 :returns: Number of regular files.
358 """
359 return len(self._files)
361 @readonly
362 def SymbolicLinkCount(self) -> int:
363 """
364 Read-only property to access the number of symbolic links in a directory.
366 :returns: Number of symbolic links.
367 """
368 return len(self._symbolicLinks)
370 @readonly
371 def SubdirectoryCount(self) -> int:
372 """
373 Read-only property to access the number of subdirectories in a directory.
375 :returns: Number of subdirectories.
376 """
377 return len(self._subdirectories)
379 @readonly
380 def TotalFileCount(self) -> int:
381 """
382 Read-only property to access the total number of files in all child hierarchy levels (recursively).
384 .. hint::
386 Files include regular files and symbolic links.
388 :returns: Total number of files.
389 """
390 return sum(d.TotalFileCount for d in self._subdirectories.values()) + len(self._files) + len(self._symbolicLinks)
392 @readonly
393 def TotalRegularFileCount(self) -> int:
394 """
395 Read-only property to access the total number of regular files in all child hierarchy levels (recursively).
397 :returns: Total number of regular files.
398 """
399 return sum(d.TotalRegularFileCount for d in self._subdirectories.values()) + len(self._files)
401 @readonly
402 def TotalSymbolicLinkCount(self) -> int:
403 """
404 Read-only property to access the total number of symbolic links in all child hierarchy levels (recursively).
406 :returns: Total number of symbolic links.
407 """
408 return sum(d.TotalSymbolicLinkCount for d in self._subdirectories.values()) + len(self._symbolicLinks)
410 @readonly
411 def TotalSubdirectoryCount(self) -> int:
412 """
413 Read-only property to access the total number of subdirectories in all child hierarchy levels (recursively).
415 :returns: Total number of subdirectories.
416 """
417 return len(self._subdirectories) + sum(d.TotalSubdirectoryCount for d in self._subdirectories.values())
419 @readonly
420 def Subdirectories(self) -> Generator["Directory", None, None]:
421 """
422 Iterate all direct subdirectories of the directory.
424 :returns: A generator to iterate all direct subdirectories.
425 """
426 return (d for d in self._subdirectories.values())
428 @readonly
429 def Files(self) -> Generator[Union["Filename", "SymbolicLink"], None, None]:
430 """
431 Iterate all direct files of the directory.
433 .. hint::
435 Files include regular files and symbolic links.
437 :returns: A generator to iterate all direct files.
438 """
439 return (f for f in chain(self._files.values(), self._symbolicLinks.values()))
441 @readonly
442 def RegularFiles(self) -> Generator["Filename", None, None]:
443 """
444 Iterate all direct regular files of the directory.
446 :returns: A generator to iterate all direct regular files.
447 """
448 return (f for f in self._files.values())
450 @readonly
451 def SymbolicLinks(self) -> Generator["SymbolicLink", None, None]:
452 """
453 Iterate all direct symbolic links of the directory.
455 :returns: A generator to iterate all direct symbolic links.
456 """
457 return (l for l in self._symbolicLinks.values())
459 @readonly
460 def Path(self) -> Path:
461 """
462 Read-only property to access the equivalent Path instance for accessing the represented directory.
464 :returns: Path to the directory.
465 :raises FilesystemException: If no parent is set.
466 """
467 if self._path is not None:
468 return self._path
470 if self._parent is None:
471 raise FilesystemException(f"No parent or root set for directory.")
473 self._path = self._parent.Path / self._name
474 return self._path
476 @readonly
477 def ScanDuration(self) -> float:
478 """
479 Read-only property to access the time needed to scan a directory structure including all subelements (recursively).
481 :returns: The scan duration in seconds.
482 :raises FilesystemException: If the directory was not scanned.
483 """
484 if self._scanDuration is None:
485 raise FilesystemException(f"Directory was not scanned, yet.")
487 return self._scanDuration
489 @readonly
490 def AggregateDuration(self) -> float:
491 """
492 Read-only property to access the time needed to aggregate the directory's and subelement's properties (recursively).
494 :returns: The aggregation duration in seconds.
495 :raises FilesystemException: If the directory properties were not aggregated.
496 """
497 if self._scanDuration is None:
498 raise FilesystemException(f"Directory properties were not aggregated, yet.")
500 return self._aggregateDuration
502 def Copy(self, parent: Nullable["Directory"] = None) -> "Directory":
503 """
504 Copy the directory structure including all subelements and link it to the given parent.
506 .. hint::
508 Statistics like aggregated directory size are copied too. |br|
509 There is no rescan or repeated aggregation needed.
511 :param parent: The parent element of the copied directory.
512 :returns: A deep copy of the directory structure.
513 """
514 dir = Directory(self._name, parent=parent)
515 dir._size = self._size
517 for subdir in self._subdirectories.values():
518 subdir.Copy(dir)
520 for file in self._files.values():
521 file.Copy(dir)
523 for link in self._symbolicLinks.values():
524 link.Copy(dir)
526 return dir
528 def Collapse(self, func: Callable[["Directory"], bool]) -> bool:
529 # if len(self._subdirectories) == 0 or all(subdir.Collapse(func) for subdir in self._subdirectories.values()):
530 if len(self._subdirectories) == 0:
531 if func(self):
532 # print(f"collapse 1 {self.Path}")
533 self._collapsed = True
534 self._subdirectories.clear()
535 self._files.clear()
536 self._symbolicLinks.clear()
538 return True
539 else:
540 return False
542 # if all(subdir.Collapse(func) for subdir in self._subdirectories.values())
543 collapsible = True
544 for subdir in self._subdirectories.values():
545 result = subdir.Collapse(func)
546 collapsible = collapsible and result
548 if collapsible:
549 # print(f"collapse 2 {self.Path}")
550 self._collapsed = True
551 self._subdirectories.clear()
552 self._files.clear()
553 self._symbolicLinks.clear()
555 return True
556 else:
557 return False
559 def ToTree(self, format: Nullable[Callable[[Node], str]] = None) -> Node:
560 """
561 Convert the directory to a :class:`~pyTooling.Tree.Node`.
563 The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the directory. Additional data is
564 attached to the node's key-value store:
566 ``kind``
567 The node's kind. See :class:`NodeKind`.
568 ``size``
569 The directory's aggregated size.
571 :param format: A user defined formatting function for tree nodes.
572 :returns: A tree node representing this directory.
573 """
574 if format is None:
575 def format(node: Node) -> str:
576 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"
578 directoryNode = Node(
579 value=self,
580 keyValuePairs={
581 "kind": NodeKind.File,
582 "size": self._size
583 },
584 format=format
585 )
586 directoryNode.AddChildren(
587 e.ToTree(format) for e in chain(self._subdirectories.values()) #, self._files.values(), self._symbolicLinks.values())
588 )
590 return directoryNode
592 def __eq__(self, other) -> bool:
593 """
594 Compare two Directory instances for equality.
596 :param other: Parameter to compare against.
597 :returns: ``True``, if both directories and all its subelements are equal.
598 :raises TypeError: If parameter ``other`` is not of type :class:`Directory`.
599 """
600 if not isinstance(other, Directory):
601 ex = TypeError("Parameter 'other' is not of type Directory.")
602 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
603 raise ex
605 if not all(dir1 == dir2 for _, dir1, dir2 in zipdicts(self._subdirectories, other._subdirectories)):
606 return False
608 if not all(file1 == file2 for _, file1, file2 in zipdicts(self._files, other._files)):
609 return False
611 if not all(link1 == link2 for _, link1, link2 in zipdicts(self._symbolicLinks, other._symbolicLinks)):
612 return False
614 return True
616 def __ne__(self, other: Any) -> bool:
617 """
618 Compare two Directory instances for inequality.
620 :param other: Parameter to compare against.
621 :returns: ``True``, if both directories and all its subelements are unequal.
622 :raises TypeError: If parameter ``other`` is not of type :class:`Directory`.
623 """
624 return not self.__eq__(other)
626 def __repr__(self) -> str:
627 return f"Directory: {self.Path}"
629 def __str__(self) -> str:
630 return self._name
633@export
634class Filename(Element[Directory]):
635 _file: Nullable["File"]
637 def __init__(
638 self,
639 name: str,
640 file: Nullable["File"] = None,
641 parent: Nullable[Directory] = None
642 ) -> None:
643 super().__init__(name, None, parent)
645 if file is None: 645 ↛ 648line 645 didn't jump to line 648 because the condition on line 645 was always true
646 self._file = None
647 else:
648 self._file = file
649 file._parents.append(self)
651 if parent is not None:
652 parent._files[name] = self
654 if parent._root is not None:
655 self._root = parent._root
657 @Element.Root.setter
658 def Root(self, value: "Root") -> None:
659 self._root = value
661 if self._file is not None: 661 ↛ exitline 661 didn't return from function 'Root' because the condition on line 661 was always true
662 self._file._root = value
664 @Element.Parent.setter
665 def Parent(self, value: _ParentType) -> None:
666 Element.Parent.fset(self, value)
668 value._files[self._name] = self
670 if isinstance(value, Root): 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true
671 self.Root = value
673 @readonly
674 def File(self) -> Nullable["File"]:
675 return self._file
677 @readonly
678 def Size(self) -> int:
679 if self._file is None:
680 raise ToolingException(f"Filename isn't linked to a File object.")
682 return self._file._size
684 @readonly
685 def Path(self) -> Path:
686 if self._parent is None:
687 raise ToolingException(f"Filename has no parent object.")
689 return self._parent.Path / self._name
691 def Copy(self, parent: Directory) -> "Filename":
692 fileID = self._file._id
694 if fileID in parent._root._ids:
695 file = parent._root._ids[fileID]
696 else:
697 fileSize = self._file._size
698 file = File(fileID, fileSize)
700 parent._root._ids[fileID] = file
702 return Filename(self._name, file, parent=parent)
704 def ToTree(self) -> Node:
705 def format(node: Node) -> str:
706 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"
708 fileNode = Node(
709 value=self,
710 keyValuePairs={
711 "kind": NodeKind.File,
712 "size": self._size
713 },
714 format=format
715 )
717 return fileNode
719 def __eq__(self, other) -> bool:
720 """
721 Compare two Filename instances for equality.
723 :param other: Parameter to compare against.
724 :returns: ``True``, if both filenames are equal.
725 :raises TypeError: If parameter ``other`` is not of type :class:`Filename`.
726 """
727 if not isinstance(other, Filename):
728 ex = TypeError("Parameter 'other' is not of type Filename.")
729 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
730 raise ex
732 return self._name == other._name and self.Size == other.Size
734 def __ne__(self, other: Any) -> bool:
735 """
736 Compare two Filename instances for inequality.
738 :param other: Parameter to compare against.
739 :returns: ``True``, if both filenames are unequal.
740 :raises TypeError: If parameter ``other`` is not of type :class:`Filename`.
741 """
742 if not isinstance(other, Filename):
743 ex = TypeError("Parameter 'other' is not of type Filename.")
744 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
745 raise ex
747 return self._name != other._name or self.Size != other.Size
749 def __repr__(self) -> str:
750 return f"File: {self.Path}"
752 def __str__(self) -> str:
753 return self._name
756@export
757class SymbolicLink(Element[Directory]):
758 _target: Path
760 def __init__(
761 self,
762 name: str,
763 target: Path,
764 parent: Nullable[Directory]
765 ) -> None:
766 super().__init__(name, None, parent)
768 self._target = target
770 if parent is not None:
771 parent._symbolicLinks[name] = self
773 if parent._root is not None:
774 self._root = parent._root
776 @readonly
777 def Path(self) -> Path:
778 return self._parent.Path / self._name
780 @readonly
781 def Target(self) -> Path:
782 return self._target
784 def Copy(self, parent: Directory) -> "SymbolicLink":
785 return SymbolicLink(self._name, self._target, parent=parent)
787 def ToTree(self) -> Node:
788 def format(node: Node) -> str:
789 return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"
791 symbolicLinkNode = Node(
792 value=self,
793 keyValuePairs={
794 "kind": NodeKind.SymbolicLink,
795 "size": self._size
796 },
797 format=format
798 )
800 return symbolicLinkNode
802 def __eq__(self, other) -> bool:
803 """
804 Compare two SymbolicLink instances for equality.
806 :param other: Parameter to compare against.
807 :returns: ``True``, if both symbolic links are equal.
808 :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`.
809 """
810 if not isinstance(other, SymbolicLink):
811 ex = TypeError("Parameter 'other' is not of type SymbolicLink.")
812 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
813 raise ex
815 return self._name == other._name and self._target == other._target
817 def __ne__(self, other: Any) -> bool:
818 """
819 Compare two SymbolicLink instances for inequality.
821 :param other: Parameter to compare against.
822 :returns: ``True``, if both symbolic links are unequal.
823 :raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`.
824 """
825 if not isinstance(other, SymbolicLink):
826 ex = TypeError("Parameter 'other' is not of type SymbolicLink.")
827 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
828 raise ex
830 return self._name != other._name or self._target != other._target
832 def __repr__(self) -> str:
833 return f"SymLink: {self.Path} -> {self._target}"
835 def __str__(self) -> str:
836 return self._name
839@export
840class Root(Directory):
841 _ids: Dict[int, "File"] #: Dictionary of file identifier - file objects pairs found while scanning the directory structure.
843 def __init__(
844 self,
845 rootDirectory: Path,
846 collectSubdirectories: bool = True
847 ) -> None:
848 if rootDirectory is None: 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true
849 raise ValueError(f"Parameter 'path' is None.")
850 elif not isinstance(rootDirectory, Path): 850 ↛ 851line 850 didn't jump to line 851 because the condition on line 850 was never true
851 raise TypeError(f"Parameter 'path' is not of type Path.")
852 elif not rootDirectory.exists(): 852 ↛ 853line 852 didn't jump to line 853 because the condition on line 852 was never true
853 raise ToolingException(f"Path '{rootDirectory}' doesn't exist.") from FileNotFoundError(rootDirectory)
855 self._ids = {}
857 super().__init__(rootDirectory.name)
858 self._root = self
859 self._path = rootDirectory
861 if collectSubdirectories: 861 ↛ 862line 861 didn't jump to line 862 because the condition on line 861 was never true
862 self._collectSubdirectories()
863 self._connectSymbolicLinks()
865 @readonly
866 def TotalHardLinkCount(self) -> int:
867 return sum(l for f in self._ids.values() if (l := len(f._parents)) > 1)
869 @readonly
870 def TotalHardLinkCount2(self) -> int:
871 return sum(1 for f in self._ids.values() if len(f._parents) > 1)
873 @readonly
874 def TotalHardLinkCount3(self) -> int:
875 return sum(1 for f in self._ids.values() if len(f._parents) == 1)
877 @readonly
878 def Size2(self) -> int:
879 return sum(f._size for f in self._ids.values() if len(f._parents) > 1)
881 @readonly
882 def Size3(self) -> int:
883 return sum(f._size * len(f._parents) for f in self._ids.values() if len(f._parents) > 1)
885 @readonly
886 def TotalUniqueFileCount(self) -> int:
887 return len(self._ids)
889 @readonly
890 def Path(self) -> Path:
891 """
892 Read-only property to access the path of the filesystem statistics root.
894 :returns: Path to the root of the filesystem statistics root directory.
895 """
896 return self._path
898 def Copy(self) -> "Root":
899 """
900 Copy the directory structure including all subelements and link it to the given parent.
902 The duration for the deep copy process is provided in :attr:`ScanDuration`
904 .. hint::
906 Statistics like aggregated directory size are copied too. |br|
907 There is no rescan or repeated aggregation needed.
909 :returns: A deep copy of the directory structure.
910 """
911 with Stopwatch() as sw:
912 root = Root(self._path, False)
913 root._size = self._size
915 for subdir in self._subdirectories.values():
916 subdir.Copy(root)
918 for file in self._files.values():
919 file.Copy(root)
921 for link in self._symbolicLinks.values():
922 link.Copy(root)
924 root._scanDuration = sw.Duration
925 root._aggregateDuration = 0.0
927 return root
929 def __repr__(self) -> str:
930 return f"Root: {self.Path} (dirs: {self.TotalSubdirectoryCount}, files: {self.TotalRegularFileCount}, symlinks: {self.TotalSymbolicLinkCount})"
932 def __str__(self) -> str:
933 return self._name
936@export
937class File(Base):
938 _id: int
939 _parents: List[Filename]
941 def __init__(
942 self,
943 id: int,
944 size: int,
945 parent: Nullable[Filename] = None
946 ) -> None:
947 self._id = id
948 if parent is None:
949 super().__init__(None, size)
950 self._parents = []
951 else:
952 super().__init__(parent._root, size)
953 self._parents = [parent]
954 parent._file = self
956 @readonly
957 def ID(self) -> int:
958 """
959 Read-only property to access the file object's unique identifier.
961 :returns: Unique file object identifier.
962 """
963 return self._id
965 @readonly
966 def Parents(self) -> List[Filename]:
967 """
968 Read-only property to access the list of filenames using the file object.
970 .. hint::
972 This allows to check if a file object has multiple filenames a.k.a hardlinks.
974 :returns: List of filenames for the file object.
975 """
976 return self._parents
978 def AddParent(self, file: Filename) -> None:
979 if file._file is not None: 979 ↛ 980line 979 didn't jump to line 980 because the condition on line 979 was never true
980 raise ToolingException(f"Filename is already referencing an other file object ({file._file._id}).")
982 self._parents.append(file)
983 file._file = self
985 if file._root is not None:
986 self._root = file._root