nano-dev-utils 1.4.0__py3-none-any.whl → 1.4.1__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/common.py CHANGED
@@ -61,7 +61,9 @@ def encode_dict(input_dict: dict) -> bytes:
61
61
  return b' '.join(str(v).encode() for v in input_dict.values())
62
62
 
63
63
 
64
- def str2file(content: AnyStr, filepath: str, mode: str = 'w', enc: str = 'utf-8') -> None:
64
+ def str2file(
65
+ content: AnyStr, filepath: str, mode: str = 'w', enc: str = 'utf-8'
66
+ ) -> None:
65
67
  """Simply save file directly from any string content.
66
68
 
67
69
  Args:
@@ -86,32 +88,48 @@ def str2file(content: AnyStr, filepath: str, mode: str = 'w', enc: str = 'utf-8'
86
88
 
87
89
 
88
90
  class PredicateBuilder:
89
- def build_predicate(self, allow: FilterSet, block: FilterSet) -> Callable[[str], bool]:
91
+ def build_predicate(
92
+ self, allow: FilterSet, block: FilterSet
93
+ ) -> Callable[[str], bool]:
90
94
  """Build a memory-efficient predicate function."""
91
95
  compile_patts = self.compile_patts
92
96
 
93
97
  allow_lits, allow_patts = compile_patts(allow)
94
98
  block_lits, block_patts = compile_patts(block)
95
99
 
96
- flag = (1 if allow_lits or allow_patts else 0,
97
- 1 if block_lits or block_patts else 0)
100
+ flag = (
101
+ 1 if allow_lits or allow_patts else 0,
102
+ 1 if block_lits or block_patts else 0,
103
+ )
98
104
 
99
105
  match flag: # (allow, block)
100
106
  case (0, 0):
101
107
  return lambda name: True
102
108
 
103
109
  case (0, 1):
104
- return partial(self._match_patt_with_lits,
105
- name_patts=block_patts, name_lits=block_lits, negate=True)
110
+ return partial(
111
+ self._match_patt_with_lits,
112
+ name_patts=block_patts,
113
+ name_lits=block_lits,
114
+ negate=True,
115
+ )
106
116
 
107
117
  case (1, 0):
108
- return partial(self._match_patt_with_lits, name_patts=allow_patts,
109
- name_lits=allow_lits, negate=False)
118
+ return partial(
119
+ self._match_patt_with_lits,
120
+ name_patts=allow_patts,
121
+ name_lits=allow_lits,
122
+ negate=False,
123
+ )
110
124
 
111
125
  case (1, 1):
112
- return partial(self._allow_block_predicate,
113
- allow_lits=allow_lits, allow_patts=allow_patts,
114
- block_lits=block_lits, block_patts=block_patts)
126
+ return partial(
127
+ self._allow_block_predicate,
128
+ allow_lits=allow_lits,
129
+ allow_patts=allow_patts,
130
+ block_lits=block_lits,
131
+ block_patts=block_patts,
132
+ )
115
133
 
116
134
  @staticmethod
117
135
  def compile_patts(fs: FilterSet) -> tuple[set[str], list[re.Pattern]]:
@@ -119,7 +137,7 @@ class PredicateBuilder:
119
137
  return set(), []
120
138
  literals, patterns = set(), []
121
139
  for item in fs:
122
- if "*" in item or "?" in item or "[" in item:
140
+ if '*' in item or '?' in item or '[' in item:
123
141
  patterns.append(re.compile(fnmatch.translate(item)))
124
142
  else:
125
143
  literals.add(item)
@@ -130,15 +148,27 @@ class PredicateBuilder:
130
148
  """Return True if name matches any compiled regex pattern."""
131
149
  return any(pat.fullmatch(name) for pat in patterns)
132
150
 
133
- def _match_patt_with_lits(self, name: str, *, name_lits: set[str],
134
- name_patts: list[re.Pattern], negate: bool = False) -> bool:
151
+ def _match_patt_with_lits(
152
+ self,
153
+ name: str,
154
+ *,
155
+ name_lits: set[str],
156
+ name_patts: list[re.Pattern],
157
+ negate: bool = False,
158
+ ) -> bool:
135
159
  """Return True if name is in literals or matches any pattern."""
136
160
  res = name in name_lits or self._match_patts(name, name_patts)
137
161
  return not res if negate else res
138
162
 
139
- def _allow_block_predicate(self, name: str,
140
- *, allow_lits: set[str], allow_patts: list[re.Pattern],
141
- block_lits: set[str], block_patts: list[re.Pattern]) -> bool:
163
+ def _allow_block_predicate(
164
+ self,
165
+ name: str,
166
+ *,
167
+ allow_lits: set[str],
168
+ allow_patts: list[re.Pattern],
169
+ block_lits: set[str],
170
+ block_patts: list[re.Pattern],
171
+ ) -> bool:
142
172
  """Return True if name is allowed and not blocked (block takes precedence)."""
143
173
  if name in block_lits or self._match_patts(name, block_patts):
144
174
  return False
@@ -3,7 +3,7 @@ import re
3
3
 
4
4
  from collections.abc import Generator
5
5
  from pathlib import Path
6
- from typing_extensions import LiteralString, Callable, Any
6
+ from typing_extensions import Callable, Any
7
7
 
8
8
  from .common import str2file, FilterSet, PredicateBuilder
9
9
 
@@ -22,22 +22,23 @@ class FileTreeDisplay:
22
22
  visual representations of directories and files.
23
23
  Supports exclusion lists, configurable indentation, and custom prefix styles.
24
24
  """
