timeout-dead 0.1.0__tar.gz → 0.1.2__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.
Files changed (24) hide show
  1. timeout_dead-0.1.2/.vscode/settings.json +8 -0
  2. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/AGENTS.md +25 -13
  3. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/PKG-INFO +3 -2
  4. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/README.md +2 -1
  5. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/src/timeout_dead/_version.py +3 -3
  6. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/src/timeout_dead/cli.py +39 -18
  7. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/src/timeout_dead.egg-info/SOURCES.txt +1 -0
  8. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/tests/conftest.py +1 -1
  9. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/tests/test_cli.py +18 -3
  10. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/.github/workflows/publish.yml +0 -0
  11. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/.github/workflows/pyright.yml +0 -0
  12. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/.github/workflows/ruff.yml +0 -0
  13. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/.github/workflows/tests.yml +0 -0
  14. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/.gitignore +0 -0
  15. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/CODE-STYLE.md +0 -0
  16. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/CONTRIBUTING.md +0 -0
  17. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/LICENSE +0 -0
  18. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/MANIFEST.in +0 -0
  19. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/icon.png +0 -0
  20. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/pyproject.toml +0 -0
  21. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/ruff.toml +0 -0
  22. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/setup.cfg +0 -0
  23. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/src/timeout_dead/__init__.py +0 -0
  24. {timeout_dead-0.1.0 → timeout_dead-0.1.2}/uv.lock +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "cSpell.words": [
3
+ "creationflags",
4
+ "getpgid",
5
+ "killpg",
6
+ "pgid"
7
+ ]
8
+ }
@@ -37,20 +37,32 @@ If the user asks "what should the commit message be?" — **suggest a message bu
37
37
 
38
38
  ## Workflow & Verification Commands
39
39
 
40
+ ### Setup
41
+
42
+ ```bash
43
+ uv tool install timeout-dead
44
+ ```
45
+
46
+ Если уже установлена — убедись что работает:
47
+
48
+ ```bash
49
+ time-d -h
50
+ ```
51
+
40
52
  ### Time limit (HARD REQUIREMENT)
41
53
 
42
54
  **Every bash command MUST complete within 60 seconds.** All commands must be invoked through the timeout wrapper:
43
55
 
44
56
  ```bash
45
- timeout-dead --sec 60 "<your command>"
57
+ time-d --sec 60 "<your command>"
46
58
  ```
47
59
 
48
60
  Long-running daemons must use `nohup ... >/dev/null 2>&1 &` so the wrapper returns immediately.
49
61
 
50
- ### Setup
62
+ ### Install dependencies
51
63
 
52
64
  ```bash
53
- timeout-dead --sec 60 "uv sync"
65
+ time-d --sec 60 "uv sync"
54
66
  ```
55
67
 
56
68
  ### Verify after changes
@@ -58,16 +70,16 @@ timeout-dead --sec 60 "uv sync"
58
70
  Run **all** checks in this order — treat errors as blockers:
59
71
 
60
72
  ```bash
61
- timeout-dead --sec 60 "ruff check ."
62
- timeout-dead --sec 60 "ruff format --check ."
63
- timeout-dead --sec 60 "pyright ."
64
- timeout-dead --sec 60 "python -m pytest tests/ -v"
73
+ time-d --sec 60 "ruff check ."
74
+ time-d --sec 60 "ruff format --check ."
75
+ time-d --sec 60 "pyright ."
76
+ time-d --sec 60 "python -m pytest tests/ -v"
65
77
  ```
66
78
 
67
79
  ### Fix formatting & imports
68
80
 
69
81
  ```bash
70
- timeout-dead --sec 60 "ruff check --fix . && ruff format ."
82
+ time-d --sec 60 "ruff check --fix . && ruff format ."
71
83
  ```
72
84
 
73
85
  **LSP is mandatory.** Configure `pyright-langserver` and `ruff server` in your editor. After every change, confirm lint, format, and type-check show **0 errors**. `ruff format` is the single source of truth for formatting — no `black`, no `isort`.
@@ -75,7 +87,7 @@ timeout-dead --sec 60 "ruff check --fix . && ruff format ."
75
87
  ### Run a single test
76
88
 
