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.

@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nano_dev_utils
3
- Version: 0.5.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.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,3 +0,0 @@
1
- # Default ignored files
2
- /shelf/
3
- /workspace.xml
@@ -1,6 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <settings>
3
- <option name="USE_PROJECT_PROFILE" value="false" />
4
- <version value="1.0" />
5
- </settings>
6
- </component>
@@ -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,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="" vcs="Git" />
5
- </component>
6
- </project>
@@ -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
- &quot;customColor&quot;: &quot;&quot;,
22
- &quot;associatedIndex&quot;: 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="&quot;&quot;" />
50
- <option name="_new_target" value="&quot;$PROJECT_DIR$/tests&quot;" />
51
- <option name="_new_targetType" value="&quot;PATH&quot;" />
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,10 +0,0 @@
1
- """nano-dev-utils - A collection of small Python utilities for developers.
2
- Copyright (c) 2025 Yaron Dayan
3
- """
4
-
5
- from .test_release_ports import PROXY_SERVER, CLIENT_PORT
6
-
7
- __all__ = [
8
- 'PROXY_SERVER',
9
- 'CLIENT_PORT',
10
- ]
@@ -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