file-tree-display 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- file_tree_display/__init__.py +33 -0
- file_tree_display/__main__.py +129 -0
- file_tree_display/__version__.py +1 -0
- file_tree_display/_constants.py +2 -0
- file_tree_display/ftd.py +325 -0
- file_tree_display-1.0.0.dist-info/METADATA +344 -0
- file_tree_display-1.0.0.dist-info/RECORD +10 -0
- file_tree_display-1.0.0.dist-info/WHEEL +4 -0
- file_tree_display-1.0.0.dist-info/entry_points.txt +2 -0
- file_tree_display-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""file-tree-display - A utility for generating a visually structured directory tree.
|
|
2
|
+
Copyright (c) 2025 Yaron Dayan
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
|
|
8
|
+
from nano_dev_utils.common import (
|
|
9
|
+
update,
|
|
10
|
+
str2file,
|
|
11
|
+
PredicateBuilder,
|
|
12
|
+
FilterSet,
|
|
13
|
+
load_cfg_file,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .ftd import FileTreeDisplay
|
|
17
|
+
from ._constants import PKG_NAME
|
|
18
|
+
|
|
19
|
+
filetree_display = FileTreeDisplay(root_dir=str(Path.cwd()))
|
|
20
|
+
predicate_builder = PredicateBuilder
|
|
21
|
+
|
|
22
|
+
__version__ = version(PKG_NAME)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'update',
|
|
26
|
+
'str2file',
|
|
27
|
+
'PredicateBuilder',
|
|
28
|
+
'predicate_builder',
|
|
29
|
+
'FilterSet',
|
|
30
|
+
'filetree_display',
|
|
31
|
+
'FileTreeDisplay',
|
|
32
|
+
'load_cfg_file',
|
|
33
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""CLI interface for FileTreeDisplay"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .ftd import FileTreeDisplay
|
|
11
|
+
from nano_dev_utils.common import load_cfg_file
|
|
12
|
+
from ._constants import DEFAULT_SFX
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse() -> argparse.Namespace:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description='Display or export a formatted file tree of a directory.',
|
|
18
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
parser.add_argument('--cfg', type=str, help='Path to JSON config file.')
|
|
22
|
+
parser.add_argument('--root-dir', '-r', type=str, help='Root directory to display.')
|
|
23
|
+
parser.add_argument('--filepath', '-o', type=str, help='Output file path.')
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
'--ignore-dirs', nargs='*', default=None, help='Directories to ignore.'
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
'--ignore-files', nargs='*', default=None, help='Files to ignore.'
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
'--include-dirs', nargs='*', default=None, help='Directories to include.'
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
'--include-files', nargs='*', default=None, help='Files to include.'
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
'--style',
|
|
38
|
+
'-s',
|
|
39
|
+
choices=['classic', 'dash', 'arrow', 'plus'],
|
|
40
|
+
default='classic',
|
|
41
|
+
help='Tree connector style.',
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
'--indent', '-i', type=int, default=2, help='Indent width per level.'
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
'--files-first',
|
|
48
|
+
'-f',
|
|
49
|
+
action='store_true',
|
|
50
|
+
default=False,
|
|
51
|
+
help='List files before directories.',
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
'--skip-sorting', action='store_true', default=False, help='Disable sorting.'
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
'--sort-key',
|
|
58
|
+
choices=['natural', 'lex', 'custom'],
|
|
59
|
+
default='natural',
|
|
60
|
+
help='Sorting mode.',
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
'--reverse', action='store_true', default=False, help='Reverse sort order.'
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
'--no-save', action='store_true', default=False, help='Do not save to file.'
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
'--printout',
|
|
70
|
+
'-p',
|
|
71
|
+
action='store_true',
|
|
72
|
+
default=False,
|
|
73
|
+
help='Print tree to stdout.',
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
'--version', '-v', action='version', version=f'{FileTreeDisplay.get_version()}'
|
|
77
|
+
)
|
|
78
|
+
return parser.parse_args()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def merge_config(
|
|
82
|
+
cli_args: argparse.Namespace, cfg_dict: dict[str, Any]
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Merge CLI args and config file into a unified options dict."""
|
|
85
|
+
merged = {**cfg_dict}
|
|
86
|
+
for k, v in vars(cli_args).items():
|
|
87
|
+
if v is not None and k != 'cfg':
|
|
88
|
+
merged[k] = v
|
|
89
|
+
return merged
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> None:
|
|
93
|
+
args = parse()
|
|
94
|
+
cfg_dict = load_cfg_file(args.cfg)
|
|
95
|
+
opts = merge_config(args, cfg_dict)
|
|
96
|
+
|
|
97
|
+
root_dir = Path(opts.get('root_dir') or Path.cwd())
|
|
98
|
+
if not root_dir.exists():
|
|
99
|
+
sys.exit(f"Error: root directory '{root_dir}' does not exist.")
|
|
100
|
+
|
|
101
|
+
filepath = opts.get('filepath')
|
|
102
|
+
if not filepath and not opts.get('no-save'):
|
|
103
|
+
filepath = str(root_dir.with_name(f'{root_dir.name}{DEFAULT_SFX}'))
|
|
104
|
+
|
|
105
|
+
ftd = FileTreeDisplay(
|
|
106
|
+
root_dir=str(root_dir),
|
|
107
|
+
filepath=filepath,
|
|
108
|
+
ignore_dirs=opts.get('ignore_dirs') or [],
|
|
109
|
+
ignore_files=opts.get('ignore_files') or [],
|
|
110
|
+
include_dirs=opts.get('include_dirs') or [],
|
|
111
|
+
include_files=opts.get('include_files') or [],
|
|
112
|
+
style=opts.get('style', 'classic'),
|
|
113
|
+
indent=int(opts.get('indent', 2)),
|
|
114
|
+
files_first=bool(opts.get('files_first', False)),
|
|
115
|
+
skip_sorting=bool(opts.get('skip_sorting', False)),
|
|
116
|
+
sort_key_name=opts.get('sort_key', 'natural'),
|
|
117
|
+
reverse=bool(opts.get('reverse', False)),
|
|
118
|
+
save2file=not opts.get('no_save', False),
|
|
119
|
+
printout=opts.get('printout', False),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
ftd.file_tree_display()
|
|
124
|
+
except Exception as e:
|
|
125
|
+
sys.exit(f'Error: {e}')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == '__main__':
|
|
129
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.0'
|
file_tree_display/ftd.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from collections.abc import Generator
|
|
6
|
+
from itertools import chain
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing_extensions import Callable, Any
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
12
|
+
from nano_dev_utils.common import str2file, FilterSet, PredicateBuilder
|
|
13
|
+
from ._constants import PKG_NAME
|
|
14
|
+
|
|
15
|
+
_NUM_SPLIT = re.compile(r'(\d+)').split
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileTreeDisplay:
|
|
19
|
+
"""Generate and display a visual file tree of a directory.
|
|
20
|
+
|
|
21
|
+
This class builds a directory tree structure and yields formatted
|
|
22
|
+
visual representations of directories and files.
|
|
23
|
+
Supports exclusion lists, configurable indentation, and custom prefix styles.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__slots__ = (
|
|
27
|
+
'root_path',
|
|
28
|
+
'filepath',
|
|
29
|
+
'ignore_dirs',
|
|
30
|
+
'ignore_files',
|
|
31
|
+
'include_dirs',
|
|
32
|
+
'include_files',
|
|
33
|
+
'style',
|
|
34
|
+
'indent',
|
|
35
|
+
'files_first',
|
|
36
|
+
'skip_sorting',
|
|
37
|
+
'sort_key_name',
|
|
38
|
+
'reverse',
|
|
39
|
+
'custom_sort',
|
|
40
|
+
'save2file',
|
|
41
|
+
'printout',
|
|
42
|
+
'style_dict',
|
|
43
|
+
'sort_keys',
|
|
44
|
+
'pb',
|
|
45
|
+
'dir_filter',
|
|
46
|
+
'file_filter',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
root_dir: str | None = None,
|
|
52
|
+
filepath: str | None = None,
|
|
53
|
+
ignore_dirs: FilterSet = None,
|
|
54
|
+
ignore_files: FilterSet = None,
|
|
55
|
+
include_dirs: FilterSet = None,
|
|
56
|
+
include_files: FilterSet = None,
|
|
57
|
+
style: str = 'classic',
|
|
58
|
+
indent: int = 2,
|
|
59
|
+
files_first: bool = False,
|
|
60
|
+
skip_sorting: bool = False,
|
|
61
|
+
sort_key_name: str = 'natural',
|
|
62
|
+
reverse: bool = False,
|
|
63
|
+
custom_sort: Callable[[str], Any] | None = None,
|
|
64
|
+
save2file: bool = True,
|
|
65
|
+
printout: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Initialize the FileTreeDisplay instance.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
root_dir (str): Root directory to traverse.
|
|
71
|
+
filepath: str | None: full output file path.
|
|
72
|
+
ignore_dirs (list[str] | set[str] | None): Directory names or patterns to ignore.
|
|
73
|
+
ignore_files (list[str] | set[str] | None): File names or patterns to ignore.
|
|
74
|
+
include_dirs (list[str] | set[str] | None): Directory names or patterns to include.
|
|
75
|
+
include_files (list[str] | set[str] | None): File names or patterns to include.
|
|
76
|
+
style (str): Character(s) used to represent hierarchy levels. Defaults to " ".
|
|
77
|
+
indent (int): Number of style characters used per hierarchy level. Defaults to 2.
|
|
78
|
+
files_first (bool): Determines whether to list files first. Defaults to False.
|
|
79
|
+
skip_sorting (bool): Skip sorting directly, even if configured.
|
|
80
|
+
sort_key_name (str): sorting key name, e.g. 'lex' for lexicographic or 'custom'. Defaults to 'natural'.
|
|
81
|
+
reverse (bool): reversed sorting.
|
|
82
|
+
custom_sort (Callable[[str], Any] | None):
|
|
83
|
+
save2file (bool): save file tree info to a file.
|
|
84
|
+
printout (bool): print file tree info.
|
|
85
|
+
"""
|
|
86
|
+
self.root_path = Path(root_dir) if root_dir else Path.cwd()
|
|
87
|
+
self.filepath = filepath
|
|
88
|
+
self.ignore_dirs = set(ignore_dirs or [])
|
|
89
|
+
self.ignore_files = set(ignore_files or [])
|
|
90
|
+
self.include_dirs = set(include_dirs or [])
|
|
91
|
+
self.include_files = set(include_files or [])
|
|
92
|
+
self.style = style
|
|
93
|
+
self.indent = indent
|
|
94
|
+
self.files_first = files_first
|
|
95
|
+
self.skip_sorting = skip_sorting
|
|
96
|
+
self.sort_key_name = sort_key_name
|
|
97
|
+
self.reverse = reverse
|
|
98
|
+
self.custom_sort = custom_sort
|
|
99
|
+
self.save2file = save2file
|
|
100
|
+
self.printout = printout
|
|
101
|
+
|
|
102
|
+
self.style_dict: dict = {
|
|
103
|
+
'classic': self.connector_styler('├── ', '└── '),
|
|
104
|
+
'dash': self.connector_styler('|-- ', '`-- '),
|
|
105
|
+
'arrow': self.connector_styler('├─> ', '└─> '),
|
|
106
|
+
'plus': self.connector_styler('+--- ', '\\--- '),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
self.sort_keys = {
|
|
110
|
+
'natural': self._nat_key,
|
|
111
|
+
'lex': self._lex_key,
|
|
112
|
+
'custom': self.custom_sort,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
self.pb = PredicateBuilder()
|
|
116
|
+
self.dir_filter = self.pb.build_predicate(self.include_dirs, self.ignore_dirs)
|
|
117
|
+
self.file_filter = self.pb.build_predicate(
|
|
118
|
+
self.include_files, self.ignore_files
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def get_version():
|
|
123
|
+
try:
|
|
124
|
+
return version(PKG_NAME)
|
|
125
|
+
except PackageNotFoundError:
|
|
126
|
+
return 'unknown'
|
|
127
|
+
|
|
128
|
+
def format_style(self) -> dict:
|
|
129
|
+
style, style_dict = self.style, self.style_dict
|
|
130
|
+
style_keys = list(style_dict.keys())
|
|
131
|
+
if style not in style_keys:
|
|
132
|
+
raise ValueError(f"'{style}' is invalid: must be one of {style_keys}\n")
|
|
133
|
+
return style_dict[style]
|
|
134
|
+
|
|
135
|
+
def init(self, *args, **kwargs) -> None:
|
|
136
|
+
self.__init__(*args, **kwargs)
|
|
137
|
+
|
|
138
|
+
def update_predicates(self) -> None:
|
|
139
|
+
self.dir_filter = self.pb.build_predicate(self.include_dirs, self.ignore_dirs)
|
|
140
|
+
self.file_filter = self.pb.build_predicate(
|
|
141
|
+
self.include_files, self.ignore_files
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _nat_key(name: str) -> list[int | str | Any]:
|
|
146
|
+
"""Natural sorting key"""
|
|
147
|
+
return [
|
|
148
|
+
int(part) if part.isdigit() else part.lower() for part in _NUM_SPLIT(name)
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _lex_key(name: str) -> str:
|
|
153
|
+
"""Lexicographic sorting key"""
|
|
154
|
+
return name.lower()
|
|
155
|
+
|
|
156
|
+
def _resolve_sort_key(self) -> Callable[[str], Any]:
|
|
157
|
+
sort_key_name, sort_keys = self.sort_key_name, self.sort_keys
|
|
158
|
+
key = sort_keys.get(sort_key_name)
|
|
159
|
+
if key is None:
|
|
160
|
+
if self.sort_key_name == 'custom':
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"custom_sort function must be specified when sort_key_name='custom'"
|
|
163
|
+
)
|
|
164
|
+
raise ValueError(
|
|
165
|
+
f'Invalid sort key name: "{sort_key_name}"! '
|
|
166
|
+
f'Currently defined keys are: {list(sort_keys.keys())}'
|
|
167
|
+
)
|
|
168
|
+
return key
|
|
169
|
+
|
|
170
|
+
def file_tree_display(self) -> str:
|
|
171
|
+
"""Generates a directory tree and saves it to a text file.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
str: The complete directory tree as a single CRLF-delimited string.
|
|
175
|
+
"""
|
|
176
|
+
root_path_str = str(self.root_path)
|
|
177
|
+
filepath = self.filepath
|
|
178
|
+
|
|
179
|
+
if not self.root_path.is_dir():
|
|
180
|
+
raise NotADirectoryError(f"The path '{root_path_str}' is not a directory.")
|
|
181
|
+
|
|
182
|
+
style = self.format_style()
|
|
183
|
+
sort_key = None if self.skip_sorting else self._resolve_sort_key()
|
|
184
|
+
dir_filter, file_filter = self.dir_filter, self.file_filter
|
|
185
|
+
files_first, reverse = self.files_first, self.reverse
|
|
186
|
+
indent = self.indent
|
|
187
|
+
|
|
188
|
+
iterator = self._build_tree(
|
|
189
|
+
root_path_str,
|
|
190
|
+
prefix='',
|
|
191
|
+
style=style,
|
|
192
|
+
sort_key=sort_key,
|
|
193
|
+
files_first=files_first,
|
|
194
|
+
dir_filter=dir_filter,
|
|
195
|
+
file_filter=file_filter,
|
|
196
|
+
reverse=reverse,
|
|
197
|
+
indent=indent,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
tree_info = self.get_tree_info(iterator)
|
|
201
|
+
|
|
202
|
+
if self.save2file and filepath:
|
|
203
|
+
str2file(tree_info, filepath)
|
|
204
|
+
|
|
205
|
+
if self.printout:
|
|
206
|
+
print(tree_info)
|
|
207
|
+
|
|
208
|
+
self.get_num_of_entries(tree_info)
|
|
209
|
+
|
|
210
|
+
return tree_info
|
|
211
|
+
|
|
212
|
+
def get_tree_info(self, iterator: Generator[str, None, None]) -> str:
|
|
213
|
+
buf = io.StringIO()
|
|
214
|
+
write = buf.write
|
|
215
|
+
write(f'{self.root_path.name}/\n')
|
|
216
|
+
buf.writelines(f'{line}\n' for line in iterator)
|
|
217
|
+
out = buf.getvalue()
|
|
218
|
+
return out[:-1] if out.endswith('\n') else out
|
|
219
|
+
|
|
220
|
+
def _build_tree(
|
|
221
|
+
self,
|
|
222
|
+
dir_path: str,
|
|
223
|
+
*,
|
|
224
|
+
prefix: str,
|
|
225
|
+
style: dict,
|
|
226
|
+
sort_key: Callable[[str], Any] | None,
|
|
227
|
+
files_first: bool,
|
|
228
|
+
dir_filter: Callable[[str], bool],
|
|
229
|
+
file_filter: Callable[[str], bool],
|
|
230
|
+
reverse: bool,
|
|
231
|
+
indent: int,
|
|
232
|
+
) -> Generator[str, None, None]:
|
|
233
|
+
"""Yields lines representing a formatted folder structure using a recursive DFS.
|
|
234
|
+
The internal recursive generator has runtime consts threaded through to avoid attrib. lookups.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
dir_path (str): The directory path or disk drive currently being traversed.
|
|
238
|
+
prefix (str): Hierarchical prefix applied to each level.
|
|
239
|
+
|
|
240
|
+
Yields:
|
|
241
|
+
str: A formatted text representation of the folder structure.
|
|
242
|
+
"""
|
|
243
|
+
branch = style['branch']
|
|
244
|
+
end = style['end']
|
|
245
|
+
vertical = style['vertical']
|
|
246
|
+
space = style['space']
|
|
247
|
+
|
|
248
|
+
recurse = self._build_tree
|
|
249
|
+
|
|
250
|
+
dirs: list[tuple[str, str]] = []
|
|
251
|
+
files: list[str] = []
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
with os.scandir(dir_path) as it:
|
|
255
|
+
append_dir = dirs.append
|
|
256
|
+
append_file = files.append
|
|
257
|
+
for entry in it:
|
|
258
|
+
name = entry.name
|
|
259
|
+
try:
|
|
260
|
+
is_dir = entry.is_dir(follow_symlinks=False)
|
|
261
|
+
except OSError:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
if is_dir:
|
|
265
|
+
if dir_filter(name):
|
|
266
|
+
append_dir((name, entry.path))
|
|
267
|
+
else:
|
|
268
|
+
if file_filter(name):
|
|
269
|
+
append_file(name)
|
|
270
|
+
|
|
271
|
+
except (PermissionError, OSError, FileNotFoundError) as e:
|
|
272
|
+
if isinstance(e, PermissionError):
|
|
273
|
+
yield '[Permission Denied]'
|
|
274
|
+
else:
|
|
275
|
+
yield '[Error reading directory]'
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
if sort_key:
|
|
279
|
+
dirs.sort(key=lambda d: sort_key(d[0]), reverse=reverse)
|
|
280
|
+
files.sort(key=sort_key, reverse=reverse)
|
|
281
|
+
|
|
282
|
+
# Compute combined sequence without extra temporary tuples where possible
|
|
283
|
+
f_iter: Iterable[tuple[str, None, bool]] = ((f, None, False) for f in files)
|
|
284
|
+
d_iter: Iterable[tuple[str, str, bool]] = ((d[0], d[1], True) for d in dirs)
|
|
285
|
+
seq: Iterable[tuple[str, str | None, bool]] = (
|
|
286
|
+
chain(f_iter, d_iter) if files_first else chain(d_iter, f_iter)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
combined = list(seq)
|
|
290
|
+
last_index = len(combined) - 1
|
|
291
|
+
|
|
292
|
+
for idx, (name, path, is_dir) in enumerate(combined):
|
|
293
|
+
is_last = idx == last_index
|
|
294
|
+
connector = end if is_last else branch
|
|
295
|
+
formatted_name = f'{name}/' if is_dir else name
|
|
296
|
+
yield f'{prefix}{connector}{formatted_name}'
|
|
297
|
+
extension = space if is_last else vertical
|
|
298
|
+
|
|
299
|
+
if is_dir and path:
|
|
300
|
+
yield from recurse(
|
|
301
|
+
path,
|
|
302
|
+
prefix=prefix + extension,
|
|
303
|
+
style=style,
|
|
304
|
+
sort_key=sort_key,
|
|
305
|
+
files_first=files_first,
|
|
306
|
+
dir_filter=dir_filter,
|
|
307
|
+
file_filter=file_filter,
|
|
308
|
+
reverse=reverse,
|
|
309
|
+
indent=indent,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def connector_styler(self, branch: str, end: str) -> dict:
|
|
313
|
+
indent = self.indent
|
|
314
|
+
return {
|
|
315
|
+
'space': ' ' * indent,
|
|
316
|
+
'vertical': f'│{" " * (indent - 1)}',
|
|
317
|
+
'branch': branch,
|
|
318
|
+
'end': end,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def get_num_of_entries(tree_info: str) -> None:
|
|
323
|
+
num_of_folders = tree_info.count('/') - 1
|
|
324
|
+
num_of_files = tree_info.count('\n') - num_of_folders
|
|
325
|
+
print(f'Scanned {num_of_folders} folders and {num_of_files} files.')
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: file_tree_display
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A utility for generating a visually structured directory tree.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yaronday/file_tree_display
|
|
6
|
+
Project-URL: Issues, https://github.com/yaronday/file_tree_display/issues
|
|
7
|
+
Project-URL: license, https://github.com/yaronday/file_tree_display/blob/master/LICENSE
|
|
8
|
+
Author-email: Yaron Dayan <yaronday77@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: build>=1.3.0
|
|
15
|
+
Requires-Dist: nano-dev-utils>=1.6.2
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# file_tree_display
|
|
19
|
+
|
|
20
|
+
A utility for generating a visually structured directory tree.
|
|
21
|
+
|
|
22
|
+
## Modules
|
|
23
|
+
|
|
24
|
+
## `file_tree_display.py`
|
|
25
|
+
|
|
26
|
+
This core module supports recursive traversal, customizable hierarchy styles, and inclusion / exclusion
|
|
27
|
+
patterns for directories and files.
|
|
28
|
+
Output can be displayed in the console or saved to a file.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#### Key Features
|
|
32
|
+
|
|
33
|
+
- Recursively displays and logs directory trees
|
|
34
|
+
- Efficient directory traversal
|
|
35
|
+
- Blazing fast (see Benchmarks below)
|
|
36
|
+
- Generates human-readable file tree structure
|
|
37
|
+
- Supports including / ignoring specific directories or files via pattern matching
|
|
38
|
+
- Customizable tree display output
|
|
39
|
+
- Optionally saves the resulting tree to a text file
|
|
40
|
+
- Lightweight, flexible and easily configurable
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Benchmarks
|
|
44
|
+
|
|
45
|
+
The measurements were carried out on unfiltered folders containing multiple files und subdirectories, using SSD.
|
|
46
|
+
Avg. time was measured over 20 runs per configuration, using `timeit` decorator I've implemented in this package.
|
|
47
|
+
|
|
48
|
+
Comparing FileTreeDisplay (FTD) with
|
|
49
|
+
[win_tree_wrapper](https://github.com/yaronday/nano_dev_utils/blob/master/benchmark/win_tree_wrapper.py)
|
|
50
|
+
(Windows [tree](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/tree)
|
|
51
|
+
wrapper which I've implemented for this purpose).
|
|
52
|
+
[Benchtest code](https://github.com/yaronday/nano_dev_utils/blob/master/benchmark/benchtest.py)
|
|
53
|
+
|
|
54
|
+
### Performance Comparison — FTD vs.`tree`
|
|
55
|
+
|
|
56
|
+
<table>
|
|
57
|
+
<tr><th>Test Context </th><th>Results</th></tr>
|
|
58
|
+
<tr><td>
|
|
59
|
+
|
|
60
|
+
| Metric | Test1 | Test2 |
|
|
61
|
+
|:---------------------|:-------|:-----------|
|
|
62
|
+
| **Files** | 10553 | 138492 |
|
|
63
|
+
| **Folders** | 1235 | 20428 |
|
|
64
|
+
| **Wrapper Overhead** | ~30 ms | negligible |
|
|
65
|
+
|
|
66
|
+
</td><td>
|
|
67
|
+
|
|
68
|
+
| Tool | T1 (s) | T2 (s) | Relative Speed |
|
|
69
|
+
|:---------|:------:|:------:|:--------------:|
|
|
70
|
+
| **FTD** | 0.196 | 2.900* | — |
|
|
71
|
+
| **tree** | 0.390 | 5.018 | ~2x slower |
|
|
72
|
+
|
|
73
|
+
</td></tr> </table>
|
|
74
|
+
|
|
75
|
+
***Without sorting** FTD takes 162 ms and 2.338 s for Test1 and Test2, respectively.
|
|
76
|
+
FTD is roughly **1.7x–2.4x faster** than the native `tree` binary across both datasets.
|
|
77
|
+
|
|
78
|
+
### Brief Analysis
|
|
79
|
+
|
|
80
|
+
### I. Linear scaling as a function of entries
|
|
81
|
+
FTD performance scales almost perfectly linearly with total entries:
|
|
82
|
+
|
|
83
|
+
* **T1:** 10 k files → 0.2 s
|
|
84
|
+
* **T2:** 138 k files → 2.9 s
|
|
85
|
+
→ ~14x more files → ~15x more runtime => expected by linearity.
|
|
86
|
+
|
|
87
|
+
### II. Figuring out why `tree` is nearly 2 times slower than my FTD
|
|
88
|
+
Although `tree` is implemented in C, it incurs more I/O work:
|
|
89
|
+
* Performs full `lstat()` on each entry (permissions, timestamps, etc.).
|
|
90
|
+
* Prints incrementally to `stdout` → many system calls (syscalls).
|
|
91
|
+
* Handles color / formatting output.
|
|
92
|
+
|
|
93
|
+
My FTD avoids this by:
|
|
94
|
+
|
|
95
|
+
* Using `os.scandir()` (caching stat info).
|
|
96
|
+
* Filtering and sorting in-memory.
|
|
97
|
+
* Buffering output before optional print/write.
|
|
98
|
+
|
|
99
|
+
Result: lower syscall count and fewer I/O stalls.
|
|
100
|
+
|
|
101
|
+
### III. Python overhead is clearly negligible
|
|
102
|
+
Even at 2.9 s for ~160K entries, throughput ~55K entries/s — close to filesystem limits on SSDs.
|
|
103
|
+
Measured wrapper overhead (~30 ms) is < 1 % of total runtime.
|
|
104
|
+
|
|
105
|
+
### Key Insights
|
|
106
|
+
|
|
107
|
+
| Observation | Explanation |
|
|
108
|
+
|:-------------------------------|:----------------------------------------------------|
|
|
109
|
+
| **FTD ~2x faster than `tree`** | Avoids per-file printing and redundant stats. |
|
|
110
|
+
| **I/O-bound execution** | Filesystem metadata fetch dominates total time. |
|
|
111
|
+
| **Linear runtime scaling** | Recursive generator design adds no hidden overhead. |
|
|
112
|
+
| **Stable memory footprint** | Uses streaming generators and `StringIO` buffering. |
|
|
113
|
+
|
|
114
|
+
### Conclusions
|
|
115
|
+
|
|
116
|
+
* **FTD outperforms `tree` by roughly 2x** on both moderate and large datasets.
|
|
117
|
+
* **Runtime scales linearly** with total directory entries.
|
|
118
|
+
* **Python layer overhead is negligible** — performance is bounded by kernel I/O.
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
#### Class Overview
|
|
122
|
+
|
|
123
|
+
**`FileTreeDisplay`**
|
|
124
|
+
Constructs and manages the visual representation of a folder structure of a path or of a disk drive.
|
|
125
|
+
|
|
126
|
+
**Initialization Parameters**
|
|
127
|
+
|
|
128
|
+
| Parameter | Type | Description |
|
|
129
|
+
|:--------------------------------------|:------------------------------------|:---------------------------------------------------------------------------------|
|
|
130
|
+
| `root_dir` | `str` | Path to the directory to scan. |
|
|
131
|
+
| `filepath` | `str / None` | Optional output destination for the saved file tree. |
|
|
132
|
+
| `ignore_dirs` | `list[str] or set[str] or None` | Directory names or patterns to skip. |
|
|
133
|
+
| `ignore_files` | `list[str] or set[str] or None` | File names or patterns to skip. |
|
|
134
|
+
| `include_dirs` | `list[str] or set[str] or None` | Only include specified folder names or patterns. |
|
|
135
|
+
| `include_files` | `list[str] or set[str] or None` | Only include specified file names or patterns, '*.pdf' - only include pdfs. |
|
|
136
|
+
| `style` | `str` | Characters used to mark hierarchy levels. Defaults to `'classic'`. |
|
|
137
|
+
| `indent` | `int` | Number of style characters per level. Defaults `2`. |
|
|
138
|
+
| `files_first` | `bool` | Determines whether to list files first. Defaults to False. |
|
|
139
|
+
| `skip_sorting` | `bool` | Skip sorting directly, even if configured. |
|
|
140
|
+
| `sort_key_name` | `str` | Sort key. Lexicographic ('lex') or 'custom'. Defaults to 'natural'. |
|
|
141
|
+
| `reverse` | `bool` | Reversed sorting order. |
|
|
142
|
+
| `custom_sort` | `Callable[[str], Any] / None` | Custom sort key function. |
|
|
143
|
+
| `title` | `str` | Custom title shown in the output. |
|
|
144
|
+
| `save2file` | `bool` | Save file tree (folder structure) info into a file. |
|
|
145
|
+
| `printout` | `bool` | Print file tree info. |
|
|
146
|
+
|
|
147
|
+
#### Core Methods
|
|
148
|
+
|
|
149
|
+
- `file_tree_display(save2file: bool = True) -> str | None`
|
|
150
|
+
Generates the directory tree. If `save2file=True`, saves the output; otherwise prints it directly.
|
|
151
|
+
|
|
152
|
+
- `_build_tree(dir_path, *, prefix, style, sort_key,
|
|
153
|
+
files_first, dir_filter, file_filter, reverse, indent) -> Generator[str, None, None]`
|
|
154
|
+
Recursively traverses the directory tree in depth-first order (DFS) and yields formatted lines representing the file and folder structure.
|
|
155
|
+
|
|
156
|
+
| Parameter | Type | Description |
|
|
157
|
+
|-------------------------------------|-------------------------|------------------------------------------------------------------------------|
|
|
158
|
+
| **`dir_path`** | `str` | Path to the directory being traversed. |
|
|
159
|
+
| **`prefix`** | `str` | Current indentation prefix for nested entries. |
|
|
160
|
+
| **`style`** | `dict[str, str]` | Connector style mapping with keys: `branch`, `end`, `space`, and `vertical`. |
|
|
161
|
+
| **`sort_key`** | `Callable[[str], Any]` | Function used to sort directory and file names. |
|
|
162
|
+
| **`files_first`** | `bool` | If `True`, list files before subdirectories. |
|
|
163
|
+
| **`dir_filter`**, **`file_filter`** | `Callable[[str], bool]` | Predicates to include or exclude directories and files. |
|
|
164
|
+
| **`reverse`** | `bool` | If `True`, reverses the sort order. |
|
|
165
|
+
| **`indent`** | `int` | Number of spaces (or repeated characters) per indentation level. |
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#### Example Usage
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
from nano_dev_utils.file_tree_display import FileTreeDisplay
|
|
173
|
+
|
|
174
|
+
root = r'c:/your_root_dir'
|
|
175
|
+
target_path = r'c:/your_target_path'
|
|
176
|
+
filename = 'filetree.md'
|
|
177
|
+
filepath = str(Path(target_path, filename))
|
|
178
|
+
|
|
179
|
+
ftd = FileTreeDisplay(root_dir=root,
|
|
180
|
+
ignore_dirs=['.git', 'node_modules', '.idea'],
|
|
181
|
+
ignore_files=['.gitignore', '*.toml'],
|
|
182
|
+
style='classic',
|
|
183
|
+
include_dirs=['src', 'tests', 'snapshots'],
|
|
184
|
+
filepath=filepath,
|
|
185
|
+
sort_key_name='custom',
|
|
186
|
+
custom_sort=(lambda x: any(ext in x.lower() for ext in ('jpg', 'png'))),
|
|
187
|
+
files_first=True,
|
|
188
|
+
reverse=True
|
|
189
|
+
)
|
|
190
|
+
ftd.file_tree_display()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Custom connector style
|
|
194
|
+
You can define and register your own connector styles at runtime by adding entries to style_dict:
|
|
195
|
+
|
|
196
|
+
```Python
|
|
197
|
+
from nano_dev_utils.file_tree_display import FileTreeDisplay
|
|
198
|
+
ftd = FileTreeDisplay(root_dir=".")
|
|
199
|
+
ftd.style_dict["plus2"] = ftd.connector_styler("+-- ", "+== ")
|
|
200
|
+
ftd.style = "plus2"
|
|
201
|
+
ftd.printout = True
|
|
202
|
+
ftd.file_tree_display()
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
#### Error Handling
|
|
207
|
+
|
|
208
|
+
The module raises well-defined exceptions for common issues:
|
|
209
|
+
|
|
210
|
+
- `NotADirectoryError` when the path is not a directory
|
|
211
|
+
- `PermissionError` for unreadable directories or write-protected files
|
|
212
|
+
- `OSError` for general I/O or write failures
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
## FTD CLI — File Tree Display
|
|
216
|
+
|
|
217
|
+
The **FTD CLI** (`ftd`) is a lightweight, configurable command-line tool for displaying or exporting a formatted file tree of a given directory.
|
|
218
|
+
It’s designed for flexibility, allowing users to control inclusion/exclusion rules, visual style, sorting behavior, and output options.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### Features
|
|
223
|
+
|
|
224
|
+
* Generate a clean, formatted file tree of any directory
|
|
225
|
+
* Choose from multiple connector styles (`classic`, `dash`, `arrow`, `plus`)
|
|
226
|
+
* Fine-grained control over included/excluded files and directories
|
|
227
|
+
* Flexible sorting modes (`natural`, `lex`, or `custom`)
|
|
228
|
+
* Optionally export the tree to a file or print directly to stdout
|
|
229
|
+
* Supports configuration via JSON config file
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Usage
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
ftd [OPTIONS]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
ftd --root-dir ./src --style arrow --files-first --printout
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
This displays the tree structure of the `src` directory using the *arrow* connector style, listing files before directories, and prints the output to the console.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### Command-Line Options
|
|
250
|
+
|
|
251
|
+
| Option | Alias | Type | Description | Default |
|
|
252
|
+
|-------------------|-------|--------|------------------------------------------------------------|-----------|
|
|
253
|
+
| `--cfg` | – | `str` | Path to a JSON configuration file. | – |
|
|
254
|
+
| `--root-dir` | `-r` | `str` | Root directory to display. | – |
|
|
255
|
+
| `--filepath` | `-o` | `str` | Output file path for exported tree. | – |
|
|
256
|
+
| `--ignore-dirs` | – | `str…` | Directories to exclude from the tree. | – |
|
|
257
|
+
| `--ignore-files` | – | `str…` | Files to exclude from the tree. | – |
|
|
258
|
+
| `--include-dirs` | – | `str…` | Directories to explicitly include. | – |
|
|
259
|
+
| `--include-files` | – | `str…` | Files to explicitly include. | – |
|
|
260
|
+
| `--style` | `-s` | `str` | Tree connector style (`classic`, `dash`, `arrow`, `plus`). | `classic` |
|
|
261
|
+
| `--indent` | `-i` | `int` | Indent width (spaces) per directory level. | `2` |
|
|
262
|
+
| `--files-first` | `-f` | flag | List files before directories. | `False` |
|
|
263
|
+
| `--skip-sorting` | – | flag | Disable sorting altogether. | `False` |
|
|
264
|
+
| `--sort-key` | – | `str` | Sorting key: `natural`, `lex`, or `custom`. | `natural` |
|
|
265
|
+
| `--reverse` | – | flag | Reverse the sort order. | `False` |
|
|
266
|
+
| `--no-save` | – | flag | Do not save output to file (useful with `--printout`). | `False` |
|
|
267
|
+
| `--printout` | `-p` | flag | Print the generated tree to stdout. | `False` |
|
|
268
|
+
| `--version` | `-v` | flag | Show the current version of FTD and exit. | – |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### Configuration via JSON file
|
|
273
|
+
|
|
274
|
+
FTD can also load parameters from a JSON config file:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"root_dir": "./project",
|
|
279
|
+
"ignore_dirs": ["__pycache__", "build"],
|
|
280
|
+
"ignore_files": [".DS_Store"],
|
|
281
|
+
"style": "dash",
|
|
282
|
+
"files_first": true,
|
|
283
|
+
"printout": true
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Run it using:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
ftd --cfg path/to/config.json
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Command-line arguments always **override** config file values.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### Examples
|
|
298
|
+
|
|
299
|
+
Display the current directory with default settings:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
ftd -r .
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Display a tree with dashed connectors, ignoring test folders, and print it:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
ftd -r ./src --ignore-dirs tests --style dash --printout
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Export a formatted tree to a file:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
ftd -r ./app --filepath tree.txt
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Reverse lexical sort with 4-space indentation:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
ftd -r ./data --sort-key lex --reverse --indent 4
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### 🧾 Output Example
|
|
326
|
+
|
|
327
|
+
```text
|
|
328
|
+
src
|
|
329
|
+
├── main.py
|
|
330
|
+
├── utils
|
|
331
|
+
│ ├── helpers.py
|
|
332
|
+
│ └── validators.py
|
|
333
|
+
└── tests
|
|
334
|
+
├── test_main.py
|
|
335
|
+
└── test_utils.py
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
***
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
This project is licensed under the MIT License.
|
|
344
|
+
See [LICENSE](https://github.com/yaronday/file_tree_display/blob/master/LICENSE) for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
file_tree_display/__init__.py,sha256=jkyGGykmiDBixkjDQD2N1p4cXQzbU8YlpmyiY4tUOyg,718
|
|
2
|
+
file_tree_display/__main__.py,sha256=o74jt4m-8ycgmmKELMGXwdQDnQBJtx9ymHgJCI4l-9Y,4160
|
|
3
|
+
file_tree_display/__version__.py,sha256=crlYURA_2Ujkpz7iVPHovvxZRtvt4QBXF5AAFJzRfz0,23
|
|
4
|
+
file_tree_display/_constants.py,sha256=C4oom9OD9QrJ6wW53ULhmuHLCIKw9pmXkHjLrjOcMAU,63
|
|
5
|
+
file_tree_display/ftd.py,sha256=T8OJXpry9BgcTlfg5MPGb-yM_SpKHZgluG2OwElC2ys,11764
|
|
6
|
+
file_tree_display-1.0.0.dist-info/METADATA,sha256=8FdkRn1OwNq2WukopnNQvDOSsI03L0SImi5lUacyU6o,15505
|
|
7
|
+
file_tree_display-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
file_tree_display-1.0.0.dist-info/entry_points.txt,sha256=L_gNQlsvydWniPChrLbA6a36_5-YuxVqMq9i4a6cEU8,56
|
|
9
|
+
file_tree_display-1.0.0.dist-info/licenses/LICENSE,sha256=Muenl7Bw_LdtHZtlOMAP7Kt97gDCq8WWp2605eDWhHU,1089
|
|
10
|
+
file_tree_display-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yaron Dayan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|