Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork938
Clarify Git.execute and Popen arguments#1688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
b79198a
13e1593
74b68e9
133271b
fc755da
2d1efdc
a8a43fe
9fa1cee
790a790
c3fde7f
ab95886
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -66,10 +66,10 @@ | ||
"with_extended_output", | ||
"with_exceptions", | ||
"as_process", | ||
"output_stream", | ||
"stdout_as_string", | ||
"kill_after_timeout", | ||
"with_stdout", | ||
Comment on lines -69 to +72 MemberAuthor
| ||
"universal_newlines", | ||
"shell", | ||
"env", | ||
@@ -105,7 +105,7 @@ def handle_process_output( | ||
)->None: | ||
"""Registers for notifications to learn that process output is ready to read, and dispatches lines to | ||
the respective line handlers. | ||
This function returns once the finalizer returns. | ||
:return: result of finalizer | ||
:param process: subprocess.Popen instance | ||
@@ -294,9 +294,7 @@ def __setstate__(self, d: Dict[str, Any]) -> None: | ||
@classmethod | ||
defrefresh(cls,path:Union[None,PathLike]=None)->bool: | ||
"""This gets called by the refresh function (see the top level __init__).""" | ||
# discern which path to refresh with | ||
ifpathisnotNone: | ||
new_git=os.path.expanduser(path) | ||
@@ -446,9 +444,9 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: | ||
ifis_cygwin: | ||
url=cygpath(url) | ||
else: | ||
"""Remove anybackslashes from urls to be written in config files. | ||
Windows might create configfiles containing paths withbackslashes, | ||
but git stops liking them as it will escape the backslashes. | ||
Hence we undo the escaping just to be sure. | ||
""" | ||
@@ -464,8 +462,8 @@ def check_unsafe_protocols(cls, url: str) -> None: | ||
Check for unsafe protocols. | ||
Apart from the usual protocols (http, git, ssh), | ||
Git allows "remote helpers" that have the form ``<transport>::<address>``, | ||
one of these helpers (``ext::``) can be used to invoke any arbitrary command. | ||
See: | ||
@@ -517,7 +515,7 @@ def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: | ||
self.status:Union[int,None]=None | ||
def_terminate(self)->None: | ||
"""Terminate the underlying process.""" | ||
ifself.procisNone: | ||
return | ||
@@ -572,7 +570,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int: | ||
"""Wait for the process and return its status code. | ||
:param stderr: Previously read value of stderr, in case stderr is already closed. | ||
:warn:May deadlock if output or error pipes are used and not handled separately. | ||
:raise GitCommandError: if the return status is not 0""" | ||
ifstderrisNone: | ||
stderr_b=b"" | ||
@@ -605,13 +603,12 @@ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> byte | ||
# END auto interrupt | ||
classCatFileContentStream(object): | ||
"""Object representing a sized read-only stream returning the contents of | ||
an object. | ||
It behaves like a stream, but counts the data read and simulates an empty | ||
stream once our sized content region is empty. | ||
If not all data is read to the end of theobject's lifetime, we read the | ||
rest to assure the underlying stream continues to work.""" | ||
__slots__:Tuple[str, ...]= ("_stream","_nbr","_size") | ||
@@ -740,11 +737,11 @@ def __getattr__(self, name: str) -> Any: | ||
defset_persistent_git_options(self,**kwargs:Any)->None: | ||
"""Specify command line options to the git executable | ||
for subsequent subcommand calls. | ||
:param kwargs: | ||
is a dict of keyword arguments. | ||
These arguments are passed as in _call_process | ||
but will be passed to the git command rather than | ||
the subcommand. | ||
""" | ||
@@ -775,7 +772,7 @@ def version_info(self) -> Tuple[int, int, int, int]: | ||
""" | ||
:return: tuple(int, int, int, int) tuple with integers representing the major, minor | ||
and additional version numbers as parsed from git version. | ||
This value is generated on demand and is cached.""" | ||
returnself._version_info | ||
@overload | ||
@@ -842,16 +839,16 @@ def execute( | ||
strip_newline_in_stdout:bool=True, | ||
**subprocess_kwargs:Any, | ||
)->Union[str,bytes,Tuple[int,Union[str,bytes],str],AutoInterrupt]: | ||
"""Handles executing the command and consumes and returns the returned | ||
information (stdout). | ||
:param command: | ||
The command argument list to execute. | ||
It should be a sequence of program arguments, or a string. The | ||
program to execute is the first item in the args sequence or string. | ||
:param istream: | ||
Standard input filehandle passed to`subprocess.Popen`. | ||
:param with_extended_output: | ||
Whether to return a (status, stdout, stderr) tuple. | ||
@@ -862,8 +859,7 @@ def execute( | ||
:param as_process: | ||
Whether to return the created process instance directly from which | ||
streams can be read on demand. This will render with_extended_output and | ||
with_exceptions ineffective - the caller will have to deal with the details. | ||
It is important to note that the process will be placed into an AutoInterrupt | ||
wrapper that will interrupt the process once it goes out of scope. If you | ||
use the command in iterators, you should pass the whole process instance | ||
@@ -876,13 +872,34 @@ def execute( | ||
always be created with a pipe due to issues with subprocess. | ||
This merely is a workaround as data will be copied from the | ||
output pipe to the given output stream directly. | ||
Judging from the implementation, you shouldn't use thisparameter! | ||
:param stdout_as_string: | ||
If False, thecommand's standard output will be bytes. Otherwise, it will be | ||
decoded into a string using the default encoding (usuallyUTF-8). | ||
The latter can fail, if the output contains binary data. | ||
:param kill_after_timeout: | ||
Specifies a timeout in seconds for the git command, after which the process | ||
should be killed. This will have no effect if as_process is set to True. It is | ||
set to None by default and will let the process run until the timeout is | ||
explicitly specified. This feature is not supported on Windows. It's also worth | ||
noting that kill_after_timeout uses SIGKILL, which can have negative side | ||
effects on a repository. For example, stale locks in case of ``git gc`` could | ||
render the repository incapable of accepting changes until the lock is manually | ||
removed. | ||
:param with_stdout: | ||
If True, default True, we open stdout on the created process. | ||
:param universal_newlines: | ||
if True, pipes will be opened as text, and lines are split at | ||
all known line endings. | ||
:param shell: | ||
Whether to invoke commands through a shell (see `Popen(..., shell=True)`). | ||
It overrides :attr:`USE_SHELL` if it is not `None`. | ||
:param env: | ||
A dictionary of environment variables to be passed to `subprocess.Popen`. | ||
@@ -891,38 +908,23 @@ def execute( | ||
one invocation of write() method. If the given number is not positive then | ||
the default value is used. | ||
:param strip_newline_in_stdout: | ||
Whether to strip the trailing ``\\n`` of the command stdout. | ||
:param subprocess_kwargs: | ||
Keyword arguments to be passed to`subprocess.Popen`. Please note that | ||
some of the valid kwargs are already set by this method; the ones you | ||
specify may not be the same ones. | ||
:return: | ||
* str(output) if extended_output = False (Default) | ||
* tuple(int(status), str(stdout), str(stderr)) if extended_output = True | ||
If output_stream is True, the stdout value will be your output stream: | ||
* output_stream if extended_output = False | ||
* tuple(int(status), output_stream, str(stderr)) if extended_output = True | ||
Notethatgit is executed with``LC_MESSAGES="C"`` to ensure consistent | ||
output regardless of system language. | ||
:raise GitCommandError: | ||
@@ -971,18 +973,15 @@ def execute( | ||
# end handle | ||
stdout_sink=PIPEifwith_stdoutelsegetattr(subprocess,"DEVNULL",None)oropen(os.devnull,"wb") | ||
ifshellisNone: | ||
shell=self.USE_SHELL | ||
log.debug( | ||
"Popen(%s, cwd=%s,stdin=%s, shell=%s,universal_newlines=%s)", | ||
redacted_command, | ||
cwd, | ||
"<valid stream>"ifistreamelse"None", | ||
shell, | ||
universal_newlines, | ||
) | ||
try: | ||
withmaybe_patch_caller_env: | ||
@@ -1010,8 +1009,8 @@ def execute( | ||
ifas_process: | ||
returnself.AutoInterrupt(proc,command) | ||
defkill_process(pid:int)->None: | ||
"""Callback to kill a process.""" | ||
p=Popen( | ||
["ps","--ppid",str(pid)], | ||
stdout=PIPE, | ||
@@ -1044,7 +1043,7 @@ def _kill_process(pid: int) -> None: | ||
ifkill_after_timeoutisnotNone: | ||
kill_check=threading.Event() | ||
watchdog=threading.Timer(kill_after_timeout,kill_process,args=(proc.pid,)) | ||
# Wait for the process to return | ||
status=0 | ||
@@ -1208,7 +1207,7 @@ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]: | ||
def__call__(self,**kwargs:Any)->"Git": | ||
"""Specify command line options to the git executable | ||
for a subcommand call. | ||
:param kwargs: | ||
is a dict of keyword arguments. | ||
@@ -1246,7 +1245,7 @@ def _call_process( | ||
self,method:str,*args:Any,**kwargs:Any | ||
)->Union[str,bytes,Tuple[int,Union[str,bytes],str],"Git.AutoInterrupt"]: | ||
"""Run the given git command with the specified arguments and return | ||
the result as astring. | ||
:param method: | ||
is the command. Contained "_" characters will be converted to dashes, | ||
@@ -1255,7 +1254,7 @@ def _call_process( | ||
:param args: | ||
is the list of arguments. If None is included, it will be pruned. | ||
This allows your commands to call git more conveniently as None | ||
is realized as non-existent. | ||
:param kwargs: | ||
It contains key-values for the following: | ||
@@ -1385,7 +1384,7 @@ def get_object_header(self, ref: str) -> Tuple[str, str, int]: | ||
returnself.__get_object_header(cmd,ref) | ||
defget_object_data(self,ref:str)->Tuple[str,str,int,bytes]: | ||
"""As get_object_header, but returns object data as well. | ||
:return: (hexsha, type_string, size_as_int, data_string) | ||
:note: not threadsafe""" | ||
@@ -1395,10 +1394,10 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: | ||
return (hexsha,typename,size,data) | ||
defstream_object_data(self,ref:str)->Tuple[str,str,int,"Git.CatFileContentStream"]: | ||
"""As get_object_header, but returns the data as a stream. | ||
:return: (hexsha, type_string, size_as_int, stream) | ||
:note: This method is not threadsafe, you need one independent Command instance per thread to be safe!""" | ||
cmd=self._get_persistent_cmd("cat_file_all","cat_file",batch=True) | ||
hexsha,typename,size=self.__get_object_header(cmd,ref) | ||
cmd_stdout=cmd.stdoutifcmd.stdoutisnotNoneelseio.BytesIO() | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -4,7 +4,7 @@ | ||
# | ||
# This module is part of GitPython and is released under | ||
# the BSD License: https://opensource.org/license/bsd-3-clause/ | ||
importinspect | ||
import logging | ||
import os | ||
import os.path as osp | ||
@@ -40,6 +40,13 @@ def tearDown(self): | ||
gc.collect() | ||
def _assert_logged_for_popen(self, log_watcher, name, value): | ||
re_name = re.escape(name) | ||
re_value = re.escape(str(value)) | ||
re_line = re.compile(fr"DEBUG:git.cmd:Popen\(.*\b{re_name}={re_value}[,)]") | ||
match_attempts = [re_line.match(message) for message in log_watcher.output] | ||
self.assertTrue(any(match_attempts), repr(log_watcher.output)) | ||
@mock.patch.object(Git, "execute") | ||
def test_call_process_calls_execute(self, git): | ||
git.return_value = "" | ||
@@ -96,10 +103,8 @@ def _do_shell_combo(self, value_in_call, value_from_class): | ||
# git.cmd gets Popen via a "from" import, so patch it there. | ||
with mock.patch.object(cmd, "Popen", wraps=cmd.Popen) as mock_popen: | ||
# Use a command with no arguments (besides the program name), so it runs | ||
# with or without a shell, on all OSes, with the same effect. | ||
self.git.execute(["git"], with_exceptions=False, shell=value_in_call) | ||
return mock_popen | ||
@@ -115,14 +120,19 @@ def test_it_uses_shell_or_not_as_specified(self, case): | ||
def test_it_logs_if_it_uses_a_shell(self, case): | ||
"""``shell=`` in the log message agrees with what is passed to `Popen`.""" | ||
value_in_call, value_from_class = case | ||
with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher: | ||
mock_popen = self._do_shell_combo(value_in_call, value_from_class) | ||
self._assert_logged_for_popen(log_watcher, "shell", mock_popen.call_args.kwargs["shell"]) | ||
@ddt.data( | ||
("None", None), | ||
("<valid stream>", subprocess.PIPE), | ||
) | ||
def test_it_logs_istream_summary_for_stdin(self, case): | ||
expected_summary, istream_argument = case | ||
with self.assertLogs(cmd.log, level=logging.DEBUG) as log_watcher: | ||
self.git.execute(["git", "version"], istream=istream_argument) | ||
self._assert_logged_for_popen(log_watcher, "stdin", expected_summary) | ||
def test_it_executes_git_and_returns_result(self): | ||
self.assertRegex(self.git.execute(["git", "version"]), r"^git version [\d\.]{2}.*$") | ||
@@ -364,3 +374,11 @@ def counter_stderr(line): | ||
self.assertEqual(count[1], line_count) | ||
self.assertEqual(count[2], line_count) | ||
def test_execute_kwargs_set_agrees_with_method(self): | ||
parameter_names = inspect.signature(cmd.Git.execute).parameters.keys() | ||
self_param, command_param, *most_params, extra_kwargs_param = parameter_names | ||
self.assertEqual(self_param, "self") | ||
self.assertEqual(command_param, "command") | ||
self.assertEqual(set(most_params), cmd.execute_kwargs) # Most important. | ||
self.assertEqual(extra_kwargs_param, "subprocess_kwargs") | ||
Comment on lines +378 to +384 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Or execute_kwargs=inspect.signature(Git.execute).parameters.keys()- {"self","command","subprocess_kwargs"} Right now it is defined very high up in the module, even higher than
Anyway, I do not think any of the changes I suggest in this comment need to be made in this pull request. But I wanted to mention the issue just in case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Removing As |