77
89
  ```bash
78
- timeout-dead --sec 60 "python -m pytest tests/test_file.py::test_name -v"
90
+ time-d --sec 60 "python -m pytest tests/test_file.py::test_name -v"
79
91
  ```
80
92
 
81
93
  ### Mandatory testing
@@ -170,19 +182,19 @@ class ConflictError(BaseError):
170
182
  ### Unit Tests
171
183
 
172
184
  - Tests live in `tests/` mirroring the source structure.
173
- - Run unit tests: `timeout-dead --sec 60 "pytest tests/ -v --ignore=tests/integration"`.
185
+ - Run unit tests: `time-d --sec 60 "pytest tests/ -v --ignore=tests/integration"`.
174
186
 
175
187
  ### Integration Tests
176
188
 
177
189
  - Place integration tests in `tests/integration/`. Use `@pytest.mark.integration` marker.
178
- - Run integration tests: `timeout-dead --sec 60 "pytest tests/integration/ -v -m \"integration\""`.
179
- - Run unit tests only: `timeout-dead --sec 60 "pytest tests/ -v -m \"not integration\""`.
190
+ - Run integration tests: `time-d --sec 60 "pytest tests/integration/ -v -m \"integration\""`.
191
+ - Run unit tests only: `time-d --sec 60 "pytest tests/ -v -m \"not integration\""`.
180
192
 
181
193
  ### Coverage
182
194
 
183
195
  - Aim for high coverage of core business logic. Use `pytest-cov` to measure:
184
196
  ```bash
185
- timeout-dead --sec 60 "pytest tests/ --cov=src/timeout_dead --cov-report=term-missing"
197
+ time-d --sec 60 "pytest tests/ --cov=src/timeout_dead --cov-report=term-missing"
186
198
  ```
187
199
 
188
200
  ## Environment & Configuration
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: timeout-dead
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Lightweight command timeout utility with zero runtime dependencies
5
5
  Author-email: Dmitry Krivoruchko <umbrella.leaf.for.work@gmail.com>
6
6
  License-Expression: Unlicense
@@ -77,7 +77,7 @@ timeout-dead --no-output "curl -s https://example.com"
77
77
  ## Usage
78
78
 
79
79
  ```
80
- usage: timeout-dead [-h] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
80
+ usage: timeout-dead [-h] [-v] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
81
81
 
82
82
  Lightweight command timeout utility.
83
83
 
@@ -86,6 +86,7 @@ positional arguments:
86
86
 
87
87
  options:
88
88
  -h, --help show this help message and exit
89
+ -v, --version show version and exit
89
90
  --sec SECONDS timeout in seconds (default: 60)
90
91
  --signal SIGNAL signal to send on timeout (TERM, KILL, HUP, INT)
91
92
  --no-output suppress normal output (stdout, stderr, header, footer)
@@ -51,7 +51,7 @@ timeout-dead --no-output "curl -s https://example.com"
51
51
  ## Usage
52
52
 
53
53
  ```
54
- usage: timeout-dead [-h] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
54
+ usage: timeout-dead [-h] [-v] [--sec SECONDS] [--signal SIGNAL] [--no-output] COMMAND ...
55
55
 
56
56
  Lightweight command timeout utility.
57
57
 
@@ -60,6 +60,7 @@ positional arguments:
60
60
 
61
61
  options:
62
62
  -h, --help show this help message and exit
63
+ -v, --version show version and exit
63
64
  --sec SECONDS timeout in seconds (default: 60)
64
65
  --signal SIGNAL signal to send on timeout (TERM, KILL, HUP, INT)
65
66
  --no-output suppress normal output (stdout, stderr, header, footer)
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.0'
22
- __version_tuple__ = version_tuple = (0, 1, 0)
21
+ __version__ = version = '0.1.2'
22
+ __version_tuple__ = version_tuple = (0, 1, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g568c64836'
24
+ __commit_id__ = commit_id = 'g92736f7f7'
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- """Легковесная утилита для запуска команд с таймаутом."""
3
+ """Lightweight command timeout utility."""
4
4
 
5
5
  import argparse
6
6
  import os
@@ -10,6 +10,8 @@ import subprocess
10
10
  import sys
11
11
  import threading
12
12
  import time
13
+ from importlib.metadata import PackageNotFoundError
14
+ from importlib.metadata import version as _get_version
13
15
 
14
16
 
15
17
  # MARK: Constants
@@ -22,7 +24,7 @@ class _Const:
22
24
  HEADER_SEPARATOR: str = "-" * 60
23
25
 
24
26
  MSG_NO_COMMAND: str = "Error: no command specified"
25
- MSG_BASH_NOT_FOUND: str = "bash not found in PATH — Git Bash is required"
27
+ MSG_BASH_NOT_FOUND: str = "bash not found in PATH"
26
28
  MSG_TIMEOUT: str = "Timeout exceeded {} seconds"
27
29
  MSG_EXEC_ERROR: str = "Execution error: {}"
28
30
 
@@ -35,6 +37,17 @@ class _Const:
35
37
  "INT": signal.SIGINT,
36
38
  }
