Skip to content

Commit 9402070

Browse files
jet-logicjet-logic
authored andcommitted
default to use parent stdin
1 parent c3699e8 commit 9402070

File tree

6 files changed

+46
-79
lines changed

6 files changed

+46
-79
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build and Publish to PyPI
1+
name: Publish
22

33
on:
44
push:

pyproject.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ build-backend = "setuptools.build_meta"
55
[tool.setuptools.dynamic]
66
version = {attr = "runce.__init__.__version__"}
77

8+
9+
[tool.black]
10+
line-length = 127
11+
12+
[tool.flake8]
13+
max-line-length = 127
814
[project]
915
name = "runce"
1016
dynamic = ["version"]
@@ -17,10 +23,6 @@ classifiers = [
1723
"Development Status :: 4 - Beta",
1824
"Intended Audience :: Developers",
1925
"Programming Language :: Python :: 3",
20-
"Programming Language :: Python :: 3.7",
21-
"Programming Language :: Python :: 3.8",
22-
"Programming Language :: Python :: 3.9",
23-
"Programming Language :: Python :: 3.10",
2426
]
2527

2628
[project.scripts]

runce/cli.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from argparse import ArgumentParser
22
from shlex import join
3-
from sys import stderr, stdout
4-
from shutil import copyfileobj
3+
from sys import stderr, stdout, platform
54
from subprocess import Popen, PIPE, run
6-
import sys
7-
from typing import Dict, Any
8-
from .utils import check_pid, filesizepu, kill_pid, tail_bytes, tail_file
5+
from typing import Any
6+
from .utils import check_pid, filesizepu, kill_pid, tail_bytes
97
from .main import Main, flag, arg
108
from .procdb import ProcessDB as Manager
119

@@ -25,13 +23,13 @@ def __missing__(self, key: str) -> str:
2523
return self["cmd"]
2624
return join(self["cmd"])
2725
elif key == "pid_status":
28-
return "Running" if check_pid(self["pid"]) else "Stopped"
26+
return "Live" if check_pid(self["pid"]) else "Done"
2927
raise KeyError(f"No {key!r}")
3028

3129

3230
def format_prep(f: str):
3331

34-
def fn(x: Dict[str, Any]) -> str:
32+
def fn(x: "dict[str, Any]") -> str:
3533
return f.format_map(FormatDict(x))
3634

3735
return fn
@@ -56,7 +54,7 @@ def ambiguous(name):
5654
class Clean(Main):
5755
"""Clean up dead processes."""
5856

59-
ids: list[str] = arg("ID", "run ids", nargs="*")
57+
ids: "list[str]" = arg("ID", "run ids", nargs="*")
6058

6159
def add_arguments(self, argp: ArgumentParser) -> None:
6260
argp.description = "Clean up entries for non-existing processes"
@@ -78,7 +76,7 @@ def start(self) -> None:
7876
class Status(Main):
7977
"""Check process status."""
8078

