nano-dev-utils 0.2.1__tar.gz → 0.3.4__tar.gz

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.

Files changed (23) hide show
  1. nano_dev_utils-0.3.4/.idea/workspace.xml +271 -0
  2. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/PKG-INFO +2 -3
  3. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/README.md +0 -1
  4. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/pyproject.toml +5 -4
  5. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/src/nano_dev_utils/__init__.py +2 -1
  6. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/src/nano_dev_utils/dynamic_importer.py +4 -2
  7. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/src/nano_dev_utils/release_ports.py +14 -14
  8. nano_dev_utils-0.3.4/src/nano_dev_utils/timers.py +35 -0
  9. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/tests/__init__.py +4 -0
  10. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/tests/testing_release_ports.py +118 -71
  11. nano_dev_utils-0.3.4/tests/testing_timer.py +157 -0
  12. nano_dev_utils-0.2.1/.idea/workspace.xml +0 -69
  13. nano_dev_utils-0.2.1/src/nano_dev_utils/timers.py +0 -24
  14. nano_dev_utils-0.2.1/tests/testing_timer.py +0 -63
  15. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/.gitignore +0 -0
  16. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/.idea/.gitignore +0 -0
  17. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  18. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/.idea/misc.xml +0 -0
  19. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/.idea/modules.xml +0 -0
  20. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/.idea/nano_dev_utils.iml +0 -0
  21. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/.idea/vcs.xml +0 -0
  22. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/LICENSE.md +0 -0
  23. {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.4}/tests/testing_dynamic_importer.py +0 -0
@@ -0,0 +1,271 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="96bbbefe-efb6-42c4-93da-e069ac3e654f" name="Changes" comment="minor update" />
8
+ <option name="SHOW_DIALOG" value="false" />
9
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
10
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
11
+ <option name="LAST_RESOLUTION" value="IGNORE" />
12
+ </component>
13
+ <component name="Git.Settings">
14
+ <option name="RECENT_BRANCH_BY_REPOSITORY">
15
+ <map>
16
+ <entry key="$PROJECT_DIR$" value="refactor1" />
17
+ </map>
18
+ </option>
19
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
20
+ </component>
21
+ <component name="ProjectColorInfo">{
22
+ &quot;associatedIndex&quot;: 5
23
+ }</component>
24
+ <component name="ProjectId" id="2wBUoAyjEyavXXmNnhB2xcAn4VQ" />
25
+ <component name="ProjectViewState">
26
+ <option name="hideEmptyMiddlePackages" value="true" />
27
+ <option name="showLibraryContents" value="true" />
28
+ <option name="showMembers" value="true" />
29
+ </component>
30
+ <component name="PropertiesComponent"><![CDATA[{
31
+ "keyToString": {
32
+ "Python tests.Python tests in testing_release_ports.py.executor": "Run",
33
+ "Python tests.Python tests in testing_timer.py.executor": "Run",
34
+ "Python tests.Python tests in tests.executor": "Run",
35
+ "RunOnceActivity.ShowReadmeOnStart": "true",
36
+ "git-widget-placeholder": "master",
37
+ "ignore.virus.scanning.warn.message": "true",
38
+ "last_opened_file_path": "C:/GitHubWS/nano_dev_utils",
39
+ "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
40
+ }
41
+ }]]></component>
42
+ <component name="RunManager" selected="Python tests.Python tests in tests">
43
+ <configuration name="Python tests in testing_release_ports.py" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
44
+ <module name="nano_dev_utils" />
45
+ <option name="ENV_FILES" value="" />
46
+ <option name="INTERPRETER_OPTIONS" value="" />
47
+ <option name="PARENT_ENVS" value="true" />
48
+ <option name="SDK_HOME" value="" />
49
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
50
+ <option name="IS_MODULE_SDK" value="true" />
51
+ <option name="ADD_CONTENT_ROOTS" value="true" />
52
+ <option name="ADD_SOURCE_ROOTS" value="true" />
53
+ <option name="_new_additionalArguments" value="&quot;&quot;" />
54
+ <option name="_new_target" value="&quot;$PROJECT_DIR$/tests/testing_release_ports.py&quot;" />
55
+ <option name="_new_targetType" value="&quot;PATH&quot;" />
56
+ <method v="2" />
57
+ </configuration>
58
+ <configuration name="Python tests in testing_timer.py" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
59
+ <module name="nano_dev_utils" />
60
+ <option name="ENV_FILES" value="" />
61
+ <option name="INTERPRETER_OPTIONS" value="" />
62
+ <option name="PARENT_ENVS" value="true" />
63
+ <option name="SDK_HOME" value="" />
64
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
65
+ <option name="IS_MODULE_SDK" value="true" />
66
+ <option name="ADD_CONTENT_ROOTS" value="true" />
67
+ <option name="ADD_SOURCE_ROOTS" value="true" />
68
+ <option name="_new_additionalArguments" value="&quot;&quot;" />
69
+ <option name="_new_target" value="&quot;$PROJECT_DIR$/tests/testing_timer.py&quot;" />
70
+ <option name="_new_targetType" value="&quot;PATH&quot;" />
71
+ <method v="2" />
72
+ </configuration>
73
+ <configuration name="Python tests in tests" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
74
+ <module name="nano_dev_utils" />
75
+ <option name="ENV_FILES" value="" />
76
+ <option name="INTERPRETER_OPTIONS" value="" />
77
+ <option name="PARENT_ENVS" value="true" />
78
+ <option name="SDK_HOME" value="" />
79
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
80
+ <option name="IS_MODULE_SDK" value="true" />
81
+ <option name="ADD_CONTENT_ROOTS" value="true" />
82
+ <option name="ADD_SOURCE_ROOTS" value="true" />
83
+ <option name="_new_additionalArguments" value="&quot;&quot;" />
84
+ <option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
85
+ <option name="_new_targetType" value="&quot;PATH&quot;" />
86
+ <method v="2" />
87
+ </configuration>
88
+ <recent_temporary>
89
+ <list>
90
+ <item itemvalue="Python tests.Python tests in tests" />
91
+ <item itemvalue="Python tests.Python tests in testing_timer.py" />
92
+ <item itemvalue="Python tests.Python tests in testing_release_ports.py" />
93
+ <item itemvalue="Python tests.Python tests in testing_release_ports.py" />
94
+ </list>
95
+ </recent_temporary>
96
+ </component>
97
+ <component name="SharedIndexes">
98
+ <attachedChunks>
99
+ <set>
100
+ <option value="bundled-python-sdk-5b207ade9991-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.17890.14" />
101
+ </set>
102
+ </attachedChunks>
103
+ </component>
104
+ <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
105
+ <component name="TaskManager">
106
+ <task active="true" id="Default" summary="Default task">
107
+ <changelist id="96bbbefe-efb6-42c4-93da-e069ac3e654f" name="Changes" comment="" />
108
+ <created>1745514632336</created>
109
+ <option name="number" value="Default" />
110
+ <option name="presentableId" value="Default" />
111
+ <updated>1745514632336</updated>
112
+ </task>
113
+ <task id="LOCAL-00001" summary="renamed to formal PYPI package name">
114
+ <option name="closed" value="true" />
115
+ <created>1745516978652</created>
116
+ <option name="number" value="00001" />
117
+ <option name="presentableId" value="LOCAL-00001" />
118
+ <option name="project" value="LOCAL" />
119
+ <updated>1745516978652</updated>
120
+ </task>
121
+ <task id="LOCAL-00002" summary="improving unit test for release_ports (WIP)">
122
+ <option name="closed" value="true" />
123
+ <created>1745586382614</created>
124
+ <option name="number" value="00002" />
125
+ <option name="presentableId" value="LOCAL-00002" />
126
+ <option name="project" value="LOCAL" />
127
+ <updated>1745586382614</updated>
128
+ </task>
129
+ <task id="LOCAL-00003" summary="reminder - timers">
130
+ <option name="closed" value="true" />
131
+ <created>1745593807782</created>
132
+ <option name="number" value="00003" />
133
+ <option name="presentableId" value="LOCAL-00003" />
134
+ <option name="project" value="LOCAL" />
135
+ <updated>1745593807782</updated>
136
+ </task>
137
+ <task id="LOCAL-00004" summary="release_ports unit-testing improvements">
138
+ <option name="closed" value="true" />
139
+ <created>1745676371943</created>
140
+ <option name="number" value="00004" />
141
+ <option name="presentableId" value="LOCAL-00004" />
142
+ <option name="project" value="LOCAL" />
143
+ <updated>1745676371943</updated>
144
+ </task>
145
+ <task id="LOCAL-00005" summary="minor updates">
146
+ <option name="closed" value="true" />
147
+ <created>1745679672667</created>
148
+ <option name="number" value="00005" />
149
+ <option name="presentableId" value="LOCAL-00005" />
150
+ <option name="project" value="LOCAL" />
151
+ <updated>1745679672667</updated>
152
+ </task>
153
+ <task id="LOCAL-00006" summary="minor updates: _mock_pid_retrieval">
154
+ <option name="closed" value="true" />
155
+ <created>1745703394575</created>
156
+ <option name="number" value="00006" />
157
+ <option name="presentableId" value="LOCAL-00006" />
158
+ <option name="project" value="LOCAL" />
159
+ <updated>1745703394575</updated>
160
+ </task>
161
+ <task id="LOCAL-00007" summary="Improving code for Timer">
162
+ <option name="closed" value="true" />
163
+ <created>1745709597198</created>
164
+ <option name="number" value="00007" />
165
+ <option name="presentableId" value="LOCAL-00007" />
166
+ <option name="project" value="LOCAL" />
167
+ <updated>1745709597198</updated>
168
+ </task>
169
+ <task id="LOCAL-00008" summary="Optional: modernized style for py&gt;=3.10">
170
+ <option name="closed" value="true" />
171
+ <created>1745712059758</created>
172
+ <option name="number" value="00008" />
173
+ <option name="presentableId" value="LOCAL-00008" />
174
+ <option name="project" value="LOCAL" />
175
+ <updated>1745712059758</updated>
176
+ </task>
177
+ <task id="LOCAL-00009" summary="essential field updates">
178
+ <option name="closed" value="true" />
179
+ <created>1745712315250</created>
180
+ <option name="number" value="00009" />
181
+ <option name="presentableId" value="LOCAL-00009" />
182
+ <option name="project" value="LOCAL" />
183
+ <updated>1745712315250</updated>
184
+ </task>
185
+ <task id="LOCAL-00010" summary="dunder init update">
186
+ <option name="closed" value="true" />
187
+ <created>1745712644438</created>
188
+ <option name="number" value="00010" />
189
+ <option name="presentableId" value="LOCAL-00010" />
190
+ <option name="project" value="LOCAL" />
191
+ <updated>1745712644438</updated>
192
+ </task>
193
+ <task id="LOCAL-00011" summary="minor update">
194
+ <option name="closed" value="true" />
195
+ <created>1745713148443</created>
196
+ <option name="number" value="00011" />
197
+ <option name="presentableId" value="LOCAL-00011" />
198
+ <option name="project" value="LOCAL" />
199
+ <updated>1745713148443</updated>
200
+ </task>
201
+ <task id="LOCAL-00012" summary="fixed authors and url for pip show">
202
+ <option name="closed" value="true" />
203
+ <created>1745713896439</created>
204
+ <option name="number" value="00012" />
205
+ <option name="presentableId" value="LOCAL-00012" />
206
+ <option name="project" value="LOCAL" />
207
+ <updated>1745713896439</updated>
208
+ </task>
209
+ <task id="LOCAL-00013" summary="version update">
210
+ <option name="closed" value="true" />
211
+ <created>1745713915283</created>
212
+ <option name="number" value="00013" />
213
+ <option name="presentableId" value="LOCAL-00013" />
214
+ <option name="project" value="LOCAL" />
215
+ <updated>1745713915283</updated>
216
+ </task>
217
+ <task id="LOCAL-00014" summary="minor update">
218
+ <option name="closed" value="true" />
219
+ <created>1745714123280</created>
220
+ <option name="number" value="00014" />
221
+ <option name="presentableId" value="LOCAL-00014" />
222
+ <option name="project" value="LOCAL" />
223
+ <updated>1745714123280</updated>
224
+ </task>
225
+ <task id="LOCAL-00015" summary="minor update">
226
+ <option name="closed" value="true" />
227
+ <created>1745715895476</created>
228
+ <option name="number" value="00015" />
229
+ <option name="presentableId" value="LOCAL-00015" />
230
+ <option name="project" value="LOCAL" />
231
+ <updated>1745715895476</updated>
232
+ </task>
233
+ <task id="LOCAL-00016" summary="minor update">
234
+ <option name="closed" value="true" />
235
+ <created>1745715934159</created>
236
+ <option name="number" value="00016" />
237
+ <option name="presentableId" value="LOCAL-00016" />
238
+ <option name="project" value="LOCAL" />
239
+ <updated>1745715934159</updated>
240
+ </task>
241
+ <option name="localTasksCounter" value="17" />
242
+ <servers />
243
+ </component>
244
+ <component name="Vcs.Log.Tabs.Properties">
245
+ <option name="TAB_STATES">
246
+ <map>
247
+ <entry key="MAIN">
248
+ <value>
249
+ <State />
250
+ </value>
251
+ </entry>
252
+ </map>
253
+ </option>
254
+ </component>
255
+ <component name="VcsManagerConfiguration">
256
+ <MESSAGE value="renamed to formal PYPI package name" />
257
+ <MESSAGE value="improving unit test for release_ports (WIP)" />
258
+ <MESSAGE value="reminder - timers" />
259
+ <MESSAGE value="release_ports unit-testing improvements" />
260
+ <MESSAGE value="minor updates" />
261
+ <MESSAGE value="minor updates: _mock_pid_retrieval" />
262
+ <MESSAGE value="Improving code for Timer" />
263
+ <MESSAGE value="Optional: modernized style for py&gt;=3.10" />
264
+ <MESSAGE value="essential field updates" />
265
+ <MESSAGE value="dunder init update" />
266
+ <MESSAGE value="fixed authors and url for pip show" />
267
+ <MESSAGE value="version update" />
268
+ <MESSAGE value="minor update" />
269
+ <option name="LAST_COMMIT_MESSAGE" value="minor update" />
270
+ </component>
271
+ </project>
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nano_dev_utils
3
- Version: 0.2.1
3
+ Version: 0.3.4
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
7
7
  Author-email: Yaron Dayan <yaronday77@gmail.com>