37
39
 
40
+ @staticmethod
41
+ def _resolve_version() -> str:
42
+ try:
43
+ return _get_version("timeout-dead")
44
+
45
+ except PackageNotFoundError:
46
+ return "unknown"
47
+
48
+
49
+ _PROJECT_VERSION: str = _Const._resolve_version()
50
+
38
51
 
39
52
  # MARK: Private Helpers
40
53
  # ------------------------------------------------
@@ -49,13 +62,13 @@ def _is_windows() -> bool:
49
62
 
50
63
  def _find_bash() -> str:
51
64
  """
52
- Ищет bash в PATH.
65
+ Locate bash executable in PATH.
53
66
 
54
67
  Returns:
55
- str: путь к исполняемому файлу bash
68
+ str: path to the bash executable
56
69
 
57
70
  Raises:
58
- SystemExit: если bash не найден
71
+ SystemExit: if bash is not found
59
72
  """
60
73
 
61
74
  bash_path = shutil.which("bash")
@@ -77,7 +90,7 @@ def _terminate_process(
77
90
  force: bool = False,
78
91
  signal_num: int = signal.SIGTERM,
79
92
  ) -> None:
80
- """Завершает процессмягко (выбранным сигналом) или жёстко."""
93
+ """Terminate the process gracefully (chosen signal) or forcefully."""
81
94
 
82
95
  if process.poll() is not None:
83
96
  return
@@ -115,7 +128,7 @@ def _kill_with_timeout(
115
128
  timeout: int,
116
129
  signal_num: int = signal.SIGTERM,
117
130
  ) -> None:
118
- """Убивает процесс по таймауту с двухэтапной логикой."""
131
+ """Kill the process after timeout with two-stage logic."""
119
132
 
120
133
  if process.poll() is not None:
121
134
  return
@@ -140,16 +153,16 @@ def run_command(
140
153
  no_output: bool = False,
141
154
  ) -> int:
142
155
  """
143
- Запускает команду с таймаутом через bash.
156
+ Run a command with timeout via bash.
144
157
 
145
158
  Args:
146
- command_string (str): команда для выполнения
147
- timeout (int): таймаут в секундах
148
- signal_name (str): имя сигнала для мягкого завершения
149
- no_output (bool): подавлять ли обычный вывод
159
+ command_string (str): command to execute
160
+ timeout (int): timeout in seconds
161
+ signal_name (str): signal name for graceful termination
162
+ no_output (bool): suppress normal output
150
163
 
151
164
  Returns:
152
- int: код возврата процесса (-1 при ошибке запуска)
165
+ int: process return code (-1 on launch error)
153
166
  """
154
167
 
155
168
  signal_num = _Const.SIGNAL_MAP.get(signal_name, signal.SIGTERM)
@@ -157,7 +170,7 @@ def run_command(
157
170
  timer: threading.Timer | None = None
158
171
 
159
172
  try:
160
- creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if _is_windows() else 0
173
+ creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) if _is_windows() else 0
161
174
  start_new_session = not _is_windows()
162
175
 
