p4cmd 2.10.3__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.3 → p4cmd-2.11.0}/PKG-INFO +28 -1
- {p4cmd-2.10.3 → p4cmd-2.11.0}/README.md +27 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/__init__.py +1 -1
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/p4cmd.py +34 -108
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/utils.py +5 -3
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/PKG-INFO +28 -1
- {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/test_p4client.py +49 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/LICENSE +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/p4errors.py +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/p4file.py +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/__init__.py +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_MOVED.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/SOURCES.txt +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/dependency_links.txt +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/requires.txt +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/top_level.txt +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/setup.cfg +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/setup.py +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/__init__.py +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/conftest.py +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/test_integration.py +0 -0
- {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/test_p4file.py +0 -0
- {p4cmd-2.10.3 → 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
|
|
@@ -60,6 +60,33 @@ p4 = p4cmd.P4Client.from_env()
|
|
|
60
60
|
|
|
61
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`.
|
|
62
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
|
+
|
|
63
90
|
## P4Client API
|
|
64
91
|
|
|
65
92
|
### Connection & settings
|
|
@@ -36,6 +36,33 @@ p4 = p4cmd.P4Client.from_env()
|
|
|
36
36
|
|
|
37
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`.
|
|
38
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
|
+
|
|
39
66
|
## P4Client API
|
|
40
67
|
|
|
41
68
|
### Connection & settings
|
|
@@ -11,24 +11,34 @@ from . import p4errors
|
|
|
11
11
|
from .p4file import P4File, Status
|
|
12
12
|
from .utils import split_list_into_strings_of_length, convert_to_list, decode_dictionaries, validate_not_empty
|
|
13
13
|
|
|
14
|
+
_log = logging.getLogger(__name__)
|
|
15
|
+
|
|
14
16
|
MAX_CMD_LEN = 8190
|
|
15
17
|
MAX_ARG_LEN = 8000 # max length of args string when combined, close to max, but leaving some extra margin
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class P4Client(object):
|
|
19
|
-
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):
|
|
20
22
|
"""
|
|
21
23
|
Make a new P4Client
|
|
22
24
|
|
|
23
25
|
:param perforce_root: *string* root of your Perforce workspace. This would also be where your .p4config file is
|
|
24
26
|
:param user: *string* P4USER, if None will be tried to be found automatically
|
|
25
27
|
:param client: *string* P4CLIENT, if None will be tried to be found automatically
|
|
26
|
-
:param silent: *bool* if True, suppresses error messages to cut down on terminal spam
|
|
27
28
|
:param max_parallel_connections: *int* max number of connections to use while syncing/submitting. This requires
|
|
28
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)``.
|
|
29
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
|
+
|
|
30
41
|
self.perforce_root = perforce_root
|
|
31
|
-
self.silent = silent
|
|
32
42
|
|
|
33
43
|
self.max_parallel_connections = max_parallel_connections
|
|
34
44
|
self.user = user
|
|
@@ -36,8 +46,7 @@ class P4Client(object):
|
|
|
36
46
|
self.server = server
|
|
37
47
|
|
|
38
48
|
if not self.__p4config_exists():
|
|
39
|
-
|
|
40
|
-
logging.warning("No .p4config file found in %s!" % self.perforce_root)
|
|
49
|
+
_log.warning("No .p4config file found in %s!" % self.perforce_root)
|
|
41
50
|
|
|
42
51
|
if user is None:
|
|
43
52
|
self.user = self.get_p4_setting("P4USER")
|
|
@@ -142,7 +151,7 @@ class P4Client(object):
|
|
|
142
151
|
|
|
143
152
|
if len(command) > MAX_CMD_LEN:
|
|
144
153
|
# This shouldn't happen, but just in case the command prefix end up really long
|
|
145
|
-
|
|
154
|
+
_log.warning(
|
|
146
155
|
f"Command length: {format(len(command))} exceeds MAX_CMD_LEN {MAX_CMD_LEN} on command: {MAX_CMD_LEN}")
|
|
147
156
|
|
|
148
157
|
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True, cwd=self.perforce_root) as pipe:
|
|
@@ -184,7 +193,7 @@ class P4Client(object):
|
|
|
184
193
|
msg = msg.decode("utf-8", errors="replace")
|
|
185
194
|
messages.append(msg.strip())
|
|
186
195
|
for msg in messages:
|
|
187
|
-
|
|
196
|
+
_log.warning(f"p4 {cmd}: {msg}")
|
|
188
197
|
if raise_on_errors:
|
|
189
198
|
raise p4errors.P4CommandError(cmd, messages)
|
|
190
199
|
|
|
@@ -396,12 +405,12 @@ class P4Client(object):
|
|
|
396
405
|
|
|
397
406
|
parts = output.split(" ")
|
|
398
407
|
if len(parts) < 2:
|
|
399
|
-
|
|
408
|
+
_log.error(f"Unexpected output from p4 change: {output!r}")
|
|
400
409
|
return None
|
|
401
410
|
try:
|
|
402
411
|
return int(parts[1])
|
|
403
412
|
except ValueError:
|
|
404
|
-
|
|
413
|
+
_log.error(f"Could not parse changelist number from: {output!r}")
|
|
405
414
|
return None
|
|
406
415
|
|
|
407
416
|
def changelist_exists(self, changelist):
|
|
@@ -449,14 +458,10 @@ class P4Client(object):
|
|
|
449
458
|
|
|
450
459
|
return True
|
|
451
460
|
except subprocess.CalledProcessError as e:
|
|
452
|
-
|
|
453
|
-
if not self.silent:
|
|
454
|
-
logging.error(f"Failed to update changelist description: {e}")
|
|
461
|
+
_log.error(f"Failed to update changelist description: {e}")
|
|
455
462
|
return False
|
|
456
463
|
except OSError as e:
|
|
457
|
-
|
|
458
|
-
if not self.silent:
|
|
459
|
-
logging.error(f"Failed to update changelist description: {e}")
|
|
464
|
+
_log.error(f"Failed to update changelist description: {e}")
|
|
460
465
|
return False
|
|
461
466
|
|
|
462
467
|
@validate_not_empty
|
|
@@ -471,11 +476,6 @@ class P4Client(object):
|
|
|
471
476
|
file_list = convert_to_list(file_list)
|
|
472
477
|
changelist = self.__ensure_changelist(changelist)
|
|
473
478
|
info_dicts = self.run_cmd("reopen", args=["-c", changelist], file_list=file_list)
|
|
474
|
-
|
|
475
|
-
for info_dict in info_dicts:
|
|
476
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
477
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
478
|
-
|
|
479
479
|
return info_dicts
|
|
480
480
|
|
|
481
481
|
def combine_changelists(self, source_changelists, target_changelist):
|
|
@@ -539,8 +539,6 @@ class P4Client(object):
|
|
|
539
539
|
:return: *list* of info dictionaries
|
|
540
540
|
"""
|
|
541
541
|
file_list = convert_to_list(file_list)
|
|
542
|
-
if not self.silent:
|
|
543
|
-
self.__validate_file_list(file_list)
|
|
544
542
|
if unchanged_only:
|
|
545
543
|
info_dicts = self.run_cmd("revert", ["-a"], file_list=file_list)
|
|
546
544
|
else:
|
|
@@ -563,9 +561,6 @@ class P4Client(object):
|
|
|
563
561
|
:return: *list* of info dicts
|
|
564
562
|
"""
|
|
565
563
|
folder_list = convert_to_list(folder_list)
|
|
566
|
-
if not self.silent:
|
|
567
|
-
self.__validate_file_list(folder_list)
|
|
568
|
-
|
|
569
564
|
cleaned_folder_list = [self._normalize_folder(f) for f in folder_list]
|
|
570
565
|
|
|
571
566
|
if unchanged_only:
|
|
@@ -612,9 +607,6 @@ class P4Client(object):
|
|
|
612
607
|
:return: *list* of info dicts
|
|
613
608
|
"""
|
|
614
609
|
folder_list = convert_to_list(folder_list)
|
|
615
|
-
if not self.silent:
|
|
616
|
-
self.__validate_file_list(folder_list)
|
|
617
|
-
|
|
618
610
|
cleaned_folder_list = [self._normalize_folder(f) for f in folder_list]
|
|
619
611
|
|
|
620
612
|
info_dicts = self.run_cmd("sync", args=["--parallel", f"threads={self.max_parallel_connections}"],
|
|
@@ -640,9 +632,6 @@ class P4Client(object):
|
|
|
640
632
|
|
|
641
633
|
initial_arg_list = ["-f", "--parallel", f"threads={self.max_parallel_connections}"] if force else ["--parallel",
|
|
642
634
|
f"threads={self.max_parallel_connections}"]
|
|
643
|
-
if not self.silent:
|
|
644
|
-
self.__validate_file_list(file_list)
|
|
645
|
-
|
|
646
635
|
info_dicts = self.run_cmd("sync", args=initial_arg_list, file_list=file_list)
|
|
647
636
|
|
|
648
637
|
if verify:
|
|
@@ -651,7 +640,7 @@ class P4Client(object):
|
|
|
651
640
|
if local_file_path is None:
|
|
652
641
|
continue
|
|
653
642
|
if not os.path.isfile(local_file_path):
|
|
654
|
-
|
|
643
|
+
_log.warning(f"File didn't exist after syncing, try force syncing it instead: {local_file_path}")
|
|
655
644
|
|
|
656
645
|
return info_dicts
|
|
657
646
|
|
|
@@ -701,8 +690,6 @@ class P4Client(object):
|
|
|
701
690
|
:return: *list* of info dicts
|
|
702
691
|
"""
|
|
703
692
|
folder_list = convert_to_list(folder_list)
|
|
704
|
-
if not self.silent:
|
|
705
|
-
self.__validate_file_list(folder_list)
|
|
706
693
|
cleaned_folder_list = [self._normalize_folder(f) for f in folder_list]
|
|
707
694
|
changelist = self.__ensure_changelist(changelist)
|
|
708
695
|
args = self._build_reconcile_args(changelist, add, edit, delete)
|
|
@@ -720,9 +707,6 @@ class P4Client(object):
|
|
|
720
707
|
:return: *list* of info dictionaries
|
|
721
708
|
"""
|
|
722
709
|
file_list = convert_to_list(file_list)
|
|
723
|
-
if not self.silent:
|
|
724
|
-
self.__validate_file_list(file_list)
|
|
725
|
-
|
|
726
710
|
changelist = self.__ensure_changelist(changelist)
|
|
727
711
|
|
|
728
712
|
info_dicts = self.run_cmd("delete", args=["-c", changelist], file_list=file_list)
|
|
@@ -790,13 +774,10 @@ class P4Client(object):
|
|
|
790
774
|
file_list = self.get_files_in_changelist(changelist)
|
|
791
775
|
else:
|
|
792
776
|
file_list = convert_to_list(file_list)
|
|
793
|
-
if not self.silent:
|
|
794
|
-
self.__validate_file_list(file_list)
|
|
795
777
|
|
|
796
778
|
# if there are no files to shelve, return empty list
|
|
797
779
|
if not file_list:
|
|
798
|
-
|
|
799
|
-
logging.warning(f"No files to shelve in changelist {changelist}")
|
|
780
|
+
_log.warning(f"No files to shelve in changelist {changelist}")
|
|
800
781
|
return []
|
|
801
782
|
|
|
802
783
|
# iuild args list based on options
|
|
@@ -807,10 +788,6 @@ class P4Client(object):
|
|
|
807
788
|
# run the shelve command
|
|
808
789
|
info_dicts = self.run_cmd("shelve", args=args, file_list=file_list)
|
|
809
790
|
|
|
810
|
-
for info_dict in info_dicts:
|
|
811
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
812
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
813
|
-
|
|
814
791
|
# revert files if requested
|
|
815
792
|
if revert_after_shelve and file_list:
|
|
816
793
|
revert_info_dicts = self.revert_files(file_list)
|
|
@@ -851,35 +828,20 @@ class P4Client(object):
|
|
|
851
828
|
# prepare file list if provided
|
|
852
829
|
if file_list is not None:
|
|
853
830
|
file_list = convert_to_list(file_list)
|
|
854
|
-
if not self.silent:
|
|
855
|
-
self.__validate_file_list(file_list)
|
|
856
831
|
else:
|
|
857
832
|
file_list = []
|
|
858
833
|
|
|
859
834
|
# run the unshelve command
|
|
860
835
|
info_dicts = self.run_cmd("unshelve", args=args, file_list=file_list)
|
|
861
836
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
866
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
867
|
-
has_error = True
|
|
837
|
+
has_error = any(
|
|
838
|
+
self.__get_dict_value(d, "code") == "error" for d in info_dicts
|
|
839
|
+
)
|
|
868
840
|
|
|
869
841
|
# delete shelved files if requested and unshelve was successful
|
|
870
842
|
if delete_shelved_files and not has_error:
|
|
871
|
-
# build the arguments for the shelve -d command
|
|
872
843
|
delete_args = ["-d", "-c", str(source_changelist)]
|
|
873
|
-
|
|
874
|
-
# if file_list provided, use it for the delete operation as well
|
|
875
844
|
delete_info_dicts = self.run_cmd("shelve", args=delete_args, file_list=file_list)
|
|
876
|
-
|
|
877
|
-
# check for errors in the delete operation
|
|
878
|
-
for info_dict in delete_info_dicts:
|
|
879
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
880
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
881
|
-
|
|
882
|
-
# add the delete info to the return data
|
|
883
845
|
info_dicts.extend(delete_info_dicts)
|
|
884
846
|
|
|
885
847
|
return info_dicts
|
|
@@ -897,8 +859,7 @@ class P4Client(object):
|
|
|
897
859
|
|
|
898
860
|
# Don't allow deleting shelves from the default changelist
|
|
899
861
|
if changelist == "default":
|
|
900
|
-
|
|
901
|
-
logging.error("Cannot delete shelved files from the default changelist")
|
|
862
|
+
_log.error("Cannot delete shelved files from the default changelist")
|
|
902
863
|
return [{"code": "error", "data": "Cannot delete shelved files from the default changelist"}]
|
|
903
864
|
|
|
904
865
|
# Check if the changelist has shelved files
|
|
@@ -913,8 +874,7 @@ class P4Client(object):
|
|
|
913
874
|
break
|
|
914
875
|
|
|
915
876
|
if not has_shelved_files:
|
|
916
|
-
|
|
917
|
-
logging.warning(f"No shelved files found in changelist {changelist}")
|
|
877
|
+
_log.warning(f"No shelved files found in changelist {changelist}")
|
|
918
878
|
return [{"code": "info", "data": f"No shelved files found in changelist {changelist}"}]
|
|
919
879
|
|
|
920
880
|
# Delete all shelved files in the changelist
|
|
@@ -922,14 +882,10 @@ class P4Client(object):
|
|
|
922
882
|
return info_dicts
|
|
923
883
|
|
|
924
884
|
except subprocess.CalledProcessError as e:
|
|
925
|
-
|
|
926
|
-
if not self.silent:
|
|
927
|
-
logging.error(f"Failed to delete shelved files: {e}")
|
|
885
|
+
_log.error(f"Failed to delete shelved files: {e}")
|
|
928
886
|
return [{"code": "error", "data": str(e)}]
|
|
929
887
|
except OSError as e:
|
|
930
|
-
|
|
931
|
-
if not self.silent:
|
|
932
|
-
logging.error(f"Failed to delete shelved files: {e}")
|
|
888
|
+
_log.error(f"Failed to delete shelved files: {e}")
|
|
933
889
|
return [{"code": "error", "data": str(e)}]
|
|
934
890
|
|
|
935
891
|
@validate_not_empty
|
|
@@ -967,8 +923,6 @@ class P4Client(object):
|
|
|
967
923
|
:return: *list* of info dictionaries
|
|
968
924
|
"""
|
|
969
925
|
file_list = convert_to_list(file_list)
|
|
970
|
-
if not self.silent:
|
|
971
|
-
self.__validate_file_list(file_list)
|
|
972
926
|
|
|
973
927
|
files_for_add = []
|
|
974
928
|
files_for_checkout = []
|
|
@@ -1014,15 +968,9 @@ class P4Client(object):
|
|
|
1014
968
|
:return: *list* of info dictionaries
|
|
1015
969
|
"""
|
|
1016
970
|
file_list = convert_to_list(file_list)
|
|
1017
|
-
if not self.silent:
|
|
1018
|
-
self.__validate_file_list(file_list)
|
|
1019
|
-
|
|
1020
971
|
changelist = self.__ensure_changelist(changelist)
|
|
1021
972
|
|
|
1022
973
|
info_dicts = self.run_cmd("edit", args=["-c", changelist], file_list=file_list)
|
|
1023
|
-
for info_dict in info_dicts:
|
|
1024
|
-
if self.__get_dict_value(info_dict, "code") == "error" and not self.silent:
|
|
1025
|
-
logging.error(self.__get_dict_value(info_dict, "data"))
|
|
1026
974
|
return info_dicts
|
|
1027
975
|
|
|
1028
976
|
@validate_not_empty
|
|
@@ -1034,9 +982,6 @@ class P4Client(object):
|
|
|
1034
982
|
:return: *list* of info dictionaries
|
|
1035
983
|
"""
|
|
1036
984
|
file_list = convert_to_list(file_list)
|
|
1037
|
-
if not self.silent:
|
|
1038
|
-
self.__validate_file_list(file_list)
|
|
1039
|
-
|
|
1040
985
|
changelist = self.__ensure_changelist(changelist)
|
|
1041
986
|
|
|
1042
987
|
info_dicts = self.run_cmd("add", args=["-c", changelist], file_list=file_list)
|
|
@@ -1100,7 +1045,7 @@ class P4Client(object):
|
|
|
1100
1045
|
cl_description = desc_value.rstrip("\n")
|
|
1101
1046
|
|
|
1102
1047
|
if not cl_description:
|
|
1103
|
-
|
|
1048
|
+
_log.warning(f"The CL description is empty in this return object!\n{pformat(info_dict)}")
|
|
1104
1049
|
|
|
1105
1050
|
if not case_sensitive:
|
|
1106
1051
|
cl_description = cl_description.lower()
|
|
@@ -1240,13 +1185,13 @@ class P4Client(object):
|
|
|
1240
1185
|
with socket.create_connection((host, port), timeout=timeout):
|
|
1241
1186
|
return True
|
|
1242
1187
|
except socket.timeout:
|
|
1243
|
-
|
|
1188
|
+
_log.warning(f"Connection to {host} on port {port} timed out after {timeout} seconds, the running command might not complete correctly")
|
|
1244
1189
|
return False
|
|
1245
1190
|
except ConnectionRefusedError:
|
|
1246
|
-
|
|
1191
|
+
_log.warning(f"Connection to {host} on port {port} refused to connect, is the server still up?")
|
|
1247
1192
|
return False
|
|
1248
1193
|
except OSError as e:
|
|
1249
|
-
|
|
1194
|
+
_log.warning(f"oserror: {e.strerror or e}")
|
|
1250
1195
|
return False
|
|
1251
1196
|
|
|
1252
1197
|
def __server_address(self):
|
|
@@ -1373,7 +1318,7 @@ class P4Client(object):
|
|
|
1373
1318
|
while last_dir != current_dir:
|
|
1374
1319
|
if os.path.exists(current_dir):
|
|
1375
1320
|
if ".p4config" in os.listdir(current_dir):
|
|
1376
|
-
|
|
1321
|
+
_log.info(".p4config found in %s" % current_dir)
|
|
1377
1322
|
self.perforce_root = current_dir
|
|
1378
1323
|
return True
|
|
1379
1324
|
|
|
@@ -1381,22 +1326,3 @@ class P4Client(object):
|
|
|
1381
1326
|
current_dir = os.path.dirname(last_dir)
|
|
1382
1327
|
return False
|
|
1383
1328
|
|
|
1384
|
-
def __validate_file_list(self, file_list):
|
|
1385
|
-
"""
|
|
1386
|
-
Validation function to ensure correct files are being synced to correct workspaces & clients etc.
|
|
1387
|
-
Is an extendable function
|
|
1388
|
-
:param file_list: List of files to iterate
|
|
1389
|
-
:return:
|
|
1390
|
-
"""
|
|
1391
|
-
if not self.perforce_root:
|
|
1392
|
-
raise p4errors.WorkSpaceError(f"self.perforce_root (value: {self.perforce_root}) is not set!")
|
|
1393
|
-
|
|
1394
|
-
file_list = convert_to_list(file_list)
|
|
1395
|
-
|
|
1396
|
-
# Easy utility to check that the file is underneath the correct perforce root
|
|
1397
|
-
# Quicker than waiting for the result of a p4 fstat
|
|
1398
|
-
for f in file_list:
|
|
1399
|
-
if not f.lower().startswith(self.perforce_root.lower()) and not f.lower().startswith(
|
|
1400
|
-
self.depot_root.lower()):
|
|
1401
|
-
raise Exception(f'{f} is not under perforce root: {self.perforce_root}, {self.depot_root}')
|
|
1402
|
-
|
|
@@ -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
|
|
@@ -60,6 +60,33 @@ p4 = p4cmd.P4Client.from_env()
|
|
|
60
60
|
|
|
61
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`.
|
|
62
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
|
+
|
|
63
90
|
## P4Client API
|
|
64
91
|
|
|
65
92
|
### Connection & settings
|
|
@@ -742,3 +742,52 @@ def test_get_local_paths_missing_path(p4client):
|
|
|
742
742
|
["//depot/project/file.txt", "//depot/project/unmapped.txt"]
|
|
743
743
|
)
|
|
744
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
|