25
+
25
26
  def __init__(
26
- self,
27
- root_dir: str | None = None,
28
- filepath: str | None = None,
29
- ignore_dirs: FilterSet = None,
30
- ignore_files: FilterSet = None,
31
- include_dirs: FilterSet = None,
32
- include_files: FilterSet = None,
33
- style: str = ' ',
34
- indent: int = 2,
35
- files_first: bool = False,
36
- sort_key_name: str = 'natural',
37
- reverse: bool = False,
38
- custom_sort: Callable[[str], Any] | None = None,
39
- save2file: bool = True,
40
- printout: bool = False,
27
+ self,
28
+ root_dir: str | None = None,
29
+ filepath: str | None = None,
30
+ ignore_dirs: FilterSet = None,
31
+ ignore_files: FilterSet = None,
32
+ include_dirs: FilterSet = None,
33
+ include_files: FilterSet = None,
34
+ style: str = ' ',
35
+ indent: int = 2,
36
+ files_first: bool = False,
37
+ sort_key_name: str = 'natural',
38
+ reverse: bool = False,
39
+ custom_sort: Callable[[str], Any] | None = None,
40
+ save2file: bool = True,
41
+ printout: bool = False,
41
42
  ) -> None:
42
43
  """Initialize the FileTreeDisplay instance.
43
44
 
@@ -82,7 +83,9 @@ class FileTreeDisplay:
82
83
 
83
84
  self.pb = PredicateBuilder()
84
85
  self.dir_filter = self.pb.build_predicate(self.include_dirs, self.ignore_dirs)
85
- self.file_filter = self.pb.build_predicate(self.include_files, self.ignore_files)
86
+ self.file_filter = self.pb.build_predicate(
87
+ self.include_files, self.ignore_files
88
+ )
86
89
 
87
90
  def init(self, *args, **kwargs) -> None:
88
91
  self.__init__(*args, **kwargs)
@@ -95,13 +98,16 @@ class FileTreeDisplay:
95
98
 
96
99
  def update_predicates(self):
97
100
  self.dir_filter = self.pb.build_predicate(self.include_dirs, self.ignore_dirs)
98
- self.file_filter = self.pb.build_predicate(self.include_files, self.ignore_files)
101
+ self.file_filter = self.pb.build_predicate(
102
+ self.include_files, self.ignore_files
103
+ )
99
104
 
100
105
  @staticmethod
101
- def _nat_key(name: str) -> list[int | LiteralString]:
106
+ def _nat_key(name: str) -> list[int | str | Any]:
102
107
  """Natural sorting key"""
103
- return [int(part) if part.isdigit() else part.lower()
104
- for part in _NUM_SPLIT(name)]
108
+ return [
109
+ int(part) if part.isdigit() else part.lower() for part in _NUM_SPLIT(name)
110
+ ]
105
111
 
106
112
  @staticmethod
107
113
  def _lex_key(name: str) -> str:
@@ -127,7 +133,7 @@ class FileTreeDisplay:
127
133
 
128
134
  tree_info = self.get_tree_info(iterator)
129
135
 
130
- if self.save2file:
136
+ if self.save2file and filepath:
131
137
  str2file(tree_info, filepath)
132
138
  return filepath
133
139
 
@@ -141,8 +147,7 @@ class FileTreeDisplay:
141
147
  lines.extend(list(iterator))
142
148
  return '\n'.join(lines)
143
149
 
144
- def build_tree(self, dir_path: str,
145
- prefix: str = '') -> Generator[str, None, None]:
150
+ def build_tree(self, dir_path: str, prefix: str = '') -> Generator[str, None, None]:
146
151
  """Yields formatted directory tree lines, using a recursive DFS.
