nano-dev-utils 0.5.5__tar.gz → 0.5.6__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.5.5 → nano_dev_utils-0.5.6}/PKG-INFO +2 -1
- {nano_dev_utils-0.5.5 → nano_dev_utils-0.5.6}/pyproject.toml +4 -2
- nano_dev_utils-0.5.5/.idea/.gitignore +0 -3
- nano_dev_utils-0.5.5/.idea/inspectionProfiles/profiles_settings.xml +0 -6
- nano_dev_utils-0.5.5/.idea/misc.xml +0 -7
- nano_dev_utils-0.5.5/.idea/modules.xml +0 -8
- nano_dev_utils-0.5.5/.idea/nano_dev_utils.iml +0 -10
- nano_dev_utils-0.5.5/.idea/vcs.xml +0 -6
- nano_dev_utils-0.5.5/.idea/workspace.xml +0 -100
- nano_dev_utils-0.5.5/src/nano_dev_utils/__init__.py +0 -18
- nano_dev_utils-0.5.5/src/nano_dev_utils/dynamic_importer.py +0 -27
- nano_dev_utils-0.5.5/src/nano_dev_utils/release_ports.py +0 -155
- nano_dev_utils-0.5.5/src/nano_dev_utils/timers.py +0 -38
- nano_dev_utils-0.5.5/tests/__init__.py +0 -10
- nano_dev_utils-0.5.5/tests/test_dynamic_importer.py +0 -62
- nano_dev_utils-0.5.5/tests/test_release_ports.py +0 -307
- nano_dev_utils-0.5.5/tests/test_timer.py +0 -159
- {nano_dev_utils-0.5.5 → nano_dev_utils-0.5.6}/.gitignore +0 -0
- {nano_dev_utils-0.5.5 → nano_dev_utils-0.5.6}/LICENSE +0 -0
- {nano_dev_utils-0.5.5 → nano_dev_utils-0.5.6}/README.md +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nano_dev_utils
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
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
|
+
Project-URL: license, https://github.com/yaronday/nano_dev_utils/blob/master/LICENSE
|
|
7
8
|
Author-email: Yaron Dayan <yaronday77@gmail.com>
|
|
8
9
|
License: MIT
|
|
9
10
|
License-File: LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nano_dev_utils"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.6"
|
|
4
4
|
|
|
5
5
|
authors = [
|
|
6
6
|
{ name="Yaron Dayan", email="yaronday77@gmail.com" },
|
|
@@ -26,9 +26,11 @@ test = [
|
|
|
26
26
|
[project.urls]
|
|
27
27
|
Homepage = "https://github.com/yaronday/nano_utils"
|
|
28
28
|
Issues = "https://github.com/yaronday/nano_utils/issues"
|
|
29
|
+
license = "https://github.com/yaronday/nano_dev_utils/blob/master/LICENSE"
|
|
29
30
|
|
|
30
31
|
[build-system]
|
|
31
32
|
requires = ["hatchling >= 1.26"]
|
|
32
33
|
build-backend = "hatchling.build"
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
[tool.hatch.build]
|
|
36
|
+
include = ["LICENSE"]
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="Black">
|
|
4
|
-
<option name="sdkName" value="Python 3.10" />
|
|
5
|
-
</component>
|
|
6
|
-
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
|
|
7
|
-
</project>
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="ProjectModuleManager">
|
|
4
|
-
<modules>
|
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/nano_dev_utils.iml" filepath="$PROJECT_DIR$/.idea/nano_dev_utils.iml" />
|
|
6
|
-
</modules>
|
|
7
|
-
</component>
|
|
8
|
-
</project>
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<module type="PYTHON_MODULE" version="4">
|
|
3
|
-
<component name="NewModuleRootManager">
|
|
4
|
-
<content url="file://$MODULE_DIR$">
|
|
5
|
-
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
|
6
|
-
</content>
|
|
7
|
-
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
|
|
8
|
-
<orderEntry type="sourceFolder" forTests="false" />
|
|
9
|
-
</component>
|
|
10
|
-
</module>
|
|
@@ -1,100 +0,0 @@
|
|
|
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="1859e23b-7665-4b92-98cc-65e07a208923" name="Changes" comment="license relative path">
|
|
8
|
-
<change beforePath="$PROJECT_DIR$/LICENSE.md" beforeDir="false" afterPath="$PROJECT_DIR$/LICENSE" afterDir="false" />
|
|
9
|
-
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
|
10
|
-
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
|
11
|
-
</list>
|
|
12
|
-
<option name="SHOW_DIALOG" value="false" />
|
|
13
|
-
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
14
|
-
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
15
|
-
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
16
|
-
</component>
|
|
17
|
-
<component name="Git.Settings">
|
|
18
|
-
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
19
|
-
</component>
|
|
20
|
-
<component name="ProjectColorInfo">{
|
|
21
|
-
"customColor": "",
|
|
22
|
-
"associatedIndex": 5
|
|
23
|
-
}</component>
|
|
24
|
-
<component name="ProjectId" id="2wVV0HQbcggEgX5PEayC1Mcx9Gd" />
|
|
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 tests.executor": "Run",
|
|
33
|
-
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
34
|
-
"git-widget-placeholder": "master",
|
|
35
|
-
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
|
|
36
|
-
}
|
|
37
|
-
}]]></component>
|
|
38
|
-
<component name="RunManager">
|
|
39
|
-
<configuration name="Python tests in tests" type="tests" factoryName="Autodetect" temporary="true" nameIsGenerated="true">
|
|
40
|
-
<module name="nano_dev_utils" />
|
|
41
|
-
<option name="ENV_FILES" value="" />
|
|
42
|
-
<option name="INTERPRETER_OPTIONS" value="" />
|
|
43
|
-
<option name="PARENT_ENVS" value="true" />
|
|
44
|
-
<option name="SDK_HOME" value="" />
|
|
45
|
-
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
|
|
46
|
-
<option name="IS_MODULE_SDK" value="true" />
|
|
47
|
-
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
48
|
-
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
49
|
-
<option name="_new_additionalArguments" value="""" />
|
|
50
|
-
<option name="_new_target" value=""$PROJECT_DIR$/tests"" />
|
|
51
|
-
<option name="_new_targetType" value=""PATH"" />
|
|
52
|
-
<method v="2" />
|
|
53
|
-
</configuration>
|
|
54
|
-
<recent_temporary>
|
|
55
|
-
<list>
|
|
56
|
-
<item itemvalue="Python tests.Python tests in tests" />
|
|
57
|
-
</list>
|
|
58
|
-
</recent_temporary>
|
|
59
|
-
</component>
|
|
60
|
-
<component name="SharedIndexes">
|
|
61
|
-
<attachedChunks>
|
|
62
|
-
<set>
|
|
63
|
-
<option value="bundled-python-sdk-5b207ade9991-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.17890.14" />
|
|
64
|
-
</set>
|
|
65
|
-
</attachedChunks>
|
|
66
|
-
</component>
|
|
67
|
-
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
68
|
-
<component name="TaskManager">
|
|
69
|
-
<task active="true" id="Default" summary="Default task">
|
|
70
|
-
<changelist id="1859e23b-7665-4b92-98cc-65e07a208923" name="Changes" comment="" />
|
|
71
|
-
<created>1746126505743</created>
|
|
72
|
-
<option name="number" value="Default" />
|
|
73
|
-
<option name="presentableId" value="Default" />
|
|
74
|
-
<updated>1746126505743</updated>
|
|
75
|
-
</task>
|
|
76
|
-
<task id="LOCAL-00001" summary=".">
|
|
77
|
-
<option name="closed" value="true" />
|
|
78
|
-
<created>1746126585369</created>
|
|
79
|
-
<option name="number" value="00001" />
|
|
80
|
-
<option name="presentableId" value="LOCAL-00001" />
|
|
81
|
-
<option name="project" value="LOCAL" />
|
|
82
|
-
<updated>1746126585369</updated>
|
|
83
|
-
</task>
|
|
84
|
-
<task id="LOCAL-00002" summary="license relative path">
|
|
85
|
-
<option name="closed" value="true" />
|
|
86
|
-
<created>1746126934201</created>
|
|
87
|
-
<option name="number" value="00002" />
|
|
88
|
-
<option name="presentableId" value="LOCAL-00002" />
|
|
89
|
-
<option name="project" value="LOCAL" />
|
|
90
|
-
<updated>1746126934201</updated>
|
|
91
|
-
</task>
|
|
92
|
-
<option name="localTasksCounter" value="3" />
|
|
93
|
-
<servers />
|
|
94
|
-
</component>
|
|
95
|
-
<component name="VcsManagerConfiguration">
|
|
96
|
-
<MESSAGE value="." />
|
|
97
|
-
<MESSAGE value="license relative path" />
|
|
98
|
-
<option name="LAST_COMMIT_MESSAGE" value="license relative path" />
|
|
99
|
-
</component>
|
|
100
|
-
</project>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"""nano-dev-utils - A collection of small Python utilities for developers.
|
|
2
|
-
Copyright (c) 2025 Yaron Dayan
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from .dynamic_importer import Importer
|
|
6
|
-
from .timers import Timer
|
|
7
|
-
from .release_ports import PortsRelease, PROXY_SERVER, INSPECTOR_CLIENT
|
|
8
|
-
from importlib.metadata import version
|
|
9
|
-
|
|
10
|
-
__version__ = version('nano-dev-utils')
|
|
11
|
-
|
|
12
|
-
__all__ = [
|
|
13
|
-
'Importer',
|
|
14
|
-
'Timer',
|
|
15
|
-
'PortsRelease',
|
|
16
|
-
'PROXY_SERVER',
|
|
17
|
-
'INSPECTOR_CLIENT',
|
|
18
|
-
]
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from types import ModuleType
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
import importlib
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Importer:
|
|
8
|
-
def __init__(self):
|
|
9
|
-
self.imported_modules = {}
|
|
10
|
-
|
|
11
|
-
def import_mod_from_lib(self, library: str, module_name: str) -> ModuleType | Any:
|
|
12
|
-
"""Lazily imports and caches a specific submodule from a given library.
|
|
13
|
-
:param library: The name of the library.
|
|
14
|
-
:param module_name: The name of the module to import.
|
|
15
|
-
:return: The imported module.
|
|
16
|
-
"""
|
|
17
|
-
if module_name in self.imported_modules:
|
|
18
|
-
return self.imported_modules[module_name]
|
|
19
|
-
|
|
20
|
-
lib_mod = f'{library}.{module_name}'
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
module = importlib.import_module(lib_mod)
|
|
24
|
-
self.imported_modules[module_name] = module
|
|
25
|
-
return module
|
|
26
|
-
except ModuleNotFoundError as e:
|
|
27
|
-
raise ImportError(f'Could not import {lib_mod}') from e
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import platform
|
|
2
|
-
import subprocess
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
|
-
lgr = logging.getLogger(__name__)
|
|
6
|
-
"""Module-level logger. Configure using logging.basicConfig() in your application."""
|
|
7
|
-
|
|
8
|
-
PROXY_SERVER = 6277
|
|
9
|
-
INSPECTOR_CLIENT = 6274
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class PortsRelease:
|
|
13
|
-
def __init__(self, default_ports: list[int] | None = None):
|
|
14
|
-
self.default_ports: list[int] = (
|
|
15
|
-
default_ports
|
|
16
|
-
if default_ports is not None
|
|
17
|
-
else [PROXY_SERVER, INSPECTOR_CLIENT]
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
@staticmethod
|
|
21
|
-
def _log_process_found(port: int, pid: int) -> str:
|
|
22
|
-
return f'Process ID (PID) found for port {port}: {pid}.'
|
|
23
|
-
|
|
24
|
-
@staticmethod
|
|
25
|
-
def _log_process_terminated(pid: int, port: int) -> str:
|
|
26
|
-
return f'Process {pid} (on port {port}) terminated successfully.'
|
|
27
|
-
|
|
28
|
-
@staticmethod
|
|
29
|
-
def _log_no_process(port: int) -> str:
|
|
30
|
-
return f'No process found listening on port {port}.'
|
|
31
|
-
|
|
32
|
-
@staticmethod
|
|
33
|
-
def _log_invalid_port(port: int) -> str:
|
|
34
|
-
return f'Invalid port number: {port}. Skipping.'
|
|
35
|
-
|
|
36
|
-
@staticmethod
|
|
37
|
-
def _log_terminate_failed(
|
|
38
|
-
pid: int, port: int | None = None, error: str | None = None
|
|
39
|
-
) -> str:
|
|
40
|
-
base_msg = f'Failed to terminate process {pid}'
|
|
41
|
-
if port:
|
|
42
|
-
base_msg += f' (on port {port})'
|
|
43
|
-
if error:
|
|
44
|
-
base_msg += f'. Error: {error}'
|
|
45
|
-
return base_msg
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def _log_line_parse_failed(line: str) -> str:
|
|
49
|
-
return f'Could not parse PID from line: {line}'
|
|
50
|
-
|
|
51
|
-
@staticmethod
|
|
52
|
-
def _log_unexpected_error(e: Exception) -> str:
|
|
53
|
-
return f'An unexpected error occurred: {e}'
|
|
54
|
-
|
|
55
|
-
@staticmethod
|
|
56
|
-
def _log_cmd_error(error: bytes) -> str:
|
|
57
|
-
return f'Error running command: {error.decode()}'
|
|
58
|
-
|
|
59
|
-
@staticmethod
|
|
60
|
-
def _log_unsupported_os() -> str:
|
|
61
|
-
return f'Unsupported OS: {platform.system()}'
|
|
62
|
-
|
|
63
|
-
def get_pid_by_port(self, port: int) -> int | None:
|
|
64
|
-
"""Gets the process ID (PID) listening on the specified port."""
|
|
65
|
-
system = platform.system()
|
|
66
|
-
try:
|
|
67
|
-
cmd: str = {
|
|
68
|
-
'Windows': f'netstat -ano | findstr :{port}',
|
|
69
|
-
'Linux': f'ss -lntp | grep :{port}',
|
|
70
|
-
'Darwin': f'lsof -i :{port}',
|
|
71
|
-
}.get(system, '')
|
|
72
|
-
if not cmd:
|
|
73
|
-
lgr.error(self._log_unsupported_os())
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
process = subprocess.Popen(
|
|
77
|
-
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
78
|
-
)
|
|
79
|
-
output, error = process.communicate()
|
|
80
|
-
if error:
|
|
81
|
-
lgr.error(self._log_cmd_error(error))
|
|
82
|
-
return None
|
|
83
|
-
|
|
84
|
-
lines: list[str] = output.decode().splitlines()
|
|
85
|
-
for line in lines:
|
|
86
|
-
if str(port) in line:
|
|
87
|
-
parts: list[str] = line.split()
|
|
88
|
-
if system == 'Windows' and len(parts) > 4:
|
|
89
|
-
try:
|
|
90
|
-
return int(parts[4])
|
|
91
|
-
except ValueError:
|
|
92
|
-
lgr.error(self._log_line_parse_failed(line))
|
|
93
|
-
return None
|
|
94
|
-
elif system == 'Linux':
|
|
95
|
-
for part in parts:
|
|
96
|
-
if 'pid=' in part:
|
|
97
|
-
try:
|
|
98
|
-
return int(part.split('=')[1])
|
|
99
|
-
except ValueError:
|
|
100
|
-
lgr.error(self._log_line_parse_failed(line))
|
|
101
|
-
return None
|
|
102
|
-
elif system == 'Darwin' and len(parts) > 1:
|
|
103
|
-
try:
|
|
104
|
-
return int(parts[1])
|
|
105
|
-
except ValueError:
|
|
106
|
-
lgr.error(self._log_line_parse_failed(line))
|
|
107
|
-
return None
|
|
108
|
-
return None
|
|
109
|
-
except Exception as e:
|
|
110
|
-
lgr.error(self._log_unexpected_error(e))
|
|
111
|
-
return None
|
|
112
|
-
|
|
113
|
-
def kill_process(self, pid: int) -> bool:
|
|
114
|
-
"""Kills the process with the specified PID."""
|
|
115
|
-
try:
|
|
116
|
-
cmd: str = {
|
|
117
|
-
'Windows': f'taskkill /F /PID {pid}',
|
|
118
|
-
'Linux': f'kill -9 {pid}',
|
|
119
|
-
'Darwin': f'kill -9 {pid}',
|
|
120
|
-
}.get(platform.system(), '') # fallback to empty string
|
|
121
|
-
if not cmd:
|
|
122
|
-
lgr.error(self._log_unsupported_os())
|
|
123
|
-
return False
|
|
124
|
-
process = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE)
|
|
125
|
-
_, error = process.communicate()
|
|
126
|
-
if process.returncode:
|
|
127
|
-
error_msg = error.decode()
|
|
128
|
-
lgr.error(self._log_terminate_failed(pid=pid, error=error_msg))
|
|
129
|
-
return False
|
|
130
|
-
return True
|
|
131
|
-
except Exception as e:
|
|
132
|
-
lgr.error(self._log_unexpected_error(e))
|
|
133
|
-
return False
|
|
134
|
-
|
|
135
|
-
def release_all(self, ports: list[int] | None = None) -> None:
|
|
136
|
-
try:
|
|
137
|
-
ports_to_release: list[int] = self.default_ports if ports is None else ports
|
|
138
|
-
|
|
139
|
-
for port in ports_to_release:
|
|
140
|
-
if not isinstance(port, int):
|
|
141
|
-
lgr.error(self._log_invalid_port(port))
|
|
142
|
-
continue
|
|
143
|
-
|
|
144
|
-
pid: int | None = self.get_pid_by_port(port)
|
|
145
|
-
if pid is None:
|
|
146
|
-
lgr.info(self._log_no_process(port))
|
|
147
|
-
continue
|
|
148
|
-
|
|
149
|
-
lgr.info(self._log_process_found(port, pid))
|
|
150
|
-
if self.kill_process(pid):
|
|
151
|
-
lgr.info(self._log_process_terminated(pid, port))
|
|
152
|
-
else:
|
|
153
|
-
lgr.error(self._log_terminate_failed(pid=pid, port=port))
|
|
154
|
-
except Exception as e:
|
|
155
|
-
lgr.error(self._log_unexpected_error(e))
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from functools import wraps
|
|
2
|
-
import time
|
|
3
|
-
from typing import Callable, ParamSpec, TypeVar
|
|
4
|
-
|
|
5
|
-
P = ParamSpec('P')
|
|
6
|
-
R = TypeVar('R')
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Timer:
|
|
10
|
-
def __init__(self, precision=4, verbose=False):
|
|
11
|
-
self.precision = precision
|
|
12
|
-
self.verbose = verbose
|
|
13
|
-
self.units = [(1e9, 's'), (1e6, 'ms'), (1e3, 'μs'), (1.0, 'ns')]
|
|
14
|
-
|
|
15
|
-
def timeit(self, func: Callable[P, R]) -> Callable[P, R]:
|
|
16
|
-
"""Decorator that times function execution with automatic unit scaling."""
|
|
17
|
-
|
|
18
|
-
@wraps(func)
|
|
19
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
20
|
-
start = time.perf_counter_ns()
|
|
21
|
-
result = func(*args, **kwargs)
|
|
22
|
-
elapsed = time.perf_counter_ns() - start
|
|
23
|
-
|
|
24
|
-
value = elapsed
|
|
25
|
-
unit = 'ns'
|
|
26
|
-
|
|
27
|
-
for divisor, unit in self.units:
|
|
28
|
-
if elapsed >= divisor or unit == 'ns':
|
|
29
|
-
value = elapsed / divisor
|
|
30
|
-
break
|
|
31
|
-
|
|
32
|
-
extra_info = f'{args} {kwargs} ' if self.verbose else ''
|
|
33
|
-
print(
|
|
34
|
-
f'{func.__name__} {extra_info}took {value:.{self.precision}f} [{unit}]'
|
|
35
|
-
)
|
|
36
|
-
return result
|
|
37
|
-
|
|
38
|
-
return wrapper
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from types import ModuleType
|
|
3
|
-
from src.nano_dev_utils.dynamic_importer import Importer
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@pytest.fixture
|
|
7
|
-
def importer():
|
|
8
|
-
return Importer()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture
|
|
12
|
-
def mock_module():
|
|
13
|
-
module = ModuleType('mock_module')
|
|
14
|
-
setattr(module, 'attribute', 'test_value')
|
|
15
|
-
return module
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_import_mod_from_lib_success_new_import(importer, mock_module, mocker):
|
|
19
|
-
mock_import = mocker.patch('importlib.import_module', return_value=mock_module)
|
|
20
|
-
module = importer.import_mod_from_lib('test_library', 'test_module')
|
|
21
|
-
|
|
22
|
-
assert module == mock_module
|
|
23
|
-
assert 'test_module' in importer.imported_modules
|
|
24
|
-
assert importer.imported_modules['test_module'] == mock_module
|
|
25
|
-
mock_import.assert_called_once_with('test_library.test_module')
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_import_mod_from_lib_success_cached_import(importer, mock_module, mocker):
|
|
29
|
-
importer.imported_modules['cached_module'] = mock_module
|
|
30
|
-
mock_import = mocker.patch('importlib.import_module')
|
|
31
|
-
|
|
32
|
-
module = importer.import_mod_from_lib('another_library', 'cached_module')
|
|
33
|
-
|
|
34
|
-
assert module == mock_module
|
|
35
|
-
mock_import.assert_not_called()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_import_mod_from_lib_failure(importer, mocker):
|
|
39
|
-
mocker.patch(
|
|
40
|
-
'importlib.import_module',
|
|
41
|
-
side_effect=ModuleNotFoundError(
|
|
42
|
-
"No module named 'nonexistent_library.missing_module'"
|
|
43
|
-
),
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
with pytest.raises(
|
|
47
|
-
ImportError, match='Could not import nonexistent_library.missing_module'
|
|
48
|
-
):
|
|
49
|
-
importer.import_mod_from_lib('nonexistent_library', 'missing_module')
|
|
50
|
-
|
|
51
|
-
assert 'missing_module' not in importer.imported_modules
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_import_mod_from_lib_returns_any(importer, mocker):
|
|
55
|
-
mock_object = 'not a module'
|
|
56
|
-
mocker.patch.dict('sys.modules', {'yet_another_library.some_object': mock_object})
|
|
57
|
-
|
|
58
|
-
result = importer.import_mod_from_lib('yet_another_library', 'some_object')
|
|
59
|
-
|
|
60
|
-
assert result == mock_object
|
|
61
|
-
assert 'some_object' in importer.imported_modules
|
|
62
|
-
assert importer.imported_modules['some_object'] == mock_object
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import pytest
|
|
3
|
-
from src.nano_dev_utils import release_ports as rp
|
|
4
|
-
|
|
5
|
-
PROXY_SERVER = rp.PROXY_SERVER
|
|
6
|
-
CLIENT_PORT = rp.INSPECTOR_CLIENT
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@pytest.fixture
|
|
10
|
-
def ports_release():
|
|
11
|
-
return rp.PortsRelease()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@pytest.fixture
|
|
15
|
-
def mock_logger(mocker):
|
|
16
|
-
logger = mocker.MagicMock(spec=logging.Logger)
|
|
17
|
-
mocker.patch.object(rp, 'lgr', logger)
|
|
18
|
-
return logger
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def encode_dict(input_dict: dict) -> bytes:
|
|
22
|
-
return b' '.join(str(v).encode() for v in input_dict.values())
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def remove_file_handlers():
|
|
26
|
-
"""Temporarily remove any file handlers from the root logger"""
|
|
27
|
-
root_logger = logging.getLogger()
|
|
28
|
-
existing_file_handlers = [
|
|
29
|
-
handler
|
|
30
|
-
for handler in root_logger.handlers
|
|
31
|
-
if isinstance(handler, logging.FileHandler)
|
|
32
|
-
]
|
|
33
|
-
for handler in existing_file_handlers:
|
|
34
|
-
root_logger.removeHandler(handler)
|
|
35
|
-
handler.close()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@pytest.fixture(autouse=True)
|
|
39
|
-
def cleanup():
|
|
40
|
-
remove_file_handlers()
|
|
41
|
-
yield
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def mock_pid_retrieval(ports_release, mocker, entry, port):
|
|
45
|
-
mock_process = mocker.MagicMock()
|
|
46
|
-
encoded_entry = encode_dict(entry)
|
|
47
|
-
mock_process.communicate.return_value = (encoded_entry, '')
|
|
48
|
-
mocker.patch('subprocess.Popen', return_value=mock_process)
|
|
49
|
-
pid = ports_release.get_pid_by_port(port)
|
|
50
|
-
return pid, encoded_entry
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_get_pid_by_port_linux_success(ports_release, mock_logger, mocker):
|
|
54
|
-
mock_popen = mocker.patch('subprocess.Popen')
|
|
55
|
-
mocker.patch('platform.system', return_value='Linux')
|
|
56
|
-
# ss - lntp command response structure
|
|
57
|
-
port = 8080 # local port
|
|
58
|
-
peer_port = '*'
|
|
59
|
-
local_addr = '::' # :: - listening to all available interfaces
|
|
60
|
-
peer_addr = '::'
|
|
61
|
-
_pid = 1234
|
|
62
|
-
fd = 4 # file descriptor
|
|
63
|
-
ss_entry = {
|
|
64
|
-
'netid': 'tcp6',
|
|
65
|
-
'rx_q': 0,
|
|
66
|
-
'tx_q': 0,
|
|
67
|
-
'local_addr_port': f'{local_addr}:{port}',
|
|
68
|
-
'peer_addr_port': f'{peer_addr}:{peer_port}',
|
|
69
|
-
'process_name': 'users:python3',
|
|
70
|
-
'pid': f'pid={_pid}',
|
|
71
|
-
'fd': f'fd={fd}',
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
mock_process = mocker.MagicMock()
|
|
75
|
-
mock_process.communicate.return_value = (encode_dict(ss_entry), '')
|
|
76
|
-
mock_popen.return_value = mock_process
|
|
77
|
-
|
|
78
|
-
pid = ports_release.get_pid_by_port(port)
|
|
79
|
-
assert pid == 1234
|
|
80
|
-
mock_popen.assert_called_once_with(
|
|
81
|
-
f'ss -lntp | grep :{port}',
|
|
82
|
-
shell=True,
|
|
83
|
-
stdout=-1,
|
|
84
|
-
stderr=-1,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_get_pid_by_port_windows_success(ports_release, mock_logger, mocker):
|
|
89
|
-
mock_popen = mocker.patch('subprocess.Popen')
|
|
90
|
-
mocker.patch('platform.system', return_value='Windows')
|
|
91
|
-
|
|
92
|
-
# netstat -ano command response structure (Windows)
|
|
93
|
-
port = 9000
|
|
94
|
-
_pid = 5678
|
|
95
|
-
netstat_entry = {
|
|
96
|
-
'protocol': 'TCP',
|
|
97
|
-
'local_addr_and_port ': f'0.0.0.0:{port}',
|
|
98
|
-
'remote_addr_and_port': '0.0.0.0:0',
|
|
99
|
-
'state_and_pid': f'LISTENING {_pid}\n',
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
mock_process = mocker.MagicMock()
|
|
103
|
-
mock_process.communicate.return_value = (encode_dict(netstat_entry), '')
|
|
104
|
-
mock_popen.return_value = mock_process
|
|
105
|
-
|
|
106
|
-
pid = ports_release.get_pid_by_port(port)
|
|
107
|
-
assert pid == 5678
|
|
108
|
-
mock_popen.assert_called_once_with(
|
|
109
|
-
f'netstat -ano | findstr :{port}',
|
|
110
|
-
shell=True,
|
|
111
|
-
stdout=-1,
|
|
112
|
-
stderr=-1,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_get_pid_by_port_darwin_success(ports_release, mock_logger, mocker):
|
|
117
|
-
mock_popen = mocker.patch('subprocess.Popen')
|
|
118
|
-
mocker.patch('platform.system', return_value='Darwin')
|
|
119
|
-
# lsof -i command response structure (MacOS)
|
|
120
|
-
port = 7000
|
|
121
|
-
_pid = 1111
|
|
122
|
-
lsof_entry = {
|
|
123
|
-
'command': 'python3', # Process name
|
|
124
|
-
'pid': f'{_pid}', # Process ID (integer)
|
|
125
|
-
'user': 'user', # User running the process
|
|
126
|
-
'fd': '10u', # File descriptor (read/write)
|
|
127
|
-
'type': 'IPv4', # Network connection type (IPv4/IPv6)
|
|
128
|
-
'device': '0xabcdef0123456789', # Kernel device identifier
|
|
129
|
-
'size_off': '0t0', # Size/offset (0 for sockets)
|
|
130
|
-
'protocol': 'TCP', # Protocol (TCP/UDP)
|
|
131
|
-
'name': f'*:{port} (LISTEN)', # Combined address & state (optional)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
mock_process = mocker.MagicMock()
|
|
135
|
-
mock_process.communicate.return_value = (encode_dict(lsof_entry), '')
|
|
136
|
-
mock_popen.return_value = mock_process
|
|
137
|
-
|
|
138
|
-
pid = ports_release.get_pid_by_port(port)
|
|
139
|
-
assert pid == 1111
|
|
140
|
-
mock_popen.assert_called_once_with(
|
|
141
|
-
f'lsof -i :{port}',
|
|
142
|
-
shell=True,
|
|
143
|
-
stdout=-1,
|
|
144
|
-
stderr=-1,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def test_get_pid_by_port_unsupported_os(ports_release, mock_logger, mocker):
|
|
149
|
-
mocker.patch('platform.system', return_value='UnsupportedOS')
|
|
150
|
-
pid = ports_release.get_pid_by_port(1234)
|
|
151
|
-
assert pid is None
|
|
152
|
-
mock_logger.error.assert_called_once_with(ports_release._log_unsupported_os())
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def test_get_pid_by_port_no_process(ports_release, mock_logger, mocker):
|
|
156
|
-
port = 9999
|
|
157
|
-
mocker.patch('platform.system', return_value='Linux')
|
|
158
|
-
mock_process = mocker.MagicMock()
|
|
159
|
-
mock_process.communicate.return_value = (b'', b'')
|
|
160
|
-
mocker.patch('subprocess.Popen', return_value=mock_process)
|
|
161
|
-
|
|
162
|
-
pid = ports_release.get_pid_by_port(port)
|
|
163
|
-
assert pid is None
|
|
164
|
-
mock_logger.error.assert_not_called()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def test_get_pid_by_port_command_error(ports_release, mock_logger, mocker):
|
|
168
|
-
mock_popen = mocker.patch('subprocess.Popen')
|
|
169
|
-
mocker.patch('platform.system', return_value='Linux')
|
|
170
|
-
port = 80
|
|
171
|
-
err = 'Error occurred'
|
|
172
|
-
mock_process = mocker.MagicMock()
|
|
173
|
-
mock_process.communicate.return_value = (b'', err.encode())
|
|
174
|
-
mock_popen.return_value = mock_process
|
|
175
|
-
pid = ports_release.get_pid_by_port(port)
|
|
176
|
-
assert pid is None
|
|
177
|
-
mock_logger.error.assert_called_once_with(f'Error running command: {err}')
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def test_get_pid_by_port_parse_error(ports_release, mock_logger, mocker):
|
|
181
|
-
mocker.patch('platform.system', return_value='Linux')
|
|
182
|
-
port = 8080
|
|
183
|
-
peer_port = '*'
|
|
184
|
-
local_addr = '::'
|
|
185
|
-
peer_addr = '::'
|
|
186
|
-
_pid = 'invalid'
|
|
187
|
-
fd = 4
|
|
188
|
-
ss_entry = {
|
|
189
|
-
'netid': 'tcp6',
|
|
190
|
-
'rx_q': 0,
|
|
191
|
-
'tx_q': 0,
|
|
192
|
-
'local_addr_port': f'{local_addr}:{port}',
|
|
193
|
-
'peer_addr_port': f'{peer_addr}:{peer_port}',
|
|
194
|
-
'process_name': 'users:python3',
|
|
195
|
-
'pid': f'pid={_pid}',
|
|
196
|
-
'fd': f'fd={fd}',
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
pid, enc_entry = mock_pid_retrieval(ports_release, mocker, ss_entry, port)
|
|
200
|
-
assert pid is None
|
|
201
|
-
mock_logger.error.assert_called_once_with(
|
|
202
|
-
f'Could not parse PID from line: {enc_entry.decode()}'
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def test_get_pid_by_port_unexpected_exception(ports_release, mock_logger, mocker):
|
|
207
|
-
mocker.patch('platform.system', return_value='Linux')
|
|
208
|
-
err = Exception('Unexpected')
|
|
209
|
-
port = 1234
|
|
210
|
-
mocker.patch('subprocess.Popen', side_effect=err)
|
|
211
|
-
pid = ports_release.get_pid_by_port(port)
|
|
212
|
-
assert pid is None
|
|
213
|
-
mock_logger.error.assert_called_once_with(f'An unexpected error occurred: {err}')
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def test_kill_process_success(ports_release, mock_logger, mocker):
|
|
217
|
-
mocker.patch('platform.system', return_value='Linux')
|
|
218
|
-
mock_popen = mocker.patch('subprocess.Popen')
|
|
219
|
-
|
|
220
|
-
port = 5678
|
|
221
|
-
mock_process = mocker.MagicMock()
|
|
222
|
-
mock_process.returncode = 0
|
|
223
|
-
mock_process.communicate.return_value = (b'', b'')
|
|
224
|
-
mock_popen.return_value = mock_process
|
|
225
|
-
result = ports_release.kill_process(port)
|
|
226
|
-
assert result is True
|
|
227
|
-
mock_popen.assert_called_once_with(f'kill -9 {port}', shell=True, stderr=-1)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def test_kill_process_fail(ports_release, mock_logger, mocker):
|
|
231
|
-
pid = 1234
|
|
232
|
-
err = 'Access denied'
|
|
233
|
-
mocker.patch('platform.system', return_value='Windows')
|
|
234
|
-
mock_popen = mocker.patch('subprocess.Popen')
|
|
235
|
-
mock_process = mocker.MagicMock()
|
|
236
|
-
mock_process.returncode = 1
|
|
237
|
-
mock_process.communicate.return_value = (b'', err.encode())
|
|
238
|
-
mock_popen.return_value = mock_process
|
|
239
|
-
result = ports_release.kill_process(pid)
|
|
240
|
-
assert result is False
|
|
241
|
-
mock_logger.error.assert_called_once_with(
|
|
242
|
-
ports_release._log_terminate_failed(pid=pid, error=err)
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def test_kill_process_unsupported_os(ports_release, mock_logger, mocker):
|
|
247
|
-
mocker.patch('platform.system', return_value='UnsupportedOS')
|
|
248
|
-
pid = 9999
|
|
249
|
-
result = ports_release.kill_process(pid)
|
|
250
|
-
assert result is False
|
|
251
|
-
mock_logger.error.assert_called_once_with(ports_release._log_unsupported_os())
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def test_kill_process_unexpected_exception(ports_release, mock_logger, mocker):
|
|
255
|
-
err = Exception('Another error')
|
|
256
|
-
mocker.patch('platform.system', return_value='Linux')
|
|
257
|
-
mocker.patch('subprocess.Popen', side_effect=err)
|
|
258
|
-
|
|
259
|
-
pid = 4321
|
|
260
|
-
result = ports_release.kill_process(pid)
|
|
261
|
-
assert result is False
|
|
262
|
-
mock_logger.error.assert_called_once_with(ports_release._log_unexpected_error(err))
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def test_release_all_default_ports_success(ports_release, mock_logger, mocker):
|
|
266
|
-
mock_get_pid = mocker.patch.object(ports_release, 'get_pid_by_port')
|
|
267
|
-
mock_kill = mocker.patch.object(ports_release, 'kill_process')
|
|
268
|
-
|
|
269
|
-
pid1, pid2 = 1111, 2222
|
|
270
|
-
mock_get_pid.side_effect = [pid1, pid2]
|
|
271
|
-
mock_kill.side_effect = [True, True]
|
|
272
|
-
ports_release.release_all()
|
|
273
|
-
|
|
274
|
-
assert mock_get_pid.call_args_list == [
|
|
275
|
-
mocker.call(PROXY_SERVER),
|
|
276
|
-
mocker.call(CLIENT_PORT),
|
|
277
|
-
]
|
|
278
|
-
assert mock_kill.call_args_list == [mocker.call(pid1), mocker.call(pid2)]
|
|
279
|
-
mock_logger.info.assert_has_calls(
|
|
280
|
-
[
|
|
281
|
-
mocker.call(ports_release._log_process_found(PROXY_SERVER, pid1)),
|
|
282
|
-
mocker.call(ports_release._log_process_terminated(pid1, PROXY_SERVER)),
|
|
283
|
-
mocker.call(ports_release._log_process_found(CLIENT_PORT, pid2)),
|
|
284
|
-
mocker.call(ports_release._log_process_terminated(pid2, CLIENT_PORT)),
|
|
285
|
-
]
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def test_release_all_invalid_port(ports_release, mock_logger, mocker):
|
|
290
|
-
mock_get_pid = mocker.patch.object(ports_release, 'get_pid_by_port')
|
|
291
|
-
mock_kill = mocker.patch.object(ports_release, 'kill_process')
|
|
292
|
-
ports = ['invalid', 1234, 5678]
|
|
293
|
-
mock_get_pid.side_effect = [None, None]
|
|
294
|
-
ports_release.release_all(ports=ports)
|
|
295
|
-
assert mock_get_pid.call_args_list == [mocker.call(ports[1]), mocker.call(ports[2])]
|
|
296
|
-
|
|
297
|
-
mock_kill.assert_not_called()
|
|
298
|
-
mock_logger.error.assert_called_once_with(ports_release._log_invalid_port(ports[0]))
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def test_release_all_unexpected_exception(ports_release, mock_logger, mocker):
|
|
302
|
-
err = Exception('Release all error')
|
|
303
|
-
mocker.patch.object(ports_release, 'get_pid_by_port', side_effect=err)
|
|
304
|
-
port = 9010
|
|
305
|
-
ports_release.release_all(ports=[port])
|
|
306
|
-
mock_logger.error.assert_called_once_with(ports_release._log_unexpected_error(err))
|
|
307
|
-
ports_release.get_pid_by_port.assert_called_once_with(port)
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
import time
|
|
3
|
-
from src.nano_dev_utils.timers import Timer
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_initialization():
|
|
7
|
-
timer = Timer()
|
|
8
|
-
assert timer.precision == 4
|
|
9
|
-
assert not timer.verbose
|
|
10
|
-
|
|
11
|
-
timer_custom = Timer(precision=6, verbose=True)
|
|
12
|
-
assert timer_custom.precision == 6
|
|
13
|
-
assert timer_custom.verbose
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_timeit_simple(mocker):
|
|
17
|
-
mock_print = mocker.patch('builtins.print')
|
|
18
|
-
mock_time = mocker.patch('time.perf_counter_ns', side_effect=[0, 9.23467e5])
|
|
19
|
-
timer = Timer(precision=2)
|
|
20
|
-
|
|
21
|
-
@timer.timeit
|
|
22
|
-
def sample_function():
|
|
23
|
-
return 'result'
|
|
24
|
-
|
|
25
|
-
result = sample_function()
|
|
26
|
-
assert result == 'result'
|
|
27
|
-
mock_time.assert_any_call()
|
|
28
|
-
mock_print.assert_called_once_with('sample_function took 923.47 [μs]')
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_timeit_no_args_kwargs(mocker):
|
|
32
|
-
mock_print = mocker.patch('builtins.print')
|
|
33
|
-
mock_time = mocker.patch('time.perf_counter_ns', side_effect=[1.0, 1.5])
|
|
34
|
-
timer = Timer(precision=2, verbose=True)
|
|
35
|
-
|
|
36
|
-
@timer.timeit
|
|
37
|
-
def yet_another_function():
|
|
38
|
-
return 'yet another result'
|
|
39
|
-
|
|
40
|
-
result = yet_another_function()
|
|
41
|
-
assert result == 'yet another result'
|
|
42
|
-
mock_time.assert_any_call()
|
|
43
|
-
mock_print.assert_called_once_with('yet_another_function () {} took 0.50 [ns]')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_multithreaded_timing(mocker):
|
|
47
|
-
"""Test timer works correctly across threads"""
|
|
48
|
-
mock_print = mocker.patch('builtins.print')
|
|
49
|
-
timer = Timer()
|
|
50
|
-
results = []
|
|
51
|
-
|
|
52
|
-
@timer.timeit
|
|
53
|
-
def threaded_operation():
|
|
54
|
-
time.sleep(0.1)
|
|
55
|
-
return threading.get_ident()
|
|
56
|
-
|
|
57
|
-
def run_in_thread():
|
|
58
|
-
results.append(threaded_operation())
|
|
59
|
-
|
|
60
|
-
threads = [threading.Thread(target=run_in_thread) for _ in range(3)]
|
|
61
|
-
|
|
62
|
-
for t in threads:
|
|
63
|
-
t.start()
|
|
64
|
-
for t in threads:
|
|
65
|
-
t.join()
|
|
66
|
-
|
|
67
|
-
# Should have 3 print calls (one per thread)
|
|
68
|
-
assert mock_print.call_count == 3
|
|
69
|
-
# All thread IDs should be different
|
|
70
|
-
assert len(set(results)) == 3
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_verbose_mode(mocker):
|
|
74
|
-
"""Test that verbose mode includes positional and
|
|
75
|
-
keyword arguments in output and preserves the wrapped func result"""
|
|
76
|
-
mock_print = mocker.patch('builtins.print')
|
|
77
|
-
mocker.patch('time.perf_counter_ns', side_effect=[1e4, 5.23456e4])
|
|
78
|
-
verbose_timer = Timer(verbose=True)
|
|
79
|
-
|
|
80
|
-
@verbose_timer.timeit
|
|
81
|
-
def func_with_args(a, b, c=3):
|
|
82
|
-
return a + b + c
|
|
83
|
-
|
|
84
|
-
res = func_with_args(1, 2, c=4)
|
|
85
|
-
output = mock_print.call_args[0][0]
|
|
86
|
-
assert '(1, 2)' in output # checking positional args
|
|
87
|
-
assert "'c': 4" in output # checking kwargs
|
|
88
|
-
mock_print.assert_called_once_with(
|
|
89
|
-
"func_with_args (1, 2) {'c': 4} took 42.3456 [μs]"
|
|
90
|
-
)
|
|
91
|
-
assert res == 7 # checking returned value preservation
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_nested_timers(mocker):
|
|
95
|
-
"""Test that nested timers work correctly"""
|
|
96
|
-
mock_print = mocker.patch('builtins.print')
|
|
97
|
-
timer = Timer()
|
|
98
|
-
|
|
99
|
-
@timer.timeit
|
|
100
|
-
def outer():
|
|
101
|
-
@timer.timeit
|
|
102
|
-
def inner():
|
|
103
|
-
time.sleep(0.1)
|
|
104
|
-
|
|
105
|
-
return inner()
|
|
106
|
-
|
|
107
|
-
outer()
|
|
108
|
-
|
|
109
|
-
# Should have two print calls (inner and outer)
|
|
110
|
-
assert mock_print.call_count == 2
|
|
111
|
-
inner_output = mock_print.call_args_list[0][0][0]
|
|
112
|
-
outer_output = mock_print.call_args_list[1][0][0]
|
|
113
|
-
|
|
114
|
-
inner_time = float(inner_output.split('took ')[1].split(' [')[0])
|
|
115
|
-
outer_time = float(outer_output.split('took ')[1].split(' [')[0])
|
|
116
|
-
|
|
117
|
-
assert outer_time > inner_time
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def test_unit_scaling(mocker):
|
|
121
|
-
"""Test the time unit selection logic directly"""
|
|
122
|
-
mock_print = mocker.patch('builtins.print')
|
|
123
|
-
|
|
124
|
-
boundary_cases = [
|
|
125
|
-
(1e3 - 1, 'ns'),
|
|
126
|
-
(1e3, 'μs'),
|
|
127
|
-
(1e6 - 1, 'μs'),
|
|
128
|
-
(1e6, 'ms'),
|
|
129
|
-
(1e9 - 1, 'ms'),
|
|
130
|
-
(1e9, 's'),
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
for ns, expected_unit in boundary_cases:
|
|
134
|
-
mocker.patch('time.perf_counter_ns', side_effect=[0, ns])
|
|
135
|
-
timer = Timer(precision=2)
|
|
136
|
-
|
|
137
|
-
@timer.timeit
|
|
138
|
-
def dummy():
|
|
139
|
-
pass
|
|
140
|
-
|
|
141
|
-
dummy()
|
|
142
|
-
printed_output = mock_print.call_args[0][0]
|
|
143
|
-
assert expected_unit in printed_output, (
|
|
144
|
-
f"Failed for {ns:,}ns → Expected '{expected_unit}' in output. "
|
|
145
|
-
f'Got: {printed_output}'
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def test_function_metadata_preserved():
|
|
150
|
-
"""Test that function metadata (name, docstring) is preserved"""
|
|
151
|
-
timer = Timer(precision=3)
|
|
152
|
-
|
|
153
|
-
@timer.timeit
|
|
154
|
-
def dummy_func():
|
|
155
|
-
"""Test docstring"""
|
|
156
|
-
pass
|
|
157
|
-
|
|
158
|
-
assert dummy_func.__name__ == 'dummy_func'
|
|
159
|
-
assert dummy_func.__doc__ == 'Test docstring'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|