nano-dev-utils 1.4.0__py3-none-any.whl → 1.5.2__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.

Potentially problematic release.


This version of nano-dev-utils might be problematic. Click here for more details.

nano_dev_utils/timers.py CHANGED
@@ -21,11 +21,20 @@ lgr = logging.getLogger(__name__)
21
21
  P = ParamSpec('P')
22
22
  R = TypeVar('R')
23
23
 
24
+ NS_IN_US = 1_000
25
+ NS_IN_MS = 1_000_000
26
+ NS_IN_SEC = 1_000_000_000
27
+ NS_IN_MIN = 60 * NS_IN_SEC
28
+ NS_IN_HOUR = 60 * NS_IN_MIN
29
+
24
30
 
25
31
  class Timer:
26
- def __init__(self, precision: int = 4, verbose: bool = False):
32
+ def __init__(
33
+ self, precision: int = 4, verbose: bool = False, printout: bool = False
34
+ ):
27
35
  self.precision = precision
28
36
  self.verbose = verbose
37
+ self.printout = printout
29
38
 
30
39
  def init(self, *args, **kwargs) -> None:
31
40
  self.__init__(*args, **kwargs)
@@ -33,6 +42,9 @@ class Timer:
33
42
  def update(self, attrs: dict[str, Any]) -> None:
34
43
  update(self, attrs)
35
44
 
