-
-
Notifications
You must be signed in to change notification settings - Fork 933
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b79198a
13e1593
74b68e9
133271b
fc755da
2d1efdc
a8a43fe
9fa1cee
790a790
c3fde7f
ab95886
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/ | ||
import contextlib | ||
import inspect | ||
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. Since git | ||
# errors out when run with no arguments, we swallow that error. | ||
with contextlib.suppress(GitCommandError): | ||
self.git.execute(["git"], shell=value_in_call) | ||
# 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"]) | ||
|
||
popen_shell_arg = mock_popen.call_args.kwargs["shell"] | ||
expected_message = re.compile(rf"DEBUG:git.cmd:Popen\(.*\bshell={popen_shell_arg}\b.*\)") | ||
match_attempts = [expected_message.fullmatch(message) for message in log_watcher.output] | ||
self.assertTrue(any(match_attempts), repr(log_watcher.output)) | ||
@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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes only the order in which they are given, not the contents, and the set is equal to the same value as before.
However, I am wondering if
command
should actually be added here. Although the@overload
-decorated stubs forGit.execute
define some parameters as keyword-only, in the actual definition no parameter is keyword-only and there is definitional symmetry betweencommand
and the others. Should I worry about what happens if someone has agit-command
script that they can usually run asgit command
and tries to rung.command()
whereg
is agit.cmd.Git
instance? Or related situations?Another ramification of the parameters not being keyword-only is that, because they can be passed positionally, it is a breaking change to add a new one to
Git.execute
elsewhere than the end. Still, regarding #1686 (comment), if you think astdin
parameter should be added as a synonym ofistream
, this could still be done even withstdin
at the end, with the docstring items that document the parameters referring to each other to overcome any confusion. I am inclined to say the added complexity is not worthwhile (for example, the function would have to handle the case where they are both passed and with inconsistent values). But a possible argument against this is that thetext
synonym ofuniversal_newlines
could also be added.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In these situations it would be great to know why
command
was put there in the first place, and I for one do not remember. I know there never was an issue related tocommand
specifically, which seems to indicate it's a limitation worth ignoring or fixing in a non-breaking fashion (which seems possible).Regarding
istream
, I'd think maintaining stability would let me sleep better at night especially since you seem to agree that it's used consistently here and ingitdb
. Probably along with the documentation improvements, all is well and better than it was before, which even in the past didn't seem to have caused enough confusion to cause an issue to be opened.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean why it was omitted from the
execute_kwargs
set? If so, my guess is that it was intended to be used positionally while the others were intended to be keyword-only. This is judging by how they are given as keyword-only arguments in the@overload
s, where they are preceded by a*,
item in the list of formal parameters, which is absent in the actual function definition (thus causing them to accept both positional and keyword arguments).But it may be that I am misunderstanding what you mean here. It is also possible that their keyword-only status in the
@overload
s was intentionally different from the ultimate definition and intended to provide guidance. (I'm not sure. The@overload
s are missing most of the arguments, which confuses tooling and seems unintentional.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am happy to adopt your conclusion in that this is not intentional and since there are negative side-effects, like tooling not working correctly, it's certainly something that could be improved.
If against all odds such an action does create downstream breakage, it could also be undone with a patch release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should make the parameters in the real definition actually keyword-only, since that is a breaking change. Or, at least, I would not rush to that. It seems likely to me that, at least for the first few, people are passing them positionally.
I believe it is merely the absence of many parameters from the
@overload
-decorated stubs (which precede the real definition) that is causing VS Code not to show or autocomplete some arguments. Adding the missing parameters to the@overload
-decorated stubs should be sufficient to fix that, and it shouldn't be a breaking change because (a) they are just type stubs, so it doesn't break runtime behavior, (b) the actual change seems compatible even relative to what the type stubs said before, and (c) GitPython doesn't have static typing stability currently anyway, because althoughpy.typed
is included in the package,mypy
checks don't pass (nor, per #1659, dopyright
checks pass).