147
152
  Intended order of appearance is with a preference to subdirectories first.
148
153
 
@@ -163,9 +168,10 @@ class FileTreeDisplay:
163
168
 
164
169
  if sort_key is None:
165
170
  if sort_key_name == 'custom':
166
- raise ValueError("custom_sort function must be specified"
167
- " when sort_key_name='custom'")
168
- raise ValueError(f"Invalid sort key name: {sort_key_name}")
171
+ raise ValueError(
172
+ "custom_sort function must be specified when sort_key_name='custom'"
173
+ )
174
+ raise ValueError(f'Invalid sort key name: {sort_key_name}')
169
175
 
170
176
  try:
171
177
  with os.scandir(dir_path) as entries:
@@ -181,7 +187,11 @@ class FileTreeDisplay:
181
187
  append_file(name)
182
188
 
183
189
  except (PermissionError, OSError) as e:
184
- msg = '[Permission Denied]' if isinstance(e, PermissionError) else '[Error reading directory]'
190
+ msg = (
191
+ '[Permission Denied]'
192
+ if isinstance(e, PermissionError)
193
+ else '[Error reading directory]'
194
+ )
185
195
  yield f'{next_prefix}{msg}'
186
196
  return
187
197
 
nano_dev_utils/timers.py CHANGED
@@ -23,9 +23,12 @@ R = TypeVar('R')
23
23
 
24
24
 
25
25
  class Timer:
26
- def __init__(self, precision: int = 4, verbose: bool = False):
26
+ def __init__(
27
+ self, precision: int = 4, verbose: bool = False, printout: bool = False
28
+ ):
27
29
  self.precision = precision
28
30
  self.verbose = verbose
31
+ self.printout = printout
29
32
 
30
33
  def init(self, *args, **kwargs) -> None:
31
34
  self.__init__(*args, **kwargs)
@@ -33,6 +36,9 @@ class Timer:
33
36
  def update(self, attrs: dict[str, Any]) -> None:
34
37
  update(self, attrs)
35
38
 