81-
ids: list[str] = arg("ID", "run ids", nargs="*")
79+
ids: "list[str]" = arg("ID", "run ids", nargs="*")
8280
format: str = flag(
8381
"f",
8482
"format of entry line",
@@ -99,7 +97,7 @@ def start(self) -> None:
9997
class Kill(Main):
10098
"""Kill running processes."""
10199

102-
ids: list[str] = arg("ID", "run ids", nargs="+")
100+
ids: "list[str]" = arg("ID", "run ids", nargs="+")
103101
dry_run: bool = flag("dry-run", "dry run (don't actually kill)", default=False)
104102
remove: bool = flag("remove", "remove entry after killing", default=False)
105103
group: bool = flag("group", "kill process group", default=False)
@@ -139,7 +137,7 @@ def _tail(n: float, u="", out="", tab=None):
139137
if u:
140138
stdout.buffer.write(tail_bytes(out, int(n)))
141139
elif n > 0:
142-
if sys.platform.startswith("win"):
140+
if platform.startswith("win"):
143141
cmd = [
144142
"powershell",
145143
"-c",
@@ -158,19 +156,16 @@ def _tail(n: float, u="", out="", tab=None):
158156
class Tail(Main):
159157
"""Tail process output."""
160158

161-
ids: list[str] = arg("ID", "run ids", nargs="*")
159+
ids: "list[str]" = arg("ID", "run ids", nargs="*")
162160
format: str = flag("header", "header format")
163161
lines: str = flag("n", "lines", "how many lines or bytes")
164-
existing: bool = flag(
165-
"x", "only-existing", "only show existing processes", default=False
166-
)
162+
existing: bool = flag("x", "only-existing", "only show existing processes", default=False)
167163
tab: bool = flag("t", "tab", "prefix tab space", default=False)
168164
err: bool = flag("e", "err", "output stderr", default=False)
169165
p_open: str = "=== "
170166
p_close: str = " ==="
171167

172168
def start(self) -> None:
173-
import sys
174169

175170
if self.format == "no":
176171
hf = None
@@ -194,7 +189,7 @@ def start(self) -> None:
194189
class Run(Main):
195190
"""Run a new singleton process."""
196191

197-
args: list[str] = arg("ARG", nargs="*", metavar="arg")
192+
args: "list[str]" = arg("ARG", nargs="*", metavar="arg")
198193
tail: int = flag("t", "tail", "tail the output with n lines", default=0)
199194
run_id: str = flag("id", "Unique run identifier", default="")
200195
cwd: str = flag("Working directory for the command")
@@ -219,9 +214,7 @@ def start(self) -> None:
219214
s = ["🚨", r"Found: PID={pid} ({pid_status}) {name}"]
220215
else:
221216
# Start new process
222-
e = sp.spawn(
223-
args, name, overwrite=self.overwrite, cwd=self.cwd, split=self.split
224-
)
217+
e = sp.spawn(args, name, overwrite=self.overwrite, cwd=self.cwd, split=self.split)
225218
s = ["🚀", r"Started: PID={pid} ({pid_status}) {name}"]
226219
assert e
227220
try:
@@ -271,7 +264,7 @@ def start(self) -> None:
271264
class Restart(Main):
272265
"""Restart a process."""
273266

274-
ids: list[str] = arg("ID", "run ids", nargs="+")
267+
ids: "list[str]" = arg("ID", "run ids", nargs="+")
275268
tail: int = flag("t", "tail", "tail the output with n lines", default=0)
276269

277270
def init_argparse(self, argp: ArgumentParser) -> None:
@@ -294,8 +287,7 @@ class App(Main):
294287
def init_argparse(self, argp: ArgumentParser) -> None:
295288
argp.prog = "runce"
296289
argp.description = (
297-
"Runce (Run Once) - Ensures commands run exactly once.\n"
298-
"Guarantees singleton execution per unique ID."
290+
"Runce (Run Once) - Ensures commands run exactly once.\n" "Guarantees singleton execution per unique ID."
299291
)
300292
return super().init_argparse(argp)
301293

runce/main.py

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import TYPE_CHECKING
22

3-
__version__ = "0.0.6"
3+
__version__ = "0.0.7"
44
if TYPE_CHECKING:
55
from argparse import ArgumentParser
66
from typing import Sequence, Generator
@@ -15,28 +15,20 @@ def __init__(self, *args: str, **kwargs):
1515
self.args = args
1616
self.kwargs = kwargs
1717

18-
def _add(
19-
self, name: str, type_: type, argp: "ArgumentParser", that: object
20-
) -> None:
18+
def _add(self, name: str, klass: type, argp: "ArgumentParser", that: object) -> None:
2119
"""Add argument to parser."""
2220
args = []
2321
kwargs = {**self.kwargs}
2422
flag_arg = kwargs.pop("flag", None)
2523
action = kwargs.get("action")
2624
const = kwargs.get("const")
2725
default = kwargs.get("default", INVALID)
26+
kind = klass if isinstance(klass, type) else type
2827

2928
if action is None:
30-
3129
if const is not None:
32-
kwargs["action"] = (
33-
"append_const"
34-
if issubclass(type_, list) or isinstance(default, list)
35-
else "store_const"
36-
)
37-
elif type_ is None:
38-
kwargs["action"] = "store"
39-
elif isinstance(type_, type) and issubclass(type_, bool):
30+
kwargs["action"] = "append_const" if issubclass(kind, list) or isinstance(default, list) else "store_const"
31+
elif issubclass(kind, bool):
4032
if default is None:
4133
try:
4234
from argparse import BooleanOptionalAction
@@ -49,9 +41,7 @@ def _add(
4941
else:
5042
assert default is INVALID or default is False
5143
kwargs["action"] = "store_true"
52-
elif (isinstance(type_, type) and issubclass(type_, list)) or isinstance(
53-
default, list
54-
):
44+
elif issubclass(kind, list) or isinstance(default, list):
5545
if "nargs" not in kwargs:
5646
kwargs["action"] = "append"
5747
if "default" not in kwargs:
@@ -60,17 +50,13 @@ def _add(
6050
kwargs["action"] = "store"
6151

6252
parser = kwargs.pop("parser", None)
63-
if kwargs.get("action") == "count":
53+
if kwargs.get("action") in ("count", "store_const"):
6454
pass
6555
elif parser:
6656
kwargs["type"] = parser
67-
elif (
68-
type_ is not bool
69-
and type(type_) is type
70-
and issubclass(type_, (int, float, str))
71-
):
72-
kwargs["type"] = type_
73-
# print(name, type_, that, "_add", action, flag_arg)
57+
elif kind is not bool and issubclass(kind, (int, float, str)):
58+
kwargs["type"] = kind
59+
7460
if flag_arg is None:
7561
for x in self.args:
7662
if " " in x or "\t" in x:
@@ -83,9 +69,7 @@ def _add(
8369
else:
8470

8571
def add_args(x: str) -> None:
86-
args.append(
87-
x if x.startswith("-") else (f"--{x}" if len(x) > 1 else f"-{x}")
88-
)
72+
args.append(x if x.startswith("-") else (f"--{x}" if len(x) > 1 else f"-{x}"))
8973

9074
for x in self.args:
9175
if " " in x or "\t" in x:
@@ -135,17 +119,11 @@ def __getattr__(self, name: str) -> object:
135119
try:
136120
m = super().__getattr__
137121
except AttributeError:
138-
raise AttributeError(
139-
f"{self.__class__.__name__} has no attribute {name}"
140-
) from None
122+
raise AttributeError(f"{self.__class__.__name__} has no attribute {name}") from None
141123
else:
142124
return m(name)
143125

144-
def main(
145-
self,
146-
args: "Sequence[str]|None" = None,
147-
argp: "ArgumentParser|None" = None,
148-
):
126+
def main(self, args: "Sequence[str]|None" = None, argp: "ArgumentParser|None" = None):
149127
"""Entry point for CLI execution.
150128
Args:
151129
args: Command-line arguments (optional).
@@ -190,9 +168,7 @@ def sub_args(self):
190168
"""Yield subcommands."""
191169
yield None, {}
192170

193-
def parse_arguments(
194-
self, argp: "ArgumentParser", args: "Sequence[str]|None"
195-
) -> None:
171+
def parse_arguments(self, argp: "ArgumentParser", args: "Sequence[str]|None") -> None:
196172
"""Parse command line arguments."""
197173
p = self._walk_subparsers(argp)
198174

@@ -226,12 +202,14 @@ def _walk_subparsers(self, argp: "ArgumentParser", root=None):
226202
return s
227203

228204
def _arg_parents_and_self(self):
205+
# type: () -> Generator[Main, object, None]
229206
c: "Main | None" = self
230207
while c is not None:
231208
yield c
232209
c = c._arg_parent
233210

234211
def _arg_parents(self):
212+
# type: () -> Generator[Main, object, None]
235213
c = self._arg_parent
236214
while c is not None:
237215
yield c

runce/spawn.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from json import dump, load
33
from uuid import uuid4
44
from pathlib import Path
5-
from subprocess import DEVNULL, STDOUT, Popen
5+
from subprocess import DEVNULL, PIPE, STDOUT, Popen
66
from time import time
77
from .utils import generate_pseudowords, get_base_name, look_multiple
88

@@ -23,9 +23,7 @@ def __getattr__(self, name: str) -> object:
2323
try:
2424
m = super().__getattr__
2525
except AttributeError:
26-
raise AttributeError(
27-
f"{self.__class__.__name__} has no attribute {name}"
28-
) from None
26+
raise AttributeError(f"{self.__class__.__name__} has no attribute {name}") from None
2927
else:
3028
return m(name)
3129

@@ -77,7 +75,10 @@ def spawn(
7775
so = se = Path(out_file) if out_file else data_dir / f"{base_name}.log"
7876
po_kwa["stdout"] = so.open(f"{mode}b")
7977
po_kwa["stderr"] = STDOUT
78+
if po_kwa.get("stdin") is None:
79+
from sys import stdin
8080

81+
po_kwa["stdin"] = stdin.buffer
8182
po_kwa.setdefault("start_new_session", True)
8283
po_kwa.setdefault("close_fds", True)
8384
po_kwa.setdefault("stdin", DEVNULL)
@@ -112,11 +113,7 @@ def all(self):
112113
return
113114

114115
for child in self.data_dir.iterdir():
115-
if (
116-
child.is_file()
117-
and child.name.endswith(".run.json")
118-
and child.stat().st_size > 0
119-
):
116+
if child.is_file() and child.name.endswith(".run.json") and child.stat().st_size > 0:
120117
try:
121118
with child.open() as f:
122119
d: dict[str, int | str] = load(f)
@@ -143,9 +140,7 @@ def drop(self, entry: "dict[str, object]", clean_up=True):
143140
if clean_up or k == "file":
144141
remove(v)
145142

146-
def find_names(
147-
self, names: "list[str]", ambiguous=lambda x: None, not_found=lambda x: None
148-
):
143+
def find_names(self, names: "list[str]", ambiguous=lambda x: None, not_found=lambda x: None):
149144
if names:
150145
yield from look_multiple(names, self.all(), ambiguous, not_found)
151146
else:

tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def run_runce(self, *args, stdout_only=False):
2121
# Combine stdout and stderr for verification
2222
return o
2323

24-
def assertRegexInListOnce(self, regex: str, ls: list[str]):
24+
def assertRegexInListOnce(self, regex: str, ls: 'list[str]'):
2525
b = [1 for x in ls if re.search(regex, x)]
2626
self.assertEqual(len(b), 1, f"{regex} in {ls!r}")
2727

0 commit comments

Comments
 (0)