163
176
  process = subprocess.Popen(
@@ -190,6 +203,7 @@ def run_command(
190
203
  except Exception as e:
191
204
  if timer:
192
205
  timer.cancel()
206
+
193
207
  if process and process.poll() is None:
194
208
  try:
195
209
  _terminate_process(process, force=True)
@@ -206,13 +220,20 @@ def run_command(
206
220
 
207
221
 
208
222
  def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
209
- """Разбирает аргументы командной строки."""
223
+ """Parse command-line arguments."""
210
224
 
211
225
  parser = argparse.ArgumentParser(
212
226
  description="Lightweight command timeout utility.",
213
227
  formatter_class=argparse.RawDescriptionHelpFormatter,
214
228
  )
215
229
 
230
+ parser.add_argument(
231
+ "-v",
232
+ "--version",
233
+ action="version",
234
+ version=f"timeout-dead {_PROJECT_VERSION}",
235
+ )
236
+
216
237
  parser.add_argument(
217
238
  "--sec",
218
239
  type=int,
@@ -251,7 +272,7 @@ def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
251
272
 
252
273
 
253
274
  def print_header(command: str, timeout: int) -> None:
254
- """Выводит заголовок выполнения."""
275
+ """Print execution header."""
255
276
 
256
277
  print(f"Running: {command}")
257
278
  print(f"Timeout: {timeout} seconds")
@@ -262,7 +283,7 @@ def print_header(command: str, timeout: int) -> None:
262
283
 
263
284
 
264
285
  def print_footer(return_code: int) -> None:
265
- """Выводит футер выполнения."""
286
+ """Print execution footer."""
266
287
 
267
288
  print(_Const.HEADER_SEPARATOR)
268
289
  print(f"Exit code: {return_code}")
@@ -272,7 +293,7 @@ def print_footer(return_code: int) -> None:
272
293
 
273
294
 
274
295
  def main(argv: list[str] | None = None) -> None:
275
- """Главная точка входа."""
296
+ """Main entry point."""
276
297
 
277
298
  args = parse_arguments(argv)
278
299
 
@@ -13,6 +13,7 @@ uv.lock
13
13
  .github/workflows/pyright.yml
14
14
  .github/workflows/ruff.yml
15
15
  .github/workflows/tests.yml
16
+ .vscode/settings.json
16
17
  src/timeout_dead/__init__.py
17
18
  src/timeout_dead/_version.py
18
19
  src/timeout_dead/cli.py
@@ -9,7 +9,7 @@ import pytest
9
9
 
10
10
  @pytest.fixture
11
11
  def temp_dir() -> Iterator[Path]:
12
- """Временная директория для тестов."""
12
+ """Temporary directory for tests."""
13
13
 
14
14
  with tempfile.TemporaryDirectory() as tmp:
15
15
  yield Path(tmp)
@@ -1,4 +1,4 @@
1
- """Тесты CLI утилиты timeout-dead."""
1
+ """Tests for timeout-dead CLI utility."""
2
2
 
3
3
  import os
4
4
  import signal
@@ -30,7 +30,7 @@ def _run_cli(
30
30
  *args: str,
31
31
  timeout: int = 10,
32
32
  ) -> subprocess.CompletedProcess[str]:
33
- """Запускает timeout-dead как subprocess."""
33
+ """Run timeout-dead as a subprocess."""
34
34
 
35
35
  return subprocess.run(
36
36
  [sys.executable, "-m", "timeout_dead.cli", *args],
@@ -245,7 +245,7 @@ class TestSignalSelection:
245
245
  reason="SIGINT file-based test requires Unix signal handling",
246
246
  )
247
247
  def test_signal_int_handling(self, tmp_path: Path) -> None:
248
- """Python-скрипт ловит SIGINT и записывает номер сигнала в файл."""
248
+ """Python script catches SIGINT and writes signal number to file."""
249
249
 
250
250
  marker = tmp_path / "signal.txt"
251
251
  script_path = tmp_path / "handler.py"
@@ -395,6 +395,21 @@ class TestIntegration:
395
395
  assert "--sec" in result.stdout
396
396
  assert "--signal" in result.stdout
397
397
  assert "--no-output" in result.stdout
398
+ assert "--version" in result.stdout
399
+
400
+ # ------------------------------------------------
401
+
402
+ def test_cli_version_long(self) -> None:
403
+ result = _run_cli("--version")
404
+ assert result.returncode == 0
405
+ assert "timeout-dead" in result.stdout
406
+
407
+ # ------------------------------------------------
408
+
409
+ def test_cli_version_short(self) -> None:
410
+ result = _run_cli("-v")
411
+ assert result.returncode == 0
412
+ assert "timeout-dead" in result.stdout
398
413
 
399
414
  # ------------------------------------------------
400
415
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes