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.
Files changed (31) hide show
  1. {p4cmd-2.10.3 → p4cmd-2.11.0}/PKG-INFO +28 -1
  2. {p4cmd-2.10.3 → p4cmd-2.11.0}/README.md +27 -0
  3. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/__init__.py +1 -1
  4. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/p4cmd.py +34 -108
  5. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/utils.py +5 -3
  6. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/PKG-INFO +28 -1
  7. {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/test_p4client.py +49 -0
  8. {p4cmd-2.10.3 → p4cmd-2.11.0}/LICENSE +0 -0
  9. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/p4errors.py +0 -0
  10. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/p4file.py +0 -0
  11. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/__init__.py +0 -0
  12. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
  13. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_MOVED.png +0 -0
  14. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
  15. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
  16. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
  17. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
  18. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
  19. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
  20. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
  21. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/SOURCES.txt +0 -0
  22. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/dependency_links.txt +0 -0
  23. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/requires.txt +0 -0
  24. {p4cmd-2.10.3 → p4cmd-2.11.0}/p4cmd.egg-info/top_level.txt +0 -0
  25. {p4cmd-2.10.3 → p4cmd-2.11.0}/setup.cfg +0 -0
  26. {p4cmd-2.10.3 → p4cmd-2.11.0}/setup.py +0 -0
  27. {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/__init__.py +0 -0
  28. {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/conftest.py +0 -0
  29. {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/test_integration.py +0 -0
  30. {p4cmd-2.10.3 → p4cmd-2.11.0}/tests/test_p4file.py +0 -0
  31. {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.10.3
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
@@ -1,4 +1,4 @@
1
- VERSION = "2.10.3"
1
+ VERSION = "2.11.0"
2
2
 
3
3
  from .p4cmd import P4Client
4
4
  from .p4file import P4File
@@ -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=True, max_parallel_connections=4):
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
- if not silent:
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
- logging.warning(
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
- logging.warning(f"p4 {cmd}: {msg}")
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
- logging.error(f"Unexpected output from p4 change: {output!r}")
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
- logging.error(f"Could not parse changelist number from: {output!r}")
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
- logging.debug(f"p4 command failed: {e}")
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
- logging.debug(f"OS error: {e}")
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
- logging.warning(f"File didn't exist after syncing, try force syncing it instead: {local_file_path}")
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
- if not self.silent:
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
- # check for errors in the unshelve operation
863
- has_error = False
864
- for info_dict in info_dicts:
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
- if not self.silent:
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
- if not self.silent:
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
- logging.debug(f"p4 command failed: {e}")
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
- logging.debug(f"OS error: {e}")
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
- logging.warning(f"The CL description is empty in this return object!\n{pformat(info_dict)}")
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
- logging.warning(f"Connection to {host} on port {port} timed out after {timeout} seconds, the running command might not complete correctly")
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
- logging.warning(f"Connection to {host} on port {port} refused to connect, is the server still up?")
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
- logging.warning(f"oserror: {e.strerror or e}")
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
- logging.info(".p4config found in %s" % current_dir)
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
- logging.warning(f"Empty file list provided to {func.__name__}, operation skipped.")
70
- logging.warning(f"args: {args}")
71
- logging.warning(f"kwargs: {kwargs}")
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.10.3
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