Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
33fb15f
instld mention
pomponchik Jul 23, 2024
2ecfdad
autosplit of commands by default
pomponchik Aug 13, 2024
3e0b119
a new version of cantok
pomponchik Aug 13, 2024
c796748
a new version of emptylog
pomponchik Aug 13, 2024
945155f
readme fixed
pomponchik Aug 13, 2024
8d00128
no extra f-strings
pomponchik Aug 13, 2024
11731ac
no extra imports in readme examples
pomponchik Aug 13, 2024
5a7831b
oslex instead of mslex
pomponchik Aug 13, 2024
4b2177f
mypy ignore comment
pomponchik Aug 13, 2024
061651c
new test
pomponchik Aug 13, 2024
d723f12
an old version of oslex
pomponchik Aug 13, 2024
1fb7e91
no cover comments
pomponchik Aug 13, 2024
e3bef6f
no version tag
pomponchik Aug 13, 2024
079d073
added a new type of exceptions - WrongCommandError
pomponchik Aug 13, 2024
3529a80
a new test
pomponchik Aug 13, 2024
b3f8a8e
skip the wrong command test for windows
pomponchik Aug 13, 2024
ca9936a
no cover comments for not-windows specific code
pomponchik Aug 13, 2024
90b2a1c
TODO comments about mslex
pomponchik Aug 13, 2024
c3c09ee
use shlex join for command instead of str join
pomponchik Aug 13, 2024
50b0583
no shlex join
pomponchik Aug 14, 2024
b197d50
no extra todos
pomponchik Aug 14, 2024
27ba315
new test
pomponchik Aug 15, 2024
5630928
python command instead of python3 in a test
pomponchik Aug 15, 2024
ea36e49
readme
pomponchik Aug 15, 2024
12487c6
hello world test
pomponchik Aug 15, 2024
0a5a6b7
readme
pomponchik Aug 15, 2024
17b9725
readme
pomponchik Aug 15, 2024
dda9ace
readme
pomponchik Aug 15, 2024
f7e3d5b
different tests for windows and not windows
pomponchik Aug 16, 2024
06dc62c
readme
pomponchik Aug 16, 2024
05f58b8
new version of the cantok
pomponchik Aug 16, 2024
615ebdb
fixed a windows test
pomponchik Aug 18, 2024
07b65de
fixed a windows test
pomponchik Aug 18, 2024
7b77c4c
print in a windows test
pomponchik Aug 18, 2024
09b859d
print in a windows test
pomponchik Aug 18, 2024
2249658
-python3.7 +python3.13 in CI workflows
Mar 2, 2025
284ba1d
classifiers
Mar 2, 2025
ad34811
classifiers
Mar 2, 2025
2338158
up version of the cantok
Mar 2, 2025
70af7bf
new version of thre emptylog
Mar 2, 2025
ca57d29
the tests workflow name
Mar 2, 2025
7c6c88c
new versions of dependencies
Mar 2, 2025
a291ae1
no emptylog in the requirements file
Mar 2, 2025
1ebf657
lint command in the CI
Mar 2, 2025
6cc193d
some prints in a test
Mar 2, 2025
dab1833
some print in a test
Mar 2, 2025
5749da7
some print in a test
Mar 2, 2025
13bee0d
fix for one test
Mar 2, 2025
eb761fb
fix for one test
Mar 2, 2025
3e536f3
fix for one test
Mar 2, 2025
4824456
fix for one test
Mar 2, 2025
94d19ed
fix for one test
Mar 2, 2025
2d05c82
fix for one test
Mar 2, 2025
feec987
fix for one test
Mar 2, 2025
f404a1b
fix for one test
Mar 2, 2025
4ec27f7
fix for one test
Mar 2, 2025
d11e894
fix for one test
Mar 2, 2025
c6ae7a8
fix for one test
Mar 2, 2025
544671a
fix for one test
Mar 2, 2025
ec1db3c
fix for one test
Mar 2, 2025
8b5f1fd
fix for one test
Mar 2, 2025
14dacaf
fix for one test
Mar 2, 2025
3221b35
fix for one test
Mar 2, 2025
65e1f99
fix for one test
Mar 2, 2025
1b6cd16
fix for one test
Mar 2, 2025
990a529
delete the oslex package
Mar 3, 2025
2e5d296
no extra skips of tests
Mar 3, 2025
757dcbd
posix mode for shlex
Mar 3, 2025
52d3a37
try to search the problem using print
Mar 3, 2025
1ee3746
try to search the problem using print
Mar 3, 2025
38029bc
shell is true for popen
Mar 3, 2025
9323edd
fir for windows
Mar 6, 2025
4a53ec0
no extra imports
Mar 6, 2025
f64d05d
no extra imports
Mar 6, 2025
b8f9d42
a pragma comment
Mar 6, 2025
043e1e3
try to add shell=True in windows
Mar 6, 2025
9c0a81b
try to add shell=True in windows
Mar 6, 2025
ee52da9
try to add shell=True in windows
Mar 6, 2025
80a9075
nonsplitting mode now is default (only for windows)
Mar 6, 2025
90d559b
split flag
Mar 6, 2025
665cc0c
no extra arguments for Popen
Mar 6, 2025
bf47d78
no extra imports
Mar 6, 2025
0508d01
no extra imports
Mar 6, 2025
319467f
+1 test (only for windows)
Mar 6, 2025
8f6e60c
pragma comment
Mar 6, 2025
742023c
boolean fix
Mar 6, 2025
c8bb011
windows adaptation for 1 test
Mar 8, 2025
ea15925
windows adaptation for 1 test
Mar 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -37,8 +37,8 @@ jobs:

- name: Run ruff
shell: bash
run: ruff suby
run: ruff check suby

- name: Run ruff for tests
shell: bash
run: ruff tests
run: ruff check tests
4 changes: 2 additions & 2 deletions .github/workflows/tests_and_coverage.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: New tests
name: Tests

on:
push
Expand All @@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v2
Expand Down
48 changes: 0 additions & 48 deletions .github/workflows/tests_and_coverage_old.yml

This file was deleted.

81 changes: 59 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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)
- [**Command parsing**](#command-parsing)
- [**Output**](#output)
- [**Logging**](#logging)
- [**Exceptions**](#exceptions)
Expand All @@ -45,10 +46,12 @@ And use:
```python
import suby

suby('python', '-c', 'print("hello, world!")')
suby('python -c "print(\'hello, world!\')"')
# > hello, world!
```

You can also quickly try out this and other packages without having to install using [instld](https://github.com/pomponchik/instld).


## Run subprocess and look at the result

Expand All @@ -64,10 +67,10 @@ If you use static type checking and get an error that it is impossible to call t
from suby import suby
```

Let's try to call `suby`. You can use strings or [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects as positional arguments, but now we call it with only simple strings:
Let's try to call `suby`:

```python
result = suby('python', '-c', 'print("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)
```
Expand All @@ -81,28 +84,68 @@ We can see that it returns an object of the `SubprocessResult` class. It contain
- **killed_by_token** - a boolean flag indicating whether the subprocess was killed due to [token](https://cantok.readthedocs.io/en/latest/the_pattern/) cancellation.


## Command parsing

Each command you use to call `suby` is passed to a special [system call](https://en.wikipedia.org/wiki/System_call). Which one exactly depends on the operating system you are using. But regardless of the specific operating system, this system call usually accepts not one whole line of input, but a list of substrings. This means that somewhere under the hood, `suby` should cut the string you passed. The rules for this cutting are usually also different for different operating systems and depend on the specific shell you prefer. `suby` uses the [CMD](https://en.wikipedia.org/wiki/Cmd.exe) as a standard for [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) and [POSIX](https://en.wikipedia.org/wiki/POSIX) for POSIX-compatible systems.

In most cases, you will not notice any differences in the parsing rules. For example, the following line:

```bash
python -c "print('hello, world!')"
```

... on Windows should be escaped like here:

```python
suby('python -c "print^(\'hello,world^!\'^)"')
```

... and on other systems like here:

```python
suby('python -c "print(\'hello, world!\')"')
```

You can pass not only strings to suby, but also [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects:

```python
import sys
from pathlib import Path

suby(Path(sys.executable), '-c print(777)')
# This will work too.
```

If for some reason you want to disable the automatic splitting of strings into parts, pass `split=False`:

```python
suby('python', '-c', 'print(777)', split=False)
```

Of course, in this case, you will have to cut the command by yourself.


## Output

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):

```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)
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)
suby('python -c "print(\'hello, world!\')"', catch_output=True)
# There's nothing here.
```

Expand All @@ -115,7 +158,6 @@ By default, `suby` does not log command execution. However, you can pass a logge

```python
import logging
import suby

logging.basicConfig(
level=logging.INFO,
Expand All @@ -125,15 +167,15 @@ logging.basicConfig(
]
)

suby('python', '-c', 'pass', logger=logging.getLogger('logger_name'))
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)
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"".
```
Expand All @@ -150,10 +192,8 @@ 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.RunningCommandError`:

```python
import suby

try:
suby('python', '-c', '1/0')
suby('python -c 1/0')
except suby.RunningCommandError as e:
print(e)
# > Error when executing the command "python -c 1/0".
Expand All @@ -166,7 +206,7 @@ except suby.RunningCommandError as e:
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)
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)
```
Expand All @@ -175,7 +215,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)
suby('python -c "import time; time.sleep(10_000)"', timeout=1)
except suby.TimeoutCancellationError as e:
print(e.result)
# > SubprocessResult(id='a80dc26cd03211eea347320319d7541c', stdout='', stderr='', returncode=-9, killed_by_token=True)
Expand All @@ -192,19 +232,18 @@ So, you can pass your cancellation tokens to `suby`. By default, canceling a tok

```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)
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))
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)
```

Expand All @@ -215,9 +254,7 @@ print(suby('python', '-c', 'import time; time.sleep(10_000)', token=token, catch
You can set a timeout for `suby`. It must be an integer greater than zero, which indicates the number of seconds that the subprocess 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)
suby('python -c "import time; time.sleep(10_000)"', timeout=1)
# > cantok.errors.TimeoutCancellationError: The timeout of 1 seconds has expired.
```

Expand All @@ -227,7 +264,7 @@ The exception corresponding to this token was be reimported to `suby`:

```python
try:
suby('python', '-c', 'import time; time.sleep(10_000)', timeout=1)
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.
Expand All @@ -236,6 +273,6 @@ except suby.TimeoutCancellationError as e: # As you can see, TimeoutCancellatio
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))
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)
```
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"

[project]
name = "suby"
version = "0.0.3"
version = "0.0.4"
authors = [
{ name="Evgeniy Blinov", email="[email protected]" },
]
description = 'Slightly simplified subprocesses'
readme = "README.md"
requires-python = ">=3.7"
requires-python = ">=3.8"
dependencies = [
'emptylog>=0.0.7',
'cantok>=0.0.24',
'emptylog>=0.0.9',
'cantok>=0.0.32',
]
classifiers = [
"Operating System :: OS Independent",
Expand All @@ -22,12 +22,12 @@ classifiers = [
'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',
'Programming Language :: Python :: 3.13',
'License :: OSI Approved :: MIT License',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
Expand Down
17 changes: 8 additions & 9 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
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
emptylog>=0.0.7
full_match==0.0.1
pytest==7.4.3
coverage==7.6.1
build==1.2.2.post1
twine==6.1.0
mypy==1.14.1
ruff==0.9.9
mutmut==3.2.3
full_match==0.0.2
2 changes: 1 addition & 1 deletion suby/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sys

from suby.proxy_module import ProxyModule as ProxyModule
from suby.errors import RunningCommandError as RunningCommandError # noqa: F401
from suby.errors import RunningCommandError as RunningCommandError, WrongCommandError as WrongCommandError # noqa: F401

from cantok import TimeoutCancellationError as TimeoutCancellationError # noqa: F401

Expand Down
3 changes: 3 additions & 0 deletions suby/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ class RunningCommandError(Exception):
def __init__(self, message: str, subprocess_result: SubprocessResult) -> None:
self.result = subprocess_result
super().__init__(message)

class WrongCommandError(Exception):
pass
Loading
Loading