Skip to content
This repository was archived by the owner on Jun 15, 2025. It is now read-only.

Commit f6f585c

Browse files
committed
cleanup and fix package build. fix unicode escape for 3.12
1 parent 0cab7b9 commit f6f585c

File tree

11 files changed

+128
-513
lines changed

11 files changed

+128
-513
lines changed

.gitignore

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
__pycache__
2-
__pypackages__
2+
.python-version
33
.pytest_cache
44
.ruff_cache
5-
.pdm-python
6-
build
75
dist
8-
pypi-*

pdm.lock

Lines changed: 0 additions & 443 deletions
This file was deleted.

pyproject.toml

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-shellrunner"
3-
version = "0.3.2"
3+
version = "0.3.3"
44
description = "Write safe shell scripts in Python."
55
authors = [
66
{name = "adamhl8", email = "adamhl@pm.me"},
@@ -28,42 +28,46 @@ keywords = ["shell", "scripting", "bash", "zsh", "fish"]
2828
"Source" = "https://github.com/adamhl8/python-shellrunner"
2929
"Bug Tracker" = "https://github.com/adamhl8/python-shellrunner/issues"
3030

31-
[tool.pdm.dev-dependencies]
32-
dev = [
33-
"black>=23.3.0",
34-
"ruff>=0.0.261",
35-
"pytest>=7.3.1",
31+
[tool.rye]
32+
managed = true
33+
dev-dependencies = [
34+
"black>=23.9.1",
35+
"ruff>=0.0.292",
36+
"pytest>=7.4.2",
3637
"pyroma>=4.2",
37-
"types-psutil>=5.9.5.11",
38+
"types-psutil>=5.9.5.16",
3839
]
3940

40-
[tool.pdm.scripts]
41-
test = {cmd = "pytest src"}
42-
pyright = {shell = "pyright src"}
43-
ruff = {cmd = "ruff check src"}
44-
lint = {composite = ["pyright", "ruff"]}
45-
format = {cmd = "black src"}
46-
pyroma = {cmd = "pyroma ."}
47-
48-
[tool.pytest.ini_options]
49-
pythonpath = ["src"]
41+
[tool.rye.scripts]
42+
test = "pytest shellrunner"
43+
"lint:pyright" = "pyright ."
44+
"lint:ruff" = "ruff check ."
45+
lint = { chain = ["lint:pyright", "lint:ruff"] }
46+
format = "black ."
47+
"lint:pyroma" = "pyroma ."
5048

5149
[tool.pyright]
5250
typeCheckingMode = "strict"
53-
extraPaths = ["__pypackages__/3.10/lib", "src"]
51+
52+
[tool.black]
53+
line-length = 120
54+
target-version = ["py312"]
5455

5556
[tool.ruff]
5657
line-length = 120
57-
target-version = "py310"
58+
target-version = "py312"
5859
select = ["E", "F", "W", "I", "N", "UP", "BLE", "FBT", "B", "A", "COM", "C4", "T10", "EM", "EXE", "ISC", "ICN", "G",
5960
"INP", "PIE", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SIM", "TID", "TCH", "ARG", "PTH", "ERA", "PL", "PLC", "PLE",
6061
"PLR", "PLW", "TRY", "RUF"]
6162
ignore = ["E501", "PLR0915", "PLR0912", "PLR0913", "PLC1901"]
6263

63-
[tool.black]
64-
line-length = 120
65-
target-version = ["py310"]
66-
6764
[build-system]
68-
requires = ["pdm-backend"]
69-
build-backend = "pdm.backend"
65+
requires = ["hatchling"]
66+
build-backend = "hatchling.build"
67+
68+
[tool.hatch.build]
69+
packages = ["shellrunner"]
70+
exclude = ["test*.py"]
71+
72+
[tool.hatch.metadata]
73+
allow-direct-references = true

requirements-dev.lock

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# generated by rye
2+
# use `rye lock` or `rye sync` to update this lockfile
3+
#
4+
# last locked with the following flags:
5+
# pre: false
6+
# features: []
7+
# all-features: false
8+
9+
-e file:.
10+
black==23.9.1
11+
build==1.0.3
12+
certifi==2023.7.22
13+
charset-normalizer==3.3.0
14+
click==8.1.7
15+
docutils==0.20.1
16+
idna==3.4
17+
iniconfig==2.0.0
18+
mypy-extensions==1.0.0
19+
packaging==23.2
20+
pathspec==0.11.2
21+
platformdirs==3.11.0
22+
pluggy==1.3.0
23+
psutil==5.9.5
24+
pygments==2.16.1
25+
pyproject-hooks==1.0.0
26+
pyroma==4.2
27+
pytest==7.4.2
28+
requests==2.31.0
29+
ruff==0.0.292
30+
trove-classifiers==2023.9.19
31+
types-psutil==5.9.5.16
32+
urllib3==2.0.6
33+
# The following packages are considered to be unsafe in a requirements file:
34+
setuptools==68.2.2

requirements.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# generated by rye
2+
# use `rye lock` or `rye sync` to update this lockfile
3+
#
4+
# last locked with the following flags:
5+
# pre: false
6+
# features: []
7+
# all-features: false
8+
9+
-e file:.
10+
psutil==5.9.5

shellrunner/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from ._exceptions import ShellCommandError, ShellCommandResult, ShellResolutionError, ShellRunnerError
2+
from ._shellrunner import run as X # noqa: N812
3+
4+
__all__ = [
5+
"ShellCommandError",
6+
"ShellCommandResult",
7+
"ShellResolutionError",
8+
"ShellRunnerError",
9+
"X",
10+
]

shellrunner/_exceptions.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import NamedTuple
2+
3+
4+
class ShellCommandResult(NamedTuple):
5+
out: str
6+
status: int
7+
pipestatus: list[int]
8+
9+
10+
class ShellRunnerError(RuntimeError):
11+
pass
12+
13+
14+
class ShellCommandError(ShellRunnerError):
15+
def __init__(self, message: str, result: ShellCommandResult):
16+
super().__init__(message)
17+
self.out = result.out
18+
self.status = result.status
19+
self.pipestatus = result.pipestatus
20+
21+
22+
class ShellResolutionError(ShellRunnerError):
23+
pass
24+
25+
26+
class EnvironmentVariableError(ShellRunnerError):
27+
pass

src/shellrunner.py renamed to shellrunner/_shellrunner.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
import sys
33
from inspect import cleandoc
44

5-
from helpers import (
5+
from ._exceptions import ShellCommandError, ShellCommandResult, ShellResolutionError, ShellRunnerError
6+
from ._utils import (
67
Env,
7-
ShellCommandError,
8-
ShellCommandResult,
9-
ShellResolutionError,
10-
ShellRunnerError,
118
get_parent_shell_path,
129
resolve_option,
1310
resolve_shell_path,
@@ -21,7 +18,7 @@
2118
# show_commands (Optional) - If True, the current command will be printed before execution. | Default is True
2219

2320

24-
def X( # noqa: N802
21+
def run(
2522
command: str | list[str],
2623
*,
2724
shell: str | None = None,
@@ -52,13 +49,13 @@ def X( # noqa: N802
5249
# The only way to reliably stop executing commands on an error is to exit from the shell itself. Killing the subprocess does not happen nearly fast enough.
5350
# To do this, we append a command for each command that is passed in. Ultimately, we need to get PIPESTATUS, process it, and exit based on that. PIPESTATUS looks like: "0 1 0".
5451
# We don't need to also get $?/$status because PIPESTATUS gives the status of single command anyway.
55-
# Rather than write a separate script for each shell to process PIPESTATUS (e.g. bash would requier a different script than fish), we can pass PIPESTATUS into a python script.
52+
# Rather than write a separate script for each shell to process PIPESTATUS (e.g. bash would require a different script than fish), we can pass PIPESTATUS into a python script.
5653
# We execute this python script by passing it to the parent python executable (sys.executable) via the -c flag.
5754
# The following python code (status_check) takes in PIPESTATUS, prints it (so we can capture it from stdout later on), loops through each status, and exits (with a non-zero status if there is one).
5855
status_check = r"""
5956
import sys
6057
pipestatus = sys.argv[1]
61-
print(b'\\u2f4c'.decode('unicode_escape') + f' : {pipestatus} : ' + b'\\u2f8f'.decode('unicode_escape'), end='')
58+
print('\u2f4c' + f' : {pipestatus} : ' + '\u2f8f', end='')
6259
for status in [int(x) for x in pipestatus.split()]:
6360
if status != 0: sys.exit(status)
6461
"""
@@ -77,12 +74,12 @@ def X( # noqa: N802
7774
pipestatus_var = status_var
7875
if shell_name == "bash":
7976
pipestatus_var = r"${PIPESTATUS[*]}"
80-
if shell_name == "zsh" or shell_name == "fish":
77+
if shell_name in ("zsh", "fish"):
8178
status_var = r"$status"
8279
pipestatus_var = r"$pipestatus"
8380

8481
# If check argument is false, we won't exit after a non-zero exit status.
85-
# When run in shell that masks pipeline errors (e.g. bash), status_var will be 0 even though an error may have occured in a pipeline. Ultimately this doesn't matter because we don't care about the exit status of the shell itself.
82+
# When run in shell that masks pipeline errors (e.g. bash), status_var will be 0 even though an error may have occurred in a pipeline. Ultimately this doesn't matter because we don't care about the exit status of the shell itself.
8683
exit_command = f' || exit "{status_var}"' if check else ""
8784

8885
# This command is appended after each passed in command. If status_check exits with a non-zero status, we exit the shell.
@@ -119,7 +116,7 @@ def X( # noqa: N802
119116
raise ShellRunnerError(message)
120117

121118
capture_output = True
122-
# If we are still receving output or poll() is None, we know the command is still running.
119+
# If we are still receiving output or poll() is None, we know the command is still running.
123120
# We must use stdout.read(1) rather than readline() in order to properly print commands that prompt the user for input. We must also forcibly flush the stream in the print statement for the same reason.
124121
while (out := process.stdout.read(1)) or process.poll() is None:
125122
# If we detect our marker, we know we are done with the previous command and have printed PIPESTATUS. Capture stdout to pipestatus instead.
@@ -142,7 +139,7 @@ def X( # noqa: N802
142139
pipestatus_list = [int(x) for x in pipestatus.split()]
143140
capture_output = True
144141

145-
# If we don't recieve any exit status, something went wrong.
142+
# If we don't receive any exit status, something went wrong.
146143
if not pipestatus_list:
147144
message = "Something went wrong. Failed to capture an exit status."
148145
raise ShellRunnerError(message)

src/helpers.py renamed to shellrunner/_utils.py

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,11 @@
11
import os
22
from pathlib import Path
33
from shutil import which
4-
from typing import NamedTuple, TypeVar
4+
from typing import TypeVar
55

66
from psutil import Process
77

8-
9-
class ShellCommandResult(NamedTuple):
10-
out: str
11-
status: int
12-
pipestatus: list[int]
13-
14-
15-
class ShellRunnerError(RuntimeError):
16-
pass
17-
18-
19-
class ShellCommandError(ShellRunnerError):
20-
def __init__(self, message: str, result: ShellCommandResult):
21-
super().__init__(message)
22-
self.out = result.out
23-
self.status = result.status
24-
self.pipestatus = result.pipestatus
25-
26-
27-
class ShellResolutionError(ShellRunnerError):
28-
pass
29-
30-
31-
class EnvironmentVariableError(ShellRunnerError):
32-
pass
8+
from ._exceptions import EnvironmentVariableError, ShellResolutionError
339

3410

3511
# Returns the full path of parent process/shell. That way commands are executed using the same shell that invoked this script.

src/test_shellrunner.py renamed to shellrunner/test_shellrunner.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
from typing import NamedTuple
66

77
import pytest
8-
from helpers import EnvironmentVariableError, ShellCommandError, ShellResolutionError, get_parent_shell_path
9-
from shellrunner import X
8+
9+
from shellrunner import ShellCommandError, ShellResolutionError, X
10+
11+
from ._exceptions import EnvironmentVariableError
12+
from ._utils import get_parent_shell_path
1013

1114

1215
class ShellInfo(NamedTuple):
@@ -128,7 +131,7 @@ def test_pipeline_with_unknown_command_raises_error(self, shell: str, shell_comm
128131
assert str(cm.value).startswith(shell_command_error_message)
129132
assert cm.value.status == 127
130133
# fish will not run a pipeline whatsoever if any command is unknown so we will only ever get one status.
131-
if shell == "sh" or shell == "fish":
134+
if shell in ("sh", "fish"):
132135
assert cm.value.pipestatus == [127]
133136
else:
134137
assert cm.value.pipestatus == [0, 127]

0 commit comments

Comments
 (0)