From d7b77675f30af4aa404b1ee7926412971e5fa7c3 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 00:04:06 +0300 Subject: [PATCH 01/61] some basics --- .github/workflows/lint.yml | 36 ++++++++ .github/workflows/release.yml | 34 +++++++ .github/workflows/tests_and_coverage.yml | 45 ++++++++++ .gitignore | 13 +++ .ruff.toml | 1 + pyproject.toml | 44 +++++++++ requirements_dev.txt | 7 ++ suby/__init__.py | 6 ++ suby/errors.py | 2 + suby/proxy_module.py | 110 +++++++++++++++++++++++ suby/py.typed | 0 tests/__init__.py | 0 tests/test_proxy_module.py | 10 +++ 13 files changed, 308 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests_and_coverage.yml create mode 100644 .gitignore create mode 100644 .ruff.toml create mode 100644 pyproject.toml create mode 100644 requirements_dev.txt create mode 100644 suby/__init__.py create mode 100644 suby/errors.py create mode 100644 suby/proxy_module.py create mode 100644 suby/py.typed create mode 100644 tests/__init__.py create mode 100644 tests/test_proxy_module.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..be7dfed --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: Lint + +on: + push + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Install the library + shell: bash + run: pip install . + + - name: Run mypy + shell: bash + run: mypy suby --strict + + - name: Run ruff + shell: bash + run: ruff suby diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fd1689b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + # Specifying a GitHub environment is optional, but strongly encouraged + environment: release + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Build the project + shell: bash + run: python -m build . + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/tests_and_coverage.yml b/.github/workflows/tests_and_coverage.yml new file mode 100644 index 0000000..bb84f16 --- /dev/null +++ b/.github/workflows/tests_and_coverage.yml @@ -0,0 +1,45 @@ +name: Tests + +on: + push + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install the library + shell: bash + run: pip install . + + - name: Install dependencies + shell: bash + run: pip install -r requirements_dev.txt + + - name: Print all libs + shell: bash + run: pip list + + - name: Run tests and show coverage on the command line + run: coverage run --source=suby --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m + + - name: Upload reports to codecov + env: + CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} + if: runner.os == 'Linux' + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + find . -iregex "codecov.*" + chmod +x codecov + ./codecov -t ${CODECOV_TOKEN} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e817e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +__pycache__ +venv +.pytest_cache +build +dist +*.egg-info +test.py +.coverage +.coverage.* +.idea +.ruff_cache +.mutmut-cache diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..ec5b9a4 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1 @@ +ignore = ['E501', 'E712'] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e60d826 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["setuptools==68.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "suby" +version = "0.0.1" +authors = [ + { name="Evgeniy Blinov", email="zheni-b@yandex.ru" }, +] +description = 'Slightly simplified subprocesses' +readme = "README.md" +requires-python = ">=3.7" +dependencies = [ + 'emptylog', +] +classifiers = [ + "Operating System :: OS Independent", + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries', +] + +[tool.setuptools.package-data] +"suby" = ["py.typed"] + +[tool.mutmut] +paths_to_mutate="suby" +runner="pytest" + +[project.urls] +'Source' = 'https://github.com/pomponchik/suby' +'Tracker' = 'https://github.com/pomponchik/suby/issues' diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..44f8cfc --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,7 @@ +pytest==7.4.2 +coverage==7.2.7 +build==0.9.0 +twine==4.0.2 +mypy==1.4.1 +ruff==0.0.290 +mutmut==2.4.4 diff --git a/suby/__init__.py b/suby/__init__.py new file mode 100644 index 0000000..ca05484 --- /dev/null +++ b/suby/__init__.py @@ -0,0 +1,6 @@ +import sys + +from suby.proxy_module import ProxyModule as ProxyModule + + +sys.modules[__name__].__class__ = ProxyModule diff --git a/suby/errors.py b/suby/errors.py new file mode 100644 index 0000000..b683070 --- /dev/null +++ b/suby/errors.py @@ -0,0 +1,2 @@ +class RunningCommandError(Exception): + pass diff --git a/suby/proxy_module.py b/suby/proxy_module.py new file mode 100644 index 0000000..5a2ba22 --- /dev/null +++ b/suby/proxy_module.py @@ -0,0 +1,110 @@ +import sys +from time import sleep +from dataclasses import dataclass, field +from threading import Thread, Lock +from subprocess import Popen, PIPE +from typing import List, Callable, Union, Optional, Any +from functools import partial + +from emptylog import EmptyLogger, LoggerProtocol +from cantok import AbstractToken, SimpleToken, TimeoutToken, CancellationError + +from suby.errors import RunningCommandError + + +@dataclass +class SubprocessResult: + stdout: Optional[str] = None + stderr: Optional[str] = None + returncode: Optional[int] = None + killed_by_token: bool = False + + +class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] + lock = Lock() + + def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerProtocol = EmptyLogger(), stdout_callback: Callable[[str], Any] = partial(print, end=''), stderr_callback: Callable[[str], Any] = sys.stderr.write, timeout: Optional[Union[int, float]] = None, token: Optional[AbstractToken] = None) -> SubprocessResult: + """ + About reading from strout and stderr: https://stackoverflow.com/a/28319191/14522393 + """ + if timeout is not None and token is None: + token = TimeoutToken(timeout) + elif timeout is not None: + token += TimeoutToken(timeout) + + arguments_string_representation = ' '.join([argument if ' ' not in argument else f'"{argument}"' for argument in arguments]) + + stdout_buffer = [] + stderr_buffer = [] + result = SubprocessResult() + + logger.info(f'The beginning of the execution of the command "{arguments_string_representation}".') + + with Popen(list(arguments), stdout=PIPE, stderr=PIPE, bufsize=1, universal_newlines=True) as process: + stderr_reading_thread = self.run_stderr_thread(process, stderr_buffer, result, catch_output, stderr_callback) + if token is not None: + killing_thread = self.run_killing_thread(process, token, result) + + for line in process.stdout: + stdout_buffer.append(line) + if not catch_output: + stdout_callback(line) + + stderr_reading_thread.join() + if token is not None: + killing_thread.join() + + self.fill_result(stdout_buffer, stderr_buffer, process.returncode, result) + + if process.returncode != 0: + if result.killed_by_token: + try: + token.check() + except CancellationError as e: + e.result = result + raise e + else: + message = f'Error when executing the command "{arguments_string_representation}".' + logger.error(message) + exception = RunningCommandError(message) + exception.result = result + raise exception + + else: + logger.info(f'The command "{arguments_string_representation}" has been successfully executed.') + + return result + + def run_killing_thread(self, process: Popen, token: AbstractToken, result: SubprocessResult) -> None: + thread = Thread(target=self.killing_loop, args=(process, token, result)) + thread.start() + return thread + + @staticmethod + def killing_loop(process: Popen, token: AbstractToken, result: SubprocessResult) -> None: + while True: + if not token: + process.kill() + result.killed_by_token = True + break + if process.poll() is not None: + break + sleep(0.0001) + + def run_stderr_thread(self, process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> Thread: + thread = Thread(target=self.read_stderr, args=(process, stderr_buffer, result, catch_output, stderr_callback)) + thread.start() + return thread + + @staticmethod + def read_stderr(process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> None: + for line in process.stderr: + stderr_buffer.append(line) + if not catch_output: + stderr_callback(line) + + @staticmethod + def fill_result(stdout_buffer: List[str], stderr_buffer: List[str], returncode: int, result: SubprocessResult) -> None: + result.stdout = ''.join(stdout_buffer) + result.stderr = ''.join(stderr_buffer) + result.returncode = returncode diff --git a/suby/py.typed b/suby/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py new file mode 100644 index 0000000..663261c --- /dev/null +++ b/tests/test_proxy_module.py @@ -0,0 +1,10 @@ +import sys +import suby + + +def test_normal_way(): + result = suby(sys.executable, '-c', 'print("kek")') + + assert result.stdout == 'kek\n' + assert result.stderr == '' + assert result.returncode == 0 From 69f7aa068e611dc3e59322ab5eb7885cca8e1a8b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 00:06:22 +0300 Subject: [PATCH 02/61] cantok as a dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e60d826..3ee7bf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ readme = "README.md" requires-python = ">=3.7" dependencies = [ 'emptylog', + 'cantok', ] classifiers = [ "Operating System :: OS Independent", From 9bf336bc3c1a9b1200cc33fc0212c6ae09d21042 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 00:11:32 +0300 Subject: [PATCH 03/61] type hints --- suby/proxy_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 5a2ba22..fb7bf63 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -34,8 +34,8 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr arguments_string_representation = ' '.join([argument if ' ' not in argument else f'"{argument}"' for argument in arguments]) - stdout_buffer = [] - stderr_buffer = [] + stdout_buffer: List[str] = [] + stderr_buffer: List[str] = [] result = SubprocessResult() logger.info(f'The beginning of the execution of the command "{arguments_string_representation}".') @@ -45,7 +45,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr if token is not None: killing_thread = self.run_killing_thread(process, token, result) - for line in process.stdout: + for line in process.stdout: # type: ignore[union-attr] stdout_buffer.append(line) if not catch_output: stdout_callback(line) From 014508a04c7fec24674e63c77d1f027bc9b4b0c6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 00:15:52 +0300 Subject: [PATCH 04/61] type hints --- suby/errors.py | 7 ++++++- suby/proxy_module.py | 13 ++----------- suby/subprocess_result.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 suby/subprocess_result.py diff --git a/suby/errors.py b/suby/errors.py index b683070..becd624 100644 --- a/suby/errors.py +++ b/suby/errors.py @@ -1,2 +1,7 @@ +from suby.subprocess_result import SubprocessResult + + class RunningCommandError(Exception): - pass + def __init__(self, message: str, subprocess_result: SubprocessResult) -> None: + self.result = subprocess_result + super().__init__(self, message) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index fb7bf63..59c984a 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -1,6 +1,5 @@ import sys from time import sleep -from dataclasses import dataclass, field from threading import Thread, Lock from subprocess import Popen, PIPE from typing import List, Callable, Union, Optional, Any @@ -10,14 +9,7 @@ from cantok import AbstractToken, SimpleToken, TimeoutToken, CancellationError from suby.errors import RunningCommandError - - -@dataclass -class SubprocessResult: - stdout: Optional[str] = None - stderr: Optional[str] = None - returncode: Optional[int] = None - killed_by_token: bool = False +from suby.subprocess_result import SubprocessResult class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] @@ -66,8 +58,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr else: message = f'Error when executing the command "{arguments_string_representation}".' logger.error(message) - exception = RunningCommandError(message) - exception.result = result + exception = RunningCommandError(message, result) raise exception else: diff --git a/suby/subprocess_result.py b/suby/subprocess_result.py new file mode 100644 index 0000000..3fa3966 --- /dev/null +++ b/suby/subprocess_result.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class SubprocessResult: + stdout: Optional[str] = None + stderr: Optional[str] = None + returncode: Optional[int] = None + killed_by_token: bool = False From f708e4447e97af2a82cfc691d24e0cbcdcb4083b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 00:17:30 +0300 Subject: [PATCH 05/61] no extra code --- suby/proxy_module.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 59c984a..7564ee9 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -1,6 +1,6 @@ import sys from time import sleep -from threading import Thread, Lock +from threading import Thread from subprocess import Popen, PIPE from typing import List, Callable, Union, Optional, Any from functools import partial @@ -13,8 +13,6 @@ class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] - lock = Lock() - def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerProtocol = EmptyLogger(), stdout_callback: Callable[[str], Any] = partial(print, end=''), stderr_callback: Callable[[str], Any] = sys.stderr.write, timeout: Optional[Union[int, float]] = None, token: Optional[AbstractToken] = None) -> SubprocessResult: """ About reading from strout and stderr: https://stackoverflow.com/a/28319191/14522393 @@ -71,6 +69,11 @@ def run_killing_thread(self, process: Popen, token: AbstractToken, result: Subpr thread.start() return thread + def run_stderr_thread(self, process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> Thread: + thread = Thread(target=self.read_stderr, args=(process, stderr_buffer, result, catch_output, stderr_callback)) + thread.start() + return thread + @staticmethod def killing_loop(process: Popen, token: AbstractToken, result: SubprocessResult) -> None: while True: @@ -82,11 +85,6 @@ def killing_loop(process: Popen, token: AbstractToken, result: SubprocessResult) break sleep(0.0001) - def run_stderr_thread(self, process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> Thread: - thread = Thread(target=self.read_stderr, args=(process, stderr_buffer, result, catch_output, stderr_callback)) - thread.start() - return thread - @staticmethod def read_stderr(process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> None: for line in process.stderr: From 880659fad1bdaae540108af6c555b98ee25fa4fc Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 13:37:47 +0300 Subject: [PATCH 06/61] certain versions of deps --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ee7bf4..3e952ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,8 @@ description = 'Slightly simplified subprocesses' readme = "README.md" requires-python = ">=3.7" dependencies = [ - 'emptylog', - 'cantok', + 'emptylog>=0.0.2', + 'cantok>=0.0.17', ] classifiers = [ "Operating System :: OS Independent", From 1f1ade122a72148921a343682ed2bf143d59c45e Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 13:39:28 +0300 Subject: [PATCH 07/61] type hint --- suby/proxy_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 7564ee9..3d61e10 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -64,7 +64,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr return result - def run_killing_thread(self, process: Popen, token: AbstractToken, result: SubprocessResult) -> None: + def run_killing_thread(self, process: Popen, token: AbstractToken, result: SubprocessResult) -> Thread: thread = Thread(target=self.killing_loop, args=(process, token, result)) thread.start() return thread From b80103f0c636784e703abac7e07a52e66a4d569f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 14:14:38 +0300 Subject: [PATCH 08/61] type hints --- suby/proxy_module.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 3d61e10..fc4e517 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -49,7 +49,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr if process.returncode != 0: if result.killed_by_token: try: - token.check() + token.check() # type: ignore[union-attr] except CancellationError as e: e.result = result raise e @@ -64,18 +64,18 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr return result - def run_killing_thread(self, process: Popen, token: AbstractToken, result: SubprocessResult) -> Thread: + def run_killing_thread(self, process: Popen, token: AbstractToken, result: SubprocessResult) -> Thread: # type: ignore[type-arg] thread = Thread(target=self.killing_loop, args=(process, token, result)) thread.start() return thread - def run_stderr_thread(self, process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> Thread: + def run_stderr_thread(self, process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> Thread: # type: ignore[type-arg] thread = Thread(target=self.read_stderr, args=(process, stderr_buffer, result, catch_output, stderr_callback)) thread.start() return thread @staticmethod - def killing_loop(process: Popen, token: AbstractToken, result: SubprocessResult) -> None: + def killing_loop(process: Popen, token: AbstractToken, result: SubprocessResult) -> None: # type: ignore[type-arg] while True: if not token: process.kill() @@ -86,7 +86,7 @@ def killing_loop(process: Popen, token: AbstractToken, result: SubprocessResult) sleep(0.0001) @staticmethod - def read_stderr(process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> None: + def read_stderr(process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> None: # type: ignore[type-arg] for line in process.stderr: stderr_buffer.append(line) if not catch_output: From 446983ef267116fd18b5346ac9e0ee35564d9509 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 14:17:05 +0300 Subject: [PATCH 09/61] mypy ignore comments --- suby/proxy_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index fc4e517..e558c78 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -20,7 +20,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr if timeout is not None and token is None: token = TimeoutToken(timeout) elif timeout is not None: - token += TimeoutToken(timeout) + token += TimeoutToken(timeout) # type: ignore[operator] arguments_string_representation = ' '.join([argument if ' ' not in argument else f'"{argument}"' for argument in arguments]) @@ -51,7 +51,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr try: token.check() # type: ignore[union-attr] except CancellationError as e: - e.result = result + e.result = result # type: ignore[attr-defined] raise e else: message = f'Error when executing the command "{arguments_string_representation}".' @@ -87,7 +87,7 @@ def killing_loop(process: Popen, token: AbstractToken, result: SubprocessResult) @staticmethod def read_stderr(process: Popen, stderr_buffer: List[str], result: SubprocessResult, catch_output: bool, stderr_callback: Callable[[str], Any]) -> None: # type: ignore[type-arg] - for line in process.stderr: + for line in process.stderr: # type: ignore[union-attr] stderr_buffer.append(line) if not catch_output: stderr_callback(line) From fed4fd35f75146af4d8ff68fa979421fb02391d6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 15:34:27 +0300 Subject: [PATCH 10/61] no extra import --- suby/proxy_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index e558c78..46f0f7c 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -6,7 +6,7 @@ from functools import partial from emptylog import EmptyLogger, LoggerProtocol -from cantok import AbstractToken, SimpleToken, TimeoutToken, CancellationError +from cantok import AbstractToken, TimeoutToken, CancellationError from suby.errors import RunningCommandError from suby.subprocess_result import SubprocessResult From ab56d283404dc54e8c23411de9412660bdb70a85 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 15:40:02 +0300 Subject: [PATCH 11/61] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3e817e0..c19f2d3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ test.py .idea .ruff_cache .mutmut-cache +.mypy_cache From 782549bfcb5ab103f5b5ffb2a6f603c46b7b3bc0 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 15:48:52 +0300 Subject: [PATCH 12/61] catch_exceptions flag --- suby/proxy_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 46f0f7c..357d611 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -13,7 +13,7 @@ class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] - def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerProtocol = EmptyLogger(), stdout_callback: Callable[[str], Any] = partial(print, end=''), stderr_callback: Callable[[str], Any] = sys.stderr.write, timeout: Optional[Union[int, float]] = None, token: Optional[AbstractToken] = None) -> SubprocessResult: + def __call__(self, *arguments: str, catch_output: bool = False, catch_exceptions: bool = False, logger: LoggerProtocol = EmptyLogger(), stdout_callback: Callable[[str], Any] = partial(print, end=''), stderr_callback: Callable[[str], Any] = sys.stderr.write, timeout: Optional[Union[int, float]] = None, token: Optional[AbstractToken] = None) -> SubprocessResult: """ About reading from strout and stderr: https://stackoverflow.com/a/28319191/14522393 """ @@ -46,7 +46,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, logger: LoggerPr self.fill_result(stdout_buffer, stderr_buffer, process.returncode, result) - if process.returncode != 0: + if process.returncode != 0 and not catch_exceptions: if result.killed_by_token: try: token.check() # type: ignore[union-attr] From 5a0aa37586ffb15b0b634e934cff8c89db6cba3b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 15:55:55 +0300 Subject: [PATCH 13/61] tests --- tests/test_proxy_module.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 663261c..b59dd66 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -1,4 +1,6 @@ import sys +from time import perf_counter + import suby @@ -8,3 +10,34 @@ def test_normal_way(): assert result.stdout == 'kek\n' assert result.stderr == '' assert result.returncode == 0 + + +def test_stderr_catching(): + result = suby(sys.executable, '-c', 'import sys; sys.stderr.write("kek")') + + assert result.stdout == '' + assert result.stderr == 'kek' + assert result.returncode == 0 + + +def test_catch_exception(): + result = suby(sys.executable, '-c', 'raise ValueError', catch_exceptions=True) + + assert 'ValueError' in result.stderr + assert result.returncode != 0 + + +def test_timeout(): + sleep_time = 100000 + timeout = 0.001 + + start_time = perf_counter() + result = suby(sys.executable, '-c', f'import time; time.sleep({sleep_time})', timeout=timeout, catch_exceptions=True) + end_time = perf_counter() + + assert result.returncode != 0 + assert result.stdout == '' + assert result.stderr == '' + + assert (end_time - start_time) < sleep_time + assert (end_time - start_time) >= timeout From 4d0c231bd169021cc9a60ac6e1451a298d8825d9 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 17:15:09 +0300 Subject: [PATCH 14/61] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57eee4d..fcb6b28 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# subi \ No newline at end of file +# suby From 225606b67fc83b94b31cb1bbaec80ca88badf954 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 17:23:12 +0300 Subject: [PATCH 15/61] +1 test --- tests/test_proxy_module.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index b59dd66..296a212 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -1,6 +1,9 @@ import sys from time import perf_counter +import pytest +from cantok import TimeoutCancellationError + import suby @@ -41,3 +44,23 @@ def test_timeout(): assert (end_time - start_time) < sleep_time assert (end_time - start_time) >= timeout + + +def test_timeout_without_catching_exception(): + sleep_time = 100000 + timeout = 0.001 + + with pytest.raises(TimeoutCancellationError): + suby(sys.executable, '-c', f'import time; time.sleep({sleep_time})', timeout=timeout) + + start_time = perf_counter() + try: + result = suby(sys.executable, '-c', f'import time; time.sleep({sleep_time})', timeout=timeout) + except TimeoutCancellationError as e: + assert e.result.stdout == '' + assert e.result.stderr == '' + assert e.result.returncode != 0 + end_time = perf_counter() + + assert (end_time - start_time) < sleep_time + assert (end_time - start_time) >= timeout From 9254737cf068a010962bcd1e19e997b562ca4a9b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 21:20:50 +0300 Subject: [PATCH 16/61] new version of emptylog --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3e952ea..443aec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ description = 'Slightly simplified subprocesses' readme = "README.md" requires-python = ">=3.7" dependencies = [ - 'emptylog>=0.0.2', + 'emptylog>=0.0.3', 'cantok>=0.0.17', ] classifiers = [ From 7a886b603cd319a33466829e520b84fe0f838c5b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 21:24:38 +0300 Subject: [PATCH 17/61] +1 reimport --- suby/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/suby/__init__.py b/suby/__init__.py index ca05484..e7948c9 100644 --- a/suby/__init__.py +++ b/suby/__init__.py @@ -1,6 +1,7 @@ import sys from suby.proxy_module import ProxyModule as ProxyModule +from suby.errors import RunningCommandError as RunningCommandError # noqa: F401 sys.modules[__name__].__class__ = ProxyModule From d922119a4e69dff693dabf1c06b4984f74ea35c0 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:10:36 +0300 Subject: [PATCH 18/61] autoflushing --- suby/callbacks.py | 10 ++++++++++ suby/proxy_module.py | 3 ++- suby/subprocess_result.py | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 suby/callbacks.py diff --git a/suby/callbacks.py b/suby/callbacks.py new file mode 100644 index 0000000..9014ab0 --- /dev/null +++ b/suby/callbacks.py @@ -0,0 +1,10 @@ +import sys + + +def stderr_with_flush(string: str) -> None: + sys.stderr.write(string) + sys.stderr.flush() + +def stdout_with_flush(string: str) -> None: + print(string, end='') + sys.stdout.flush() diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 357d611..1918fce 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -10,10 +10,11 @@ from suby.errors import RunningCommandError from suby.subprocess_result import SubprocessResult +from suby.callbacks import stdout_with_flush, stderr_with_flush class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] - def __call__(self, *arguments: str, catch_output: bool = False, catch_exceptions: bool = False, logger: LoggerProtocol = EmptyLogger(), stdout_callback: Callable[[str], Any] = partial(print, end=''), stderr_callback: Callable[[str], Any] = sys.stderr.write, timeout: Optional[Union[int, float]] = None, token: Optional[AbstractToken] = None) -> SubprocessResult: + def __call__(self, *arguments: str, catch_output: bool = False, catch_exceptions: bool = False, logger: LoggerProtocol = EmptyLogger(), stdout_callback: Callable[[str], Any] = stdout_with_flush, stderr_callback: Callable[[str], Any] = stderr_with_flush, timeout: Optional[Union[int, float]] = None, token: Optional[AbstractToken] = None) -> SubprocessResult: """ About reading from strout and stderr: https://stackoverflow.com/a/28319191/14522393 """ diff --git a/suby/subprocess_result.py b/suby/subprocess_result.py index 3fa3966..9dd6185 100644 --- a/suby/subprocess_result.py +++ b/suby/subprocess_result.py @@ -1,9 +1,11 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional +from uuid import uuid1 @dataclass class SubprocessResult: + id: str = field(default_factory=lambda: str(uuid1()).replace('-', '')) stdout: Optional[str] = None stderr: Optional[str] = None returncode: Optional[int] = None From 439056087956da1dd90e47bc7714e4746c350aaf Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:10:46 +0300 Subject: [PATCH 19/61] tests --- tests/test_proxy_module.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 296a212..9780407 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -1,10 +1,13 @@ import sys from time import perf_counter +from io import StringIO +from contextlib import redirect_stdout, redirect_stderr import pytest from cantok import TimeoutCancellationError import suby +from suby import RunningCommandError def test_normal_way(): @@ -64,3 +67,42 @@ def test_timeout_without_catching_exception(): assert (end_time - start_time) < sleep_time assert (end_time - start_time) >= timeout + + +def test_exception_in_subprocess_without_catching(): + with pytest.raises(RunningCommandError, match=f'Error when executing the command "{sys.executable} -c "raise ValueError"".'): + suby(sys.executable, '-c', 'raise ValueError') + + try: + suby(sys.executable, '-c', 'raise ValueError') + except RunningCommandError as e: + assert e.result.stdout == '' + assert 'ValueError' in e.result.stderr + assert e.result.returncode != 0 + + +def test_not_catching_output(): + stderr_buffer = StringIO() + stdout_buffer = StringIO() + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + result = suby(sys.executable, '-c', 'print("kek1", end=""); import sys; sys.stderr.write("kek2")', catch_output=False) + + stderr = stderr_buffer.getvalue() + stdout = stdout_buffer.getvalue() + + assert result.returncode == 0 + assert stderr == 'kek2' + assert stdout == 'kek1' + + +def test_catching_output(): + stderr_buffer = StringIO() + stdout_buffer = StringIO() + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + result = suby(sys.executable, '-c', 'print("kek1", end=""); import sys; sys.stderr.write("kek2")', catch_output=True) + + assert result.returncode == 0 + assert stderr_buffer.getvalue() == '' + assert stdout_buffer.getvalue() == '' From ff353b005aa7a79797403eb4f604f8dd183581b1 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:13:06 +0300 Subject: [PATCH 20/61] emptylog in requirements --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index 44f8cfc..6b880b0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,3 +5,4 @@ twine==4.0.2 mypy==1.4.1 ruff==0.0.290 mutmut==2.4.4 +emptylog>=0.0.3 From 46aed4bca87d3491d404db048d0e0b3f0623bfe6 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:17:26 +0300 Subject: [PATCH 21/61] tests --- tests/test_subprocess_result.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_subprocess_result.py diff --git a/tests/test_subprocess_result.py b/tests/test_subprocess_result.py new file mode 100644 index 0000000..f99d547 --- /dev/null +++ b/tests/test_subprocess_result.py @@ -0,0 +1,14 @@ +from suby.subprocess_result import SubprocessResult + + +def test_auto_id(): + assert SubprocessResult().id != SubprocessResult().id + assert isinstance(SubprocessResult().id, str) + assert len(SubprocessResult().id) > 10 + + +def test_default_values(): + assert SubprocessResult().stdout is None + assert SubprocessResult().stderr is None + assert SubprocessResult().returncode is None + assert SubprocessResult().killed_by_token == False From fb4b43d4f55daeaecb3bfea3cea4078a19d5ecda Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:20:08 +0300 Subject: [PATCH 22/61] pytest --- tests/test_callbacks.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test_callbacks.py diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py new file mode 100644 index 0000000..467b6d6 --- /dev/null +++ b/tests/test_callbacks.py @@ -0,0 +1,26 @@ +from io import StringIO +from contextlib import redirect_stdout, redirect_stderr + +from suby.callbacks import stdout_with_flush, stderr_with_flush + + +def test_output_to_stdout(): + stderr_buffer = StringIO() + stdout_buffer = StringIO() + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + stdout_with_flush('kek') + + assert stderr_buffer.getvalue() == '' + assert stdout_buffer.getvalue() == 'kek' + + +def test_output_to_stderr(): + stderr_buffer = StringIO() + stdout_buffer = StringIO() + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + stderr_with_flush('kek') + + assert stderr_buffer.getvalue() == 'kek' + assert stdout_buffer.getvalue() == '' From 9a98b9ecfdc7019bac8628fd60c3e54a657dc023 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:27:02 +0300 Subject: [PATCH 23/61] exception init --- suby/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suby/errors.py b/suby/errors.py index becd624..f501c62 100644 --- a/suby/errors.py +++ b/suby/errors.py @@ -4,4 +4,4 @@ class RunningCommandError(Exception): def __init__(self, message: str, subprocess_result: SubprocessResult) -> None: self.result = subprocess_result - super().__init__(self, message) + super().__init__(message) From 436696dad4ac970ce8cf3407f2edf603e2a2d7e2 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:27:18 +0300 Subject: [PATCH 24/61] +1 test --- tests/test_errors.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/test_errors.py diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..cfbe997 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,10 @@ +from suby.errors import RunningCommandError +from suby.subprocess_result import SubprocessResult + + +def test_init_exception_and_raise(): + try: + raise RunningCommandError('kek', SubprocessResult()) + except RunningCommandError as e: + assert str(e) == 'kek' + assert isinstance(e.result, SubprocessResult) From 22c1673582897bf5a4f05d29f688ee8051dfe90f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:41:32 +0300 Subject: [PATCH 25/61] added logging when a command was cancelled by token --- suby/proxy_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 1918fce..607fe23 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -49,6 +49,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, catch_exceptions if process.returncode != 0 and not catch_exceptions: if result.killed_by_token: + logger.error(f'The execution of the "{arguments_string_representation}" command was canceled using a cancellation token.') try: token.check() # type: ignore[union-attr] except CancellationError as e: From 9b9482a1b2855d92b5e9315b14c8cce407b9304f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:52:02 +0300 Subject: [PATCH 26/61] logging in all cases --- suby/proxy_module.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 607fe23..1668a10 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -47,19 +47,25 @@ def __call__(self, *arguments: str, catch_output: bool = False, catch_exceptions self.fill_result(stdout_buffer, stderr_buffer, process.returncode, result) - if process.returncode != 0 and not catch_exceptions: - if result.killed_by_token: - logger.error(f'The execution of the "{arguments_string_representation}" command was canceled using a cancellation token.') - try: - token.check() # type: ignore[union-attr] - except CancellationError as e: - e.result = result # type: ignore[attr-defined] - raise e + if process.returncode != 0: + if not catch_exceptions: + if result.killed_by_token: + logger.error(f'The execution of the "{arguments_string_representation}" command was canceled using a cancellation token.') + try: + token.check() # type: ignore[union-attr] + except CancellationError as e: + e.result = result # type: ignore[attr-defined] + raise e + else: + message = f'Error when executing the command "{arguments_string_representation}".' + logger.error(message) + exception = RunningCommandError(message, result) + raise exception else: - message = f'Error when executing the command "{arguments_string_representation}".' - logger.error(message) - exception = RunningCommandError(message, result) - raise exception + if result.killed_by_token: + logger.error(f'The execution of the "{arguments_string_representation}" command was canceled using a cancellation token.') + else: + logger.error(f'Error when executing the command "{arguments_string_representation}".') else: logger.info(f'The command "{arguments_string_representation}" has been successfully executed.') From 3e3d8e2d049a036e6ae5f179c6233ff255550c91 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:52:20 +0300 Subject: [PATCH 27/61] new tests --- tests/test_proxy_module.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 9780407..02057bb 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -5,6 +5,7 @@ import pytest from cantok import TimeoutCancellationError +from emptylog import MemoryLogger import suby from suby import RunningCommandError @@ -106,3 +107,28 @@ def test_catching_output(): assert result.returncode == 0 assert stderr_buffer.getvalue() == '' assert stdout_buffer.getvalue() == '' + + +def test_logging_normal_way(): + logger = MemoryLogger() + + suby(sys.executable, '-c', 'print("kek", end="")', logger=logger, catch_output=True) + + assert len(logger.data.info) == 2 + assert len(logger.data.error) == 0 + + assert logger.data.info[0].message == f'The beginning of the execution of the command "{sys.executable} -c "print("kek", end="")"".' + assert logger.data.info[1].message == f'The command "{sys.executable} -c "print("kek", end="")"" has been successfully executed.' + + +def test_logging_with_timeout(): + logger = MemoryLogger() + + suby(sys.executable, '-c', f'import time; time.sleep({500_000})', logger=logger, catch_exceptions=True, catch_output=True, timeout=0.0001) + + print(logger.data.error) + assert len(logger.data.info) == 1 + assert len(logger.data.error) == 1 + + assert logger.data.info[0].message == f'The beginning of the execution of the command "{sys.executable} -c "import time; time.sleep(500000)"".' + assert logger.data.error[0].message == f'The execution of the "{sys.executable} -c "import time; time.sleep(500000)"" command was canceled using a cancellation token.' From 23c7721148f583542e6ab15ae3ff3c2c008696e5 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:52:43 +0300 Subject: [PATCH 28/61] no extra print --- tests/test_proxy_module.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 02057bb..2eb22d7 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -126,7 +126,6 @@ def test_logging_with_timeout(): suby(sys.executable, '-c', f'import time; time.sleep({500_000})', logger=logger, catch_exceptions=True, catch_output=True, timeout=0.0001) - print(logger.data.error) assert len(logger.data.info) == 1 assert len(logger.data.error) == 1 From bc8f8047026f7358e7c95b010ac9d8d38fd2f786 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 13 Feb 2024 23:57:36 +0300 Subject: [PATCH 29/61] simplify --- suby/proxy_module.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 1668a10..98a9578 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -59,8 +59,7 @@ def __call__(self, *arguments: str, catch_output: bool = False, catch_exceptions else: message = f'Error when executing the command "{arguments_string_representation}".' logger.error(message) - exception = RunningCommandError(message, result) - raise exception + raise RunningCommandError(message, result) else: if result.killed_by_token: logger.error(f'The execution of the "{arguments_string_representation}" command was canceled using a cancellation token.') From c48c477e3196b81758f415d03733b14a849fea2e Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 00:04:38 +0300 Subject: [PATCH 30/61] new tests --- tests/test_proxy_module.py | 40 +++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 2eb22d7..8d32b6a 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -121,7 +121,7 @@ def test_logging_normal_way(): assert logger.data.info[1].message == f'The command "{sys.executable} -c "print("kek", end="")"" has been successfully executed.' -def test_logging_with_timeout(): +def test_logging_with_expired_timeout(): logger = MemoryLogger() suby(sys.executable, '-c', f'import time; time.sleep({500_000})', logger=logger, catch_exceptions=True, catch_output=True, timeout=0.0001) @@ -131,3 +131,41 @@ def test_logging_with_timeout(): assert logger.data.info[0].message == f'The beginning of the execution of the command "{sys.executable} -c "import time; time.sleep(500000)"".' assert logger.data.error[0].message == f'The execution of the "{sys.executable} -c "import time; time.sleep(500000)"" command was canceled using a cancellation token.' + + +def test_logging_with_exception(): + logger = MemoryLogger() + + suby(sys.executable, '-c', f'1/0', logger=logger, catch_exceptions=True, catch_output=True) + + assert len(logger.data.info) == 1 + assert len(logger.data.error) == 1 + + assert logger.data.info[0].message == f'The beginning of the execution of the command "{sys.executable} -c 1/0".' + assert logger.data.error[0].message == f'Error when executing the command "{sys.executable} -c 1/0".' + + +def test_logging_with_expired_timeout_without_catching_exceptions(): + logger = MemoryLogger() + + with pytest.raises(TimeoutCancellationError): + suby(sys.executable, '-c', f'import time; time.sleep({500_000})', logger=logger, catch_output=True, timeout=0.0001) + + assert len(logger.data.info) == 1 + assert len(logger.data.error) == 1 + + assert logger.data.info[0].message == f'The beginning of the execution of the command "{sys.executable} -c "import time; time.sleep(500000)"".' + assert logger.data.error[0].message == f'The execution of the "{sys.executable} -c "import time; time.sleep(500000)"" command was canceled using a cancellation token.' + + +def test_logging_with_exception_without_catching_exceptions(): + logger = MemoryLogger() + + with pytest.raises(RunningCommandError): + suby(sys.executable, '-c', f'1/0', logger=logger, catch_output=True) + + assert len(logger.data.info) == 1 + assert len(logger.data.error) == 1 + + assert logger.data.info[0].message == f'The beginning of the execution of the command "{sys.executable} -c 1/0".' + assert logger.data.error[0].message == f'Error when executing the command "{sys.executable} -c 1/0".' From 94c2db15d3123cce6e67adb5f0680e772c8ad01d Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 01:32:04 +0300 Subject: [PATCH 31/61] test --- tests/test_errors.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_errors.py b/tests/test_errors.py index cfbe997..3b4d726 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -3,8 +3,9 @@ def test_init_exception_and_raise(): + result = SubprocessResult() try: - raise RunningCommandError('kek', SubprocessResult()) + raise RunningCommandError('kek', result) except RunningCommandError as e: assert str(e) == 'kek' - assert isinstance(e.result, SubprocessResult) + assert e.result is result From b206dacea719b68474754562ca7ebad97bd3cc26 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 01:36:30 +0300 Subject: [PATCH 32/61] re escaping in a test --- tests/test_proxy_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 8d32b6a..af525f4 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -1,3 +1,4 @@ +import re import sys from time import perf_counter from io import StringIO @@ -71,7 +72,7 @@ def test_timeout_without_catching_exception(): def test_exception_in_subprocess_without_catching(): - with pytest.raises(RunningCommandError, match=f'Error when executing the command "{sys.executable} -c "raise ValueError"".'): + with pytest.raises(RunningCommandError, match=re.escape(f'Error when executing the command "{sys.executable} -c "raise ValueError"".')): suby(sys.executable, '-c', 'raise ValueError') try: From ac32333deb2d539fb95f8f3bfff9377d48577517 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 01:37:13 +0300 Subject: [PATCH 33/61] no extra import --- suby/proxy_module.py | 1 - 1 file changed, 1 deletion(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index 98a9578..d19d1f5 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -3,7 +3,6 @@ from threading import Thread from subprocess import Popen, PIPE from typing import List, Callable, Union, Optional, Any -from functools import partial from emptylog import EmptyLogger, LoggerProtocol from cantok import AbstractToken, TimeoutToken, CancellationError From 8dfe24debb134c8ec82c3e6a77f1d71c0dc88853 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 14:20:38 +0300 Subject: [PATCH 34/61] code style --- suby/proxy_module.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/suby/proxy_module.py b/suby/proxy_module.py index d19d1f5..9237fbb 100644 --- a/suby/proxy_module.py +++ b/suby/proxy_module.py @@ -13,7 +13,17 @@ class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] - def __call__(self, *arguments: str, catch_output: bool = False, catch_exceptions: bool = False, logger: LoggerProtocol = EmptyLogger(), stdout_callback: Callable[[str], Any] = stdout_with_flush, stderr_callback: Callable[[str], Any] = stderr_with_flush, timeout: Optional[Union[int, float]] = None, token: Optional[AbstractToken] = None) -> SubprocessResult: + def __call__( + self, + *arguments: str, + catch_output: bool = False, + catch_exceptions: bool = False, + logger: LoggerProtocol = EmptyLogger(), + stdout_callback: Callable[[str], Any] = stdout_with_flush, + stderr_callback: Callable[[str], Any] = stderr_with_flush, + timeout: Optional[Union[int, float]] = None, + token: Optional[AbstractToken] = None, + ) -> SubprocessResult: """ About reading from strout and stderr: https://stackoverflow.com/a/28319191/14522393 """ From dd207df198c81df1e1f6a7e7c422a59399d6e1a7 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 14:29:30 +0300 Subject: [PATCH 35/61] +1 test --- tests/test_proxy_module.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index af525f4..80f59dc 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -5,7 +5,7 @@ from contextlib import redirect_stdout, redirect_stderr import pytest -from cantok import TimeoutCancellationError +from cantok import TimeoutCancellationError, ConditionToken from emptylog import MemoryLogger import suby @@ -170,3 +170,23 @@ def test_logging_with_exception_without_catching_exceptions(): assert logger.data.info[0].message == f'The beginning of the execution of the command "{sys.executable} -c 1/0".' assert logger.data.error[0].message == f'Error when executing the command "{sys.executable} -c 1/0".' + + +def test_only_token(): + sleep_time = 100000 + timeout = 0.1 + + start_time = perf_counter() + token = ConditionToken(lambda: perf_counter() - start_time > timeout) + + result = suby(sys.executable, '-c', f'import time; time.sleep({sleep_time})', catch_exceptions=True, token=token) + + end_time = perf_counter() + + assert result.returncode != 0 + assert result.stdout == '' + assert result.stderr == '' + assert result.killed_by_token == True + + assert end_time - start_time >= timeout + assert end_time - start_time < sleep_time From 4fbfcf0094f85ce3f279c8617f66be7bfd09a968 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 14:32:41 +0300 Subject: [PATCH 36/61] +1 test --- tests/test_proxy_module.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 80f59dc..ec3409b 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -5,7 +5,7 @@ from contextlib import redirect_stdout, redirect_stderr import pytest -from cantok import TimeoutCancellationError, ConditionToken +from cantok import TimeoutCancellationError, ConditionCancellationError, ConditionToken from emptylog import MemoryLogger import suby @@ -190,3 +190,27 @@ def test_only_token(): assert end_time - start_time >= timeout assert end_time - start_time < sleep_time + + +def test_only_token_without_catching(): + sleep_time = 100000 + timeout = 0.1 + + start_time = perf_counter() + token = ConditionToken(lambda: perf_counter() - start_time > timeout) + + try: + result = suby(sys.executable, '-c', f'import time; time.sleep({sleep_time})', token=token) + except ConditionCancellationError as e: + assert e.token is token + result = e.result + + end_time = perf_counter() + + assert result.returncode != 0 + assert result.stdout == '' + assert result.stderr == '' + assert result.killed_by_token == True + + assert end_time - start_time >= timeout + assert end_time - start_time < sleep_time From 19ddddec1cfe4e7641a608db30e8cca11cc10d84 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 14:35:36 +0300 Subject: [PATCH 37/61] +1 test --- tests/test_proxy_module.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index ec3409b..b5555ef 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -214,3 +214,51 @@ def test_only_token_without_catching(): assert end_time - start_time >= timeout assert end_time - start_time < sleep_time + + +def test_token_plus_timeout_but_timeout_is_more_without_catching(): + sleep_time = 100000 + timeout = 0.1 + + start_time = perf_counter() + token = ConditionToken(lambda: perf_counter() - start_time > timeout) + + try: + result = suby(sys.executable, '-c', f'import time; time.sleep({sleep_time})', token=token, timeout=3) + except ConditionCancellationError as e: + assert e.token is token + result = e.result + + end_time = perf_counter() + + assert result.returncode != 0 + assert result.stdout == '' + assert result.stderr == '' + assert result.killed_by_token == True + + assert end_time - start_time >= timeout + assert end_time - start_time < sleep_time + + +def test_token_plus_timeout_but_timeout_is_less_without_catching(): + sleep_time = 100000 + timeout = 0.1 + + start_time = perf_counter() + token = ConditionToken(lambda: perf_counter() - start_time > timeout) + + try: + result = suby(sys.executable, '-c', f'import time; time.sleep({sleep_time})', token=token, timeout=timeout/2) + except TimeoutCancellationError as e: + assert e.token is not token + result = e.result + + end_time = perf_counter() + + assert result.returncode != 0 + assert result.stdout == '' + assert result.stderr == '' + assert result.killed_by_token == True + + assert end_time - start_time >= timeout/2 + assert end_time - start_time < sleep_time From 2c577cd3a97986e51cb0b053f049885349e3877b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 14:38:27 +0300 Subject: [PATCH 38/61] +1 test --- tests/test_proxy_module.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index b5555ef..6cc3b04 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -5,7 +5,7 @@ from contextlib import redirect_stdout, redirect_stderr import pytest -from cantok import TimeoutCancellationError, ConditionCancellationError, ConditionToken +from cantok import TimeoutCancellationError, ConditionCancellationError, ConditionToken, SimpleToken from emptylog import MemoryLogger import suby @@ -20,6 +20,14 @@ def test_normal_way(): assert result.returncode == 0 +def test_normal_way_with_simple_token(): + result = suby(sys.executable, '-c', 'print("kek")', token=SimpleToken()) + + assert result.stdout == 'kek\n' + assert result.stderr == '' + assert result.returncode == 0 + + def test_stderr_catching(): result = suby(sys.executable, '-c', 'import sys; sys.stderr.write("kek")') From 3fa71cb47149f07dd50a9d52366cc01ff261ac10 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 15:05:20 +0300 Subject: [PATCH 39/61] new tests --- tests/test_proxy_module.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_proxy_module.py b/tests/test_proxy_module.py index 6cc3b04..34a7431 100644 --- a/tests/test_proxy_module.py +++ b/tests/test_proxy_module.py @@ -270,3 +270,41 @@ def test_token_plus_timeout_but_timeout_is_less_without_catching(): assert end_time - start_time >= timeout/2 assert end_time - start_time < sleep_time + + +def test_replace_stdout_callback(): + accumulator = [] + + stderr_buffer = StringIO() + stdout_buffer = StringIO() + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + result = suby(sys.executable, '-c', 'print("kek")', stdout_callback=lambda x: accumulator.append(x)) + + assert accumulator == ['kek\n'] + + assert result.returncode == 0 + assert result.stdout == 'kek\n' + assert result.stderr == '' + + assert stderr_buffer.getvalue() == '' + assert stdout_buffer.getvalue() == '' + + +def test_replace_stderr_callback(): + accumulator = [] + + stderr_buffer = StringIO() + stdout_buffer = StringIO() + + with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer): + result = suby(sys.executable, '-c', 'import sys; sys.stderr.write("kek")', stderr_callback=lambda x: accumulator.append(x)) + + assert accumulator == ['kek'] + + assert result.returncode == 0 + assert result.stdout == '' + assert result.stderr == 'kek' + + assert stderr_buffer.getvalue() == '' + assert stdout_buffer.getvalue() == '' From f374c56a4a5378e6231741fecd244cf0da12af59 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 15:09:43 +0300 Subject: [PATCH 40/61] badges in the readme --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index fcb6b28..7a1bfba 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ # suby + +[![Downloads](https://static.pepy.tech/badge/suby/month)](https://pepy.tech/project/suby) +[![Downloads](https://static.pepy.tech/badge/suby)](https://pepy.tech/project/suby) +[![codecov](https://codecov.io/gh/pomponchik/suby/graph/badge.svg?token=IyYI7IaSet)](https://codecov.io/gh/pomponchik/suby) +[![Lines of code](https://sloc.xyz/github/pomponchik/suby/?category=code)](https://github.com/boyter/scc/) +[![Hits-of-Code](https://hitsofcode.com/github/pomponchik/suby?branch=main)](https://hitsofcode.com/github/pomponchik/suby/view?branch=main) +[![Test-Package](https://github.com/pomponchik/suby/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/suby/actions/workflows/tests_and_coverage.yml) +[![Python versions](https://img.shields.io/pypi/pyversions/suby.svg)](https://pypi.python.org/pypi/suby) +[![PyPI version](https://badge.fury.io/py/suby.svg)](https://badge.fury.io/py/suby) +[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) From cf89f979d2099c3e3911f88db004a8de38661a31 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 15:32:53 +0300 Subject: [PATCH 41/61] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c19f2d3..32ea07b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test.py .ruff_cache .mutmut-cache .mypy_cache +html From c30972242df9973e7df49bdd8dd6558078a097d8 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 15:35:10 +0300 Subject: [PATCH 42/61] readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 7a1bfba..86c4ff9 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,11 @@ [![PyPI version](https://badge.fury.io/py/suby.svg)](https://badge.fury.io/py/suby) [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + + +Here is a small wrapper around the [subprocesses](https://docs.python.org/3/library/subprocess.html). You can find many similar wrappers, but this particular one differs from the others in the following parameters: + +- Beautiful minimalistic call syntax. +- Ability to specify your callbacks to catch `stdout` and `stderr`. +- Support for [cancellation tokens](https://github.com/pomponchik/cantok). +- You can set timeouts for subprocesses. From ee918dd79db8a37ad3d93f2737ac340a4004fec5 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 15:42:43 +0300 Subject: [PATCH 43/61] readme --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 86c4ff9..7bbb86d 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,26 @@ Here is a small wrapper around the [subprocesses](https://docs.python.org/3/libr - Ability to specify your callbacks to catch `stdout` and `stderr`. - Support for [cancellation tokens](https://github.com/pomponchik/cantok). - You can set timeouts for subprocesses. +- Logging of command execution. + + +## Table of contents + +- [**Quick start**](#quick-start) + + +## Quick start + +Install it: + +```bash +pip install suby +``` + +And use: + +```python +import suby + +suby('python', '-c', 'print("hello, world!")') +``` From d018eff2382c6436fe32d97ed2bd7c32e7b0f5cd Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 15:43:44 +0300 Subject: [PATCH 44/61] readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7bbb86d..c9cb880 100644 --- a/README.md +++ b/README.md @@ -40,4 +40,5 @@ And use: import suby suby('python', '-c', 'print("hello, world!")') +# > hello, world! ``` From 7072165f87846e0913e5be7d340aa10f3e9af463 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 16:52:32 +0300 Subject: [PATCH 45/61] readme --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index c9cb880..a8fc40f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Here is a small wrapper around the [subprocesses](https://docs.python.org/3/libr ## Table of contents - [**Quick start**](#quick-start) +- [**Run subprocess and look at the result**](#run-subprocess-and-look-at-the-result) ## Quick start @@ -42,3 +43,22 @@ import suby suby('python', '-c', 'print("hello, world!")') # > hello, world! ``` + + +## Run subprocess and look at the result + +The `suby` function returns an object of the `SubprocessResult` class. It contains the following required fields: + +- **id** - a unique string that allows you to distinguish one result of calling the same command from another. +- **stdout** - a string containing the entire buffered output of the command being run. +- **stderr** - a string containing the entire buffered stderr of the command being run. +- **returncode** - an integer indicating the return code of the subprocess. + +The simplest example of how to work with it: + +```python +import suby + +suby('python', '-c', 'print("hello, world!")') +# > hello, world! +``` From d31e263a54bff81f228f8a8eab3bf06efaf7f0bf Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 17:01:07 +0300 Subject: [PATCH 46/61] readme --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a8fc40f..5b5e346 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,14 @@ The `suby` function returns an object of the `SubprocessResult` class. It contai - **stdout** - a string containing the entire buffered output of the command being run. - **stderr** - a string containing the entire buffered stderr of the command being run. - **returncode** - an integer indicating the return code of the subprocess. +- **killed_by_token** - a boolean flag indicating whether the subprocess was killed due to token cancellation. -The simplest example of how to work with it: +The simplest example of what it might look like: ```python import suby -suby('python', '-c', 'print("hello, world!")') -# > hello, world! +result = suby('python', '-c', 'print("hello, world!")') +print(result) +# > SubprocessResult(id='e9f2d29acb4011ee8957320319d7541c', stdout='hello, world!\n', stderr='', returncode=0, killed_by_token=False) ``` From f4d775b536c8502b1c4ae06a01b31eeaf3b8737c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Wed, 14 Feb 2024 17:02:45 +0300 Subject: [PATCH 47/61] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b5e346..c53bcaa 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ suby('python', '-c', 'print("hello, world!")') The `suby` function returns an object of the `SubprocessResult` class. It contains the following required fields: -- **id** - a unique string that allows you to distinguish one result of calling the same command from another. +- `**id**` - a unique string that allows you to distinguish one result of calling the same command from another. - **stdout** - a string containing the entire buffered output of the command being run. - **stderr** - a string containing the entire buffered stderr of the command being run. - **returncode** - an integer indicating the return code of the subprocess. From d0784765254e61e8e90aec98eff03f004fad36b8 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 16 Feb 2024 01:48:26 +0300 Subject: [PATCH 48/61] up the cantok version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 443aec3..63ad4b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" requires-python = ">=3.7" dependencies = [ 'emptylog>=0.0.3', - 'cantok>=0.0.17', + 'cantok>=0.0.18', ] classifiers = [ "Operating System :: OS Independent", From cb3f88ab0c09316dd769f9c8719fe014a9aded9b Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Tue, 20 Feb 2024 23:59:49 +0300 Subject: [PATCH 49/61] readme --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c53bcaa..48abe85 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Here is a small wrapper around the [subprocesses](https://docs.python.org/3/libr - [**Quick start**](#quick-start) - [**Run subprocess and look at the result**](#run-subprocess-and-look-at-the-result) +- [**Working with exceptions**](#working-with-exceptions) +- [**Output and logging**](#output-and-logging) ## Quick start @@ -49,11 +51,11 @@ suby('python', '-c', 'print("hello, world!")') The `suby` function returns an object of the `SubprocessResult` class. It contains the following required fields: -- `**id**` - a unique string that allows you to distinguish one result of calling the same command from another. +- **id** - a unique string that allows you to distinguish one result of calling the same command from another. - **stdout** - a string containing the entire buffered output of the command being run. - **stderr** - a string containing the entire buffered stderr of the command being run. -- **returncode** - an integer indicating the return code of the subprocess. -- **killed_by_token** - a boolean flag indicating whether the subprocess was killed due to token cancellation. +- **returncode** - an integer indicating the return code of the subprocess. `0` means that the process was completed successfully, the other options usually indicate something bad. +- **killed_by_token** - a boolean flag indicating whether the subprocess was killed due to [token](https://cantok.readthedocs.io/en/latest/the_pattern/) cancellation. The simplest example of what it might look like: @@ -64,3 +66,64 @@ result = suby('python', '-c', 'print("hello, world!")') print(result) # > SubprocessResult(id='e9f2d29acb4011ee8957320319d7541c', stdout='hello, world!\n', stderr='', returncode=0, killed_by_token=False) ``` + + +## Working with exceptions + +By default, `suby` raises exceptions in three cases: + +1. If the command you are calling ended with a return code not equal to `0`. In this case, you will see an exception `suby.errors.RunningCommandError`: + +```python +import suby +from suby.errors import RunningCommandError + +try: + suby('python', '-c', '1/0') +except RunningCommandError as e: + print(e) + # > Error when executing the command "python -c 1/0". +``` + +2. If you passed a [cancellation token](https://cantok.readthedocs.io/en/latest/the_pattern/) when calling the command, and the token was canceled, an exception will be raised [corresponding to the type](https://cantok.readthedocs.io/en/latest/what_are_tokens/exceptions/) of canceled token. This part of the functionality is integrated with the [cantok](https://cantok.readthedocs.io/en/latest/) library, so we recommend that you familiarize yourself with it beforehand. Here is a small example of how to pass cancellation tokens and catch exceptions from them: + +```python +from random import randint +from cantok import ConditionToken + +token = ConditionToken(lambda: randint(1, 1000) == 7) +suby('python', '-c', 'import time; time.sleep(10_000)', token=token) +``` + +3. You have set a timeout (in seconds) for the operation and it has expired. To count the timeout "under the hood", suby uses [`TimeoutToken`](https://cantok.readthedocs.io/en/latest/types_of_tokens/TimeoutToken/). Therefore, when the timeout expires, `cantok.errors.TimeoutCancellationError` will be raised: + +```python +from cantok import TimeoutCancellationError + +try: + suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1) +except TimeoutCancellationError as e: + print(e) + # > The timeout of 1 seconds has expired. +``` + +You can prevent `suby` from raising any exceptions. To do this, set the `catch_exceptions` parameter to `True`: + +```python +result = suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1, catch_exceptions=True) +print(result) +# > SubprocessResult(id='c9125b90d03111ee9660320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True) +``` + +Keep in mind that the full result of the subprocess call can also be found through the `result` attribute of any raised exception: + +```python +try: + suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1) +except TimeoutCancellationError as e: + print(e.result) + # > SubprocessResult(id='a80dc26cd03211eea347320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True) +``` + + +## Output and logging From fb40ad3b58a20876158208963615e022db71e83f Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 22 Feb 2024 02:08:32 +0300 Subject: [PATCH 50/61] readme --- README.md | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 48abe85..9685580 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Here is a small wrapper around the [subprocesses](https://docs.python.org/3/libr - [**Quick start**](#quick-start) - [**Run subprocess and look at the result**](#run-subprocess-and-look-at-the-result) -- [**Working with exceptions**](#working-with-exceptions) -- [**Output and logging**](#output-and-logging) +- [**Exceptions**](#exceptions) +- [**Output**](#output) ## Quick start @@ -68,7 +68,7 @@ print(result) ``` -## Working with exceptions +## Exceptions By default, `suby` raises exceptions in three cases: @@ -126,4 +126,29 @@ except TimeoutCancellationError as e: ``` -## Output and logging +## Output + +By default, the `stdout` and `stderr` of the subprocess are intercepted and output to the `stdout` and `stderr` of the current process. The reading from the subprocess is continuous, and the output is every time a full line is read. For continuous reading from `stderr`, a separate thread is created in the main process, so that `stdout` and `stderr` are read independently. + +You can override the output functions for `stdout` and `stderr`. To do this, you need to pass as arguments `stdout_callback` and `stderr_callback`, respectively, some functions that accept a string as an argument. For example, you can color the output (the code example uses the [`termcolor`](https://github.com/termcolor/termcolor) library): + +```python +import suby +from termcolor import colored + +def my_new_stdout(string: str) -> None: + print(colored(string, 'red'), end='') + +suby('python', '-c', 'print("hello, world!")', stdout_callback=my_new_stdout) +# > hello, world! +# You can't see it here, but believe me, if you repeat the code at home, the output in the console will be red! +``` + +You can also completely disable the output by passing `True` as the `catch_output` parameter: + +```python +suby('python', '-c', 'print("hello, world!")', catch_output=True) +# There's nothing here. +``` + +If you specify `catch_output=True`, and at the same time redefine your functions for output, your functions will not be called either. In addition, `suby` always returns [the result](#run-subprocess-and-look-at-the-result) of executing the command, containing the full output. The `catch_output` argument can stop exactly the output, but it does not prevent the collection and buffering of the output. From 6f7863232243149d3236422de6b449cc72693ff5 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 22 Feb 2024 02:23:09 +0300 Subject: [PATCH 51/61] readme --- README.md | 91 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9685580..9ee10fd 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,9 @@ Here is a small wrapper around the [subprocesses](https://docs.python.org/3/libr - [**Quick start**](#quick-start) - [**Run subprocess and look at the result**](#run-subprocess-and-look-at-the-result) -- [**Exceptions**](#exceptions) - [**Output**](#output) +- [**Logging**](#logging) +- [**Exceptions**](#exceptions) ## Quick start @@ -68,6 +69,66 @@ print(result) ``` +## Output + +By default, the `stdout` and `stderr` of the subprocess are intercepted and output to the `stdout` and `stderr` of the current process. The reading from the subprocess is continuous, and the output is every time a full line is read. For continuous reading from `stderr`, a separate thread is created in the main process, so that `stdout` and `stderr` are read independently. + +You can override the output functions for `stdout` and `stderr`. To do this, you need to pass as arguments `stdout_callback` and `stderr_callback`, respectively, some functions that accept a string as an argument. For example, you can color the output (the code example uses the [`termcolor`](https://github.com/termcolor/termcolor) library): + +```python +import suby +from termcolor import colored + +def my_new_stdout(string: str) -> None: + print(colored(string, 'red'), end='') + +suby('python', '-c', 'print("hello, world!")', stdout_callback=my_new_stdout) +# > hello, world! +# You can't see it here, but believe me, if you repeat the code at home, the output in the console will be red! +``` + +You can also completely disable the output by passing `True` as the `catch_output` parameter: + +```python +suby('python', '-c', 'print("hello, world!")', catch_output=True) +# There's nothing here. +``` + +If you specify `catch_output=True`, and at the same time redefine your functions for output, your functions will not be called either. In addition, `suby` always returns [the result](#run-subprocess-and-look-at-the-result) of executing the command, containing the full output. The `catch_output` argument can stop exactly the output, but it does not prevent the collection and buffering of the output. + + +## Logging + +By default, `suby` does not log command execution. However, you can pass a logger object to the function, and in this case logs will be recorded at the start of the command execution and at the end of the execution: + +```python +import logging +import suby + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.StreamHandler(), + ] +) + +suby('python', '-c', 'pass', logger=logging.getLogger('logger_name')) +# > 2024-02-22 02:15:08,155 [INFO] The beginning of the execution of the command "python -c pass". +# > 2024-02-22 02:15:08,190 [INFO] The command "python -c pass" has been successfully executed. +``` + +The message about the start of the command execution is always done with the `INFO` [level](https://docs.python.org/3.8/library/logging.html#logging-levels). If the command is completed successfully, the end message will also be with the `INFO` level. And if not - `ERROR`: + +```python +suby('python', '-c', 'raise ValueError', logger=logging.getLogger('logger_name'), catch_exceptions=True, catch_output=True) +# > 2024-02-22 02:20:25,549 [INFO] The beginning of the execution of the command "python -c "raise ValueError"". +# > 2024-02-22 02:20:25,590 [ERROR] Error when executing the command "python -c "raise ValueError"". +``` + +If you don't need these details, just don't pass the logger object. + + ## Exceptions By default, `suby` raises exceptions in three cases: @@ -124,31 +185,3 @@ except TimeoutCancellationError as e: print(e.result) # > SubprocessResult(id='a80dc26cd03211eea347320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True) ``` - - -## Output - -By default, the `stdout` and `stderr` of the subprocess are intercepted and output to the `stdout` and `stderr` of the current process. The reading from the subprocess is continuous, and the output is every time a full line is read. For continuous reading from `stderr`, a separate thread is created in the main process, so that `stdout` and `stderr` are read independently. - -You can override the output functions for `stdout` and `stderr`. To do this, you need to pass as arguments `stdout_callback` and `stderr_callback`, respectively, some functions that accept a string as an argument. For example, you can color the output (the code example uses the [`termcolor`](https://github.com/termcolor/termcolor) library): - -```python -import suby -from termcolor import colored - -def my_new_stdout(string: str) -> None: - print(colored(string, 'red'), end='') - -suby('python', '-c', 'print("hello, world!")', stdout_callback=my_new_stdout) -# > hello, world! -# You can't see it here, but believe me, if you repeat the code at home, the output in the console will be red! -``` - -You can also completely disable the output by passing `True` as the `catch_output` parameter: - -```python -suby('python', '-c', 'print("hello, world!")', catch_output=True) -# There's nothing here. -``` - -If you specify `catch_output=True`, and at the same time redefine your functions for output, your functions will not be called either. In addition, `suby` always returns [the result](#run-subprocess-and-look-at-the-result) of executing the command, containing the full output. The `catch_output` argument can stop exactly the output, but it does not prevent the collection and buffering of the output. From 3ed4bb2ca52584f77a2df5190de00ab58c5de8a5 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 22 Feb 2024 02:34:53 +0300 Subject: [PATCH 52/61] readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9ee10fd..5639606 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ suby('python', '-c', 'raise ValueError', logger=logging.getLogger('logger_name') If you don't need these details, just don't pass the logger object. +If you still prefer logging, you can use any object that implements the [logger protocol](https://github.com/pomponchik/emptylog?tab=readme-ov-file#universal-logger-protocol) from the [`emptylog`](https://github.com/pomponchik/emptylog) library, including ones from third-party libraries. + ## Exceptions From f6cd470b3ab14492945820bec9c67c2074724537 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Thu, 22 Feb 2024 02:52:35 +0300 Subject: [PATCH 53/61] readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 5639606..9a4660f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Here is a small wrapper around the [subprocesses](https://docs.python.org/3/libr - [**Output**](#output) - [**Logging**](#logging) - [**Exceptions**](#exceptions) +- [**Working with Cancellation Tokens**](#working-with-cancellation-tokens) ## Quick start @@ -187,3 +188,10 @@ except TimeoutCancellationError as e: print(e.result) # > SubprocessResult(id='a80dc26cd03211eea347320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True) ``` + + +## Working with Cancellation Tokens + +`suby` is fully compatible with the [cancellation token pattern](https://cantok.readthedocs.io/en/latest/the_pattern/) and supports any token objects from the [`cantok`](https://github.com/pomponchik/cantok) library. + +The essence of the pattern is that you can pass an object to `suby`, from which it can find out whether the operation still needs to be continued or not. If not, it kills the subprocess. This pattern can be especially useful in the case of commands that are executed for a long time or for an unpredictably long time. When the result becomes unnecessary, there is no point in sitting and waiting for the command to work out. From 0a72cab1644ce5d7ace16a3ed693413f9c737070 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 16:43:28 +0300 Subject: [PATCH 54/61] reimport of the TimeoutCancellationError from the cantok --- suby/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/suby/__init__.py b/suby/__init__.py index e7948c9..b11fd3f 100644 --- a/suby/__init__.py +++ b/suby/__init__.py @@ -3,5 +3,7 @@ from suby.proxy_module import ProxyModule as ProxyModule from suby.errors import RunningCommandError as RunningCommandError # noqa: F401 +from cantok import TimeoutCancellationError as TimeoutCancellationError # noqa: F401 + sys.modules[__name__].__class__ = ProxyModule From a8fc1e0adadd8ffb0a216a088caa3b844a7b96f2 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 16:43:47 +0300 Subject: [PATCH 55/61] readme --- README.md | 79 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9a4660f..8ca77ed 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Here is a small wrapper around the [subprocesses](https://docs.python.org/3/libr - [**Logging**](#logging) - [**Exceptions**](#exceptions) - [**Working with Cancellation Tokens**](#working-with-cancellation-tokens) +- [**Timeouts**](#timeouts) ## Quick start @@ -72,7 +73,7 @@ print(result) ## Output -By default, the `stdout` and `stderr` of the subprocess are intercepted and output to the `stdout` and `stderr` of the current process. The reading from the subprocess is continuous, and the output is every time a full line is read. For continuous reading from `stderr`, a separate thread is created in the main process, so that `stdout` and `stderr` are read independently. +By default, the `stdout` and `stderr` of the subprocess are forwarded to the `stdout` and `stderr` of the current process. The reading from the subprocess is continuous, and the output is every time a full line is read. For continuous reading from `stderr`, a separate thread is created in the main process, so that `stdout` and `stderr` are read independently. You can override the output functions for `stdout` and `stderr`. To do this, you need to pass as arguments `stdout_callback` and `stderr_callback`, respectively, some functions that accept a string as an argument. For example, you can color the output (the code example uses the [`termcolor`](https://github.com/termcolor/termcolor) library): @@ -149,27 +150,9 @@ except RunningCommandError as e: # > Error when executing the command "python -c 1/0". ``` -2. If you passed a [cancellation token](https://cantok.readthedocs.io/en/latest/the_pattern/) when calling the command, and the token was canceled, an exception will be raised [corresponding to the type](https://cantok.readthedocs.io/en/latest/what_are_tokens/exceptions/) of canceled token. This part of the functionality is integrated with the [cantok](https://cantok.readthedocs.io/en/latest/) library, so we recommend that you familiarize yourself with it beforehand. Here is a small example of how to pass cancellation tokens and catch exceptions from them: +2. If you passed a [cancellation token](https://cantok.readthedocs.io/en/latest/the_pattern/) when calling the command, and the token was canceled, an exception will be raised [corresponding to the type](https://cantok.readthedocs.io/en/latest/what_are_tokens/exceptions/) of canceled token. [This part of the functionality](#working-with-cancellation-tokens) is integrated with the [cantok](https://cantok.readthedocs.io/en/latest/) library, so we recommend that you familiarize yourself with it beforehand. -```python -from random import randint -from cantok import ConditionToken - -token = ConditionToken(lambda: randint(1, 1000) == 7) -suby('python', '-c', 'import time; time.sleep(10_000)', token=token) -``` - -3. You have set a timeout (in seconds) for the operation and it has expired. To count the timeout "under the hood", suby uses [`TimeoutToken`](https://cantok.readthedocs.io/en/latest/types_of_tokens/TimeoutToken/). Therefore, when the timeout expires, `cantok.errors.TimeoutCancellationError` will be raised: - -```python -from cantok import TimeoutCancellationError - -try: - suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1) -except TimeoutCancellationError as e: - print(e) - # > The timeout of 1 seconds has expired. -``` +3. You have set a [timeout](#timeouts) for the operation and it has expired. You can prevent `suby` from raising any exceptions. To do this, set the `catch_exceptions` parameter to `True`: @@ -184,7 +167,7 @@ Keep in mind that the full result of the subprocess call can also be found throu ```python try: suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1) -except TimeoutCancellationError as e: +except suby.TimeoutCancellationError as e: print(e.result) # > SubprocessResult(id='a80dc26cd03211eea347320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True) ``` @@ -195,3 +178,55 @@ except TimeoutCancellationError as e: `suby` is fully compatible with the [cancellation token pattern](https://cantok.readthedocs.io/en/latest/the_pattern/) and supports any token objects from the [`cantok`](https://github.com/pomponchik/cantok) library. The essence of the pattern is that you can pass an object to `suby`, from which it can find out whether the operation still needs to be continued or not. If not, it kills the subprocess. This pattern can be especially useful in the case of commands that are executed for a long time or for an unpredictably long time. When the result becomes unnecessary, there is no point in sitting and waiting for the command to work out. + +So, you can pass your cancellation tokens to `suby`. By default, canceling a token causes an exception to be raised: + +```python +from random import randint +import suby +from cantok import ConditionToken + +token = ConditionToken(lambda: randint(1, 1000) == 7) # This token will be cancelled when a random unlikely event occurs. +suby('python', '-c', 'import time; time.sleep(10_000)', token=token) +# > cantok.errors.ConditionCancellationError: The cancellation condition was satisfied. +``` + +However, if you pass the `catch_exceptions=True` argument, the exception [will not be raised](#exceptions). Instead, you will get the [usual result](#run-subprocess-and-look-at-the-result) of calling `suby` with the `killed_by_token=True` flag: + +```python +token = ConditionToken(lambda: randint(1, 1000) == 7) +print(suby('python', '-c', 'import time; time.sleep(10_000)', token=token, catch_exceptions=True)) +# > SubprocessResult(id='e92ccd54d24b11ee8376320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True) +``` + +"Under the hood" a separate thread is created to track the status of the token. When the token is canceled, the thread kills the subprocess. + +## Timeouts + +You can set a timeout for `suby`. It must be an integer greater than zero, which indicates the number of seconds that the sub process can continue to run. If the timeout expires before the subprocess completes, an exception will be raised: + +```python +import suby + +suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1) +# > cantok.errors.TimeoutCancellationError: The timeout of 1 seconds has expired. +``` + +To count the timeout, "under the hood" `suby` uses [`TimeoutToken`](https://cantok.readthedocs.io/en/latest/types_of_tokens/TimeoutToken/) from the [`cantok`](https://github.com/pomponchik/cantok) library. + +The exception corresponding to this token was be reimported to `suby`: + +```python +try: + suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1) +except suby.TimeoutCancellationError as e: # As you can see, TimeoutCancellationError is available in the suby module. + print(e) + # > The timeout of 1 seconds has expired. +``` + +Just as with [regular cancellation tokens](#working-with-cancellation-tokens), you can prevent exceptions from being raised using the `catch_exceptions=True` argument: + +```python +print(suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1, catch_exceptions=True)) +# > SubprocessResult(id='ea88c518d25011eeb25e320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True) +``` From 04b4bab4d19ac5da322d25342737e65c8cceafac Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 16:50:50 +0300 Subject: [PATCH 56/61] readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ca77ed..4f2e968 100644 --- a/README.md +++ b/README.md @@ -141,11 +141,10 @@ By default, `suby` raises exceptions in three cases: ```python import suby -from suby.errors import RunningCommandError try: suby('python', '-c', '1/0') -except RunningCommandError as e: +except suby.RunningCommandError as e: print(e) # > Error when executing the command "python -c 1/0". ``` From 6ac25449a6516e03ee3daa0babb959b5efda594c Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 16:51:28 +0300 Subject: [PATCH 57/61] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f2e968..8be33f7 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ If you still prefer logging, you can use any object that implements the [logger By default, `suby` raises exceptions in three cases: -1. If the command you are calling ended with a return code not equal to `0`. In this case, you will see an exception `suby.errors.RunningCommandError`: +1. If the command you are calling ended with a return code not equal to `0`. In this case, you will see an exception `suby.RunningCommandError`: ```python import suby From 4142c99fe9f53167a2cc0205fbc8c07a5499a131 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 20:52:07 +0300 Subject: [PATCH 58/61] logos --- README.md | 2 +- docs/assets/logo_1.svg | 661 +++++++++++++++++++++++++++++++++++++++++ docs/assets/logo_2.svg | 192 ++++++++++++ 3 files changed, 854 insertions(+), 1 deletion(-) create mode 100644 docs/assets/logo_1.svg create mode 100644 docs/assets/logo_2.svg diff --git a/README.md b/README.md index 8be33f7..d24f9ed 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# suby +![logo](https://raw.githubusercontent.com/pomponchik/suby/develop/docs/assets/logo_2.svg) [![Downloads](https://static.pepy.tech/badge/suby/month)](https://pepy.tech/project/suby) [![Downloads](https://static.pepy.tech/badge/suby)](https://pepy.tech/project/suby) diff --git a/docs/assets/logo_1.svg b/docs/assets/logo_1.svg new file mode 100644 index 0000000..d41ba5a --- /dev/null +++ b/docs/assets/logo_1.svg @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + S BY + + diff --git a/docs/assets/logo_2.svg b/docs/assets/logo_2.svg new file mode 100644 index 0000000..6f5c608 --- /dev/null +++ b/docs/assets/logo_2.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SUBY + + + From 90c3b77cc10f2f0401eac84c4e49d1085fb63cd7 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 20:55:48 +0300 Subject: [PATCH 59/61] logo --- README.md | 2 +- docs/assets/logo_3.svg | 193 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 docs/assets/logo_3.svg diff --git a/README.md b/README.md index d24f9ed..3ec5897 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![logo](https://raw.githubusercontent.com/pomponchik/suby/develop/docs/assets/logo_2.svg) +![logo](https://raw.githubusercontent.com/pomponchik/suby/develop/docs/assets/logo_3.svg) [![Downloads](https://static.pepy.tech/badge/suby/month)](https://pepy.tech/project/suby) [![Downloads](https://static.pepy.tech/badge/suby)](https://pepy.tech/project/suby) diff --git a/docs/assets/logo_3.svg b/docs/assets/logo_3.svg new file mode 100644 index 0000000..6eced65 --- /dev/null +++ b/docs/assets/logo_3.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SUBY + + + From 2e7c149e362c1bdd74cc216b9c89647b4b4def06 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 20:59:15 +0300 Subject: [PATCH 60/61] logo --- README.md | 2 +- docs/assets/logo_4.svg | 190 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 docs/assets/logo_4.svg diff --git a/README.md b/README.md index 3ec5897..9185014 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![logo](https://raw.githubusercontent.com/pomponchik/suby/develop/docs/assets/logo_3.svg) +![logo](https://raw.githubusercontent.com/pomponchik/suby/develop/docs/assets/logo_4.svg) [![Downloads](https://static.pepy.tech/badge/suby/month)](https://pepy.tech/project/suby) [![Downloads](https://static.pepy.tech/badge/suby)](https://pepy.tech/project/suby) diff --git a/docs/assets/logo_4.svg b/docs/assets/logo_4.svg new file mode 100644 index 0000000..9810abd --- /dev/null +++ b/docs/assets/logo_4.svg @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SUBY + + + From 86685cd0422c211536a88a3e6b88341efa308d44 Mon Sep 17 00:00:00 2001 From: Evgeniy Blinov Date: Fri, 23 Feb 2024 21:14:41 +0300 Subject: [PATCH 61/61] logo --- README.md | 2 +- docs/assets/logo_5.svg | 196 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 docs/assets/logo_5.svg diff --git a/README.md b/README.md index 9185014..c9817c0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![logo](https://raw.githubusercontent.com/pomponchik/suby/develop/docs/assets/logo_4.svg) +![logo](https://raw.githubusercontent.com/pomponchik/suby/develop/docs/assets/logo_5.svg) [![Downloads](https://static.pepy.tech/badge/suby/month)](https://pepy.tech/project/suby) [![Downloads](https://static.pepy.tech/badge/suby)](https://pepy.tech/project/suby) diff --git a/docs/assets/logo_5.svg b/docs/assets/logo_5.svg new file mode 100644 index 0000000..b4c9385 --- /dev/null +++ b/docs/assets/logo_5.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +