p4cmd 2.10.2__tar.gz → 2.11.0__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.11.0}/PKG-INFO +31 -1
- {p4cmd-2.10.2 → p4cmd-2.11.0}/README.md +30 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/__init__.py +1 -1
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/p4cmd.py +80 -125
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/utils.py +5 -3
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd.egg-info/PKG-INFO +31 -1
- {p4cmd-2.10.2 → p4cmd-2.11.0}/tests/test_p4client.py +101 -6
- {p4cmd-2.10.2 → p4cmd-2.11.0}/LICENSE +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/p4errors.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/p4file.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/__init__.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_MOVED.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd.egg-info/SOURCES.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd.egg-info/dependency_links.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd.egg-info/requires.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/p4cmd.egg-info/top_level.txt +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/setup.cfg +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/setup.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/tests/__init__.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/tests/conftest.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/tests/test_integration.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/tests/test_p4file.py +0 -0
- {p4cmd-2.10.2 → p4cmd-2.11.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: p4cmd
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0
|
|
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
|
|
|
@@ -57,6 +60,33 @@ p4 = p4cmd.P4Client.from_env()
|
|
|
57
60
|
|
|
58
61
|
If the path you pass doesn't contain a `.p4config` file, P4Client will walk up the directory tree until it finds one and use that as `perforce_root`.
|
|
59
62
|
|
|
63
|
+
### Quieting log output
|
|
64
|
+
|
|
65
|
+
`p4cmd` logs warnings and errors through the standard `logging` module under the `p4cmd` logger. Setting the level on the `p4cmd` parent flows down to all submodules (`p4cmd.p4cmd`, `p4cmd.utils`).
|
|
66
|
+
|
|
67
|
+
**Drop everything except criticals** — matches the old `silent=True` behavior:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import logging
|
|
71
|
+
logging.getLogger("p4cmd").setLevel(logging.CRITICAL)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Drop literally everything from p4cmd** (including criticals):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
logging.getLogger("p4cmd").disabled = True
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Keep the messages but route them somewhere other than stderr** — e.g. a log file:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
p4_logger = logging.getLogger("p4cmd")
|
|
84
|
+
p4_logger.propagate = False # don't bubble to root/stderr
|
|
85
|
+
p4_logger.addHandler(logging.FileHandler("p4cmd.log"))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The old `silent=True` constructor kwarg is deprecated and now a no-op — configure the logger instead.
|
|
89
|
+
|
|
60
90
|
## P4Client API
|
|
61
91
|
|
|
62
92
|
### Connection & settings
|
|
@@ -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
|
|
|
@@ -33,6 +36,33 @@ p4 = p4cmd.P4Client.from_env()
|
|
|
33
36
|
|
|
34
37
|
If the path you pass doesn't contain a `.p4config` file, P4Client will walk up the directory tree until it finds one and use that as `perforce_root`.
|
|
35
38
|
|
|
39
|
+
### Quieting log output
|
|
40
|
+
|
|
41
|
+
`p4cmd` logs warnings and errors through the standard `logging` module under the `p4cmd` logger. Setting the level on the `p4cmd` parent flows down to all submodules (`p4cmd.p4cmd`, `p4cmd.utils`).
|
|
42
|
+
|
|
43
|
+
**Drop everything except criticals** — matches the old `silent=True` behavior:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import logging
|
|
47
|
+
logging.getLogger("p4cmd").setLevel(logging.CRITICAL)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Drop literally everything from p4cmd** (including criticals):
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
logging.getLogger("p4cmd").disabled = True
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Keep the messages but route them somewhere other than stderr** — e.g. a log file:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
p4_logger = logging.getLogger("p4cmd")
|
|
60
|
+
p4_logger.propagate = False # don't bubble to root/stderr
|
|
61
|
+
p4_logger.addHandler(logging.FileHandler("p4cmd.log"))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The old `silent=True` constructor kwarg is deprecated and now a no-op — configure the logger instead.
|
|
65
|
+
|
|
36
66
|
## P4Client API
|
|
37
67
|
|
|
38
68
|
### Connection & settings
|
|
@@ -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
|
|
@@ -10,24 +11,34 @@ from . import p4errors
|
|
|
10
11
|
from .p4file import P4File, Status
|
|
11
12
|
from .utils import split_list_into_strings_of_length, convert_to_list, decode_dictionaries, validate_not_empty
|
|
12
13
|
|
|
14
|
+
_log = logging.getLogger(__name__)
|
|
15
|
+
|
|
13
16
|
MAX_CMD_LEN = 8190
|
|
14
17
|
MAX_ARG_LEN = 8000 # max length of args string when combined, close to max, but leaving some extra margin
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class P4Client(object):
|
|
18
|
-
def __init__(self, perforce_root, user=None, client=None, server=None, silent=
|
|
21
|
+
def __init__(self, perforce_root, user=None, client=None, server=None, silent=None, max_parallel_connections=4):
|
|
19
22
|
"""
|
|
20
23
|
Make a new P4Client
|
|
21
24
|
|
|
22
25
|
:param perforce_root: *string* root of your Perforce workspace. This would also be where your .p4config file is
|
|
23
26
|
:param user: *string* P4USER, if None will be tried to be found automatically
|
|
24
27
|
:param client: *string* P4CLIENT, if None will be tried to be found automatically
|
|
25
|
-
:param silent: *bool* if True, suppresses error messages to cut down on terminal spam
|
|
26
28
|
:param max_parallel_connections: *int* max number of connections to use while syncing/submitting. This requires
|
|
27
29
|
the server to have net.parallel.max and net parallel.threads to be > 1
|
|
30
|
+
:param silent: *deprecated* — no-op. To quiet output, configure the ``p4cmd`` logger, e.g.
|
|
31
|
+
``logging.getLogger("p4cmd").setLevel(logging.CRITICAL)``.
|
|
28
32
|
"""
|
|
33
|
+
if silent is not None:
|
|
34
|
+
warnings.warn(
|
|
35
|
+
"`silent` is deprecated and has no effect; configure the `p4cmd` logger "
|
|
36
|
+
"instead, e.g. `logging.getLogger('p4cmd').setLevel(logging.CRITICAL)`.",
|
|
37
|
+
DeprecationWarning,
|
|
38
|
+
stacklevel=2,
|
|
39
|
+
)
|
|
40
|
+
|
|
29
41
|
self.perforce_root = perforce_root
|
|
30
|
-
self.silent = silent
|
|
31
42
|
|
|
32
43
|
self.max_parallel_connections = max_parallel_connections
|
|
33
44
|
self.user = user
|
|
@@ -35,8 +46,7 @@ class P4Client(object):
|
|
|
35
46
|
self.server = server
|
|
36
47
|
|
|
37
48
|
if not self.__p4config_exists():
|
|
38
|
-
|
|
39
|
-
logging.warning("No .p4config file found in %s!" % self.perforce_root)
|
|
49
|
+
_log.warning("No .p4config file found in %s!" % self.perforce_root)
|
|
40
50
|
|
|
41
51
|
if user is None:
|
|
42
52
|
self.user = self.get_p4_setting("P4USER")
|
|
@@ -84,18 +94,35 @@ class P4Client(object):
|
|
|
84
94
|
"""
|
|
85
95
|
self.max_parallel_connections = value
|
|
86
96
|
|
|
87
|
-
def run_cmd(self, cmd, args=None, file_list=None,
|
|
97
|
+
def run_cmd(self, cmd, args=None, file_list=None, marshal_output=True, online_check=True, raise_on_errors=False, **kwargs):
|
|
88
98
|
"""
|
|
89
|
-
Reads the output stream of the command and returns it as a
|
|
99
|
+
Reads the output stream of the command and returns it as a list of dicts.
|
|
90
100
|
|
|
91
101
|
:param cmd: *string* p4 command like "change", "reopen", "move"
|
|
92
102
|
:param args: *list* of string arguments like ["-c", "27277"]
|
|
93
103
|
:param file_list: *list* of string arguments like ["//depot/folder/file.atom", "D:/Games/Whatever.fbx]
|
|
94
|
-
:param
|
|
104
|
+
:param marshal_output: *bool* when True (default), passes ``-G`` so p4 emits Python marshal-format
|
|
105
|
+
output that we parse into dicts, and also passes ``-u <user> -c <client>`` for identity. Set to
|
|
106
|
+
False for commands that don't honor ``-G`` (notably ``p4 set``, which only reads local settings)
|
|
107
|
+
or when user/client haven't been resolved yet — output is then returned verbatim under the
|
|
108
|
+
``raw_output`` key.
|
|
95
109
|
:param online_check: *bool* if set to True, will first check if the remote server is reachable before executing the command.
|
|
96
110
|
:return: *list* of dictionaries with either the marshaled returns of the command or dictionaries with the
|
|
97
111
|
raw output of the command
|
|
98
112
|
"""
|
|
113
|
+
# Backwards compat: the old name `use_global_options` was misleading — it sounded like a master
|
|
114
|
+
# switch for p4's global-option family, when it really just toggles `-G` (marshal output) plus the
|
|
115
|
+
# bundled `-u`/`-c` identity flags. Renamed to `marshal_output` to point at the actual consequence
|
|
116
|
+
# (the parsing path); old kwarg kept as a deprecated alias.
|
|
117
|
+
if "use_global_options" in kwargs:
|
|
118
|
+
warnings.warn(
|
|
119
|
+
"`use_global_options` is deprecated; use `marshal_output` instead.",
|
|
120
|
+
DeprecationWarning,
|
|
121
|
+
stacklevel=2,
|
|
122
|
+
)
|
|
123
|
+
marshal_output = kwargs.pop("use_global_options")
|
|
124
|
+
if kwargs:
|
|
125
|
+
raise TypeError(f"run_cmd() got unexpected keyword arguments: {list(kwargs)}")
|
|
99
126
|
|
|
100
127
|
if not self.perforce_root:
|
|
101
128
|
raise p4errors.WorkSpaceError(f"self.perforce_root (value: {self.perforce_root}) is not set!")
|
|
@@ -117,32 +144,43 @@ class P4Client(object):
|
|
|
117
144
|
dict_list = []
|
|
118
145
|
for clamped_arg in clamped_arg_list:
|
|
119
146
|
for clamped_files in clamped_file_list:
|
|
120
|
-
if
|
|
147
|
+
if marshal_output:
|
|
121
148
|
command = f"p4 -G -u {self.user} -c {self.client} {cmd} {clamped_arg} {clamped_files}"
|
|
122
149
|
else:
|
|
123
150
|
command = f"p4 {cmd} {clamped_arg} {clamped_files}"
|
|
124
151
|
|
|
125
152
|
if len(command) > MAX_CMD_LEN:
|
|
126
153
|
# This shouldn't happen, but just in case the command prefix end up really long
|
|
127
|
-
|
|
154
|
+
_log.warning(
|
|
128
155
|
f"Command length: {format(len(command))} exceeds MAX_CMD_LEN {MAX_CMD_LEN} on command: {MAX_CMD_LEN}")
|
|
129
156
|
|
|
130
157
|
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True, cwd=self.perforce_root) as pipe:
|
|
131
158
|
output = pipe.stdout
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
except ValueError as error:
|
|
139
|
-
output_dict = {
|
|
159
|
+
if not marshal_output:
|
|
160
|
+
# Without -G, p4 emits plain text (and `p4 set` ignores -G even when passed),
|
|
161
|
+
# so marshal.load is guaranteed to fail. Read stdout directly from this Popen
|
|
162
|
+
# — it has stdin=PIPE, so it's safe in non-interactive sessions where the old
|
|
163
|
+
# check_output fallback hit OSError(WinError 50) on DuplicateHandle.
|
|
164
|
+
dict_list.append({
|
|
140
165
|
"command": command,
|
|
141
166
|
"code": "raw",
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
"raw_output": output.read(),
|
|
168
|
+
})
|
|
169
|
+
else:
|
|
170
|
+
try:
|
|
171
|
+
while True:
|
|
172
|
+
value_dict = marshal.load(output)
|
|
173
|
+
dict_list.append(value_dict)
|
|
174
|
+
except EOFError:
|
|
175
|
+
pass
|
|
176
|
+
except ValueError as error:
|
|
177
|
+
output_dict = {
|
|
178
|
+
"command": command,
|
|
179
|
+
"code": "raw",
|
|
180
|
+
"error": str(error),
|
|
181
|
+
"raw_output": subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True, cwd=self.perforce_root)
|
|
182
|
+
}
|
|
183
|
+
dict_list.append(output_dict)
|
|
146
184
|
pipe.kill()
|
|
147
185
|
|
|
148
186
|
# Check for error dicts in the results
|
|
@@ -155,7 +193,7 @@ class P4Client(object):
|
|
|
155
193
|
msg = msg.decode("utf-8", errors="replace")
|
|
156
194
|
messages.append(msg.strip())
|
|
157
195
|
for msg in messages:
|
|
158
|
-
|
|
196
|
+
_log.warning(f"p4 {cmd}: {msg}")
|
|
159
197
|
if raise_on_errors:
|
|
160
198
|
raise p4errors.P4CommandError(cmd, messages)
|
|
161
199
|
|
|
@@ -218,7 +256,7 @@ class P4Client(object):
|
|
|
218
256
|
"""
|
|
219
257
|
try:
|
|
220
258
|
# skipping the online check for setting commands
|
|
221
|
-
info_dict = self.run_cmd("set", [setting],
|
|
259
|
+
info_dict = self.run_cmd("set", [setting], marshal_output=False, online_check=False)[0]
|
|
222
260
|
except Exception as e:
|
|
223
261
|
raise p4errors.WorkSpaceError("Unable to find setting %s" % setting) from e
|
|
224
262
|
|
|
@@ -367,12 +405,12 @@ class P4Client(object):
|
|
|
367
405
|
|
|
368
406
|
parts = output.split(" ")
|
|
369
407
|
if len(parts) < 2:
|
|
370
|
-
|
|
408
|
+
_log.error(f"Unexpected output from p4 change: {output!r}")
|
|
371
409
|
return None
|
|
372
410
|
try:
|
|
373
411
|
return int(parts[1])
|
|
374
412
|
except ValueError:
|
|
375
|
-
|
|
413
|
+
_log.error(f"Could not parse changelist number from: {output!r}")
|
|
376
414
|
return None
|
|
377
415
|
|
|
378
416
|
def changelist_exists(self, changelist):
|
|
@@ -420,14 +458,10 @@ class P4Client(object):
|
|
|
420
458
|
|
|
421
459
|
return True
|
|
422
460
|
except subprocess.CalledProcessError as e:
|
|
423
|
-
|
|
424
|
-
if not self.silent:
|
|
425
|
-
logging.error(f"Failed to update changelist description: {e}")
|
|
461
|
+
_log.error(f"Failed to update changelist description: {e}")
|
|
426
462
|
return False
|
|
427
463
|
except OSError as e:
|
|
428
|
-
|
|
429
|
-
if not self.silent:
|
|
430
|
-
logging.error(f"Failed to update changelist description: {e}")
|
|
464
|
+
_log.error(f"Failed to update changelist description: {e}")
|
|
431
465
|
return False
|
|
432
466
|
|
|
433
467
|
@validate_not_empty
|
|
@@ -442,11 +476,6 @@ class P4Client(object):
|
|
|
442
476
|
file_list = convert_to_list(file_list)
|
|
443
477
|
changelist = self.__ensure_changelist(changelist)
|
|
444
478
|
info_dicts = self.run_cmd("reopen", args=["-c", changelist], file_list=file_list)
|
|
445
|
-
|
|
446
|
-
for info_dict in info_dicts:
|
|
447
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
448
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
449
|
-
|
|
450
479
|
return info_dicts
|
|
451
480
|
|
|
452
481
|
def combine_changelists(self, source_changelists, target_changelist):
|
|
@@ -510,8 +539,6 @@ class P4Client(object):
|
|
|
510
539
|
:return: *list* of info dictionaries
|
|
511
540
|
"""
|
|
512
541
|
file_list = convert_to_list(file_list)
|
|
513
|
-
if not self.silent:
|
|
514
|
-
self.__validate_file_list(file_list)
|
|
515
542
|
if unchanged_only:
|
|
516
543
|
info_dicts = self.run_cmd("revert", ["-a"], file_list=file_list)
|
|
517
544
|
else:
|
|
@@ -534,9 +561,6 @@ class P4Client(object):
|
|
|
534
561
|
:return: *list* of info dicts
|
|
535
562
|
"""
|
|
536
563
|
folder_list = convert_to_list(folder_list)
|
|
537
|
-
if not self.silent:
|
|
538
|
-
self.__validate_file_list(folder_list)
|
|
539
|
-
|
|
540
564
|
cleaned_folder_list = [self._normalize_folder(f) for f in folder_list]
|
|
541
565
|
|
|
542
566
|
if unchanged_only:
|
|
@@ -583,9 +607,6 @@ class P4Client(object):
|
|
|
583
607
|
:return: *list* of info dicts
|
|
584
608
|
"""
|
|
585
609
|
folder_list = convert_to_list(folder_list)
|
|
586
|
-
if not self.silent:
|
|
587
|
-
self.__validate_file_list(folder_list)
|
|
588
|
-
|
|
589
610
|
cleaned_folder_list = [self._normalize_folder(f) for f in folder_list]
|
|
590
611
|
|
|
591
612
|
info_dicts = self.run_cmd("sync", args=["--parallel", f"threads={self.max_parallel_connections}"],
|
|
@@ -611,9 +632,6 @@ class P4Client(object):
|
|
|
611
632
|
|
|
612
633
|
initial_arg_list = ["-f", "--parallel", f"threads={self.max_parallel_connections}"] if force else ["--parallel",
|
|
613
634
|
f"threads={self.max_parallel_connections}"]
|
|
614
|
-
if not self.silent:
|
|
615
|
-
self.__validate_file_list(file_list)
|
|
616
|
-
|
|
617
635
|
info_dicts = self.run_cmd("sync", args=initial_arg_list, file_list=file_list)
|
|
618
636
|
|
|
619
637
|
if verify:
|
|
@@ -622,7 +640,7 @@ class P4Client(object):
|
|
|
622
640
|
if local_file_path is None:
|
|
623
641
|
continue
|
|
624
642
|
if not os.path.isfile(local_file_path):
|
|
625
|
-
|
|
643
|
+
_log.warning(f"File didn't exist after syncing, try force syncing it instead: {local_file_path}")
|
|
626
644
|
|
|
627
645
|
return info_dicts
|
|
628
646
|
|
|
@@ -672,8 +690,6 @@ class P4Client(object):
|
|
|
672
690
|
:return: *list* of info dicts
|
|
673
691
|
"""
|
|
674
692
|
folder_list = convert_to_list(folder_list)
|
|
675
|
-
if not self.silent:
|
|
676
|
-
self.__validate_file_list(folder_list)
|
|
677
693
|
cleaned_folder_list = [self._normalize_folder(f) for f in folder_list]
|
|
678
694
|
changelist = self.__ensure_changelist(changelist)
|
|
679
695
|
args = self._build_reconcile_args(changelist, add, edit, delete)
|
|
@@ -691,9 +707,6 @@ class P4Client(object):
|
|
|
691
707
|
:return: *list* of info dictionaries
|
|
692
708
|
"""
|
|
693
709
|
file_list = convert_to_list(file_list)
|
|
694
|
-
if not self.silent:
|
|
695
|
-
self.__validate_file_list(file_list)
|
|
696
|
-
|
|
697
710
|
changelist = self.__ensure_changelist(changelist)
|
|
698
711
|
|
|
699
712
|
info_dicts = self.run_cmd("delete", args=["-c", changelist], file_list=file_list)
|
|
@@ -761,13 +774,10 @@ class P4Client(object):
|
|
|
761
774
|
file_list = self.get_files_in_changelist(changelist)
|
|
762
775
|
else:
|
|
763
776
|
file_list = convert_to_list(file_list)
|
|
764
|
-
if not self.silent:
|
|
765
|
-
self.__validate_file_list(file_list)
|
|
766
777
|
|
|
767
778
|
# if there are no files to shelve, return empty list
|
|
768
779
|
if not file_list:
|
|
769
|
-
|
|
770
|
-
logging.warning(f"No files to shelve in changelist {changelist}")
|
|
780
|
+
_log.warning(f"No files to shelve in changelist {changelist}")
|
|
771
781
|
return []
|
|
772
782
|
|
|
773
783
|
# iuild args list based on options
|
|
@@ -778,10 +788,6 @@ class P4Client(object):
|
|
|
778
788
|
# run the shelve command
|
|
779
789
|
info_dicts = self.run_cmd("shelve", args=args, file_list=file_list)
|
|
780
790
|
|
|
781
|
-
for info_dict in info_dicts:
|
|
782
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
783
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
784
|
-
|
|
785
791
|
# revert files if requested
|
|
786
792
|
if revert_after_shelve and file_list:
|
|
787
793
|
revert_info_dicts = self.revert_files(file_list)
|
|
@@ -822,35 +828,20 @@ class P4Client(object):
|
|
|
822
828
|
# prepare file list if provided
|
|
823
829
|
if file_list is not None:
|
|
824
830
|
file_list = convert_to_list(file_list)
|
|
825
|
-
if not self.silent:
|
|
826
|
-
self.__validate_file_list(file_list)
|
|
827
831
|
else:
|
|
828
832
|
file_list = []
|
|
829
833
|
|
|
830
834
|
# run the unshelve command
|
|
831
835
|
info_dicts = self.run_cmd("unshelve", args=args, file_list=file_list)
|
|
832
836
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
837
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
838
|
-
has_error = True
|
|
837
|
+
has_error = any(
|
|
838
|
+
self.__get_dict_value(d, "code") == "error" for d in info_dicts
|
|
839
|
+
)
|
|
839
840
|
|
|
840
841
|
# delete shelved files if requested and unshelve was successful
|
|
841
842
|
if delete_shelved_files and not has_error:
|
|
842
|
-
# build the arguments for the shelve -d command
|
|
843
843
|
delete_args = ["-d", "-c", str(source_changelist)]
|
|
844
|
-
|
|
845
|
-
# if file_list provided, use it for the delete operation as well
|
|
846
844
|
delete_info_dicts = self.run_cmd("shelve", args=delete_args, file_list=file_list)
|
|
847
|
-
|
|
848
|
-
# check for errors in the delete operation
|
|
849
|
-
for info_dict in delete_info_dicts:
|
|
850
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
851
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
852
|
-
|
|
853
|
-
# add the delete info to the return data
|
|
854
845
|
info_dicts.extend(delete_info_dicts)
|
|
855
846
|
|
|
856
847
|
return info_dicts
|
|
@@ -868,8 +859,7 @@ class P4Client(object):
|
|
|
868
859
|
|
|
869
860
|
# Don't allow deleting shelves from the default changelist
|
|
870
861
|
if changelist == "default":
|
|
871
|
-
|
|
872
|
-
logging.error("Cannot delete shelved files from the default changelist")
|
|
862
|
+
_log.error("Cannot delete shelved files from the default changelist")
|
|
873
863
|
return [{"code": "error", "data": "Cannot delete shelved files from the default changelist"}]
|
|
874
864
|
|
|
875
865
|
# Check if the changelist has shelved files
|
|
@@ -884,8 +874,7 @@ class P4Client(object):
|
|
|
884
874
|
break
|
|
885
875
|
|
|
886
876
|
if not has_shelved_files:
|
|
887
|
-
|
|
888
|
-
logging.warning(f"No shelved files found in changelist {changelist}")
|
|
877
|
+
_log.warning(f"No shelved files found in changelist {changelist}")
|
|
889
878
|
return [{"code": "info", "data": f"No shelved files found in changelist {changelist}"}]
|
|
890
879
|
|
|
891
880
|
# Delete all shelved files in the changelist
|
|
@@ -893,14 +882,10 @@ class P4Client(object):
|
|
|
893
882
|
return info_dicts
|
|
894
883
|
|
|
895
884
|
except subprocess.CalledProcessError as e:
|
|
896
|
-
|
|
897
|
-
if not self.silent:
|
|
898
|
-
logging.error(f"Failed to delete shelved files: {e}")
|
|
885
|
+
_log.error(f"Failed to delete shelved files: {e}")
|
|
899
886
|
return [{"code": "error", "data": str(e)}]
|
|
900
887
|
except OSError as e:
|
|
901
|
-
|
|
902
|
-
if not self.silent:
|
|
903
|
-
logging.error(f"Failed to delete shelved files: {e}")
|
|
888
|
+
_log.error(f"Failed to delete shelved files: {e}")
|
|
904
889
|
return [{"code": "error", "data": str(e)}]
|
|
905
890
|
|
|
906
891
|
@validate_not_empty
|
|
@@ -938,8 +923,6 @@ class P4Client(object):
|
|
|
938
923
|
:return: *list* of info dictionaries
|
|
939
924
|
"""
|
|
940
925
|
file_list = convert_to_list(file_list)
|
|
941
|
-
if not self.silent:
|
|
942
|
-
self.__validate_file_list(file_list)
|
|
943
926
|
|
|
944
927
|
files_for_add = []
|
|
945
928
|
files_for_checkout = []
|
|
@@ -985,15 +968,9 @@ class P4Client(object):
|
|
|
985
968
|
:return: *list* of info dictionaries
|
|
986
969
|
"""
|
|
987
970
|
file_list = convert_to_list(file_list)
|
|
988
|
-
if not self.silent:
|
|
989
|
-
self.__validate_file_list(file_list)
|
|
990
|
-
|
|
991
971
|
changelist = self.__ensure_changelist(changelist)
|
|
992
972
|
|
|
993
973
|
info_dicts = self.run_cmd("edit", args=["-c", changelist], file_list=file_list)
|
|
994
|
-
for info_dict in info_dicts:
|
|
995
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
996
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
997
974
|
return info_dicts
|
|
998
975
|
|
|
999
976
|
@validate_not_empty
|
|
@@ -1005,9 +982,6 @@ class P4Client(object):
|
|
|
1005
982
|
:return: *list* of info dictionaries
|
|
1006
983
|
"""
|
|
1007
984
|
file_list = convert_to_list(file_list)
|
|
1008
|
-
if not self.silent:
|
|
1009
|
-
self.__validate_file_list(file_list)
|
|
1010
|
-
|
|
1011
985
|
changelist = self.__ensure_changelist(changelist)
|
|
1012
986
|
|
|
1013
987
|
info_dicts = self.run_cmd("add", args=["-c", changelist], file_list=file_list)
|
|
@@ -1071,7 +1045,7 @@ class P4Client(object):
|
|
|
1071
1045
|
cl_description = desc_value.rstrip("\n")
|
|
1072
1046
|
|
|
1073
1047
|
if not cl_description:
|
|
1074
|
-
|
|
1048
|
+
_log.warning(f"The CL description is empty in this return object!\n{pformat(info_dict)}")
|
|
1075
1049
|
|
|
1076
1050
|
if not case_sensitive:
|
|
1077
1051
|
cl_description = cl_description.lower()
|
|
@@ -1211,13 +1185,13 @@ class P4Client(object):
|
|
|
1211
1185
|
with socket.create_connection((host, port), timeout=timeout):
|
|
1212
1186
|
return True
|
|
1213
1187
|
except socket.timeout:
|
|
1214
|
-
|
|
1188
|
+
_log.warning(f"Connection to {host} on port {port} timed out after {timeout} seconds, the running command might not complete correctly")
|
|
1215
1189
|
return False
|
|
1216
1190
|
except ConnectionRefusedError:
|
|
1217
|
-
|
|
1191
|
+
_log.warning(f"Connection to {host} on port {port} refused to connect, is the server still up?")
|
|
1218
1192
|
return False
|
|
1219
1193
|
except OSError as e:
|
|
1220
|
-
|
|
1194
|
+
_log.warning(f"oserror: {e.strerror or e}")
|
|
1221
1195
|
return False
|
|
1222
1196
|
|
|
1223
1197
|
def __server_address(self):
|
|
@@ -1344,7 +1318,7 @@ class P4Client(object):
|
|
|
1344
1318
|
while last_dir != current_dir:
|
|
1345
1319
|
if os.path.exists(current_dir):
|
|
1346
1320
|
if ".p4config" in os.listdir(current_dir):
|
|
1347
|
-
|
|
1321
|
+
_log.info(".p4config found in %s" % current_dir)
|
|
1348
1322
|
self.perforce_root = current_dir
|
|
1349
1323
|
return True
|
|
1350
1324
|
|
|
@@ -1352,22 +1326,3 @@ class P4Client(object):
|
|
|
1352
1326
|
current_dir = os.path.dirname(last_dir)
|
|
1353
1327
|
return False
|
|
1354
1328
|
|
|
1355
|
-
def __validate_file_list(self, file_list):
|
|
1356
|
-
"""
|
|
1357
|
-
Validation function to ensure correct files are being synced to correct workspaces & clients etc.
|
|
1358
|
-
Is an extendable function
|
|
1359
|
-
:param file_list: List of files to iterate
|
|
1360
|
-
:return:
|
|
1361
|
-
"""
|
|
1362
|
-
if not self.perforce_root:
|
|
1363
|
-
raise p4errors.WorkSpaceError(f"self.perforce_root (value: {self.perforce_root}) is not set!")
|
|
1364
|
-
|
|
1365
|
-
file_list = convert_to_list(file_list)
|
|
1366
|
-
|
|
1367
|
-
# Easy utility to check that the file is underneath the correct perforce root
|
|
1368
|
-
# Quicker than waiting for the result of a p4 fstat
|
|
1369
|
-
for f in file_list:
|
|
1370
|
-
if not f.lower().startswith(self.perforce_root.lower()) and not f.lower().startswith(
|
|
1371
|
-
self.depot_root.lower()):
|
|
1372
|
-
raise Exception(f'{f} is not under perforce root: {self.perforce_root}, {self.depot_root}')
|
|
1373
|
-
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import functools
|
|
3
3
|
|
|
4
|
+
_log = logging.getLogger(__name__)
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
def split_list_into_strings_of_length(input_list, max_length=100):
|
|
6
8
|
"""
|
|
@@ -66,9 +68,9 @@ def validate_not_empty(func):
|
|
|
66
68
|
file_or_folder_list = convert_to_list(file_or_folder_list)
|
|
67
69
|
|
|
68
70
|
if not file_or_folder_list:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
_log.warning(f"Empty file list provided to {func.__name__}, operation skipped.")
|
|
72
|
+
_log.warning(f"args: {args}")
|
|
73
|
+
_log.warning(f"kwargs: {kwargs}")
|
|
72
74
|
return []
|
|
73
75
|
|
|
74
76
|
return func(*args, **kwargs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: p4cmd
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0
|
|
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
|
|
|
@@ -57,6 +60,33 @@ p4 = p4cmd.P4Client.from_env()
|
|
|
57
60
|
|
|
58
61
|
If the path you pass doesn't contain a `.p4config` file, P4Client will walk up the directory tree until it finds one and use that as `perforce_root`.
|
|
59
62
|
|
|
63
|
+
### Quieting log output
|
|
64
|
+
|
|
65
|
+
`p4cmd` logs warnings and errors through the standard `logging` module under the `p4cmd` logger. Setting the level on the `p4cmd` parent flows down to all submodules (`p4cmd.p4cmd`, `p4cmd.utils`).
|
|
66
|
+
|
|
67
|
+
**Drop everything except criticals** — matches the old `silent=True` behavior:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import logging
|
|
71
|
+
logging.getLogger("p4cmd").setLevel(logging.CRITICAL)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Drop literally everything from p4cmd** (including criticals):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
logging.getLogger("p4cmd").disabled = True
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Keep the messages but route them somewhere other than stderr** — e.g. a log file:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
p4_logger = logging.getLogger("p4cmd")
|
|
84
|
+
p4_logger.propagate = False # don't bubble to root/stderr
|
|
85
|
+
p4_logger.addHandler(logging.FileHandler("p4cmd.log"))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The old `silent=True` constructor kwarg is deprecated and now a no-op — configure the logger instead.
|
|
89
|
+
|
|
60
90
|
## P4Client API
|
|
61
91
|
|
|
62
92
|
### Connection & settings
|
|
@@ -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 = [
|
|
@@ -696,3 +742,52 @@ def test_get_local_paths_missing_path(p4client):
|
|
|
696
742
|
["//depot/project/file.txt", "//depot/project/unmapped.txt"]
|
|
697
743
|
)
|
|
698
744
|
assert result == ["C:/workspace/file.txt"]
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# ---------------------------------------------------------------------------
|
|
748
|
+
# Regression: issue #14 — files mapped from outside perforce_root/depot_root
|
|
749
|
+
# must not raise on edit (previously failed when silent=False due to a buggy
|
|
750
|
+
# prefix-based pre-validation that ignored multi-mapping client views).
|
|
751
|
+
# ---------------------------------------------------------------------------
|
|
752
|
+
|
|
753
|
+
def test_edit_files_accepts_paths_outside_perforce_root_prefix(p4client):
|
|
754
|
+
"""A file mapped from a different depot path (e.g. shared Raw assets) must
|
|
755
|
+
not be rejected before p4 sees it. perforce_root is /fake/project and
|
|
756
|
+
depot_root is //depot/project — neither prefix matches this file."""
|
|
757
|
+
outside_file = "//depot/shared_audio/Raw/voice_line.wav"
|
|
758
|
+
with patch.object(p4client, "_P4Client__ensure_changelist", return_value="default"):
|
|
759
|
+
with patch.object(p4client, "run_cmd", return_value=[]) as mock_cmd:
|
|
760
|
+
p4client.edit_files([outside_file])
|
|
761
|
+
mock_cmd.assert_called_once()
|
|
762
|
+
args, kwargs = mock_cmd.call_args
|
|
763
|
+
assert outside_file in kwargs["file_list"]
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
# ---------------------------------------------------------------------------
|
|
767
|
+
# silent kwarg — deprecated
|
|
768
|
+
# ---------------------------------------------------------------------------
|
|
769
|
+
|
|
770
|
+
def test_silent_kwarg_emits_deprecation_warning(tmp_path):
|
|
771
|
+
"""Passing silent= (True or False) emits DeprecationWarning."""
|
|
772
|
+
import warnings as _warnings
|
|
773
|
+
(tmp_path / ".p4config").write_text("P4PORT=ssl:example:1666\n")
|
|
774
|
+
with patch.object(P4Client, "host_online", return_value=True):
|
|
775
|
+
with patch.object(P4Client, "run_cmd", return_value=[{b"depotFile": b"//depot/x/...", b"path": b"/fake/...", b"unmap": b""}]):
|
|
776
|
+
with _warnings.catch_warnings(record=True) as caught:
|
|
777
|
+
_warnings.simplefilter("always")
|
|
778
|
+
P4Client(str(tmp_path), user="u", client="c", server="s", silent=True)
|
|
779
|
+
assert any(issubclass(w.category, DeprecationWarning) and "silent" in str(w.message)
|
|
780
|
+
for w in caught), f"expected DeprecationWarning, got: {[str(w.message) for w in caught]}"
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def test_silent_kwarg_default_no_warning(tmp_path):
|
|
784
|
+
"""Not passing silent at all must not emit a DeprecationWarning."""
|
|
785
|
+
import warnings as _warnings
|
|
786
|
+
(tmp_path / ".p4config").write_text("P4PORT=ssl:example:1666\n")
|
|
787
|
+
with patch.object(P4Client, "host_online", return_value=True):
|
|
788
|
+
with patch.object(P4Client, "run_cmd", return_value=[{b"depotFile": b"//depot/x/...", b"path": b"/fake/...", b"unmap": b""}]):
|
|
789
|
+
with _warnings.catch_warnings(record=True) as caught:
|
|
790
|
+
_warnings.simplefilter("always")
|
|
791
|
+
P4Client(str(tmp_path), user="u", client="c", server="s")
|
|
792
|
+
assert not any(issubclass(w.category, DeprecationWarning) and "silent" in str(w.message)
|
|
793
|
+
for w in caught), f"unexpected silent warning: {[str(w.message) for w in caught]}"
|
|
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
|