refactor!: drop operators

This commit is contained in:
Daylin Morgan 2024-10-22 11:53:19 -05:00
parent 115900bf83
commit 277f7f7c0c
Signed by: daylin
GPG key ID: 950D13E9719334AD
3 changed files with 97 additions and 70 deletions

View file

@ -134,7 +134,9 @@ class Context:
for task in self._tasks.values(): for task in self._tasks.values():
if not task.targets: if not task.targets:
continue continue
print(task.name)
print(task.targets)
print(task.needs)
for target in task.targets: for target in task.targets:
if not task.needs: if not task.needs:
self._graph.add_nodes(task, target, None) self._graph.add_nodes(task, target, None)
@ -151,7 +153,7 @@ class Context:
ctx = Context() ctx = Context()
def define_env(key: str, value: str) -> None: def setenv(key: str, value: str) -> None:
ctx._env.update({key: value}) ctx._env.update({key: value})
@ -183,18 +185,6 @@ class SwyddSubResult:
class SwyddProc: class SwyddProc:
"""
usage:
sub < proc("echo $WORD world", env={'WORD':'hello'})
sub < (proc | "single proc")
sub < (proc | "echo hello" | "wc -c")
sub(proc("hello world").pipe("wc -c"))
sub < (proc & "cat -a" & "echo unreachable")
sub(proc("cat -a").then("echo unreachable")
"""
def __init__( def __init__(
self, cmd: str | None = None, output: bool = False, **kwargs: Any self, cmd: str | None = None, output: bool = False, **kwargs: Any
) -> None: ) -> None:
@ -204,6 +194,10 @@ class SwyddProc:
self.output = output self.output = output
self.cmd_kwargs = kwargs self.cmd_kwargs = kwargs
@classmethod
def __call__(cls, *args, **kwargs) -> "SwyddProc":
return cls(*args, **kwargs)
def pipe(self, proc: "str | SwyddProc") -> "SwyddPipe | SwyddProc": def pipe(self, proc: "str | SwyddProc") -> "SwyddPipe | SwyddProc":
if isinstance(proc, str): if isinstance(proc, str):
if self._cmd is None: if self._cmd is None:
@ -213,8 +207,8 @@ class SwyddProc:
elif isinstance(proc, SwyddProc): elif isinstance(proc, SwyddProc):
return SwyddPipe(proc) return SwyddPipe(proc)
def __or__(self, proc: "str | SwyddProc") -> "SwyddPipe | SwyddProc": # def __or__(self, proc: "str | SwyddProc") -> "SwyddPipe | SwyddProc":
return self.pipe(proc) # return self.pipe(proc)
def then(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq": def then(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq":
if self._cmd: if self._cmd:
@ -228,8 +222,8 @@ class SwyddProc:
else: else:
return SwyddSeq(SwyddProc(proc)) return SwyddSeq(SwyddProc(proc))
def __and__(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq": # def __and__(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq":
return self.then(proc) # return self.then(proc)
def _build_kwargs(self) -> Dict[str, Any]: def _build_kwargs(self) -> Dict[str, Any]:
sub_kwargs: Dict[str, Any] = dict(env={**os.environ, **ctx._env}) sub_kwargs: Dict[str, Any] = dict(env={**os.environ, **ctx._env})
@ -297,8 +291,8 @@ class SwyddPipe:
def pipe(self, proc: "str | SwyddProc | SwyddPipe") -> "SwyddPipe": def pipe(self, proc: "str | SwyddProc | SwyddPipe") -> "SwyddPipe":
return SwyddPipe(self, proc) return SwyddPipe(self, proc)
def __or__(self, proc: "str | SwyddProc | SwyddPipe") -> "SwyddPipe": # def __or__(self, proc: "str | SwyddProc | SwyddPipe") -> "SwyddPipe":
return self.pipe(proc) # return self.pipe(proc)
class SwyddSeq: class SwyddSeq:
@ -319,8 +313,8 @@ class SwyddSeq:
def then(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq": def then(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq":
return SwyddSeq(*self._procs, proc) return SwyddSeq(*self._procs, proc)
def __and__(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq": # def __and__(self, proc: "str | SwyddProc | SwyddSeq") -> "SwyddSeq":
return self.then(proc) # return self.then(proc)
def execute(self, output: bool = False) -> "SwyddSubResult": def execute(self, output: bool = False) -> "SwyddSubResult":
results = [] results = []
@ -342,6 +336,7 @@ class SwyddSeq:
return self.run() == 0 return self.run() == 0
# TODO: best interface for "get"
class SwyddGet: class SwyddGet:
def __call__( def __call__(
self, proc: str | SwyddProc | SwyddPipe | SwyddSeq, stdout=True, stderr=False self, proc: str | SwyddProc | SwyddPipe | SwyddSeq, stdout=True, stderr=False
@ -364,11 +359,11 @@ class SwyddGet:
output += result.stderr.strip() output += result.stderr.strip()
return output return output
def __lt__(self, proc: str | SwyddProc | SwyddPipe | SwyddSeq) -> str: # def __lt__(self, proc: str | SwyddProc | SwyddPipe | SwyddSeq) -> str:
return self.__call__(proc) # return self.__call__(proc)
#
def __lshift__(self, proc: str | SwyddProc | SwyddPipe | SwyddSeq) -> str: # def __lshift__(self, proc: str | SwyddProc | SwyddPipe | SwyddSeq) -> str:
return self.__call__(proc, stdout=False, stderr=True) # return self.__call__(proc, stdout=False, stderr=True)
def _get_caller_path() -> Path: def _get_caller_path() -> Path:
@ -392,22 +387,31 @@ class SwyddSub:
else: else:
raise ValueError(f"unspported type: {type(exec)}") raise ValueError(f"unspported type: {type(exec)}")
def __lt__(self, proc: str | SwyddPipe | SwyddProc | SwyddSeq) -> bool:
return self.__call__(proc)
# TODO: change alias to not confuse with pathlib.Path?
# asset / file ... partial to asset
class SwyddPath: class SwyddPath:
_root = None _root = None
_path = None _path = None
def __init__(self, p: Path | None = None) -> None: def __init__(self, p: Path | None = None) -> None:
if p: if p:
self._path = p self._path = Path(p)
@classmethod
def __call__(cls, p: str) -> "SwyddPath":
return cls.from_str(p)
@classmethod @classmethod
def from_str(cls, p: str) -> "SwyddPath": def from_str(cls, p: str) -> "SwyddPath":
return cls() / p return cls() / p
def read(self) -> str:
if self._path:
return self._path.read_text()
else:
raise ValueError("path is not set")
def __truediv__(self, p: str | Path) -> "SwyddPath": def __truediv__(self, p: str | Path) -> "SwyddPath":
if not (root := self._root): if not (root := self._root):
root = _get_caller_path() root = _get_caller_path()
@ -427,19 +431,27 @@ class SwyddPath:
raise ValueError("todo") raise ValueError("todo")
return self._path return self._path
def write(self, txt: str) -> None: def _write_text(self, txt: str) -> "SwyddPath":
p = self._check() p = self._check()
p.parent.mkdir(exist_ok=True) p.parent.mkdir(exist_ok=True)
p.write_text(txt + "\n") p.write_text(txt + "\n")
return self
def append(self, txt: str) -> None: def write(self, src: "str | SwyddPath") -> "SwyddPath":
if isinstance(src, str):
return self._write_text(src)
elif isinstance(src, SwyddPath):
return self._write_text(src.read())
def _append_text(self, txt: str) -> "SwyddPath":
p = self._check() p = self._check()
p.parent.mkdir(exist_ok=True) p.parent.mkdir(exist_ok=True)
with p.open("a") as f: with p.open("a") as f:
f.write(txt) f.write(txt)
f.write("\n") f.write("\n")
return self
def __mod__(self, dst: "str | SwyddPath | Path") -> None: def rename(self, dst: "str | SwyddPath | Path") -> None:
if isinstance(dst, str): if isinstance(dst, str):
dst_p = SwyddPath.from_str( dst_p = SwyddPath.from_str(
dst dst
@ -451,7 +463,7 @@ class SwyddPath:
src_p = self._check() src_p = self._check()
src_p.rename(dst_p) src_p.rename(dst_p)
def __lt__(self, src: "str | SwyddPath") -> None: def copy(self, src: "str | SwyddPath") -> None:
if isinstance(src, str): if isinstance(src, str):
self.write(src) self.write(src)
elif isinstance(src, SwyddPath): elif isinstance(src, SwyddPath):
@ -459,8 +471,11 @@ class SwyddPath:
src_p = src._check() src_p = src._check()
shutil.copyfile(src_p, dst_p) shutil.copyfile(src_p, dst_p)
def __lshift__(self, txt: str) -> None: def append(self, src: "str | SwyddPath") -> "SwyddPath":
self.append(txt) if isinstance(src, str):
return self._append_text(src)
elif isinstance(src, SwyddPath):
return self._append_text(src.read().strip())
def task(func: Callable[..., Any]) -> Callable[..., None]: def task(func: Callable[..., Any]) -> Callable[..., None]:
@ -573,14 +588,14 @@ def target_generator(
if not (target_path := Path(target)).is_file(): if not (target_path := Path(target)).is_file():
return func(*args, **kwargs) return func(*args, **kwargs)
elif not needs: elif not needs:
sys.stderr.write(f"{target} already exists\n") sys.stderr.write(f"{target_path} up to date, exiting\n")
else: else:
target_stats = target_path.stat() target_stats = target_path.stat()
needs_stats = [Path(need).stat() for need in needs] needs_stats = [Path(need).stat() for need in needs]
if any((stat.st_mtime > target_stats.st_mtime for stat in needs_stats)): if any((stat.st_mtime > target_stats.st_mtime for stat in needs_stats)):
return func(*args, **kwargs) return func(*args, **kwargs)
else: else:
sys.stderr.write("doing nothing\n") sys.stderr.write(f"{target_path} up to date, exiting\n")
return noop(*args, **kwargs) return noop(*args, **kwargs)
@ -736,6 +751,15 @@ def cli() -> None:
SwyddPath(), SwyddPath(),
) )
asset = SwyddPath()
def geterr(*args, **kwargs) -> str:
get_kwargs = dict(stderr=True, stdout=False)
get_kwargs.update(kwargs)
return get(*args, **get_kwargs)
if __name__ == "__main__": if __name__ == "__main__":
sys.stderr.write("this module should not be invoked directly\n") sys.stderr.write("this module should not be invoked directly\n")
sys.exit(1) sys.exit(1)

View file

@ -5,7 +5,7 @@ __import__("sys").path.append("src")
import shutil import shutil
import sys import sys
from swydd import cli, get, option, path, sub, task from swydd import asset, cli, get, option, proc, sub, task
@task @task
@ -13,35 +13,36 @@ def bootstrap():
"""setup swydd dev environment""" """setup swydd dev environment"""
if not shutil.which("uv"): if not shutil.which("uv"):
sys.exit("uv necessary for swydd development") sys.exit("uv necessary for swydd development")
sub < "uv sync" (proc("uv sync").then("uv run pre-commit install").run())
sub < "uv run pre-commit install"
@task @task
def tests(): def tests():
"""run pytest""" """run pytest"""
sub < "uv run pytest" sub("uv run pytest")
@task @task
@option("skip-mypy", "skip mypy") @option("skip-mypy", "skip mypy")
def check(skip_mypy: bool = False): def check(skip_mypy: bool = False):
"""run pre-commit (and mypy)""" """run pre-commit (and mypy)"""
sub < "uv run pre-commit run --all" sub("uv run pre-commit run --all")
if not skip_mypy: if not skip_mypy:
sub < "uv run mypy src/" sub("uv run mypy src/")
@task @task
def docs(): def docs():
"""build docs""" """build docs"""
tags = get < "git tag --list" tags = get("git tag --list")
versions = [line for line in tags.splitlines() if line.startswith("v")] versions = [line for line in tags.splitlines() if line.startswith("v")]
for tag in versions: for tag in versions:
(path / f"docs/{tag}/swydd.py") < ( (
get < f"git show {tag}:src/swydd/__init__.py" asset(f"docs/{tag}/swydd.py").write(
get(f"git show {tag}:src/swydd/__init__.py")
) )
(path / "docs/swydd.py") < (path / "src/swydd/__init__.py") )
asset("docs/swydd.py").write(asset("src/swydd/__init__.py"))
cli() cli()

View file

@ -1,34 +1,36 @@
from swydd import SwyddPath, get, path, pipe, proc, seq, sub from swydd import SwyddPath, asset, get, geterr, pipe, proc, seq, sub
def test_operators(): def test_sub():
assert sub < "echo 'hello'" assert sub("echo 'hello'")
def test_pipes(): def test_pipes():
assert sub < ( assert sub(
pipe | "cat ../src/swydd/__init__.py" | "grep '__version__'" | "wc -l" pipe("cat ../src/swydd/__init__.py").pipe("grep '__version__'").pipe("wc -l")
) )
assert sub < ( assert sub(
proc | "cat ../src/swydd/__init__.py" | "grep '__version__'" | "wc -l" proc("cat ../src/swydd/__init__.py").pipe("grep '__version__'").pipe("wc -l")
) )
def test_seqs(): def test_seqs():
# -a is not an arg to cat so the subprocess should return false # -a is not an arg to cat so the subprocess should return false
assert not sub < (seq & "cat -a" & "echo hello") assert not sub(seq("cat -a").then("echo hello"))
assert not sub < (proc & "cat -a" & "echo hello") assert not sub(proc("cat -a").then("echo hello"))
def test_capture(): def test_capture():
result = get < "ls src not-src" result = get("ls src not-src")
assert result == "src:\nswydd" assert result == "src:\nswydd"
result = get << "ls src not-src" result = geterr("ls src not-src")
assert result == "ls: cannot access 'not-src': No such file or directory" assert result == "ls: cannot access 'not-src': No such file or directory"
assert "hello part deux" == (get < seq & "echo 'hello'" & "echo hello part deux")
assert "" == (get < (seq & "cp" & "echo hello part deux")) assert "hello part deux" == get(proc("echo 'hello'").then("echo hello part deux"))
commands = proc("cp").then("echo hello part deux")
assert "" == get(commands)
assert "cp: missing file operand\nTry 'cp --help' for more information." == ( assert "cp: missing file operand\nTry 'cp --help' for more information." == (
get << (seq & "cp" & "echo hello part deux") geterr(commands)
) )
@ -39,19 +41,19 @@ def check_result_file(file: SwyddPath, text: str) -> bool:
def test_write_to_path(): def test_write_to_path():
result_f = path / "products/result.txt" result_f = asset("products/result.txt")
result_txt = "text to file" result_txt = "text to file"
result_f < result_txt result_f.write(result_txt)
assert check_result_file(result_f, result_txt + "\n") assert check_result_file(result_f, result_txt + "\n")
def test_copy_and_rename(): def test_copy_and_rename():
src_f = path / "fixtures/input.txt" src_f = asset("fixtures/input.txt")
dst_f = path / "products/input.txt" dst_f = asset("products/input.txt")
dst_f < src_f dst_f.copy(src_f)
assert check_result_file(dst_f, "data to copy to another file\n") assert check_result_file(dst_f, "data to copy to another file\n")
dst_f % "products/input2.txt" dst_f.rename("products/input2.txt")
assert check_result_file( assert check_result_file(
path / "products/input2.txt", "data to copy to another file\n" asset("products/input2.txt"), "data to copy to another file\n"
) )