File: __init__.py

package info (click to toggle)
python-pytooling 8.10.0-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 3,752 kB
  • sloc: python: 25,618; makefile: 13
file content (1111 lines) | stat: -rw-r--r-- 34,422 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
# ==================================================================================================================== #
#             _____           _ _               _____ _ _                     _                                        #
#  _ __  _   |_   _|__   ___ | (_)_ __   __ _  |  ___(_) | ___  ___ _   _ ___| |_ ___ _ __ ___                         #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_  | | |/ _ \/ __| | | / __| __/ _ \ '_ ` _ \                        #
# | |_) | |_| || | (_) | (_) | | | | | | (_| |_|  _| | | |  __/\__ \ |_| \__ \ ||  __/ | | | | |                       #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|   |_|_|\___||___/\__, |___/\__\___|_| |_| |_|                       #
# |_|    |___/                          |___/                       |___/                                              #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2025-2026 Patrick Lehmann - Bötzingen, Germany                                                             #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
An object-oriented file system abstraction for directory, file, symbolic link, ... statistics collection.

.. important::

   This isn't a replacement of :mod:`pathlib` introduced with Python 3.4.
"""
from os                    import scandir, readlink

from enum                  import Enum
from itertools             import chain
from pathlib               import Path
from typing                import Optional as Nullable, Dict, Generic, Generator, TypeVar, List, Any, Callable, Union

try:
	from pyTooling.Decorators  import readonly, export
	from pyTooling.Exceptions  import ToolingException
	from pyTooling.MetaClasses import ExtendedType, abstractmethod
	from pyTooling.Common      import getFullyQualifiedName, zipdicts
	from pyTooling.Stopwatch   import Stopwatch
	from pyTooling.Tree        import Node
except (ImportError, ModuleNotFoundError):  # pragma: no cover
	print("[pyTooling.Filesystem] Could not import from 'pyTooling.*'!")

	try:
		from pyTooling.Decorators  import readonly, export
		from pyTooling.Exceptions  import ToolingException
		from pyTooling.MetaClasses import ExtendedType, abstractmethod
		from pyTooling.Common      import getFullyQualifiedName
		from pyTooling.Stopwatch   import Stopwatch
		from pyTooling.Tree        import Node
	except (ImportError, ModuleNotFoundError) as ex:  # pragma: no cover
		print("[pyTooling.Filesystem] Could not import directly!")
		raise ex


__all__ = ["_ParentType"]

_ParentType = TypeVar("_ParentType", bound="Element")
"""The type variable for a parent reference."""


@export
class FilesystemException(ToolingException):
	"""Base-exception of all exceptions raised by :mod:`pyTooling.Filesystem`."""


@export
class NodeKind(Enum):
	"""
	Node kind for filesystem elements in a :ref:`tree <STRUCT/Tree>`.

	This enumeration is used when converting the filesystem statistics tree to an instance of :mod:`pyTooling.Tree`.
	"""
	Directory =    0  #: Node represents a directory.
	File =         1  #: Node represents a regular file.
	SymbolicLink = 2  #: Node represents a symbolic link.


@export
class Base(metaclass=ExtendedType, slots=True):
	"""
	Base-class for all filesystem elements in :mod:`pyTooling.Filesystem`.

	It implements a size and a reference to the root element of the filesystem.
	"""
	_root:   Nullable["Root"]  #: Reference to the root of the filesystem statistics scope.
	_size:   Nullable[int]     #: Actual or aggregated size of the filesystem element.

	def __init__(
		self,
		size: Nullable[int],
		root: Nullable["Root"]
	) -> None:
		"""
		Initialize the base-class with filesystem element size and root reference.

		:param size: Optional size of the element.
		:param root: Optional reference to the filesystem root element.
		"""
		if size is None:
			pass
		elif not isinstance(size, int):
			ex = TypeError("Parameter 'size' is not of type 'int'.")
			ex.add_note(f"Got type '{getFullyQualifiedName(size)}'.")
			raise ex

		self._size = size
		self._root = root

	@property
	def Root(self) -> Nullable["Root"]:
		"""
		Property to access the root of the filesystem statistics scope.

		:returns: Root of the filesystem statistics scope.
		"""
		return self._root

	@Root.setter
	def Root(self, value: "Root") -> None:
		self._root = value

	@readonly
	def Size(self) -> int:
		"""
		Read-only property to access the element's size in Bytes.

		:returns:                    Size in Bytes.
		:raises FilesystemException: If size is not computed, yet.
		"""
		if self._size is None:
			raise FilesystemException("Size is not computed, yet.")

		return self._size

	# FIXME: @abstractmethod
	def ToTree(self) -> Node:
		"""
		Convert a filesystem element to a node in :mod:`pyTooling.Tree`.

		The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the filesystem element. Additional data
		will be stored in the node's key-value store.

		:returns: A tree's node referencing this filesystem element.
		"""
		raise NotImplementedError()


@export
class Element(Base, Generic[_ParentType]):
	"""
	Base-class for all named elements within a filesystem.

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

	.. hint::

	   Symbolic link sources are reverse references describing which symbolic links point to this element.
	"""
	_name:        str                   #: Name of the filesystem element.
	_parent:      _ParentType           #: Reference to the filesystem element's parent (:class:`Directory`)
	_linkSources: List["SymbolicLink"]  #: A list of symbolic links pointing to this filesystem element.

	def __init__(
		self,
		name:   str,
		size:   Nullable[int] = None,
		parent: Nullable[_ParentType] = None
	) -> None:
		"""
		Initialize the element base-class with name, size and parent reference.

		:param name:   Name of the element.
		:param size:   Optional size of the element.
		:param parent: Optional parent reference.
		"""
		root = None # FIXME: if parent is None else parent._root

		super().__init__(size, root)

		self._parent = parent
		self._name = name
		self._linkSources = []

	@property
	def Parent(self) -> _ParentType:
		"""
		Property to access the element's parent.

		:returns: Parent element.
		"""
		return self._parent

	@Parent.setter
	def Parent(self, value: _ParentType) -> None:
		self._parent = value

		if value._root is not None:
			self._root = value._root

	@readonly
	def Name(self) -> str:
		"""
		Read-only property to access the element's name.

		:returns: Element name.
		"""
		return self._name

	@readonly
	def Path(self) -> Path:
		raise NotImplemented(f"Property 'Path' is abstract.")

	def AddLinkSources(self, source: "SymbolicLink") -> None:
		"""
		Add a link source of a symbolic link to the named element (reverse reference).

		:param source: The referenced symbolic link.
		"""
		if not isinstance(source, SymbolicLink):
			ex = TypeError("Parameter 'source' is not of type 'SymbolicLink'.")
			ex.add_note(f"Got type '{getFullyQualifiedName(source)}'.")
			raise ex

		self._linkSources.append(source)


@export
class Directory(Element["Directory"]):
	"""
	A **directory** represents a directory in the filesystem, which contains subdirectories, regular files and symbolic links.

	While scanning for subelements, the directory is populated with elements. Every file object added, gets registered in
	the filesystems :class:`Root` for deduplication. In case a file identifier already exists, the found filename will
	reference the same file objects. In turn, the file objects has then references to multiple filenames (parents). This
	allows to detect :term:`hardlinks <hardlink>`.

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

	After scnaning the directory for subelements, certain directory properties get aggregated. The time needed for
	aggregation is provided via :data:`AggregateDuration`.
	"""

	_path:              Nullable[Path]             #: Cached :class:`~pathlib.Path` object of this directory.
	_subdirectories:    Dict[str, "Directory"]     #: Dictionary containing name-:class:`Directory` pairs.
	_files:             Dict[str, "Filename"]      #: Dictionary containing name-:class:`Filename` pairs.
	_symbolicLinks:     Dict[str, "SymbolicLink"]  #: Dictionary containing name-:class:`SymbolicLink` pairs.
	_collapsed:         bool                       #: True, if this directory was collapsed. It contains no subelements.
	_scanDuration:      Nullable[float]            #: Duration for scanning the directory and all its subelements.
	_aggregateDuration: Nullable[float]            #: Duration for aggregating all subelements.

	def __init__(
		self,
		name:                  str,
		collectSubdirectories: bool = False,
		parent:                Nullable["Directory"] = None
	) -> None:
		"""
		Initialize the directory with name and parent reference.

		:param name:                  Name of the element.
		:param collectSubdirectories: If true, collect subdirectory statistics.
		:param parent:                Optional parent reference.
		"""
		super().__init__(name, None, parent)

		self._path = None
		self._subdirectories = {}
		self._files = {}
		self._symbolicLinks = {}
		self._collapsed = False
		self._scanDuration = None
		self._aggregateDuration = None

		if parent is not None:
			parent._subdirectories[name] = self

			if parent._root is not None:
				self._root = parent._root

		if collectSubdirectories:
			self._collectSubdirectories()

	def _collectSubdirectories(self) -> None:
		"""
		Helper method for scanning subdirectories and aggregating found element sizes therein.
		"""
		with Stopwatch() as sw1:
			self._scanSubdirectories()

		with Stopwatch() as sw2:
			self._aggregateSizes()

		self._scanDuration = sw1.Duration
		self._aggregateDuration = sw2.Duration

	def _scanSubdirectories(self) -> None:
		"""
		Helper method for scanning subdirectories (recursively) and building a
		:class:`Directory`-:class:`Filename`-:class:`File` object tree.

		If a file refers to the same filesystem internal unique ID, a hardlink (two or more filenames) to the same file
		storage object is assumed.
		"""
		try:
			items = scandir(directoryPath := self.Path)
		except PermissionError as ex:
			return

		for dirEntry in items:
			if dirEntry.is_dir(follow_symlinks=False):
				subdirectory = Directory(dirEntry.name, collectSubdirectories=True, parent=self)
			elif dirEntry.is_file(follow_symlinks=False):
				id = dirEntry.inode()
				if id in self._root._ids:
					file = self._root._ids[id]

					hardLink = Filename(dirEntry.name, file=file, parent=self)
				else:
					s = dirEntry.stat(follow_symlinks=False)
					filename = Filename(dirEntry.name, parent=self)
					file = File(id, s.st_size, parent=filename)

					self._root._ids[id] = file
			elif dirEntry.is_symlink():
				target = Path(readlink(directoryPath / dirEntry.name))
				symlink = SymbolicLink(dirEntry.name, target, parent=self)
			else:
				raise FilesystemException(f"Unknown directory element.")

	def _connectSymbolicLinks(self) -> None:
		for dir in self._subdirectories.values():
			dir._connectSymbolicLinks()

		for link in self._symbolicLinks.values():
			if link._target.is_absolute():
				pass
			else:
				target = self
				for elem in link._target.parts:
					if elem == ".":
						continue
					elif elem == "..":
						target = target._parent
						continue

					try:
						target = target._subdirectories[elem]
						continue
					except KeyError:
						pass

					try:
						target = target._files[elem]
						continue
					except KeyError:
						pass

					try:
						target = target._symbolicLinks[elem]
						continue
					except KeyError:
						pass

				target.AddLinkSources(link)

	def _aggregateSizes(self) -> None:
		self._size = (
			sum(dir._size for dir in self._subdirectories.values()) +
			sum(file._file._size for file in self._files.values())
		)

	@Element.Root.setter
	def Root(self, value: "Root") -> None:
		Element.Root.fset(self, value)

		for subdir in self._subdirectories.values():
			subdir.Root = value

		for file in self._files.values():
			file.Root = value

		for link in self._symbolicLinks.values():
			link.Root = value

	@Element.Parent.setter
	def Parent(self, value: _ParentType) -> None:
		Element.Parent.fset(self, value)

		value._subdirectories[self._name] = self

		if isinstance(value, Root):
			self.Root = value

	@readonly
	def Count(self) -> int:
		"""
		Read-only property to access the number of elements in a directory.

		:returns: Number of files plus subdirectories.
		"""
		return len(self._subdirectories) + len(self._files) + len(self._symbolicLinks)

	@readonly
	def FileCount(self) -> int:
		"""
		Read-only property to access the number of files in a directory.

		.. hint::

		   Files include regular files and symbolic links.

		:returns: Number of files.
		"""
		return len(self._files) + len(self._symbolicLinks)

	@readonly
	def RegularFileCount(self) -> int:
		"""
		Read-only property to access the number of regular files in a directory.

		:returns: Number of regular files.
		"""
		return len(self._files)

	@readonly
	def SymbolicLinkCount(self) -> int:
		"""
		Read-only property to access the number of symbolic links in a directory.

		:returns: Number of symbolic links.
		"""
		return len(self._symbolicLinks)

	@readonly
	def SubdirectoryCount(self) -> int:
		"""
		Read-only property to access the number of subdirectories in a directory.

		:returns: Number of subdirectories.
		"""
		return len(self._subdirectories)

	@readonly
	def TotalFileCount(self) -> int:
		"""
		Read-only property to access the total number of files in all child hierarchy levels (recursively).

		.. hint::

		   Files include regular files and symbolic links.

		:returns: Total number of files.
		"""
		return sum(d.TotalFileCount for d in self._subdirectories.values()) + len(self._files) + len(self._symbolicLinks)

	@readonly
	def TotalRegularFileCount(self) -> int:
		"""
		Read-only property to access the total number of regular files in all child hierarchy levels (recursively).

		:returns: Total number of regular files.
		"""
		return sum(d.TotalRegularFileCount for d in self._subdirectories.values()) + len(self._files)

	@readonly
	def TotalSymbolicLinkCount(self) -> int:
		"""
		Read-only property to access the total number of symbolic links in all child hierarchy levels (recursively).

		:returns: Total number of symbolic links.
		"""
		return sum(d.TotalSymbolicLinkCount for d in self._subdirectories.values()) + len(self._symbolicLinks)

	@readonly
	def TotalSubdirectoryCount(self) -> int:
		"""
		Read-only property to access the total number of subdirectories in all child hierarchy levels (recursively).

		:returns: Total number of subdirectories.
		"""
		return len(self._subdirectories) + sum(d.TotalSubdirectoryCount for d in self._subdirectories.values())

	@readonly
	def Subdirectories(self) -> Generator["Directory", None, None]:
		"""
		Iterate all direct subdirectories of the directory.

		:returns: A generator to iterate all direct subdirectories.
		"""
		return (d for d in self._subdirectories.values())

	@readonly
	def Files(self) -> Generator[Union["Filename", "SymbolicLink"], None, None]:
		"""
		Iterate all direct files of the directory.

		.. hint::

		   Files include regular files and symbolic links.

		:returns: A generator to iterate all direct files.
		"""
		return (f for f in chain(self._files.values(), self._symbolicLinks.values()))

	@readonly
	def RegularFiles(self) -> Generator["Filename", None, None]:
		"""
		Iterate all direct regular files of the directory.

		:returns: A generator to iterate all direct regular files.
		"""
		return (f for f in self._files.values())

	@readonly
	def SymbolicLinks(self) -> Generator["SymbolicLink", None, None]:
		"""
		Iterate all direct symbolic links of the directory.

		:returns: A generator to iterate all direct symbolic links.
		"""
		return (l for l in self._symbolicLinks.values())

	@readonly
	def Path(self) -> Path:
		"""
		Read-only property to access the equivalent Path instance for accessing the represented directory.

		:returns:                    Path to the directory.
		:raises FilesystemException: If no parent is set.
		"""
		if self._path is not None:
			return self._path

		if self._parent is None:
			raise FilesystemException(f"No parent or root set for directory.")

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

	@readonly
	def ScanDuration(self) -> float:
		"""
		Read-only property to access the time needed to scan a directory structure including all subelements (recursively).

		:returns:                    The scan duration in seconds.
		:raises FilesystemException: If the directory was not scanned.
		"""
		if self._scanDuration is None:
			raise FilesystemException(f"Directory was not scanned, yet.")

		return self._scanDuration

	@readonly
	def AggregateDuration(self) -> float:
		"""
		Read-only property to access the time needed to aggregate the directory's and subelement's properties (recursively).

		:returns:                    The aggregation duration in seconds.
		:raises FilesystemException: If the directory properties were not aggregated.
		"""
		if self._scanDuration is None:
			raise FilesystemException(f"Directory properties were not aggregated, yet.")

		return self._aggregateDuration

	def Copy(self, parent: Nullable["Directory"] = None) -> "Directory":
		"""
		Copy the directory structure including all subelements and link it to the given parent.

		.. hint::

		   Statistics like aggregated directory size are copied too. |br|
		   There is no rescan or repeated aggregation needed.

		:param parent: The parent element of the copied directory.
		:returns:      A deep copy of the directory structure.
		"""
		dir = Directory(self._name, parent=parent)
		dir._size = self._size

		for subdir in self._subdirectories.values():
			subdir.Copy(dir)

		for file in self._files.values():
			file.Copy(dir)

		for link in self._symbolicLinks.values():
			link.Copy(dir)

		return dir

	def Collapse(self, func: Callable[["Directory"], bool]) -> bool:
		# if len(self._subdirectories) == 0 or all(subdir.Collapse(func) for subdir in self._subdirectories.values()):
		if len(self._subdirectories) == 0:
			if func(self):
				# print(f"collapse 1 {self.Path}")
				self._collapsed = True
				self._subdirectories.clear()
				self._files.clear()
				self._symbolicLinks.clear()

				return True
			else:
				return False

		# if all(subdir.Collapse(func) for subdir in self._subdirectories.values())
		collapsible = True
		for subdir in self._subdirectories.values():
			result = subdir.Collapse(func)
			collapsible = collapsible and result

		if collapsible:
			# print(f"collapse 2 {self.Path}")
			self._collapsed = True
			self._subdirectories.clear()
			self._files.clear()
			self._symbolicLinks.clear()

			return True
		else:
			return False

	def ToTree(self, format: Nullable[Callable[[Node], str]] = None) -> Node:
		"""
		Convert the directory to a :class:`~pyTooling.Tree.Node`.

		The node's :attr:`~pyTooling.Tree.Node.Value` field contains a reference to the directory. Additional data is
		attached to the node's key-value store:

		``kind``
		  The node's kind. See :class:`NodeKind`.
		``size``
		  The directory's aggregated size.

		:param format: A user defined formatting function for tree nodes.
		:returns:      A tree node representing this directory.
		"""
		if format is None:
			def format(node: Node) -> str:
				return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"

		directoryNode = Node(
			value=self,
			keyValuePairs={
				"kind": NodeKind.File,
				"size": self._size
			},
			format=format
		)
		directoryNode.AddChildren(
			e.ToTree(format) for e in chain(self._subdirectories.values())  #, self._files.values(), self._symbolicLinks.values())
		)

		return directoryNode

	def __eq__(self, other) -> bool:
		"""
		Compare two Directory instances for equality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both directories and all its subelements are equal.
		:raises TypeError: If parameter ``other`` is not of type :class:`Directory`.
		"""
		if not isinstance(other, Directory):
			ex = TypeError("Parameter 'other' is not of type Directory.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

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

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

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

		return True

	def __ne__(self, other: Any) -> bool:
		"""
		Compare two Directory instances for inequality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both directories and all its subelements are unequal.
		:raises TypeError: If parameter ``other`` is not of type :class:`Directory`.
		"""
		return not self.__eq__(other)

	def __repr__(self) -> str:
		return f"Directory: {self.Path}"

	def __str__(self) -> str:
		return self._name


@export
class Filename(Element[Directory]):
	"""
	Represents a filename in the filesystem, but not the file storage object (:class:`File`).

	.. hint::

	   Filename and file storage are represented by two classes, which allows multiple names (hard links) per file storage
	   object.
	"""
	_file: Nullable["File"]

	def __init__(
		self,
		name: str,
		file: Nullable["File"] = None,
		parent: Nullable[Directory] = None
	) -> None:
		"""
		Initialize the filename with name, file (storage) object and parent reference.

		:param name:   Name of the file.
		:param size:   Optional file (storage) object.
		:param parent: Optional parent reference.
		"""
		super().__init__(name, None, parent)

		if file is None:
			self._file = None
		else:
			self._file = file
			file._parents.append(self)

		if parent is not None:
			parent._files[name] = self

			if parent._root is not None:
				self._root = parent._root

	@Element.Root.setter
	def Root(self, value: "Root") -> None:
		self._root = value

		if self._file is not None:
			self._file._root = value

	@Element.Parent.setter
	def Parent(self, value: _ParentType) -> None:
		Element.Parent.fset(self, value)

		value._files[self._name] = self

		if isinstance(value, Root):
			self.Root = value

	@readonly
	def File(self) -> Nullable["File"]:
		return self._file

	@readonly
	def Size(self) -> int:
		if self._file is None:
			raise ToolingException(f"Filename isn't linked to a File object.")

		return self._file._size

	@readonly
	def Path(self) -> Path:
		if self._parent is None:
			raise ToolingException(f"Filename has no parent object.")

		return self._parent.Path / self._name

	def Copy(self, parent: Directory) -> "Filename":
		fileID = self._file._id

		if fileID in parent._root._ids:
			file = parent._root._ids[fileID]
		else:
			fileSize = self._file._size
			file = File(fileID, fileSize)

			parent._root._ids[fileID] = file

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

	def ToTree(self) -> Node:
		def format(node: Node) -> str:
			return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"

		fileNode = Node(
			value=self,
			keyValuePairs={
				"kind": NodeKind.File,
				"size": self._size
			},
			format=format
		)

		return fileNode

	def __eq__(self, other) -> bool:
		"""
		Compare two Filename instances for equality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both filenames are equal.
		:raises TypeError: If parameter ``other`` is not of type :class:`Filename`.
		"""
		if not isinstance(other, Filename):
			ex = TypeError("Parameter 'other' is not of type Filename.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

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

	def __ne__(self, other: Any) -> bool:
		"""
		Compare two Filename instances for inequality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both filenames are unequal.
		:raises TypeError: If parameter ``other`` is not of type :class:`Filename`.
		"""
		if not isinstance(other, Filename):
			ex = TypeError("Parameter 'other' is not of type Filename.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

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

	def __repr__(self) -> str:
		return f"File: {self.Path}"

	def __str__(self) -> str:
		return self._name


@export
class SymbolicLink(Element[Directory]):
	_target: Path

	def __init__(
		self,
		name:   str,
		target: Path,
		parent: Nullable[Directory]
	) -> None:
		super().__init__(name, None, parent)

		self._target = target

		if parent is not None:
			parent._symbolicLinks[name] = self

			if parent._root is not None:
				self._root = parent._root

	@readonly
	def Path(self) -> Path:
		return self._parent.Path / self._name

	@readonly
	def Target(self) -> Path:
		return self._target

	def Copy(self, parent: Directory) -> "SymbolicLink":
		return SymbolicLink(self._name, self._target, parent=parent)

	def ToTree(self) -> Node:
		def format(node: Node) -> str:
			return f"{node['size'] * 1e-6:7.1f} MiB {node._value.Name}"

		symbolicLinkNode = Node(
			value=self,
			keyValuePairs={
				"kind": NodeKind.SymbolicLink,
				"size": self._size
			},
			format=format
		)

		return symbolicLinkNode

	def __eq__(self, other) -> bool:
		"""
		Compare two SymbolicLink instances for equality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both symbolic links are equal.
		:raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`.
		"""
		if not isinstance(other, SymbolicLink):
			ex = TypeError("Parameter 'other' is not of type SymbolicLink.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

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

	def __ne__(self, other: Any) -> bool:
		"""
		Compare two SymbolicLink instances for inequality.

		:param other:      Parameter to compare against.
		:returns:          ``True``, if both symbolic links are unequal.
		:raises TypeError: If parameter ``other`` is not of type :class:`SymbolicLink`.
		"""
		if not isinstance(other, SymbolicLink):
			ex = TypeError("Parameter 'other' is not of type SymbolicLink.")
			ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
			raise ex

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

	def __repr__(self) -> str:
		return f"SymLink: {self.Path} -> {self._target}"

	def __str__(self) -> str:
		return self._name


@export
class Root(Directory):
	"""
	A **Root** represents the root-directory in the filesystem, which contains subdirectories, regular files and symbolic links.
	"""
	_ids:  Dict[int, "File"]   #: Dictionary of file identifier - file objects pairs found while scanning the directory structure.

	def __init__(
		self,
		rootDirectory:         Path,
		collectSubdirectories: bool = True
	) -> None:
		if rootDirectory is None:
			raise ValueError(f"Parameter 'rootDirectory' is None.")
		elif not isinstance(rootDirectory, Path):
			raise TypeError(f"Parameter 'rootDirectory' is not of type 'Path'.")
		elif not rootDirectory.exists():
			raise ToolingException(f"Path '{rootDirectory}' doesn't exist.") from FileNotFoundError(rootDirectory)

		self._ids = {}

		super().__init__(rootDirectory.name)
		self._root = self
		self._path = rootDirectory

		if collectSubdirectories:
			self._collectSubdirectories()
			self._connectSymbolicLinks()

	@readonly
	def TotalHardLinkCount(self) -> int:
		return sum(l for f in self._ids.values() if (l := len(f._parents)) > 1)

	@readonly
	def TotalHardLinkCount2(self) -> int:
		return sum(1 for f in self._ids.values() if len(f._parents) > 1)

	@readonly
	def TotalHardLinkCount3(self) -> int:
		return sum(1 for f in self._ids.values() if len(f._parents) == 1)

	@readonly
	def Size2(self) -> int:
		return sum(f._size for f in self._ids.values() if len(f._parents) > 1)

	@readonly
	def Size3(self) -> int:
		return sum(f._size * len(f._parents) for f in self._ids.values() if len(f._parents) > 1)

	@readonly
	def TotalUniqueFileCount(self) -> int:
		return len(self._ids)

	@readonly
	def Path(self) -> Path:
		"""
		Read-only property to access the path of the filesystem statistics root.

		:returns: Path to the root of the filesystem statistics root directory.
		"""
		return self._path

	def Copy(self) -> "Root":
		"""
		Copy the directory structure including all subelements and link it to the given parent.

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

		.. hint::

		   Statistics like aggregated directory size are copied too. |br|
		   There is no rescan or repeated aggregation needed.

		:returns: A deep copy of the directory structure.
		"""
		with Stopwatch() as sw:
			root = Root(self._path, False)
			root._size = self._size

			for subdir in self._subdirectories.values():
				subdir.Copy(root)

			for file in self._files.values():
				file.Copy(root)

			for link in self._symbolicLinks.values():
				link.Copy(root)

		root._scanDuration = sw.Duration
		root._aggregateDuration = 0.0

		return root

	def __repr__(self) -> str:
		return f"Root: {self.Path} (dirs: {self.TotalSubdirectoryCount}, files: {self.TotalRegularFileCount}, symlinks: {self.TotalSymbolicLinkCount})"

	def __str__(self) -> str:
		return self._name


@export
class File(Base):
	"""
	A **File** represents a file storage object in the filesystem, which is accessible by one or more :class:`Filename` objects.

	Each file has an internal id, which is associated to a unique ID within the host's filesystem.
	"""
	_id:      int             #: Unique (host internal) file object ID)
	_parents: List[Filename]  #: List of reverse references to :class:`Filename` objects.

	def __init__(
		self,
		id:     int,
		size:   int,
		parent: Nullable[Filename] = None
	) -> None:
		"""
		Initialize the File storage object with an ID, size and parent reference.

		:param id:     Unique ID of the file object.
		:param size:   Size of the file object.
		:param parent: Optional parent reference.
		"""
		if not isinstance(id, int):
			ex = TypeError("Parameter 'id' is not of type 'int'.")
			ex.add_note(f"Got type '{getFullyQualifiedName(id)}'.")
			raise ex

		self._id = id

		if parent is None:
			super().__init__(size, None)
			self._parents = []
		elif isinstance(parent, Filename):
			super().__init__(size, parent._root)
			self._parents = [parent]
			parent._file = self
		else:
			ex = TypeError("Parameter 'parent' is not of type 'Filename'.")
			ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
			raise ex

	@readonly
	def ID(self) -> int:
		"""
		Read-only property to access the file object's unique identifier.

		:returns: Unique file object identifier.
		"""
		return self._id

	@readonly
	def Parents(self) -> List[Filename]:
		"""
		Read-only property to access the list of filenames using the same file storage object.

		.. hint::

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

		:returns: List of filenames for the file storage object.
		"""
		return self._parents

	def AddParent(self, file: Filename) -> None:
		"""
		Add another parent reference to a :class:`Filename`.

		:param file: Reference to a filename object.
		"""
		if not isinstance(file, Filename):
			ex = TypeError("Parameter 'file' is not of type 'Filename'.")
			ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.")
			raise ex
		elif file._file is not None:
			raise ToolingException(f"Filename is already referencing an other file object ({file._file._id}).")

		self._parents.append(file)
		file._file = self

		if file._root is not None:
			self._root = file._root