p4cmd 2.10.2__tar.gz → 2.10.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {p4cmd-2.10.2 → p4cmd-2.10.3}/PKG-INFO +4 -1
- {p4cmd-2.10.2 → p4cmd-2.10.3}/README.md +3 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/__init__.py +1 -1
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/p4cmd.py +46 -17
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd.egg-info/PKG-INFO +4 -1
- {p4cmd-2.10.2 → p4cmd-2.10.3}/tests/test_p4client.py +52 -6
- {p4cmd-2.10.2 → p4cmd-2.10.3}/LICENSE +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/p4errors.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/p4file.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/__init__.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_MOVED.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd/utils.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd.egg-info/SOURCES.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd.egg-info/dependency_links.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd.egg-info/requires.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/p4cmd.egg-info/top_level.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/setup.cfg +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/setup.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/tests/__init__.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/tests/conftest.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/tests/test_integration.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/tests/test_p4file.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.10.3}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: p4cmd
|
|
3
|
-
Version: 2.10.
|
|
3
|
+
Version: 2.10.3
|
|
4
4
|
Summary: Simple P4 python module
|
|
5
5
|
Home-page: https://github.com/nielsvaes/p4cmd
|
|
6
6
|
Author: Niels Vaes
|
|
@@ -24,6 +24,9 @@ Dynamic: summary
|
|
|
24
24
|
|
|
25
25
|
### P4CMD 🌴
|
|
26
26
|
|
|
27
|
+
[](https://github.com/nielsvaes/p4cmd/actions/workflows/tests.yml)
|
|
28
|
+
[](https://github.com/nielsvaes/p4cmd/actions/workflows/python-publish.yml)
|
|
29
|
+
|
|
27
30
|
A Python Perforce package that doesn't bring in any other packages to work.
|
|
28
31
|
Relies on the `p4` CLI installed on the system.
|
|
29
32
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
### P4CMD 🌴
|
|
2
2
|
|
|
3
|
+
[](https://github.com/nielsvaes/p4cmd/actions/workflows/tests.yml)
|
|
4
|
+
[](https://github.com/nielsvaes/p4cmd/actions/workflows/python-publish.yml)
|
|
5
|
+
|
|
3
6
|
A Python Perforce package that doesn't bring in any other packages to work.
|
|
4
7
|
Relies on the `p4` CLI installed on the system.
|
|
5
8
|
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import marshal
|
|
3
3
|
import subprocess
|
|
4
4
|
import socket
|
|
5
|
+
import warnings
|
|
5
6
|
from pprint import pformat
|
|
6
7
|
|
|
7
8
|
import logging
|
|
@@ -84,18 +85,35 @@ class P4Client(object):
|
|
|
84
85
|
"""
|
|
85
86
|
self.max_parallel_connections = value
|
|
86
87
|
|
|
87
|
-
def run_cmd(self, cmd, args=None, file_list=None,
|
|
88
|
+
def run_cmd(self, cmd, args=None, file_list=None, marshal_output=True, online_check=True, raise_on_errors=False, **kwargs):
|
|
88
89
|
"""
|
|
89
|
-
Reads the output stream of the command and returns it as a
|
|
90
|
+
Reads the output stream of the command and returns it as a list of dicts.
|
|
90
91
|
|
|
91
92
|
:param cmd: *string* p4 command like "change", "reopen", "move"
|
|
92
93
|
:param args: *list* of string arguments like ["-c", "27277"]
|
|
93
94
|
:param file_list: *list* of string arguments like ["//depot/folder/file.atom", "D:/Games/Whatever.fbx]
|
|
94
|
-
:param
|
|
95
|
+
:param marshal_output: *bool* when True (default), passes ``-G`` so p4 emits Python marshal-format
|
|
96
|
+
output that we parse into dicts, and also passes ``-u <user> -c <client>`` for identity. Set to
|
|
97
|
+
False for commands that don't honor ``-G`` (notably ``p4 set``, which only reads local settings)
|
|
98
|
+
or when user/client haven't been resolved yet — output is then returned verbatim under the
|
|
99
|
+
``raw_output`` key.
|
|
95
100
|
:param online_check: *bool* if set to True, will first check if the remote server is reachable before executing the command.
|
|
96
101
|
:return: *list* of dictionaries with either the marshaled returns of the command or dictionaries with the
|
|
97
102
|
raw output of the command
|
|
98
103
|
"""
|
|
104
|
+
# Backwards compat: the old name `use_global_options` was misleading — it sounded like a master
|
|
105
|
+
# switch for p4's global-option family, when it really just toggles `-G` (marshal output) plus the
|
|
106
|
+
# bundled `-u`/`-c` identity flags. Renamed to `marshal_output` to point at the actual consequence
|
|
107
|
+
# (the parsing path); old kwarg kept as a deprecated alias.
|
|
108
|
+
if "use_global_options" in kwargs:
|
|
109
|
+
warnings.warn(
|
|
110
|
+
"`use_global_options` is deprecated; use `marshal_output` instead.",
|
|
111
|
+
DeprecationWarning,
|
|
112
|
+
stacklevel=2,
|
|
113
|
+
)
|
|
114
|
+
marshal_output = kwargs.pop("use_global_options")
|
|
115
|
+
if kwargs:
|
|
116
|
+
raise TypeError(f"run_cmd() got unexpected keyword arguments: {list(kwargs)}")
|
|
99
117
|
|
|
100
118
|
if not self.perforce_root:
|
|
101
119
|
raise p4errors.WorkSpaceError(f"self.perforce_root (value: {self.perforce_root}) is not set!")
|
|
@@ -117,7 +135,7 @@ class P4Client(object):
|
|
|
117
135
|
dict_list = []
|
|
118
136
|
for clamped_arg in clamped_arg_list:
|
|
119
137
|
for clamped_files in clamped_file_list:
|
|
120
|
-
if
|
|
138
|
+
if marshal_output:
|
|
121
139
|
command = f"p4 -G -u {self.user} -c {self.client} {cmd} {clamped_arg} {clamped_files}"
|
|
122
140
|
else:
|
|
123
141
|
command = f"p4 {cmd} {clamped_arg} {clamped_files}"
|
|
@@ -129,20 +147,31 @@ class P4Client(object):
|
|
|
129
147
|
|
|
130
148
|
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True, cwd=self.perforce_root) as pipe:
|
|
131
149
|
output = pipe.stdout
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
except ValueError as error:
|
|
139
|
-
output_dict = {
|
|
150
|
+
if not marshal_output:
|
|
151
|
+
# Without -G, p4 emits plain text (and `p4 set` ignores -G even when passed),
|
|
152
|
+
# so marshal.load is guaranteed to fail. Read stdout directly from this Popen
|
|
153
|
+
# — it has stdin=PIPE, so it's safe in non-interactive sessions where the old
|
|
154
|
+
# check_output fallback hit OSError(WinError 50) on DuplicateHandle.
|
|
155
|
+
dict_list.append({
|
|
140
156
|
"command": command,
|
|
141
157
|
"code": "raw",
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
"raw_output": output.read(),
|
|
159
|
+
})
|
|
160
|
+
else:
|
|
161
|
+
try:
|
|
162
|
+
while True:
|
|
163
|
+
value_dict = marshal.load(output)
|
|
164
|
+
dict_list.append(value_dict)
|
|
165
|
+
except EOFError:
|
|
166
|
+
pass
|
|
167
|
+
except ValueError as error:
|
|
168
|
+
output_dict = {
|
|
169
|
+
"command": command,
|
|
170
|
+
"code": "raw",
|
|
171
|
+
"error": str(error),
|
|
172
|
+
"raw_output": subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True, cwd=self.perforce_root)
|
|
173
|
+
}
|
|
174
|
+
dict_list.append(output_dict)
|
|
146
175
|
pipe.kill()
|
|
147
176
|
|
|
148
177
|
# Check for error dicts in the results
|
|
@@ -218,7 +247,7 @@ class P4Client(object):
|
|
|
218
247
|
"""
|
|
219
248
|
try:
|
|
220
249
|
# skipping the online check for setting commands
|
|
221
|
-
info_dict = self.run_cmd("set", [setting],
|
|
250
|
+
info_dict = self.run_cmd("set", [setting], marshal_output=False, online_check=False)[0]
|
|
222
251
|
except Exception as e:
|
|
223
252
|
raise p4errors.WorkSpaceError("Unable to find setting %s" % setting) from e
|
|
224
253
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: p4cmd
|
|
3
|
-
Version: 2.10.
|
|
3
|
+
Version: 2.10.3
|
|
4
4
|
Summary: Simple P4 python module
|
|
5
5
|
Home-page: https://github.com/nielsvaes/p4cmd
|
|
6
6
|
Author: Niels Vaes
|
|
@@ -24,6 +24,9 @@ Dynamic: summary
|
|
|
24
24
|
|
|
25
25
|
### P4CMD 🌴
|
|
26
26
|
|
|
27
|
+
[](https://github.com/nielsvaes/p4cmd/actions/workflows/tests.yml)
|
|
28
|
+
[](https://github.com/nielsvaes/p4cmd/actions/workflows/python-publish.yml)
|
|
29
|
+
|
|
27
30
|
A Python Perforce package that doesn't bring in any other packages to work.
|
|
28
31
|
Relies on the `p4` CLI installed on the system.
|
|
29
32
|
|
|
@@ -668,13 +668,10 @@ def test_run_cmd_handles_string_key_error_dicts(p4client, caplog):
|
|
|
668
668
|
|
|
669
669
|
|
|
670
670
|
def test_run_cmd_value_error_fallback_no_warning(p4client, caplog):
|
|
671
|
-
"""
|
|
672
|
-
with patch("subprocess.Popen") as mock_popen
|
|
673
|
-
patch("p4cmd.p4cmd.subprocess.check_output", return_value=b"P4USER=niels.vaes (set)\n"):
|
|
674
|
-
# Empty stdout triggers ValueError on marshal.load (unknown type code in this case
|
|
675
|
-
# would also work; here we use bytes that aren't valid marshal data).
|
|
671
|
+
"""The marshal_output=False path (e.g. `p4 set`) must not log spurious error warnings."""
|
|
672
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
676
673
|
import io as _io
|
|
677
|
-
buf = _io.BytesIO(b"\
|
|
674
|
+
buf = _io.BytesIO(b"P4USER=niels.vaes (set)\n")
|
|
678
675
|
mock_popen.return_value.__enter__ = lambda s: s
|
|
679
676
|
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
680
677
|
mock_popen.return_value.stdout = buf
|
|
@@ -685,6 +682,55 @@ def test_run_cmd_value_error_fallback_no_warning(p4client, caplog):
|
|
|
685
682
|
assert caplog.records == []
|
|
686
683
|
|
|
687
684
|
|
|
685
|
+
def test_run_cmd_no_global_options_does_not_spawn_second_subprocess(p4client):
|
|
686
|
+
"""
|
|
687
|
+
Regression test for issue #13. When marshal_output=False (e.g. `p4 set`),
|
|
688
|
+
p4 outputs plain text — marshal.load is guaranteed to fail. The old fallback
|
|
689
|
+
spawned a second subprocess via check_output without pinning stdin, which
|
|
690
|
+
triggered OSError(WinError 50) on DuplicateHandle in non-interactive sessions
|
|
691
|
+
(UE commandlets, scheduled tasks, CI). The fix reads stdout directly from the
|
|
692
|
+
already-open Popen, which has stdin=PIPE.
|
|
693
|
+
"""
|
|
694
|
+
import io as _io
|
|
695
|
+
p4_set_output = b"P4USER=niels.vaes (set)\r\n"
|
|
696
|
+
with patch("subprocess.Popen") as mock_popen, \
|
|
697
|
+
patch("p4cmd.p4cmd.subprocess.check_output") as mock_check_output:
|
|
698
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
699
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
700
|
+
mock_popen.return_value.stdout = _io.BytesIO(p4_set_output)
|
|
701
|
+
mock_popen.return_value.kill = MagicMock()
|
|
702
|
+
|
|
703
|
+
result = p4client.get_p4_setting("P4USER")
|
|
704
|
+
|
|
705
|
+
assert result == "niels.vaes"
|
|
706
|
+
mock_check_output.assert_not_called()
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def test_run_cmd_use_global_options_deprecation_alias(p4client):
|
|
710
|
+
"""The old `use_global_options` kwarg is accepted but emits DeprecationWarning."""
|
|
711
|
+
import io as _io
|
|
712
|
+
import warnings as _warnings
|
|
713
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
714
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
715
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
716
|
+
mock_popen.return_value.stdout = _io.BytesIO(b"P4USER=niels.vaes (set)\n")
|
|
717
|
+
mock_popen.return_value.kill = MagicMock()
|
|
718
|
+
|
|
719
|
+
with _warnings.catch_warnings(record=True) as caught:
|
|
720
|
+
_warnings.simplefilter("always")
|
|
721
|
+
result = p4client.run_cmd("set", ["P4USER"], use_global_options=False, online_check=False)
|
|
722
|
+
|
|
723
|
+
assert result[0]["raw_output"] == b"P4USER=niels.vaes (set)\n"
|
|
724
|
+
assert any(issubclass(w.category, DeprecationWarning) and "use_global_options" in str(w.message)
|
|
725
|
+
for w in caught), f"expected DeprecationWarning for use_global_options, got: {[str(w.message) for w in caught]}"
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def test_run_cmd_unknown_kwarg_raises(p4client):
|
|
729
|
+
"""Unknown kwargs should raise TypeError, not be silently accepted."""
|
|
730
|
+
with pytest.raises(TypeError, match="unexpected keyword arguments"):
|
|
731
|
+
p4client.run_cmd("set", ["P4USER"], totally_made_up=True, online_check=False)
|
|
732
|
+
|
|
733
|
+
|
|
688
734
|
def test_get_local_paths_missing_path(p4client):
|
|
689
735
|
"""get_local_paths skips entries where path is missing (e.g. unmapped paths from p4 where)."""
|
|
690
736
|
info_dicts = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|