8
- License-Expression: MIT
8
+ License: MIT
9
9
  License-File: LICENSE.md
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
@@ -29,7 +29,6 @@ This module provides a `Timer` class for measuring the execution time of code bl
29
29
  * display time durations. Defaults to 4.
30
30
  * `verbose` (bool, optional): If `True`, the function's arguments and keyword
31
31
  * arguments will be included in the printed timing output. Defaults to `False`.
32
- * `timing_records` (list): A list to store the recorded timing durations as formatted strings.
33
32
 
34
33
  * **`timeit(self, func)`**: A decorator that measures the execution time of the decorated function.
35
34
  * When the decorated function is called, this decorator records the start and end times,
@@ -15,7 +15,6 @@ This module provides a `Timer` class for measuring the execution time of code bl
15
15
  * display time durations. Defaults to 4.
16
16
  * `verbose` (bool, optional): If `True`, the function's arguments and keyword
17
17
  * arguments will be included in the printed timing output. Defaults to `False`.
18
- * `timing_records` (list): A list to store the recorded timing durations as formatted strings.
19
18
 
20
19
  * **`timeit(self, func)`**: A decorator that measures the execution time of the decorated function.
21
20
  * When the decorated function is called, this decorator records the start and end times,
@@ -1,6 +1,7 @@
1
1
  [project]
2
2
  name = "nano_dev_utils"
3
- version = "0.2.1"
3
+ version = "0.3.4"
4
+
4
5
  authors = [
5
6
  { name="Yaron Dayan", email="yaronday77@gmail.com" },
6
7
  ]
@@ -11,7 +12,7 @@ classifiers = [
11
12
  "Programming Language :: Python :: 3",
12
13
  "Operating System :: OS Independent",
13
14
  ]
14
- license = "MIT"
15
+ license = { text = "MIT" }
15
16
  license-files = ["LICENSE.md"]
16
17
 
17
18
  [build-system]
@@ -19,5 +20,5 @@ requires = ["hatchling >= 1.26"]
19
20
  build-backend = "hatchling.build"
20
21
 
21
22
  [project.urls]
22
- Homepage = "https://github.com/yaronday/nano_utils"
23
- Issues = "https://github.com/yaronday/nano_utils/issues"
23
+ Homepage = "https://github.com/yaronday/nano_utils" # PyPI
24
+ Issues = "https://github.com/yaronday/nano_utils/issues"
@@ -1,6 +1,7 @@
1
- """Nano-Utils-Yaronday - A collection of small Python utilities for developers.
1
+ """nano-dev-utils - A collection of small Python utilities for developers.
2
2
  Copyright (c) 2025 Yaron Dayan
3
3
  """
4
+
4
5
  from .dynamic_importer import Importer
5
6
  from .timers import Timer
6
7
  from .release_ports import PortsRelease, PROXY_SERVER, INSPECTOR_CLIENT
@@ -17,12 +17,14 @@ class Importer:
17
17
  if module_name in self.imported_modules:
18
18
  return self.imported_modules[module_name]
19
19
 
20
+ lib_mod = f'{library}.{module_name}'
21
+
20
22
  try:
21
- module = importlib.import_module(f"{library}.{module_name}")
23
+ module = importlib.import_module(lib_mod)
22
24
  self.imported_modules[module_name] = module
23
25
  return module
24
26
  except ModuleNotFoundError as e:
25
- raise ImportError(f"Could not import {library}.{module_name}") from e
27
+ raise ImportError(f'Could not import {lib_mod}') from e
26
28
 
27
29
 
28
30
 
@@ -1,7 +1,6 @@
1
1
  import platform
2
2
  import subprocess
3
3
  import logging
4
- from typing import Optional
5
4
 
6
5
 
7
6
  logging.basicConfig(filename='port release.log',
@@ -15,7 +14,7 @@ INSPECTOR_CLIENT = 6274
15
14
 
16
15
 
17
16
  class PortsRelease:
18
- def __init__(self, default_ports: Optional[list[int]] = None):
17
+ def __init__(self, default_ports: list[int] | None = None):
19
18
  self.default_ports: list[int] = default_ports \
20
19
  if default_ports is not None else [PROXY_SERVER, INSPECTOR_CLIENT]
21
20
 
@@ -36,8 +35,8 @@ class PortsRelease:
36
35
  return f'Invalid port number: {port}. Skipping.'
37
36
 
38
37
  @staticmethod
39
- def _log_terminate_failed(pid: int, port: Optional[int] = None,
40
- error: Optional[str] = None) -> str:
38
+ def _log_terminate_failed(pid: int, port: int | None = None,
39
+ error: str | None = None) -> str:
41
40
  base_msg = f'Failed to terminate process {pid}'
42
41
  if port:
43
42
  base_msg += f' (on port {port})'
@@ -61,14 +60,15 @@ class PortsRelease:
61
60
  def _log_unsupported_os() -> str:
62
61
  return f'Unsupported OS: {platform.system()}'
63
62
 
64
- def get_pid_by_port(self, port: int) -> Optional[int]:
63
+ def get_pid_by_port(self, port: int) -> int | None:
65
64
  """Gets the process ID (PID) listening on the specified port."""
65
+ system = platform.system()
66
66
  try:
67
- cmd: Optional[str] = {
67
+ cmd: str = {
68
68
  "Windows": f"netstat -ano | findstr :{port}",
69
69
  "Linux": f"ss -lntp | grep :{port}",
70
70
  "Darwin": f"lsof -i :{port}",
71
- }.get(platform.system())
71
+ }.get(system, "")
72
72
  if not cmd:
73
73
  lgr.error(self._log_unsupported_os())
74
74
  return None
@@ -85,13 +85,13 @@ class PortsRelease:
85
85
  for line in lines:
86
86
  if str(port) in line:
87
87
  parts: list[str] = line.split()
88
- if platform.system() == "Windows" and len(parts) > 4:
88
+ if system == "Windows" and len(parts) > 4:
89
89
  try:
90
90
  return int(parts[4])
91
91
  except ValueError:
92
92
  lgr.error(self._log_line_parse_failed(line))
93
93
  return None
94
- elif platform.system() == "Linux":
94
+ elif system == "Linux":
95
95
  for part in parts:
96
96
  if "pid=" in part:
97
97
  try:
@@ -99,7 +99,7 @@ class PortsRelease:
99
99
  except ValueError:
100
100
  lgr.error(self._log_line_parse_failed(line))
101
101
  return None
102
- elif platform.system() == "Darwin" and len(parts) > 1:
102
+ elif system == "Darwin" and len(parts) > 1:
103
103
  try:
104
104
  return int(parts[1])
105
105
  except ValueError:
@@ -113,11 +113,11 @@ class PortsRelease:
113
113
  def kill_process(self, pid: int) -> bool:
114
114
  """Kills the process with the specified PID."""
115
115
  try:
116
- cmd: Optional[str] = {
116
+ cmd: str = {
117
117
  'Windows': f'taskkill /F /PID {pid}',
118
118
  'Linux': f'kill -9 {pid}',
119
119
  'Darwin': f'kill -9 {pid}',
120
- }.get(platform.system())
120
+ }.get(platform.system(), "") # fallback to empty string
121
121
  if not cmd:
122
122
  lgr.error(self._log_unsupported_os())
123
123
  return False
@@ -132,7 +132,7 @@ class PortsRelease:
132
132
  lgr.error(self._log_unexpected_error(e))
133
133
  return False
134
134
 
135
- def release_all(self, ports: Optional[list[int]] = None) -> None:
135
+ def release_all(self, ports: list[int] | None = None) -> None:
136
136
  try:
137
137
  ports_to_release: list[int] = self.default_ports if ports is None else ports
138
138
 
@@ -141,7 +141,7 @@ class PortsRelease:
141
141
  lgr.error(self._log_invalid_port(port))
142
142
  continue
143
143
 
144
- pid: Optional[int] = self.get_pid_by_port(port)
144
+ pid: int | None = self.get_pid_by_port(port)
145
145
  if pid is None:
146
146
  lgr.info(self._log_no_process(port))
147
147
  continue
@@ -0,0 +1,35 @@
1
+ from functools import wraps
2
+ import time
3
+
4
+
5
+ class Timer:
6
+ def __init__(self, precision=4, verbose=False):
7
+ self.precision = precision
8
+ self.verbose = verbose
9
+
10
+ def timeit(self, func):
11
+ @wraps(func)
12
+ def timeit_wrapper(*args, **kwargs):
13
+ start_time = time.perf_counter_ns()
14
+ result = func(*args, **kwargs)
15
+ end_time = time.perf_counter_ns()
16
+ total_ns = end_time - start_time
17
+
18
+ if total_ns < 1_000: # 1μs
19
+ value = total_ns
20
+ unit = "ns"
21
+ elif total_ns < 1_000_000: # < 1ms
22
+ value = total_ns / 1_000
23
+ unit = "μs"
24
+ elif total_ns < 1_000_000_000: # < 1s
25
+ value = total_ns / 1_000_000
26
+ unit = "ms"
27
+ else:
28
+ value = total_ns / 1_000_000_000
29
+ unit = "s"
30
+
31
+ extra_info = f'{args} {kwargs} ' if self.verbose else ''
32
+ print(f'{func.__name__} {extra_info}took {value:.{self.precision}f} [{unit}]')
33
+ return result
34
+
35
+ return timeit_wrapper
@@ -1,3 +1,7 @@
1
+ """nano-dev-utils - A collection of small Python utilities for developers.
2
+ Copyright (c) 2025 Yaron Dayan
3
+ """
4
+
1
5
  from .testing_timer import TestTimer
2
6
  from .testing_dynamic_importer import TestImporter
3
7
  from .testing_release_ports import (TestPortsRelease,
@@ -8,6 +8,7 @@ CLIENT_PORT = rp.INSPECTOR_CLIENT
8
8
 
9
9
 
10
10
  class TestPortsRelease(unittest.TestCase):
11
+ PORTS_RELEASE_OBJ = 'testing_release_ports.rp.PortsRelease'
11
12
 
12
13
  def setUp(self):
13
14
  self.ports_release = rp.PortsRelease()
@@ -18,6 +19,10 @@ class TestPortsRelease(unittest.TestCase):
18
19
  patch.object(rp, 'lgr', self.mock_logger).start()
19
20
  self.addCleanup(patch.stopall)
20
21
 
22
+ @staticmethod
23
+ def _encode_dict(input_dict: dict) -> bytes:
24
+ return b' '.join(str(v).encode() for v in input_dict.values())
25
+
21
26
  @staticmethod
22
27
  def remove_file_handlers():
23
28
  """Temporarily remove any file handlers from the root logger"""
@@ -30,54 +35,86 @@ class TestPortsRelease(unittest.TestCase):
30
35
  root_logger.removeHandler(handler)
31
36
  handler.close()
32
37
 
38
+ def _mock_pid_retrieval(self, mock_popen: unittest.mock.MagicMock,
39
+ entry: dict,
40
+ port: int) -> (int, bytes):
41
+ mock_process = unittest.mock.MagicMock()
42
+ encoded_entry = self._encode_dict(entry)
43
+ mock_process.communicate.return_value = (encoded_entry, '')
44
+ mock_popen.return_value = mock_process
45
+ pid = self.ports_release.get_pid_by_port(port)
46
+ return pid, encoded_entry
47
+
33
48
  def tearDown(self):
34
49
  patch.stopall()
35
50
 
36
51
  def test_get_pid_by_port_linux_success(self):
37
52
  with patch('platform.system', return_value='Linux'):
38
53
  with patch('subprocess.Popen') as mock_popen:
39
- mock_process = unittest.mock.MagicMock()
40
- mock_process.communicate.return_value = (b"tcp6 0 0 :::8080 "
41
- b":::* users:((\"python3\",)"
42
- b" pid=1234 fd=4)\n", b"")
43
- mock_popen.return_value = mock_process
44
- pid = self.ports_release.get_pid_by_port(8080)
45
- self.assertEqual(pid, 1234)
46
- mock_popen.assert_called_once_with('ss -lntp | grep :8080',
54
+ # ss - lntp command response structure
55
+ port = 8080 # local port
56
+ peer_port = '*'
57
+ local_addr = '::' # :: - listening to all available interfaces
58
+ peer_addr = '::'
59
+ _pid = 1234
60
+ fd = 4 # file descriptor
61
+ ss_entry = {
62
+ 'netid': 'tcp6',
63
+ 'rx_q': 0,
64
+ 'tx_q': 0,
65
+ 'local_addr_port': f'{local_addr}:{port}',
66
+ 'peer_addr_port': f'{peer_addr}:{peer_port}',
67
+ 'process_name': 'users:python3',
68
+ 'pid': f'pid={_pid}',
69
+ 'fd': f'fd={fd}'
70
+ }
71
+
72
+ pid, _ = self._mock_pid_retrieval(mock_popen, ss_entry, port)
73
+ self.assertEqual(pid, _pid)
74
+ mock_popen.assert_called_once_with(f'ss -lntp | grep :{port}',
47
75
  shell=True, stdout=unittest.mock.ANY,
48
76
  stderr=unittest.mock.ANY)
49
77
 
50
78
  def test_get_pid_by_port_windows_success(self):
51
79
  with patch('platform.system', return_value='Windows'):
52
80
  with patch('subprocess.Popen') as mock_popen:
53
- mock_process = unittest.mock.MagicMock()
54
- mock_process.communicate.return_value = (b"TCP 0.0.0.0:"
55
- b"9000 "
56
- b"0.0.0.0:0"
57
- b" "
58
- b"LISTENING"
59
- b" 5678\n", b"")
60
- mock_popen.return_value = mock_process
61
- pid = self.ports_release.get_pid_by_port(9000)
62
- self.assertEqual(pid, 5678)
63
- mock_popen.assert_called_once_with('netstat -ano | findstr :9000',
81
+ # netstat -ano command response structure (Windows)
82
+ port = 9000
83
+ _pid = 5678
84
+ netstat_entry = {
85
+ 'protocol': 'TCP',
86
+ 'local_addr_and_port ': f'0.0.0.0:{port}',
87
+ 'remote_addr_and_port': '0.0.0.0:0',
88
+ 'state_and_pid': f'LISTENING {_pid}\n',
89
+ }
90
+
91
+ pid, _ = self._mock_pid_retrieval(mock_popen, netstat_entry, port)
92
+ self.assertEqual(pid, _pid)
93
+ mock_popen.assert_called_once_with(f'netstat -ano | findstr :{port}',
64
94
  shell=True, stdout=unittest.mock.ANY,
65
95
  stderr=unittest.mock.ANY)
66
96
 
67
97
  def test_get_pid_by_port_darwin_success(self):
68
98
  with patch('platform.system', return_value='Darwin'):
69
99
  with patch('subprocess.Popen') as mock_popen:
70
- mock_process = unittest.mock.MagicMock()
71
- mock_process.communicate.return_value = (b"python3 "
72
- b"1111 user "
73
- b"10u IPv4 "
74
- b"0xabcdef0123456789"
75
- b" 0t0 TCP *:"
76
- b"7000 (LISTEN)\n", b"")
77
- mock_popen.return_value = mock_process
78
- pid = self.ports_release.get_pid_by_port(7000)
79
- self.assertEqual(pid, 1111)
80
- mock_popen.assert_called_once_with('lsof -i :7000',
100
+ # lsof -i command response structure (MacOS)
101
+ port = 7000
102
+ _pid = 1111
103
+ lsof_entry = {
104
+ 'command': 'python3', # Process name
105
+ 'pid': f'{_pid}', # Process ID (integer)
106
+ 'user': 'user', # User running the process
107
+ 'fd': '10u', # File descriptor (read/write)
108
+ 'type': 'IPv4', # Network connection type (IPv4/IPv6)
109
+ 'device': '0xabcdef0123456789', # Kernel device identifier
110
+ 'size_off': '0t0', # Size/offset (0 for sockets)
111
+ 'protocol': 'TCP', # Protocol (TCP/UDP)
112
+ 'name': f'*:{port} (LISTEN)' # Combined address & state (optional)
113
+ }
114
+
115
+ pid, _ = self._mock_pid_retrieval(mock_popen, lsof_entry, port)
116
+ self.assertEqual(pid, _pid)
117
+ mock_popen.assert_called_once_with(f'lsof -i :{port}',
81
118
  shell=True, stdout=unittest.mock.ANY,
82
119
  stderr=unittest.mock.ANY)
83
120
 
@@ -92,7 +129,7 @@ class TestPortsRelease(unittest.TestCase):
92
129
  with patch('platform.system', return_value='Linux'):
93
130
  with patch('subprocess.Popen') as mock_popen:
94
131
  mock_process = unittest.mock.MagicMock()
95
- mock_process.communicate.return_value = (b"", b"")
132
+ mock_process.communicate.return_value = (b'', b'')
96
133
  mock_popen.return_value = mock_process
97
134
  pid = self.ports_release.get_pid_by_port(9999)
98
135
  self.assertIsNone(pid)
@@ -100,38 +137,45 @@ class TestPortsRelease(unittest.TestCase):
100
137
  def test_get_pid_by_port_command_error(self):
101
138
  with patch('platform.system', return_value='Linux'):
102
139
  with patch('subprocess.Popen') as mock_popen:
140
+ err = 'Error occurred'
103
141
  mock_process = unittest.mock.MagicMock()
104
- mock_process.communicate.return_value = (b"", b"Error occurred")
142
+ mock_process.communicate.return_value = (b'', err.encode())
105
143
  mock_popen.return_value = mock_process
106
144
  pid = self.ports_release.get_pid_by_port(80)
107
145
  self.assertIsNone(pid)
108
- self.mock_logger.error.assert_called_once_with("Error running "
109
- "command: Error occurred")
146
+ self.mock_logger.error.assert_called_once_with(f'Error running command: {err}')
110
147
 
111
148
  def test_get_pid_by_port_parse_error(self):
112
149
  with patch('platform.system', return_value='Linux'):
113
150
  with patch('subprocess.Popen') as mock_popen:
114
- mock_process = unittest.mock.MagicMock()
115
- mock_process.communicate.return_value = (b"tcp6 0"
116
- b" 0 "
117
- b":::8080"
118
- b" "
119
- b":::* users:((\"python3\","
120
- b"pid=invalid,fd=4))\n", b"")
121
- mock_popen.return_value = mock_process
122
- pid = self.ports_release.get_pid_by_port(8080)
151
+ # ss - lntp command response structure
152
+ port = 8080 # local port
153
+ peer_port = '*'
154
+ local_addr = '::' # :: - listening to all available interfaces
155
+ peer_addr = '::'
156
+ _pid = 'invalid'
157
+ fd = 4 # file descriptor
158
+ ss_entry = {
159
+ 'netid': 'tcp6',
160
+ 'rx_q': 0,
161
+ 'tx_q': 0,
162
+ 'local_addr_port': f'{local_addr}:{port}',
163
+ 'peer_addr_port': f'{peer_addr}:{peer_port}',
164
+ 'process_name': 'users:python3',
165
+ 'pid': f'pid={_pid}',
166
+ 'fd': f'fd={fd}'
167
+ }
168
+
169
+ pid, enc_entry = self._mock_pid_retrieval(mock_popen, ss_entry, port)
123
170
  self.assertIsNone(pid)
124
- self.mock_logger.error.assert_called_once_with("Could not parse PID "
125
- "from line: tcp6 0"
126
- " 0 :::8080"
127
- " :::*"
128
- " users:((\"python3\","
129
- "pid=invalid,fd=4))")
171
+ self.mock_logger.error.assert_called_once_with(
172
+ f'Could not parse PID from line: {enc_entry.decode()}')
130
173
 
131
174
  def test_get_pid_by_port_unexpected_exception(self):
132
175
  with patch('platform.system', return_value='Linux'):
133
176
  with patch('subprocess.Popen', side_effect=Exception("Unexpected")):
134
- pid = self.ports_release.get_pid_by_port(1234)
177
+ port = 1234
178
+ pid = self.ports_release.get_pid_by_port(port)
135
179
  self.assertIsNone(pid)
136
180
  self.mock_logger.error.assert_called_once_with("An unexpected "
137
181
  "error occurred: Unexpected")
@@ -139,13 +183,14 @@ class TestPortsRelease(unittest.TestCase):
139
183
  def test_kill_process_success(self):
140
184
  with patch('platform.system', return_value='Linux'):
141
185
  with patch('subprocess.Popen') as mock_popen:
186
+ port = 5678
142
187
  mock_process = unittest.mock.MagicMock()
143
188
  mock_process.returncode = 0
144
- mock_process.communicate.return_value = (b"", b"")
189
+ mock_process.communicate.return_value = (b'', b'')
145
190
  mock_popen.return_value = mock_process
146
- result = self.ports_release.kill_process(5678)
191
+ result = self.ports_release.kill_process(port)
147
192
  self.assertTrue(result)
148
- mock_popen.assert_called_once_with('kill -9 5678',
193
+ mock_popen.assert_called_once_with(f'kill -9 {port}',
149
194
  shell=True, stderr=unittest.mock.ANY)
150
195
 
151
196
  def test_kill_process_fail(self):
@@ -155,16 +200,17 @@ class TestPortsRelease(unittest.TestCase):
155
200
  with patch('subprocess.Popen') as mock_popen:
156
201
  mock_process = unittest.mock.MagicMock()
157
202
  mock_process.returncode = 1
158
- mock_process.communicate.return_value = (b"", err.encode('utf-8'))
203
+ mock_process.communicate.return_value = (b'', err.encode())
159
204
  mock_popen.return_value = mock_process
160
205
  result = self.ports_release.kill_process(pid)
161
206
  self.assertFalse(result)
162
207
  self.mock_logger.error.assert_called_once_with(self.ports_release.
163
- _log_terminate_failed(pid=pid, error="Access denied"))
208
+ _log_terminate_failed(pid=pid, error=err))
164
209
 
165
210
  def test_kill_process_unsupported_os(self):
166
211
  with patch('platform.system', return_value='UnsupportedOS'):
167
- result = self.ports_release.kill_process(9999)
212
+ pid = 9999
213
+ result = self.ports_release.kill_process(pid)
168
214
  self.assertFalse(result)
169
215
  self.mock_logger.error.assert_called_once_with(self.ports_release.
170
216
  _log_unsupported_os())
@@ -174,13 +220,14 @@ class TestPortsRelease(unittest.TestCase):
174
220
  with (patch('platform.system', return_value='Linux')):
175
221
  with patch('subprocess.Popen',
176
222
  side_effect=err):
177
- result = self.ports_release.kill_process(4321)
223
+ pid = 4321
224
+ result = self.ports_release.kill_process(pid)
178
225
  self.assertFalse(result)
179
226
  self.mock_logger.error.assert_called_once_with(self.ports_release.
180
227
  _log_unexpected_error(err))
181
228
 
182
- @patch('testing_release_ports.rp.PortsRelease.get_pid_by_port')
183
- @patch('testing_release_ports.rp.PortsRelease.kill_process')
229
+ @patch(f'{PORTS_RELEASE_OBJ}.get_pid_by_port')
230
+ @patch(f'{PORTS_RELEASE_OBJ}.kill_process')
184
231
  def test_release_all_default_ports_success(self, mock_kill, mock_get_pid):
185
232
  pid1, pid2 = 1111, 2222
186
233
  mock_get_pid.side_effect = [pid1, pid2]
@@ -200,28 +247,28 @@ class TestPortsRelease(unittest.TestCase):
200
247
  _log_process_terminated(pid2, CLIENT_PORT))
201
248
 
202
249
  def test_release_all_invalid_port(self):
203
- with patch('testing_release_ports.rp.PortsRelease.get_pid_by_port') as mock_get_pid:
204
- with patch('testing_release_ports.rp.PortsRelease.kill_process') as mock_kill:
250
+ with patch(f'{self.PORTS_RELEASE_OBJ}.get_pid_by_port') as mock_get_pid:
251
+ with patch(f'{self.PORTS_RELEASE_OBJ}.kill_process') as mock_kill:
205
252
  # Make get_pid_by_port return None for the valid ports in this test
206
- port = "invalid"
253
+ ports = ["invalid", 1234, 5678]
207
254
  mock_get_pid.side_effect = [None, None]
208
- self.ports_release.release_all(ports=[1234, "invalid", 5678])
209
- mock_get_pid.assert_any_call(1234)
210
- mock_get_pid.assert_any_call(5678)
255
+ self.ports_release.release_all(ports=ports)
256
+ mock_get_pid.assert_any_call(ports[1])
257
+ mock_get_pid.assert_any_call(ports[2])
211
258
  self.assertEqual(mock_get_pid.call_count, 2)
212
259
  mock_kill.assert_not_called()
213
260
  self.mock_logger.error.assert_called_once_with(self.ports_release.
214
- _log_invalid_port(port))
261
+ _log_invalid_port(ports[0]))
215
262
 
216
263
  def test_release_all_unexpected_exception(self):
217
264
  err = Exception("Release all error")
218
- with patch('testing_release_ports.'
219
- 'rp.PortsRelease.get_pid_by_port',
220
- side_effect=err):
221
- self.ports_release.release_all(ports=[9010])
265
+ with patch(f'{self.PORTS_RELEASE_OBJ}.'
266
+ f'get_pid_by_port', side_effect=err):
267
+ port = 9010
268
+ self.ports_release.release_all(ports=[port])
222
269
  self.mock_logger.error.assert_called_once_with(self.ports_release.
223
270
  _log_unexpected_error(err))
224
- self.ports_release.get_pid_by_port.assert_called_once_with(9010)
271
+ self.ports_release.get_pid_by_port.assert_called_once_with(port)
225
272
 
226
273
 
227
274
  if __name__ == '__main__':
@@ -0,0 +1,157 @@
1
+ from unittest import TestCase, main
2
+ from unittest.mock import patch
3
+ import threading
4
+ import time
5
+ from src.nano_dev_utils.timers import Timer
6
+
7
+
8
+ class TestTimer(TestCase):
9
+
10
+ def test_initialization(self):
11
+ timer = Timer()
12
+ self.assertEqual(timer.precision, 4)
13
+ self.assertFalse(timer.verbose)
14
+ timer_custom = Timer(precision=6, verbose=True)
15
+ self.assertEqual(timer_custom.precision, 6)
16
+ self.assertTrue(timer_custom.verbose)
17
+
18
+ @patch('time.perf_counter_ns')
19
+ @patch('builtins.print')
20
+ def test_timeit_simple(self, mock_print, mock_perf_counter_ns):
21
+ mock_perf_counter_ns.side_effect = [0, 9.23467e5]
22
+ timer = Timer(precision=2)
23
+
24
+ @timer.timeit
25
+ def sample_function():
26
+ return "result"
27
+
28
+ result = sample_function()
29
+ self.assertEqual(result, "result")
30
+ mock_perf_counter_ns.assert_any_call()
31
+ mock_print.assert_called_once_with('sample_function took 923.47 [μs]')
32
+
33
+ @patch('time.perf_counter_ns')
34
+ @patch('builtins.print')
35
+ def test_timeit_no_args_kwargs(self, mock_print, mock_perf_counter_ns):
36
+ mock_perf_counter_ns.side_effect = [1.0, 1.5]
37
+ timer = Timer(precision=2, verbose=True)
38
+
39
+ @timer.timeit
40
+ def yet_another_function():
41
+ return "yet another result"
42
+
43
+ result = yet_another_function()
44
+ self.assertEqual(result, "yet another result")
45
+ mock_perf_counter_ns.assert_any_call()
46
+ mock_print.assert_called_once_with("yet_another_function () {} took 0.50 [ns]")
47
+
48
+ @patch('builtins.print')
49
+ def test_multithreaded_timing(self, mock_print):
50
+ """Test timer works correctly across threads"""
51
+ timer = Timer()
52
+ results = []
53
+
54
+ @timer.timeit
55
+ def threaded_operation():
56
+ time.sleep(0.1)
57
+ return threading.get_ident()
58
+
59
+ def run_in_thread():
60
+ results.append(threaded_operation())
61
+
62
+ threads = [threading.Thread(target=run_in_thread) for _ in range(3)]
63
+
64
+ for t in threads:
65
+ t.start()
66
+ for t in threads:
67
+ t.join()
68
+
69
+ # Should have 3 print calls (one per thread)
70
+ self.assertEqual(mock_print.call_count, 3)
71
+ # All thread IDs should be different
72
+ self.assertEqual(len(set(results)), 3)
73
+
74
+ @patch('time.perf_counter_ns')
75
+ @patch('builtins.print')
76
+ def test_verbose_mode(self, mock_print, mock_perf_counter_ns):
77
+ """Test that verbose mode includes positional and
78
+ keyword arguments in output and preserves the wrapped func result"""
79
+ mock_perf_counter_ns.side_effect = [1e4, 5.23456e4]
80
+ verbose_timer = Timer(verbose=True)
81
+
82
+ @verbose_timer.timeit
83
+ def func_with_args(a, b, c=3):
84
+ return a + b + c
85
+
86
+ res = func_with_args(1, 2, c=4)
87
+ output = mock_print.call_args[0][0]
88
+ self.assertIn('(1, 2)', output) # checking positional args
89
+ self.assertIn("'c': 4", output) # checking kwargs
90
+ mock_print.assert_called_once_with("func_with_args (1, 2) {'c': 4} took 42.3456 [μs]")
91
+ self.assertEqual(res, 7) # checking returned value preservation
92
+
93
+ @patch('builtins.print')
94
+ def test_nested_timers(self, mock_print):
95
+ """Test that nested timers work correctly"""
96
+ timer = Timer()
97
+
98
+ @timer.timeit
99
+ def outer():
100
+ @timer.timeit
101
+ def inner():
102
+ time.sleep(0.1)
103
+
104
+ return inner()
105
+
106
+ outer()
107
+
108
+ # Should have two print calls (inner and outer)
109
+ self.assertEqual(mock_print.call_count, 2)
110
+ inner_output = mock_print.call_args_list[0][0][0]
111
+ outer_output = mock_print.call_args_list[1][0][0]
112
+
113
+ inner_time = float(inner_output.split('took ')[1].split(' [')[0])
114
+ outer_time = float(outer_output.split('took ')[1].split(' [')[0])
115
+
116
+ self.assertGreater(outer_time, inner_time)
117
+
118
+ @patch('time.perf_counter_ns')
119
+ @patch('builtins.print')
120
+ def test_unit_scaling_logic(self, mock_print, mock_perf_counter_ns):
121
+ """Test the time unit selection logic directly"""
122
+ test_cases = [
123
+ (999, "ns"), # < 1μs
124
+ (1000, "μs"), # 1μs
125
+ (999999, "μs"), # < 1ms
126
+ (1000000, "ms"), # 1ms
127
+ (999999999, "ms"), # < 1s
128
+ (1000000000, "s") # 1s
129
+ ]
130
+
131
+ for ns, expected_unit in test_cases:
132
+ mock_perf_counter_ns.side_effect = [0, ns]
133
+ timer = Timer(precision=2)
134
+
135
+ @timer.timeit
136
+ def dummy():
137
+ pass
138
+
139
+ dummy()
140
+ output = mock_print.call_args[0][0]
141
+ self.assertIn(expected_unit, output)
142
+
143
+ def test_function_metadata_preserved(self):
144
+ """Test that function metadata (name, docstring) is preserved"""
145
+ timer = Timer(precision=3)
146
+
147
+ @timer.timeit
148
+ def dummy_func():
149
+ """Test docstring"""
150
+ pass
151
+
152
+ self.assertEqual(dummy_func.__name__, 'dummy_func')
153
+ self.assertEqual(dummy_func.__doc__, 'Test docstring')
154
+
155
+
156
+ if __name__ == '__main__':
157
+ main()
@@ -1,69 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ChangeListManager">
4
- <list default="true" id="96bbbefe-efb6-42c4-93da-e069ac3e654f" name="Changes" comment="" />
5
- <option name="SHOW_DIALOG" value="false" />
6
- <option name="HIGHLIGHT_CONFLICTS" value="true" />
7
- <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
8
- <option name="LAST_RESOLUTION" value="IGNORE" />
9
- </component>
10
- <component name="Git.Settings">
11
- <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
12
- </component>
13
- <component name="ProjectColorInfo">{
14
- &quot;associatedIndex&quot;: 5
15
- }</component>
16
- <component name="ProjectId" id="2wBUoAyjEyavXXmNnhB2xcAn4VQ" />
17
- <component name="ProjectViewState">
18
- <option name="hideEmptyMiddlePackages" value="true" />
19
- <option name="showLibraryContents" value="true" />
20
- <option name="showMembers" value="true" />
21
- </component>
22
- <component name="PropertiesComponent">{
23
- &quot;keyToString&quot;: {
24
- &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
25
- &quot;git-widget-placeholder&quot;: &quot;master&quot;,
26
- &quot;last_opened_file_path&quot;: &quot;C:/GitHubWS/nano_dev_utils&quot;
27
- }
28
- }</component>
29
- <component name="RunManager">
30
- <configuration name="Python tests in tests" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
31
- <module name="nano_dev_utils" />
32
- <option name="ENV_FILES" value="" />
33
- <option name="INTERPRETER_OPTIONS" value="" />
34
- <option name="PARENT_ENVS" value="true" />
35
- <option name="SDK_HOME" value="" />
36
- <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
37
- <option name="IS_MODULE_SDK" value="true" />
38
- <option name="ADD_CONTENT_ROOTS" value="true" />
39
- <option name="ADD_SOURCE_ROOTS" value="true" />
40
- <option name="_new_additionalArguments" value="&quot;&quot;" />
41
- <option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
42
- <option name="_new_targetType" value="&quot;PATH&quot;" />
43
- <method v="2" />
44
- </configuration>
45
- <recent_temporary>
46
- <list>
47
- <item itemvalue="Python tests.Python tests in tests" />
48
- </list>
49
- </recent_temporary>
50
- </component>
51
- <component name="SharedIndexes">
52
- <attachedChunks>
53
- <set>
54
- <option value="bundled-python-sdk-5b207ade9991-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.17890.14" />
55
- </set>
56
- </attachedChunks>
57
- </component>
58
- <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
59
- <component name="TaskManager">
60
- <task active="true" id="Default" summary="Default task">
61
- <changelist id="96bbbefe-efb6-42c4-93da-e069ac3e654f" name="Changes" comment="" />
62
- <created>1745514632336</created>
63
- <option name="number" value="Default" />
64
- <option name="presentableId" value="Default" />
65
- <updated>1745514632336</updated>
66
- </task>
67
- <servers />
68
- </component>
69
- </project>
@@ -1,24 +0,0 @@
1
- from functools import wraps
2
- import time
3
-
4
-
5
- class Timer:
6
- def __init__(self, precision=4, verbose=False):
7
- self.precision = precision
8
- self.verbose = verbose
9
-
10
- def timeit(self, func):
11
- @wraps(func)
12
- def timeit_wrapper(*args, **kwargs):
13
- start_time = time.perf_counter()
14
- result = func(*args, **kwargs)
15
- end_time = time.perf_counter()
16
- total_time = end_time - start_time
17
- extra_info = f'{args} {kwargs} ' if self.verbose else ''
18
- print(f'{func.__name__} {extra_info}took {total_time:.{self.precision}f} [s]')
19
- return result
20
- return timeit_wrapper
21
-
22
-
23
-
24
-
@@ -1,63 +0,0 @@
1
- import unittest
2
- from unittest.mock import patch
3
- from src.nano_dev_utils.timers import Timer
4
-
5
-
6
- class TestTimer(unittest.TestCase):
7
-
8
- def test_initialization(self):
9
- timer = Timer()
10
- self.assertEqual(timer.precision, 4)
11
- self.assertFalse(timer.verbose)
12
- timer_custom = Timer(precision=6, verbose=True)
13
- self.assertEqual(timer_custom.precision, 6)
14
- self.assertTrue(timer_custom.verbose)
15
-
16
- @patch('time.perf_counter')
17
- @patch('builtins.print')
18
- def test_timeit_simple(self, mock_print, mock_perf_counter):
19
- mock_perf_counter.side_effect = [0, 0.12345]
20
- timer = Timer(precision=5)
21
-
22
- @timer.timeit
23
- def sample_function():
24
- return "result"
25
-
26
- result = sample_function()
27
- self.assertEqual(result, "result")
28
- mock_perf_counter.assert_any_call()
29
- mock_print.assert_called_once_with('sample_function took 0.12345 [s]')
30
-
31
- @patch('time.perf_counter')
32
- @patch('builtins.print')
33
- def test_timeit_verbose(self, mock_print, mock_perf_counter):
34
- mock_perf_counter.side_effect = [0, 0.56789]
35
- timer = Timer(precision=3, verbose=True)
36
-
37
- @timer.timeit
38
- def another_function(arg1, kwarg1=None):
39
- return "another result"
40
-
41
- result = another_function(10, kwarg1="hello")
42
- self.assertEqual(result, "another result")
43
- mock_perf_counter.assert_any_call()
44
- mock_print.assert_called_once_with("another_function (10,) {'kwarg1': 'hello'} took 0.568 [s]")
45
-
46
- @patch('time.perf_counter')
47
- @patch('builtins.print')
48
- def test_timeit_no_args_kwargs(self, mock_print, mock_perf_counter):
49
- mock_perf_counter.side_effect = [1.0, 1.5]
50
- timer = Timer(precision=2, verbose=True)
51
-
52
- @timer.timeit
53
- def yet_another_function():
54
- return "yet another result"
55
-
56
- result = yet_another_function()
57
- self.assertEqual(result, "yet another result")
58
- mock_perf_counter.assert_any_call()
59
- mock_print.assert_called_once_with("yet_another_function () {} took 0.50 [s]")
60
-
61
-
62
- if __name__ == '__main__':
63
- unittest.main(argv=['first-arg-is-ignored'], exit=False)