45
+ def res_formatter(self, elapsed_ns: float, *, precision: int = 4) -> str:
46
+ return self._duration_formatter(elapsed_ns, precision=precision)
47
+
36
48
  def timeit(
37
49
  self,
38
50
  iterations: int = 1,
@@ -42,9 +54,9 @@ class Timer:
42
54
  """Decorator that measures execution time for sync / async functions.
43
55
 
44
56
  Args:
45
- iterations: Number of times to run the function (averaged for reporting).
46
- timeout: Optional max allowed time (in seconds); raises TimeoutError if exceeded.
47
- per_iteration: If True, enforces timeout per iteration, else cumulatively.
57
+ iterations: int: Number of times to run the function (averaged for reporting).
58
+ timeout: float: Optional max allowed time (in seconds); raises TimeoutError if exceeded.
59
+ per_iteration: bool: If True, enforces timeout per iteration, else cumulatively.
48
60
 
49
61
  Returns:
50
62
  A decorated function that behaves identically to the original, with timing logged.
@@ -53,12 +65,14 @@ class Timer:
53
65
  RP = ParamSpec('RP')
54
66
  RR = TypeVar('RR')
55
67
 
56
- precision = self.precision
68
+ precision, verbose, printout = self.precision, self.verbose, self.printout
69
+ check_timeout = self._check_timeout
70
+ duration_formatter = self._duration_formatter
71
+ format_timing_msg = self._format_timing_msg
57
72
 
58
73
  def decorator(
59
74
  func: Callable[RP, RR] | Callable[RP, Awaitable[RR]],
60
75
  ) -> Callable[RP, Any]:
61
- verbose = self.verbose
62
76
  if inspect.iscoroutinefunction(func):
63
77
  async_func = cast(Callable[RP, Awaitable[RR]], func)
64
78
 
@@ -73,21 +87,24 @@ class Timer:
73
87
  duration_ns = time.perf_counter_ns() - start_ns
74
88
  total_elapsed_ns += duration_ns
75
89
 
76
- self._check_timeout(
90
+ check_timeout(
77
91
  func_name,
78
92
  i,
79
93
  duration_ns,
80
94
  total_elapsed_ns,
81
95
  timeout,
82
96
  per_iteration,
97
+ precision,
83
98
  )
84
99
  avg_elapsed_ns = total_elapsed_ns / iterations
85
- duration_str = self._duration_formatter(avg_elapsed_ns, precision)
100
+ duration_str = duration_formatter(avg_elapsed_ns, precision)
86
101
 
87
- msg = self._formatted_msg(
102
+ msg = format_timing_msg(
88
103
  func_name, args, kwargs, duration_str, iterations, verbose
89
104
  )
90
105
  lgr.info(msg)
106
+ if printout:
107
+ print(msg)
91
108
  return cast(RR, result)
92
109
 
93
110
  return cast(Callable[RP, Awaitable[RR]], async_wrapper)
@@ -104,92 +121,94 @@ class Timer:
104
121
  result = sync_func(*args, **kwargs)
105
122
  duration_ns = time.perf_counter_ns() - start_ns
106
123
  total_elapsed_ns += duration_ns
107
- self._check_timeout(
124
+ check_timeout(
108
125
  func_name,
109
126
  i,
110
127
  duration_ns,
111
128
  total_elapsed_ns,
112
129
  timeout,
113
130
  per_iteration,
131
+ precision,
114
132
  )
115
133
  avg_elapsed_ns = total_elapsed_ns / iterations
116
- duration_str = self._duration_formatter(avg_elapsed_ns, precision)
117
- msg = self._formatted_msg(
134
+ duration_str = duration_formatter(avg_elapsed_ns, precision)
135
+ msg = format_timing_msg(
118
136
  func_name, args, kwargs, duration_str, iterations, verbose
119
137
  )
120
138
  lgr.info(msg)
139
+ if printout:
140
+ print(msg)
121
141
  return cast(RR, result)
122
142
 
123
143
  return cast(Callable[RP, RR], sync_wrapper)
124
144
 
125
145
  return decorator
126
146
 
147
+ @staticmethod
127
148
  def _check_timeout(
128
- self,
129
149
  func_name: str,
130
150
  i: int,
131
151
  duration_ns: float,
132
152
  total_elapsed_ns: float,
133
153
  timeout: float | None,
134
154
  per_iteration: bool,
155
+ precision,
135
156
  ) -> None:
136
157
  """Raise TimeoutError if timeout is exceeded."""
137
158
  if timeout is None:
138
159
  return
139
- precision = self.precision
140
- timeout_exceeded = f'{func_name} exceeded {timeout:.{precision}f}s'
160
+
161
+ timeout_exceeded = f'{func_name} exceeded {timeout:.{precision}f} s'
141
162
  if per_iteration:
142
- duration_s = duration_ns / 1e9
163
+ duration_s = duration_ns / NS_IN_SEC
143
164
  if duration_s > timeout:
144
165
  raise TimeoutError(
145
166
  f'{timeout_exceeded} on iteration {i} '
146
- f'(took {duration_s:.{precision}f}s)'
167
+ f'(took {duration_s:.{precision}f} s)'
147
168
  )
148
169
  else:
149
- total_duration_s = total_elapsed_ns / 1e9
170
+ total_duration_s = total_elapsed_ns / NS_IN_SEC
150
171
  if total_duration_s > timeout:
151
172
  raise TimeoutError(
152
173
  f'{timeout_exceeded} after {i} iterations '
153
- f'(took {total_duration_s:.{precision}f}s)'
174
+ f'(took {total_duration_s:.{precision}f} s)'
154
175
  )
155
176
 
156
177
  @staticmethod
157
178
  def _duration_formatter(elapsed_ns: float, precision: int = 4) -> str:
158
- """Convert nanoseconds to the appropriate time unit, supporting multi-unit results."""
159
- ns_sec, ns_min, ns_hour = 1e9, 6e10, 3.6e12
160
- ns_ms, ns_us = 1e6, 1e3
161
-
162
- if elapsed_ns < ns_sec:
163
- if elapsed_ns >= ns_ms:
164
- return f'{elapsed_ns / ns_ms:.{precision}f}ms'
165
- elif elapsed_ns >= ns_us:
166
- return f'{elapsed_ns / ns_us:.{precision}f}μs'
167
- return f'{elapsed_ns:.2f}ns'
168
-
169
- if elapsed_ns < ns_min:
170
- seconds = elapsed_ns / ns_sec
171
- return f'{seconds:.1f}s' if seconds < 10 else f'{seconds:.0f}s'
172
-
173
- if elapsed_ns >= ns_hour:
174
- hours = int(elapsed_ns / ns_hour)
175
- rem = elapsed_ns % ns_hour
176
- mins = int(rem / ns_min)
177
- secs = int((rem % ns_min) / ns_sec)
178
-
179
- parts = [f'{hours}h']
179
+ """Format a duration [ns] into the most appropriate time unit.
180
+
181
+ Converts ns into a human-readable string with adaptive precision.
182
+ Handles ns, μs, ms, s, m, and h, combining units where meaningful.
183
+ """
184
+ if elapsed_ns < NS_IN_SEC:
185
+ if elapsed_ns >= NS_IN_MS:
186
+ return f'{elapsed_ns / NS_IN_MS:.{precision}f} ms'
187
+ if elapsed_ns >= NS_IN_US:
188
+ return f'{elapsed_ns / NS_IN_US:.{precision}f} μs'
189
+ return f'{elapsed_ns:.{precision}f} ns'
190
+
191
+ if elapsed_ns < NS_IN_MIN:
192
+ return f'{elapsed_ns / NS_IN_SEC:.{precision}f} s'
193
+
194
+ if elapsed_ns >= NS_IN_HOUR:
195
+ hours, rem = divmod(elapsed_ns, NS_IN_HOUR)
196
+ mins, rem = divmod(rem, NS_IN_MIN)
197
+ secs = rem // NS_IN_SEC
198
+
199
+ parts = [f'{int(hours)} h']
180
200
  if mins:
181
- parts.append(f'{mins}m')
201
+ parts.append(f'{int(mins)} m')
182
202
  if secs:
183
- parts.append(f'{secs}s')
203
+ parts.append(f'{int(secs)} s')
184
204
  return ' '.join(parts)
185
205
 
186
- else:
187
- minutes = int(elapsed_ns / ns_min)
188
- seconds = int((elapsed_ns % ns_min) / ns_sec)
189
- return f'{minutes}m {seconds}s' if seconds else f'{minutes}m'
206
+ mins, rem = divmod(elapsed_ns, NS_IN_MIN)
207
+ secs = rem // NS_IN_SEC
208
+ return f'{int(mins)} m {int(secs)} s' if secs else f'{int(mins)} m'
190
209
 
191
210
  @staticmethod
192
- def _formatted_msg(
211
+ def _format_timing_msg(
193
212
  func_name: str,
194
213
  args: tuple,
195
214
  kwargs: dict,
@@ -197,6 +216,21 @@ class Timer:
197
216
  iterations: int,
198
217
  verbose: bool,
199
218
  ) -> str:
200
- extra_info = f'{args} {kwargs} ' if verbose else ''
219
+ """Formats a concise timing message for a decorated function call.
220
+
221
+ Args:
222
+ func_name (str): Name of the function being measured.
223
+ args (tuple): Positional arguments passed to the function.
224
+ kwargs (dict): Keyword arguments passed to the function.
225
+ duration_str (str): Formatted duration string (already unit-scaled).
226
+ iterations (int): Number of timing iterations used for averaging.
227
+ verbose (bool): Whether to include function arguments in the message.
228
+
229
+ Returns:
230
+ str: A formatted summary string, for example:
231
+ 'process_data took 12.31 ms (avg. over 10 runs)'
232
+ """
233
+
234
+ extra_info = f'{args!r} {kwargs!r} ' if verbose else ''
201
235
  iter_info = f' (avg. over {iterations} runs)' if iterations > 1 else ''
202
236
  return f'{func_name} {extra_info}took {duration_str}{iter_info}'
@@ -0,0 +1,372 @@
1
+ Metadata-Version: 2.4
2
+ Name: nano_dev_utils
3
+ Version: 1.5.2
4
+ Summary: A collection of small Python utilities for developers.
5
+ Project-URL: Homepage, https://github.com/yaronday/nano_utils
6
+ Project-URL: Issues, https://github.com/yaronday/nano_utils/issues
7
+ Project-URL: license, https://github.com/yaronday/nano_dev_utils/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
+ Description-Content-Type: text/markdown
16
+
17
+ # nano_dev_utils
18
+
19
+ A collection of small Python utilities for developers.
20
+ [PYPI package: nano-dev-utils](https://pypi.org/project/nano-dev-utils)
21
+
22
+ ## Modules
23
+
24
+ ### `timers.py`
25
+
26
+ This module provides a `Timer` class for measuring the execution time of code blocks and functions with additional features like timeout control and multi-iteration averaging.
27
+
28
+ #### `Timer` Class
29
+
30
+ * **`__init__(self, precision: int = 4, verbose: bool = False, printout: bool = False)`**: Initializes a `Timer` instance.
31
+ * `precision`: The number of decimal places to record and display time durations. Defaults to 4.
32
+ * `verbose`: Optionally displays the function's positional arguments (args) and keyword arguments (kwargs). Defaults to `False`.
33
+ * `printout`: Allows printing to console.
34
+
35
+ * **`def timeit(
36
+ self,
37
+ iterations: int = 1,
38
+ timeout: float | None = None,
39
+ per_iteration: bool = False,
40
+ ) -> Callable[[Callable[P, Any]], Callable[P, Any]]:`**:
41
+ Decorator that times either **sync** or **async** function execution with advanced features:
42
+ * `iterations`: Number of times to run the function (for averaging). Defaults to 1.
43
+ * `timeout`: Maximum allowed execution time in seconds. When exceeded:
44
+ * Raises `TimeoutError` immediately
45
+ * **Warning:** The function execution will be aborted mid-operation
46
+ * No return value will be available if timeout occurs
47
+ * `per_iteration`: If True, applies timeout check to each iteration; otherwise checks total time across all iterations.
48
+ * Features:
49
+ * Records execution times
50
+ * Handles timeout conditions
51
+ * Calculates average execution time across iterations
52
+ * Logs the function name and execution time (with optional arguments)
53
+ * Returns the result of the original function (unless timeout occurs)
54
+
55
+ #### Example Usage:
56
+
57
+ ```python
58
+ import time
59
+ import logging
60
+ from nano_dev_utils import timer
61
+
62
+ # if printout is not enabled, a logger must be configured in order to see timing results
63
+ logging.basicConfig(filename='timer example.log',
64
+ level=logging.INFO, # DEBUG, WARNING, ERROR, CRITICAL
65
+ format='%(asctime)s - %(levelname)s: %(message)s',
66
+ datefmt='%d-%m-%Y %H:%M:%S')
67
+
68
+ # Basic timing
69
+ @timer.timeit()
70
+ def my_function(a, b=10):
71
+ """A sample function."""
72
+ time.sleep(0.1)
73
+ return a + b
74
+
75
+ timer.init(precision=6, verbose=True)
76
+ '''Alternative options:
77
+ timer.update({'precision': 6, 'verbose': True}) # 1. Using update method
78
+
79
+ from nano_dev_utils.timers import Timer # 2. explicit instantiation
80
+ timer = Timer(precision=6, verbose=True)
81
+ '''
82
+
83
+ timer.update({'printout': True}) # allow printing to console
84
+
85
+ # Advanced usage with timeout and iterations
86
+ @timer.timeit(iterations=5, timeout=0.5, per_iteration=True)
87
+ def critical_function(x):
88
+ """Function with timeout check per iteration."""
89
+ time.sleep(0.08)
90
+ return x * 2
91
+
92
+ result1 = my_function(5, b=20) # Shows args/kwargs and timing
93
+ result2 = critical_function(10) # Runs 5 times with per-iteration timeout
94
+ ```
95
+
96
+ ### `dynamic_importer.py`
97
+
98
+ This module provides an `Importer` class for lazy loading and caching module imports.
99
+
100
+ #### `Importer` Class
101
+
102
+ * **`__init__(self)`**: Initializes an `Importer` instance with an empty dictionary `imported_modules` to cache imported modules.
103
+
104
+ * **`import_mod_from_lib(self, library: str, module_name: str) -> ModuleType | Any`**: Lazily imports a module from a specified library and caches it.
105
+ * `library` (str): The name of the library (e.g., "os", "requests").
106
+ * `module_name` (str): The name of the module to import within the library (e.g., "path", "get").
107
+ * Returns the imported module. If the module has already been imported, it returns a cached instance.
108
+ * Raises `ImportError` if the module cannot be found.
109
+
110
+ #### Example Usage:
111
+
112
+ ```python
113
+ from nano_dev_utils import importer
114
+
115
+ os_path = importer.import_mod_from_lib("os", "path")
116
+ print(f"Imported os.path: {os_path}")
117
+
118
+ requests_get = importer.import_mod_from_lib("requests", "get")
119
+ print(f"Imported requests.get: {requests_get}")
120
+
121
+ # Subsequent calls will return the cached module
122
+ os_path_again = importer.import_mod_from_lib("os", "path")
123
+ print(f"Imported os.path again (cached): {os_path_again}")
124
+ ```
125
+
126
+ ### `release_ports.py`
127
+
128
+ This module provides a `PortsRelease` class to identify and release processes
129
+ listening on specified TCP ports.
130
+ It supports Windows, Linux, and macOS.
131
+
132
+ #### `PortsRelease` Class
133
+
134
+ * **`__init__(self, default_ports: list[int] | None = None)`**:
135
+ * Initializes a `PortsRelease` instance.
136
+ * `default_ports`: A list of default ports to manage. If not provided, it defaults to `[6277, 6274]`.
137
+
138
+ * **`get_pid_by_port(self, port: int) -> int | None`**: A static method that attempts to find
139
+ a process ID (PID) listening on a given `port`.
140
+ * It uses platform-specific commands (`netstat`, `ss`, `lsof`).
141
+ * Returns the PID if found, otherwise `None`.
142
+
143
+ * **`kill_process(self, pid: int) -> bool`**: A static method that attempts to kill the process
144
+ with the given `pid`.
145
+ * It uses platform-specific commands (`taskkill`, `kill -9`).
146
+ * Returns `True` if the process was successfully killed, `False` otherwise.
147
+
148
+ * **`release_all(self, ports: list[int] | None = None) -> None`**: Releases all processes listening on the specified `ports`.
149
+ * `ports`: A list of ports to release.
150
+ * If `None`, it uses the `default_ports` defined during initialization.
151
+ * For each port, it first tries to get the PID and then attempts to kill the process.
152
+ * It logs the actions and any errors encountered. Invalid port numbers in the provided list are skipped.
153
+
154
+ #### Example Usage:
155
+
156
+ ```python
157
+ import logging
158
+ from nano_dev_utils import ports_release, PortsRelease
159
+
160
+
161
+ logging.basicConfig(filename='port release.log',
162
+ level=logging.INFO, # DEBUG, WARNING, ERROR, CRITICAL
163
+ format='%(asctime)s - %(levelname)s: %(message)s',
164
+ datefmt='%d-%m-%Y %H:%M:%S')
165
+
166
+
167
+ ports_release.release_all()
168
+
169
+ # Create an instance with custom ports
170
+ custom_ports_releaser = PortsRelease(default_ports=[8080, 9000, 6274])
171
+ custom_ports_releaser.release_all(ports=[8080, 9000])
172
+
173
+ # Release only the default ports
174
+ ports_release.release_all()
175
+ ```
176
+
177
+ ## `file_tree_display.py`
178
+
179
+ This module provides a utility for generating a visually structured directory tree.
180
+ It supports recursive traversal, customizable hierarchy styles, and inclusion / exclusion
181
+ patterns for directories and files.
182
+ Output can be displayed in the console or saved to a file.
183
+
184
+
185
+ #### Key Features
186
+
187
+ - Recursively displays and logs directory trees
188
+ - Efficient directory traversal
189
+ - Blazing fast (see Benchmarks below)
190
+ - Generates human-readable file tree structure
191
+ - Supports including / ignoring specific directories or files via pattern matching
192
+ - Customizable tree display output
193
+ - Optionally saves the resulting tree to a text file
194
+ - Lightweight, flexible and easily configurable
195
+
196
+
197
+ ### Benchmarks
198
+
199
+ The measurements were carried out on unfiltered folders containing multiple files und subdirectories, using SSD.
200
+ Avg. time was measured over 20 runs per configuration, using `timeit` decorator I've implemented in this package.
201
+
202
+ Comparing FileTreeDisplay (FTD) with
203
+ [win_tree_wrapper](https://github.com/yaronday/nano_dev_utils/blob/master/benchmark/win_tree_wrapper.py)
204
+ (Windows [tree](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/tree)
205
+ wrapper which I've implemented for this purpose).
206
+ [Benchtest code](https://github.com/yaronday/nano_dev_utils/blob/master/benchmark/benchtest.py)
207
+
208
+ ### Performance Comparison — FTD vs.`tree`
209
+
210
+ <table>
211
+ <tr><th>Test Context </th><th>Results</th></tr>
212
+ <tr><td>
213
+
214
+ | Metric | Test1 | Test2 |
215
+ |:---------------------|:-------|:-----------|
216
+ | **Files** | 10553 | 138492 |
217
+ | **Folders** | 1235 | 20428 |
218
+ | **Wrapper Overhead** | ~30 ms | negligible |
219
+
220
+ </td><td>
221
+
222
+ | Tool | T1 (s) | T2 (s) | Relative Speed |
223
+ |:---------|:------:|:------:|:--------------:|
224
+ | **FTD** | 0.196 | 2.900* | — |
225
+ | **tree** | 0.390 | 5.018 | ~2x slower |
226
+
227
+ </td></tr> </table>
228
+
229
+ ***Without sorting** FTD takes 162 ms and 2.338 s for Test1 and Test2, respectively.
230
+ FTD is roughly **1.7x–2.4x faster** than the native `tree` binary across both datasets.
231
+
232
+ ### Brief Analysis
233
+
234
+ ### I. Linear scaling as a function of entries
235
+ FTD performance scales almost perfectly linearly with total entries:
236
+
237
+ * **T1:** 10 k files → 0.2 s
238
+ * **T2:** 138 k files → 2.9 s
239
+ → ~14x more files → ~15x more runtime => expected by linearity.
240
+
241
+ ### II. Figuring out why `tree` is nearly 2 times slower than my FTD
242
+ Although `tree` is implemented in C, it incurs more I/O work:
243
+ * Performs full `lstat()` on each entry (permissions, timestamps, etc.).
244
+ * Prints incrementally to `stdout` → many system calls (syscalls).
245
+ * Handles color / formatting output.
246
+
247
+ My FTD avoids this by:
248
+
249
+ * Using `os.scandir()` (caching stat info).
250
+ * Filtering and sorting in-memory.
251
+ * Buffering output before optional print/write.
252
+
253
+ Result: lower syscall count and fewer I/O stalls.
254
+
255
+ ### III. Python overhead is clearly negligible
256
+ Even at 2.9 s for ~160K entries, throughput ~55K entries/s — close to filesystem limits on SSDs.
257
+ Measured wrapper overhead (~30 ms) is < 1 % of total runtime.
258
+
259
+ ### Key Insights
260
+
261
+ | Observation | Explanation |
262
+ |:-------------------------------|:----------------------------------------------------|
263
+ | **FTD ~2x faster than `tree`** | Avoids per-file printing and redundant stats. |
264
+ | **I/O-bound execution** | Filesystem metadata fetch dominates total time. |
265
+ | **Linear runtime scaling** | Recursive generator design adds no hidden overhead. |
266
+ | **Stable memory footprint** | Uses streaming generators and `StringIO` buffering. |
267
+
268
+ ### Conclusions
269
+
270
+ * **FTD outperforms `tree` by roughly 2x** on both moderate and large datasets.
271
+ * **Runtime scales linearly** with total directory entries.
272
+ * **Python layer overhead is negligible** — performance is bounded by kernel I/O.
273
+
274
+
275
+ #### Class Overview
276
+
277
+ **`FileTreeDisplay`**
278
+ Constructs and manages the visual representation of a folder structure of a path or of a disk drive.
279
+
280
+ **Initialization Parameters**
281
+
282
+ | Parameter | Type | Description |
283
+ |:--------------------------------------|:------------------------------------|:---------------------------------------------------------------------------------|
284
+ | `root_dir` | `str` | Path to the directory to scan. |
285
+ | `filepath` | `str / None` | Optional output destination for the saved file tree. |
286
+ | `ignore_dirs` | `list[str] or set[str] or None` | Directory names or patterns to skip. |
287
+ | `ignore_files` | `list[str] or set[str] or None` | File names or patterns to skip. |
288
+ | `include_dirs` | `list[str] or set[str] or None` | Only include specified folder names or patterns. |
289
+ | `include_files` | `list[str] or set[str] or None` | Only include specified file names or patterns, '*.pdf' - only include pdfs. |
290
+ | `style` | `str` | Characters used to mark hierarchy levels. Defaults to `'classic'`. |
291
+ | `indent` | `int` | Number of style characters per level. Defaults `2`. |
292
+ | `files_first` | `bool` | Determines whether to list files first. Defaults to False. |
293
+ | `skip_sorting` | `bool` | Skip sorting directly, even if configured. |
294
+ | `sort_key_name` | `str` | Sort key. Lexicographic ('lex') or 'custom'. Defaults to 'natural'. |
295
+ | `reverse` | `bool` | Reversed sorting order. |
296
+ | `custom_sort` | `Callable[[str], Any] / None` | Custom sort key function. |
297
+ | `title` | `str` | Custom title shown in the output. |
298
+ | `save2file` | `bool` | Save file tree (folder structure) info into a file. |
299
+ | `printout` | `bool` | Print file tree info. |
300
+
301
+ #### Core Methods
302
+
303
+ - `file_tree_display(save2file: bool = True) -> str | None`
304
+ Generates the directory tree. If `save2file=True`, saves the output; otherwise prints it directly.
305
+
306
+ - `_build_tree(dir_path, *, prefix, style, sort_key,
307
+ files_first, dir_filter, file_filter, reverse, indent) -> Generator[str, None, None]`
308
+ Recursively traverses the directory tree in depth-first order (DFS) and yields formatted lines representing the file and folder structure.
309
+
310
+ | Parameter | Type | Description |
311
+ |-------------------------------------|-------------------------|------------------------------------------------------------------------------|
312
+ | **`dir_path`** | `str` | Path to the directory being traversed. |
313
+ | **`prefix`** | `str` | Current indentation prefix for nested entries. |
314
+ | **`style`** | `dict[str, str]` | Connector style mapping with keys: `branch`, `end`, `space`, and `vertical`. |
315
+ | **`sort_key`** | `Callable[[str], Any]` | Function used to sort directory and file names. |
316
+ | **`files_first`** | `bool` | If `True`, list files before subdirectories. |
317
+ | **`dir_filter`**, **`file_filter`** | `Callable[[str], bool]` | Predicates to include or exclude directories and files. |
318
+ | **`reverse`** | `bool` | If `True`, reverses the sort order. |
319
+ | **`indent`** | `int` | Number of spaces (or repeated characters) per indentation level. |
320
+
321
+
322
+ #### Example Usage
323
+
324
+ ```python
325
+ from pathlib import Path
326
+ from nano_dev_utils.file_tree_display import FileTreeDisplay
327
+
328
+ root = r'c:/your_root_dir'
329
+ target_path = r'c:/your_target_path'
330
+ filename = 'filetree.md'
331
+ filepath = str(Path(target_path, filename))
332
+
333
+ ftd = FileTreeDisplay(root_dir=root,
334
+ ignore_dirs=['.git', 'node_modules', '.idea'],
335
+ ignore_files=['.gitignore', '*.toml'],
336
+ style='classic',
337
+ include_dirs=['src', 'tests', 'snapshots'],
338
+ filepath=filepath,
339
+ sort_key_name='custom',
340
+ custom_sort=(lambda x: any(ext in x.lower() for ext in ('jpg', 'png'))),
341
+ files_first=True,
342
+ reverse=True
343
+ )
344
+ ftd.file_tree_display()
345
+ ```
346
+
347
+ #### Custom connector style
348
+ You can define and register your own connector styles at runtime by adding entries to style_dict:
349
+
350
+ ```Python
351
+ from nano_dev_utils.file_tree_display import FileTreeDisplay
352
+ ftd = FileTreeDisplay(root_dir=".")
353
+ ftd.style_dict["plus2"] = ftd.connector_styler("+-- ", "+== ")
354
+ ftd.style = "plus2"
355
+ ftd.printout = True
356
+ ftd.file_tree_display()
357
+ ```
358
+
359
+
360
+ #### Error Handling
361
+
362
+ The module raises well-defined exceptions for common issues:
363
+
364
+ - `NotADirectoryError` when the path is not a directory
365
+ - `PermissionError` for unreadable directories or write-protected files
366
+ - `OSError` for general I/O or write failures
367
+
368
+ ***
369
+
370
+ ## License
371
+ This project is licensed under the MIT License.
372
+ See [LICENSE](https://github.com/yaronday/nano_dev_utils/blob/master/LICENSE) for details.
@@ -0,0 +1,10 @@
1
+ nano_dev_utils/__init__.py,sha256=bJNCUyssMVyNmOey-god8A2kElC4nCR9B5DsdvUrKWw,1014
2
+ nano_dev_utils/common.py,sha256=RfTmJbTLwF8ihN7FjxSKDDAwJFFFdFi66M1jbdyIcd8,5985
3
+ nano_dev_utils/dynamic_importer.py,sha256=-Mh76366lI_mP2QA_jxiVfcKCHOHeukS_j4v7fTh0xw,1028
4
+ nano_dev_utils/file_tree_display.py,sha256=FH3ipF48_hiS6gtpW8iLHwtTomVjZ4xNvlMNZdYHrv0,11195
5
+ nano_dev_utils/release_ports.py,sha256=yLWMMbN6j6kWtGTg-Nynn37-Q4b2rxkls9hs2sqeZjA,6081
6
+ nano_dev_utils/timers.py,sha256=jR3PwYB40CTqxcfEuwxSZvQc3wMUuqRy1g__0tPXjz0,8783
7
+ nano_dev_utils-1.5.2.dist-info/METADATA,sha256=Ao9EkSermacne4uHklmH_6EvgfQmP-__RkRsaCDGy_c,17607
8
+ nano_dev_utils-1.5.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ nano_dev_utils-1.5.2.dist-info/licenses/LICENSE,sha256=Muenl7Bw_LdtHZtlOMAP7Kt97gDCq8WWp2605eDWhHU,1089
10
+ nano_dev_utils-1.5.2.dist-info/RECORD,,