nano-dev-utils 0.2.1__tar.gz → 0.3.3__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.
- nano_dev_utils-0.3.3/.idea/workspace.xml +246 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/PKG-INFO +2 -3
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/README.md +0 -1
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/pyproject.toml +5 -4
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/src/nano_dev_utils/__init__.py +2 -1
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/src/nano_dev_utils/release_ports.py +14 -14
- nano_dev_utils-0.3.3/src/nano_dev_utils/timers.py +35 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/tests/__init__.py +4 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/tests/testing_release_ports.py +118 -71
- nano_dev_utils-0.3.3/tests/testing_timer.py +157 -0
- nano_dev_utils-0.2.1/.idea/workspace.xml +0 -69
- nano_dev_utils-0.2.1/src/nano_dev_utils/timers.py +0 -24
- nano_dev_utils-0.2.1/tests/testing_timer.py +0 -63
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.gitignore +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.idea/.gitignore +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.idea/misc.xml +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.idea/modules.xml +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.idea/nano_dev_utils.iml +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.idea/vcs.xml +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/LICENSE.md +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/src/nano_dev_utils/dynamic_importer.py +0 -0
- {nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/tests/testing_dynamic_importer.py +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
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="version 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
|
+
"associatedIndex": 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
|
+
"last_opened_file_path": "C:/GitHubWS/nano_dev_utils",
|
|
38
|
+
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
|
|
39
|
+
}
|
|
40
|
+
}]]></component>
|
|
41
|
+
<component name="RunManager" selected="Python tests.Python tests in tests">
|
|
42
|
+
<configuration name="Python tests in testing_release_ports.py" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
|
|
43
|
+
<module name="nano_dev_utils" />
|
|
44
|
+
<option name="ENV_FILES" value="" />
|
|
45
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
46
|
+
<option name="PARENT_ENVS" value="true" />
|
|
47
|
+
<option name="SDK_HOME" value="" />
|
|
48
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
|
|
49
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
50
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
51
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
52
|
+
<option name="_new_additionalArguments" value="""" />
|
|
53
|
+
<option name="_new_target" value=""$PROJECT_DIR$/tests/testing_release_ports.py"" />
|
|
54
|
+
<option name="_new_targetType" value=""PATH"" />
|
|
55
|
+
<method v="2" />
|
|
56
|
+
</configuration>
|
|
57
|
+
<configuration name="Python tests in testing_timer.py" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
|
|
58
|
+
<module name="nano_dev_utils" />
|
|
59
|
+
<option name="ENV_FILES" value="" />
|
|
60
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
61
|
+
<option name="PARENT_ENVS" value="true" />
|
|
62
|
+
<option name="SDK_HOME" value="" />
|
|
63
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
|
|
64
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
65
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
66
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
67
|
+
<option name="_new_additionalArguments" value="""" />
|
|
68
|
+
<option name="_new_target" value=""$PROJECT_DIR$/tests/testing_timer.py"" />
|
|
69
|
+
<option name="_new_targetType" value=""PATH"" />
|
|
70
|
+
<method v="2" />
|
|
71
|
+
</configuration>
|
|
72
|
+
<configuration name="Python tests in tests" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
|
|
73
|
+
<module name="nano_dev_utils" />
|
|
74
|
+
<option name="ENV_FILES" value="" />
|
|
75
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
76
|
+
<option name="PARENT_ENVS" value="true" />
|
|
77
|
+
<option name="SDK_HOME" value="" />
|
|
78
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
|
|
79
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
80
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
81
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
82
|
+
<option name="_new_additionalArguments" value="""" />
|
|
83
|
+
<option name="_new_target" value=""$PROJECT_DIR$/tests"" />
|
|
84
|
+
<option name="_new_targetType" value=""PATH"" />
|
|
85
|
+
<method v="2" />
|
|
86
|
+
</configuration>
|
|
87
|
+
<recent_temporary>
|
|
88
|
+
<list>
|
|
89
|
+
<item itemvalue="Python tests.Python tests in tests" />
|
|
90
|
+
<item itemvalue="Python tests.Python tests in testing_timer.py" />
|
|
91
|
+
<item itemvalue="Python tests.Python tests in testing_release_ports.py" />
|
|
92
|
+
<item itemvalue="Python tests.Python tests in testing_release_ports.py" />
|
|
93
|
+
</list>
|
|
94
|
+
</recent_temporary>
|
|
95
|
+
</component>
|
|
96
|
+
<component name="SharedIndexes">
|
|
97
|
+
<attachedChunks>
|
|
98
|
+
<set>
|
|
99
|
+
<option value="bundled-python-sdk-5b207ade9991-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.17890.14" />
|
|
100
|
+
</set>
|
|
101
|
+
</attachedChunks>
|
|
102
|
+
</component>
|
|
103
|
+
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
104
|
+
<component name="TaskManager">
|
|
105
|
+
<task active="true" id="Default" summary="Default task">
|
|
106
|
+
<changelist id="96bbbefe-efb6-42c4-93da-e069ac3e654f" name="Changes" comment="" />
|
|
107
|
+
<created>1745514632336</created>
|
|
108
|
+
<option name="number" value="Default" />
|
|
109
|
+
<option name="presentableId" value="Default" />
|
|
110
|
+
<updated>1745514632336</updated>
|
|
111
|
+
</task>
|
|
112
|
+
<task id="LOCAL-00001" summary="renamed to formal PYPI package name">
|
|
113
|
+
<option name="closed" value="true" />
|
|
114
|
+
<created>1745516978652</created>
|
|
115
|
+
<option name="number" value="00001" />
|
|
116
|
+
<option name="presentableId" value="LOCAL-00001" />
|
|
117
|
+
<option name="project" value="LOCAL" />
|
|
118
|
+
<updated>1745516978652</updated>
|
|
119
|
+
</task>
|
|
120
|
+
<task id="LOCAL-00002" summary="improving unit test for release_ports (WIP)">
|
|
121
|
+
<option name="closed" value="true" />
|
|
122
|
+
<created>1745586382614</created>
|
|
123
|
+
<option name="number" value="00002" />
|
|
124
|
+
<option name="presentableId" value="LOCAL-00002" />
|
|
125
|
+
<option name="project" value="LOCAL" />
|
|
126
|
+
<updated>1745586382614</updated>
|
|
127
|
+
</task>
|
|
128
|
+
<task id="LOCAL-00003" summary="reminder - timers">
|
|
129
|
+
<option name="closed" value="true" />
|
|
130
|
+
<created>1745593807782</created>
|
|
131
|
+
<option name="number" value="00003" />
|
|
132
|
+
<option name="presentableId" value="LOCAL-00003" />
|
|
133
|
+
<option name="project" value="LOCAL" />
|
|
134
|
+
<updated>1745593807782</updated>
|
|
135
|
+
</task>
|
|
136
|
+
<task id="LOCAL-00004" summary="release_ports unit-testing improvements">
|
|
137
|
+
<option name="closed" value="true" />
|
|
138
|
+
<created>1745676371943</created>
|
|
139
|
+
<option name="number" value="00004" />
|
|
140
|
+
<option name="presentableId" value="LOCAL-00004" />
|
|
141
|
+
<option name="project" value="LOCAL" />
|
|
142
|
+
<updated>1745676371943</updated>
|
|
143
|
+
</task>
|
|
144
|
+
<task id="LOCAL-00005" summary="minor updates">
|
|
145
|
+
<option name="closed" value="true" />
|
|
146
|
+
<created>1745679672667</created>
|
|
147
|
+
<option name="number" value="00005" />
|
|
148
|
+
<option name="presentableId" value="LOCAL-00005" />
|
|
149
|
+
<option name="project" value="LOCAL" />
|
|
150
|
+
<updated>1745679672667</updated>
|
|
151
|
+
</task>
|
|
152
|
+
<task id="LOCAL-00006" summary="minor updates: _mock_pid_retrieval">
|
|
153
|
+
<option name="closed" value="true" />
|
|
154
|
+
<created>1745703394575</created>
|
|
155
|
+
<option name="number" value="00006" />
|
|
156
|
+
<option name="presentableId" value="LOCAL-00006" />
|
|
157
|
+
<option name="project" value="LOCAL" />
|
|
158
|
+
<updated>1745703394575</updated>
|
|
159
|
+
</task>
|
|
160
|
+
<task id="LOCAL-00007" summary="Improving code for Timer">
|
|
161
|
+
<option name="closed" value="true" />
|
|
162
|
+
<created>1745709597198</created>
|
|
163
|
+
<option name="number" value="00007" />
|
|
164
|
+
<option name="presentableId" value="LOCAL-00007" />
|
|
165
|
+
<option name="project" value="LOCAL" />
|
|
166
|
+
<updated>1745709597198</updated>
|
|
167
|
+
</task>
|
|
168
|
+
<task id="LOCAL-00008" summary="Optional: modernized style for py>=3.10">
|
|
169
|
+
<option name="closed" value="true" />
|
|
170
|
+
<created>1745712059758</created>
|
|
171
|
+
<option name="number" value="00008" />
|
|
172
|
+
<option name="presentableId" value="LOCAL-00008" />
|
|
173
|
+
<option name="project" value="LOCAL" />
|
|
174
|
+
<updated>1745712059758</updated>
|
|
175
|
+
</task>
|
|
176
|
+
<task id="LOCAL-00009" summary="essential field updates">
|
|
177
|
+
<option name="closed" value="true" />
|
|
178
|
+
<created>1745712315250</created>
|
|
179
|
+
<option name="number" value="00009" />
|
|
180
|
+
<option name="presentableId" value="LOCAL-00009" />
|
|
181
|
+
<option name="project" value="LOCAL" />
|
|
182
|
+
<updated>1745712315250</updated>
|
|
183
|
+
</task>
|
|
184
|
+
<task id="LOCAL-00010" summary="dunder init update">
|
|
185
|
+
<option name="closed" value="true" />
|
|
186
|
+
<created>1745712644438</created>
|
|
187
|
+
<option name="number" value="00010" />
|
|
188
|
+
<option name="presentableId" value="LOCAL-00010" />
|
|
189
|
+
<option name="project" value="LOCAL" />
|
|
190
|
+
<updated>1745712644438</updated>
|
|
191
|
+
</task>
|
|
192
|
+
<task id="LOCAL-00011" summary="minor update">
|
|
193
|
+
<option name="closed" value="true" />
|
|
194
|
+
<created>1745713148443</created>
|
|
195
|
+
<option name="number" value="00011" />
|
|
196
|
+
<option name="presentableId" value="LOCAL-00011" />
|
|
197
|
+
<option name="project" value="LOCAL" />
|
|
198
|
+
<updated>1745713148443</updated>
|
|
199
|
+
</task>
|
|
200
|
+
<task id="LOCAL-00012" summary="fixed authors and url for pip show">
|
|
201
|
+
<option name="closed" value="true" />
|
|
202
|
+
<created>1745713896439</created>
|
|
203
|
+
<option name="number" value="00012" />
|
|
204
|
+
<option name="presentableId" value="LOCAL-00012" />
|
|
205
|
+
<option name="project" value="LOCAL" />
|
|
206
|
+
<updated>1745713896439</updated>
|
|
207
|
+
</task>
|
|
208
|
+
<task id="LOCAL-00013" summary="version update">
|
|
209
|
+
<option name="closed" value="true" />
|
|
210
|
+
<created>1745713915283</created>
|
|
211
|
+
<option name="number" value="00013" />
|
|
212
|
+
<option name="presentableId" value="LOCAL-00013" />
|
|
213
|
+
<option name="project" value="LOCAL" />
|
|
214
|
+
<updated>1745713915283</updated>
|
|
215
|
+
</task>
|
|
216
|
+
<option name="localTasksCounter" value="14" />
|
|
217
|
+
<servers />
|
|
218
|
+
</component>
|
|
219
|
+
<component name="Vcs.Log.Tabs.Properties">
|
|
220
|
+
<option name="TAB_STATES">
|
|
221
|
+
<map>
|
|
222
|
+
<entry key="MAIN">
|
|
223
|
+
<value>
|
|
224
|
+
<State />
|
|
225
|
+
</value>
|
|
226
|
+
</entry>
|
|
227
|
+
</map>
|
|
228
|
+
</option>
|
|
229
|
+
</component>
|
|
230
|
+
<component name="VcsManagerConfiguration">
|
|
231
|
+
<MESSAGE value="renamed to formal PYPI package name" />
|
|
232
|
+
<MESSAGE value="improving unit test for release_ports (WIP)" />
|
|
233
|
+
<MESSAGE value="reminder - timers" />
|
|
234
|
+
<MESSAGE value="release_ports unit-testing improvements" />
|
|
235
|
+
<MESSAGE value="minor updates" />
|
|
236
|
+
<MESSAGE value="minor updates: _mock_pid_retrieval" />
|
|
237
|
+
<MESSAGE value="Improving code for Timer" />
|
|
238
|
+
<MESSAGE value="Optional: modernized style for py>=3.10" />
|
|
239
|
+
<MESSAGE value="essential field updates" />
|
|
240
|
+
<MESSAGE value="dunder init update" />
|
|
241
|
+
<MESSAGE value="minor update" />
|
|
242
|
+
<MESSAGE value="fixed authors and url for pip show" />
|
|
243
|
+
<MESSAGE value="version update" />
|
|
244
|
+
<option name="LAST_COMMIT_MESSAGE" value="version update" />
|
|
245
|
+
</component>
|
|
246
|
+
</project>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nano_dev_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
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.
|
|
3
|
+
version = "0.3.3"
|
|
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
|
-
"""
|
|
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
|
|
@@ -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:
|
|
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:
|
|
40
|
-
error:
|
|
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) ->
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
189
|
+
mock_process.communicate.return_value = (b'', b'')
|
|
145
190
|
mock_popen.return_value = mock_process
|
|
146
|
-
result = self.ports_release.kill_process(
|
|
191
|
+
result = self.ports_release.kill_process(port)
|
|
147
192
|
self.assertTrue(result)
|
|
148
|
-
mock_popen.assert_called_once_with('kill -9
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
183
|
-
@patch('
|
|
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('
|
|
204
|
-
with patch('
|
|
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
|
-
|
|
253
|
+
ports = ["invalid", 1234, 5678]
|
|
207
254
|
mock_get_pid.side_effect = [None, None]
|
|
208
|
-
self.ports_release.release_all(ports=
|
|
209
|
-
mock_get_pid.assert_any_call(
|
|
210
|
-
mock_get_pid.assert_any_call(
|
|
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(
|
|
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('
|
|
219
|
-
'
|
|
220
|
-
|
|
221
|
-
self.ports_release.release_all(ports=[
|
|
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(
|
|
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
|
-
"associatedIndex": 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
|
-
"keyToString": {
|
|
24
|
-
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
25
|
-
"git-widget-placeholder": "master",
|
|
26
|
-
"last_opened_file_path": "C:/GitHubWS/nano_dev_utils"
|
|
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="""" />
|
|
41
|
-
<option name="_new_target" value=""$PROJECT_DIR$/tests"" />
|
|
42
|
-
<option name="_new_targetType" value=""PATH"" />
|
|
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)
|
|
File without changes
|
|
File without changes
|
{nano_dev_utils-0.2.1 → nano_dev_utils-0.3.3}/.idea/inspectionProfiles/profiles_settings.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|