39
+ def res_formatter(self, elapsed_ns: float, *, precision: int = 4) -> str:
40
+ return self._duration_formatter(elapsed_ns, precision=precision)
41
+
36
42
  def timeit(
37
43
  self,
38
44
  iterations: int = 1,
@@ -53,12 +59,14 @@ class Timer:
53
59
  RP = ParamSpec('RP')
54
60
  RR = TypeVar('RR')
55
61
 
56
- precision = self.precision
62
+ precision, verbose, printout = self.precision, self.verbose, self.printout
63
+ check_timeout = self._check_timeout
64
+ duration_formatter = self._duration_formatter
65
+ formatted_msg = self._formatted_msg
57
66
 
58
67
  def decorator(
59
68
  func: Callable[RP, RR] | Callable[RP, Awaitable[RR]],
60
69
  ) -> Callable[RP, Any]:
61
- verbose = self.verbose
62
70
  if inspect.iscoroutinefunction(func):
63
71
  async_func = cast(Callable[RP, Awaitable[RR]], func)
64
72
 
@@ -73,7 +81,7 @@ class Timer:
73
81
  duration_ns = time.perf_counter_ns() - start_ns
74
82
  total_elapsed_ns += duration_ns
75
83
 
76
- self._check_timeout(
84
+ check_timeout(
77
85
  func_name,
78
86
  i,
79
87
  duration_ns,
@@ -82,12 +90,14 @@ class Timer:
82
90
  per_iteration,
83
91
  )
84
92
  avg_elapsed_ns = total_elapsed_ns / iterations
85
- duration_str = self._duration_formatter(avg_elapsed_ns, precision)
93
+ duration_str = duration_formatter(avg_elapsed_ns, precision)
86
94
 
87
- msg = self._formatted_msg(
95
+ msg = formatted_msg(
88
96
  func_name, args, kwargs, duration_str, iterations, verbose
89
97
  )
90
98
  lgr.info(msg)
99
+ if printout:
100
+ print(msg)
91
101
  return cast(RR, result)
92
102
 
93
103
  return cast(Callable[RP, Awaitable[RR]], async_wrapper)
@@ -104,7 +114,7 @@ class Timer:
104
114
  result = sync_func(*args, **kwargs)
105
115
  duration_ns = time.perf_counter_ns() - start_ns
106
116
  total_elapsed_ns += duration_ns
107
- self._check_timeout(
117
+ check_timeout(
108
118
  func_name,
109
119
  i,
110
120
  duration_ns,
@@ -113,11 +123,13 @@ class Timer:
113
123
  per_iteration,
114
124
  )
115
125
  avg_elapsed_ns = total_elapsed_ns / iterations
116
- duration_str = self._duration_formatter(avg_elapsed_ns, precision)
117
- msg = self._formatted_msg(
126
+ duration_str = duration_formatter(avg_elapsed_ns, precision)
127
+ msg = formatted_msg(
118
128
  func_name, args, kwargs, duration_str, iterations, verbose
119
129
  )
120
130
  lgr.info(msg)
131
+ if printout:
132
+ print(msg)
121
133
  return cast(RR, result)
122
134
 
123
135
  return cast(Callable[RP, RR], sync_wrapper)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nano_dev_utils
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: A collection of small Python utilities for developers.
5
5
  Project-URL: Homepage, https://github.com/yaronday/nano_utils
6
6
  Project-URL: Issues, https://github.com/yaronday/nano_utils/issues
@@ -26,9 +26,10 @@ This module provides a `Timer` class for measuring the execution time of code bl
26
26
 
27
27
  #### `Timer` Class
28
28
 
29
- * **`__init__(self, precision: int = 4, verbose: bool = False)`**: Initializes a `Timer` instance.
29
+ * **`__init__(self, precision: int = 4, verbose: bool = False, printout: bool = False)`**: Initializes a `Timer` instance.
30
30
  * `precision`: The number of decimal places to record and display time durations. Defaults to 4.
31
31
  * `verbose`: Optionally displays the function's positional arguments (args) and keyword arguments (kwargs). Defaults to `False`.
32
+ * `printout`: Allows printing to console.
32
33
 
33
34
  * **`def timeit(
34
35
  self,
@@ -47,15 +48,22 @@ This module provides a `Timer` class for measuring the execution time of code bl
47
48
  * Records execution times
48
49
  * Handles timeout conditions
49
50
  * Calculates average execution time across iterations
50
- * Prints the function name and execution time (with optional arguments)
51
+ * Logs the function name and execution time (with optional arguments)
51
52
  * Returns the result of the original function (unless timeout occurs)
52
53
 
53
54
  #### Example Usage:
54
55
 
55
56
  ```python
56
57
  import time
58
+ import logging
57
59
  from nano_dev_utils import timer
58
60
 
61
+ # This timer version uses a logger but also allows printing (if enabled), so it has to be configured in your app, for instance:
62
+ logging.basicConfig(filename='timer example.log',
63
+ level=logging.INFO, # DEBUG, WARNING, ERROR, CRITICAL
64
+ format='%(asctime)s - %(levelname)s: %(message)s',
65
+ datefmt='%d-%m-%Y %H:%M:%S')
66
+
59
67
  # Basic timing
60
68
  @timer.timeit()
61
69
  def my_function(a, b=10):
@@ -64,17 +72,15 @@ def my_function(a, b=10):
64
72
  return a + b
65
73
 
66
74
  timer.init(precision=6, verbose=True)
67
- '''
68
- Alternatively we could have used the `update` method as well:
75
+ '''Alternative options:
76
+ timer.update({'precision': 6, 'verbose': True}) # 1. Using update method
69
77
 
70
- timer.update({'precision': 6, 'verbose': True})
71
-
72
- The above config could be also achieved via explicit instantiation:
73
-
74
- from nano_dev_utils.timers import Timer
75
- timer = Timer(precision=6, verbose=True)
78
+ from nano_dev_utils.timers import Timer # 2. explicit instantiation
79
+ timer = Timer(precision=6, verbose=True)
76
80
  '''
77
81
 
82
+ timer.update({'printout': True}) # allow printing to console
83
+
78
84
  # Advanced usage with timeout and iterations
79
85
  @timer.timeit(iterations=5, timeout=0.5, per_iteration=True)
80
86
  def critical_function(x):
@@ -151,7 +157,7 @@ import logging
151
157
  from nano_dev_utils import ports_release, PortsRelease
152
158
 
153
159
 
154
- # For configuration of logging level and format (supported already):
160
+ # configure the logger
155
161
  logging.basicConfig(filename='port release.log',
156
162
  level=logging.INFO, # DEBUG, WARNING, ERROR, CRITICAL
157
163
  format='%(asctime)s - %(levelname)s: %(message)s',
@@ -170,8 +176,9 @@ ports_release.release_all()
170
176
 
171
177
  ### `file_tree_display.py`
172
178
 
173
- This module provides a class-based utility for generating a visually structured directory tree.
174
- It supports recursive traversal, customizable hierarchy styles, and exclusion patterns for directories and files.
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.
175
182
  Output can be displayed in the console or saved to a file.
176
183
 
177
184
 
@@ -180,11 +187,12 @@ Output can be displayed in the console or saved to a file.
180
187
  - Recursively displays and logs directory trees
181
188
  - Efficient directory traversal
182
189
  - Blazing fast (see Benchmarks below)
183
- - Generates human-readable file tree structure
190
+ - Generates human-readable file tree structure
191
+ - Supports including / ignoring specific directories or files via pattern matching
184
192
  - Customizable tree display output
185
193
  - Optionally saves the resulting tree to a text file
186
- - Supports ignoring specific directories or files via pattern matching
187
- - Handles permission and read/write errors gracefully
194
+ - Lightweight, flexible and easily configurable
195
+
188
196
 
189
197
  ## Benchmarks
190
198
 
@@ -244,11 +252,13 @@ filepath = str(Path(target_path, filename))
244
252
 
245
253
  ftd = FileTreeDisplay(root_dir=root,
246
254
  ignore_dirs=['.git', 'node_modules', '.idea'],
247
- ignore_files={'.gitignore', '*.toml'}, style='—',
255
+ ignore_files=['.gitignore', '*.toml'],
256
+ style='—',
248
257
  include_dirs=['src', 'tests', 'snapshots'],
249
258
  filepath=filepath,
250
259
  sort_key_name='custom',
251
260
  custom_sort=(lambda x: any(ext in x.lower() for ext in ('jpg', 'png'))),
261
+ files_first=True,
252
262
  reverse=True
253
263
  )
254
264
  ftd.file_tree_display()
@@ -0,0 +1,10 @@
1
+ nano_dev_utils/__init__.py,sha256=bJNCUyssMVyNmOey-god8A2kElC4nCR9B5DsdvUrKWw,1014
2
+ nano_dev_utils/common.py,sha256=MsY5n9lSOjvEu0wGvmd2zQamFLLbtbjodZku5W9tuWE,5873
3
+ nano_dev_utils/dynamic_importer.py,sha256=-Mh76366lI_mP2QA_jxiVfcKCHOHeukS_j4v7fTh0xw,1028
4
+ nano_dev_utils/file_tree_display.py,sha256=RMd2l1FZgO__9EmCSKRkZ6s7tYpCx6Fe7e83fAxNxg0,8234
5
+ nano_dev_utils/release_ports.py,sha256=yLWMMbN6j6kWtGTg-Nynn37-Q4b2rxkls9hs2sqeZjA,6081
6
+ nano_dev_utils/timers.py,sha256=5Ci6IgdEfwIvRtQes4byab1GJowLdfnurUBUxorSgIc,7840
7
+ nano_dev_utils-1.4.1.dist-info/METADATA,sha256=sfJDvXpotS86VX6o1XjnwFY3CDoTNUpJkDlLoOpFsGM,12133
8
+ nano_dev_utils-1.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ nano_dev_utils-1.4.1.dist-info/licenses/LICENSE,sha256=Muenl7Bw_LdtHZtlOMAP7Kt97gDCq8WWp2605eDWhHU,1089
10
+ nano_dev_utils-1.4.1.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- nano_dev_utils/__init__.py,sha256=bJNCUyssMVyNmOey-god8A2kElC4nCR9B5DsdvUrKWw,1014
2
- nano_dev_utils/common.py,sha256=bNgiaBP5kQUxfEUN3HYp1_TnwG-SRvsiqu67yAG5Y4o,5585
3
- nano_dev_utils/dynamic_importer.py,sha256=-Mh76366lI_mP2QA_jxiVfcKCHOHeukS_j4v7fTh0xw,1028
4
- nano_dev_utils/file_tree_display.py,sha256=0pfDr1QgyPJZbtl8mKNfJO0T3Jl2vUrwt8syVn_PhsI,8192
5
- nano_dev_utils/release_ports.py,sha256=yLWMMbN6j6kWtGTg-Nynn37-Q4b2rxkls9hs2sqeZjA,6081
6
- nano_dev_utils/timers.py,sha256=Ko2RR96-Sb6hIQAPxiwCUPAK2uwJ1dP_9Teym8lx_lo,7350
7
- nano_dev_utils-1.4.0.dist-info/METADATA,sha256=z4PfsxOWteGXmLuJS5MtivqCKox9LR6wUKTjhelw9_c,11642
8
- nano_dev_utils-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- nano_dev_utils-1.4.0.dist-info/licenses/LICENSE,sha256=Muenl7Bw_LdtHZtlOMAP7Kt97gDCq8WWp2605eDWhHU,1089
10
- nano_dev_utils-1.4.0.dist-info/RECORD,,