diff --git a/docs/cmd/index.md b/docs/cmd/index.md index 4e495cb08..32fad657d 100644 --- a/docs/cmd/index.md +++ b/docs/cmd/index.md @@ -20,3 +20,58 @@ git hg svn ``` + +## Controlling commands + +### Override `run()` + +You want to control `stdout`, `stderr`, terminal output, tee'ing or logging, introspect and modify +the commands themselves. libvcs is designed to make this trivial to control. + +- Git -> `Git.` -> `Git.run` -> `run` + +You override `Git.run` method, and all `Git` commands can be intercepted. + +```python + +class MyGit(Git): + def run(self, *args, **kwargs): + return ... +``` + +You can also pass-through using `super()` + +```python + +class MyGit(Git): + def run(self, *args, **kwargs): + return super().run(*args, **kwargs) +``` + +Two possibilities: + +1. Modify args / kwargs before running them +2. Replace `run()` with a different subprocess runner + +### `LazySubprocessMixin` + +```python + +class MyGit(Git, LazySubprocessMixin): + def run(self, *args, **kwargs): + return ... +``` + +You can introspect it here. + +Instead of `git.run(...)` you'd do `git.run(...).run()`. + +Also, you can introspect and modify the output before execution + +```python +>>> mycmd = git.run(...) +>>> mycmd.flags +... +>>> mycmd.flags = '--help' +>>> mycmd.run() +``` diff --git a/libvcs/projects/git.py b/libvcs/projects/git.py index 775ed3f86..edf843226 100644 --- a/libvcs/projects/git.py +++ b/libvcs/projects/git.py @@ -22,6 +22,7 @@ from typing import Dict, Literal, Optional, TypedDict, Union from urllib import parse as urlparse +from libvcs.cmd.git import Git from libvcs.types import StrPath from .. import exc @@ -313,24 +314,26 @@ def set_remotes(self, overwrite: bool = False): def obtain(self, *args, **kwargs): """Retrieve the repository, clone if doesn't exist.""" - self.ensure_dir() - - url = self.url + clone_kwargs = {} - cmd = ["clone", "--progress"] if self.git_shallow: - cmd.extend(["--depth", "1"]) + clone_kwargs["depth"] = 1 if self.tls_verify: - cmd.extend(["-c", "http.sslVerify=false"]) - cmd.extend([url, self.dir]) + clone_kwargs["c"] = "http.sslVerify=false" self.log.info("Cloning.") - self.run(cmd, log_in_real_time=True) + + git = Git(dir=self.dir) + + # Needs to log to std out, e.g. log_in_real_time + git.clone(url=self.url, progress=True, make_parents=True, **clone_kwargs) self.log.info("Initializing submodules.") + self.run(["submodule", "init"], log_in_real_time=True) - cmd = ["submodule", "update", "--recursive", "--init"] - self.run(cmd, log_in_real_time=True) + self.run( + ["submodule", "update", "--recursive", "--init"], log_in_real_time=True + ) self.set_remotes(overwrite=True) diff --git a/tests/_internal/subprocess/test_SubprocessCommand.py b/tests/_internal/subprocess/test_SubprocessCommand.py index 493f67efa..bf2a13483 100644 --- a/tests/_internal/subprocess/test_SubprocessCommand.py +++ b/tests/_internal/subprocess/test_SubprocessCommand.py @@ -1,6 +1,9 @@ import pathlib import subprocess +import sys +import textwrap from typing import Any +from unittest import mock import pytest @@ -137,3 +140,161 @@ def test_run(tmp_path: pathlib.Path, args: list, kwargs: dict, run_kwargs: dict) response = cmd.run(**run_kwargs) assert response.returncode == 0 + + +@pytest.mark.parametrize( + "args,kwargs,run_kwargs", + [ + [ + ["ls"], + {}, + {}, + ], + [[["ls", "-l"]], {}, {}], + [[["ls", "-al"]], {}, {"stdout": subprocess.DEVNULL}], + ], + ids=idfn, +) +@mock.patch("subprocess.Popen") +def test_Popen_mock( + mock_subprocess_popen, + tmp_path: pathlib.Path, + args: list, + kwargs: dict, + run_kwargs: dict, + capsys: pytest.LogCaptureFixture, +): + process_mock = mock.Mock() + attrs = {"communicate.return_value": ("output", "error"), "returncode": 1} + process_mock.configure_mock(**attrs) + mock_subprocess_popen.return_value = process_mock + kwargs["cwd"] = tmp_path + cmd = SubprocessCommand(*args, **kwargs) + response = cmd.Popen(**run_kwargs) + + assert response.returncode == 1 + + +@pytest.mark.parametrize( + "args,kwargs,run_kwargs", + [ + [[["git", "pull", "--progress"]], {}, {}], + ], + ids=idfn, +) +@mock.patch("subprocess.Popen") +def test_Popen_git_mock( + mock_subprocess_popen, + tmp_path: pathlib.Path, + args: list, + kwargs: dict, + run_kwargs: dict, + capsys: pytest.LogCaptureFixture, +): + process_mock = mock.Mock() + attrs = {"communicate.return_value": ("output", "error"), "returncode": 1} + process_mock.configure_mock(**attrs) + mock_subprocess_popen.return_value = process_mock + kwargs["cwd"] = tmp_path + cmd = SubprocessCommand(*args, **kwargs) + response = cmd.Popen(**run_kwargs) + + stdout, stderr = response.communicate() + + assert response.returncode == 1 + assert stdout == "output" + assert stderr == "error" + + +CODE = ( + textwrap.dedent( + r""" + import sys + import time + size = 10 + for i in range(10): + time.sleep(.01) + sys.stderr.write( + '[' + "#" * i + "." * (size-i) + ']' + f' {i}/{size}' + '\n' + ) + sys.stderr.flush() +""" + ) + .strip("\n") + .lstrip() +) + + +def test_Popen_stderr( + tmp_path: pathlib.Path, + capsys: pytest.LogCaptureFixture, +): + cmd = SubprocessCommand( + [ + sys.executable, + "-c", + CODE, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=tmp_path, + ) + response = cmd.Popen() + while response.poll() is None: + stdout, stderr = response.communicate() + + assert stdout != "output" + assert stderr != "1" + assert response.returncode == 0 + + +def test_CaptureStderrMixin( + tmp_path: pathlib.Path, + capsys: pytest.LogCaptureFixture, +): + cmd = SubprocessCommand( + [ + sys.executable, + "-c", + CODE, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=tmp_path, + ) + response = cmd.Popen() + while response.poll() is None: + if response.stderr is not None: + line = response.stderr.readline().decode("utf-8").strip() + if line: + assert line.startswith("[") + assert response.returncode == 0 + + +def test_CaptureStderrMixin_error( + tmp_path: pathlib.Path, + capsys: pytest.LogCaptureFixture, +): + cmd = SubprocessCommand( + [ + sys.executable, + "-c", + CODE + + textwrap.dedent( + """ + sys.exit("FATAL") + """ + ), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=tmp_path, + ) + response = cmd.Popen() + while response.poll() is None: + if response.stderr is not None: + line = response.stderr.readline().decode("utf-8").strip() + if line: + assert line.startswith("[") or line == "FATAL" + + assert response.returncode == 1 diff --git a/tests/cmd/test_git.py b/tests/cmd/test_git.py index 5eb271399..c522b970c 100644 --- a/tests/cmd/test_git.py +++ b/tests/cmd/test_git.py @@ -3,6 +3,7 @@ import pytest +from libvcs._internal.subprocess import SubprocessCommand from libvcs.cmd import git @@ -11,3 +12,20 @@ def test_run(dir_type: Callable, tmp_path: pathlib.Path): repo = git.Git(dir=dir_type(tmp_path)) assert repo.dir == tmp_path + + +@pytest.mark.parametrize("dir_type", [str, pathlib.Path]) +def test_run_deferred(dir_type: Callable, tmp_path: pathlib.Path): + class GitDeferred(git.Git): + def run(self, args, **kwargs): + return SubprocessCommand(["git", *args], **kwargs) + + g = GitDeferred(dir=dir_type(tmp_path)) + cmd = g.run(["help"]) + + assert g.dir == tmp_path + assert cmd.args == ["git", "help"] + + assert cmd.run(capture_output=True, text=True).stdout.startswith( + "usage: git [--version] [--help] [-C ]" + ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy