p4cmd 2.9.2__tar.gz → 2.10.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.9.2 → p4cmd-2.10.0}/PKG-INFO +1 -1
  2. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/__init__.py +1 -1
  3. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/p4cmd.py +15 -1
  4. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/p4errors.py +14 -0
  5. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd.egg-info/PKG-INFO +1 -1
  6. {p4cmd-2.9.2 → p4cmd-2.10.0}/tests/test_p4client.py +76 -0
  7. {p4cmd-2.9.2 → p4cmd-2.10.0}/LICENSE +0 -0
  8. {p4cmd-2.9.2 → p4cmd-2.10.0}/README.md +0 -0
  9. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/p4file.py +0 -0
  10. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/__init__.py +0 -0
  11. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
  12. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_MOVED.png +0 -0
  13. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
  14. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
  15. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
  16. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
  17. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
  18. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
  19. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
  20. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd/utils.py +0 -0
  21. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd.egg-info/SOURCES.txt +0 -0
  22. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd.egg-info/dependency_links.txt +0 -0
  23. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd.egg-info/requires.txt +0 -0
  24. {p4cmd-2.9.2 → p4cmd-2.10.0}/p4cmd.egg-info/top_level.txt +0 -0
  25. {p4cmd-2.9.2 → p4cmd-2.10.0}/setup.cfg +0 -0
  26. {p4cmd-2.9.2 → p4cmd-2.10.0}/setup.py +0 -0
  27. {p4cmd-2.9.2 → p4cmd-2.10.0}/tests/__init__.py +0 -0
  28. {p4cmd-2.9.2 → p4cmd-2.10.0}/tests/conftest.py +0 -0
  29. {p4cmd-2.9.2 → p4cmd-2.10.0}/tests/test_integration.py +0 -0
  30. {p4cmd-2.9.2 → p4cmd-2.10.0}/tests/test_p4file.py +0 -0
  31. {p4cmd-2.9.2 → p4cmd-2.10.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: p4cmd
3
- Version: 2.9.2
3
+ Version: 2.10.0
4
4
  Summary: Simple P4 python module
5
5
  Home-page: https://github.com/nielsvaes/p4cmd
6
6
  Author: Niels Vaes
@@ -1,4 +1,4 @@
1
- VERSION = "2.9.2"
1
+ VERSION = "2.10.0"
2
2
 
3
3
  from .p4cmd import P4Client
4
4
  from .p4file import P4File
@@ -84,7 +84,7 @@ class P4Client(object):
84
84
  """
85
85
  self.max_parallel_connections = value
86
86
 
87
- def run_cmd(self, cmd, args=None, file_list=None, use_global_options=True, online_check=True):
87
+ def run_cmd(self, cmd, args=None, file_list=None, use_global_options=True, online_check=True, raise_on_errors=False):
88
88
  """
89
89
  Reads the output stream of the command and returns it as a marshaled dict.
90
90
 
@@ -145,6 +145,20 @@ class P4Client(object):
145
145
  dict_list.append(output_dict)
146
146
  pipe.kill()
147
147
 
148
+ # Check for error dicts in the results
149
+ errors = [d for d in dict_list if d.get(b"code") == b"error" or d.get("code") == "error"]
150
+ if errors:
151
+ messages = []
152
+ for e in errors:
153
+ msg = e.get(b"data") or e.get("data") or e.get(b"error") or e.get("error") or "unknown error"
154
+ if isinstance(msg, bytes):
155
+ msg = msg.decode("utf-8", errors="replace")
156
+ messages.append(msg.strip())
157
+ for msg in messages:
158
+ logging.warning(f"p4 {cmd}: {msg}")
159
+ if raise_on_errors:
160
+ raise p4errors.P4CommandError(cmd, messages)
161
+
148
162
  return dict_list
149
163
 
150
164
  def __replace_description_in_template(self, template, new_description):
@@ -15,6 +15,20 @@ class WorkSpaceError(P4cmdError):
15
15
  pass
16
16
 
17
17
 
18
+ class P4CommandError(P4cmdError):
19
+ """Raised when a P4 command returns one or more error dicts.
20
+
21
+ Attributes:
22
+ cmd: The P4 command that failed (e.g. "sync", "edit").
23
+ messages: List of error message strings from the error dicts.
24
+ """
25
+ def __init__(self, cmd, messages):
26
+ self.cmd = cmd
27
+ self.messages = messages
28
+ joined = "; ".join(str(m).strip() for m in messages)
29
+ super().__init__(f"p4 {cmd}: {joined}")
30
+
31
+
18
32
  class ServerOffline(P4cmdError):
19
33
  """Raised when the Perforce server is unreachable.
20
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: p4cmd
3
- Version: 2.9.2
3
+ Version: 2.10.0
4
4
  Summary: Simple P4 python module
5
5
  Home-page: https://github.com/nielsvaes/p4cmd
6
6
  Author: Niels Vaes
@@ -1,10 +1,12 @@
1
1
  """Tests for p4cmd/p4cmd.py — P4Client with run_cmd mocked."""
2
+ import logging
2
3
  import socket
3
4
  from unittest.mock import MagicMock, patch
4
5
 
5
6
  import pytest
6
7
 
7
8
  from p4cmd.p4cmd import P4Client
9
+ from p4cmd.p4errors import P4CommandError
8
10
  from p4cmd.p4file import P4File, Status
9
11
  from tests.conftest import make_fstat_dict
10
12
 
@@ -577,6 +579,80 @@ def test_get_depot_paths_missing_depot_file(p4client):
577
579
  assert result == ["//depot/project/file.txt"]
578
580
 
579
581
 
582
+ # ---------------------------------------------------------------------------
583
+ # run_cmd error handling
584
+ # ---------------------------------------------------------------------------
585
+
586
+ def test_run_cmd_logs_warning_on_error_dicts(p4client, caplog):
587
+ """run_cmd logs a warning for each error dict returned by p4."""
588
+ error_dict = {b"code": b"error", b"data": b"//depot/file.txt - file(s) not on client.\n"}
589
+ ok_dict = {b"code": b"stat", b"depotFile": b"//depot/other.txt"}
590
+ with patch("subprocess.Popen") as mock_popen:
591
+ import marshal as _m, io as _io
592
+ buf = _io.BytesIO()
593
+ _m.dump(error_dict, buf)
594
+ _m.dump(ok_dict, buf)
595
+ buf.seek(0)
596
+ mock_popen.return_value.__enter__ = lambda s: s
597
+ mock_popen.return_value.__exit__ = MagicMock(return_value=False)
598
+ mock_popen.return_value.stdout = buf
599
+ mock_popen.return_value.kill = MagicMock()
600
+ with caplog.at_level(logging.WARNING):
601
+ result = p4client.run_cmd("sync", file_list=["//depot/file.txt", "//depot/other.txt"])
602
+ assert len(result) == 2 # both dicts still returned
603
+ assert "file(s) not on client" in caplog.text
604
+
605
+
606
+ def test_run_cmd_raise_on_errors(p4client):
607
+ """run_cmd raises P4CommandError when raise_on_errors=True and errors are present."""
608
+ error_dict = {b"code": b"error", b"data": b"//depot/file.txt - file(s) not on client.\n"}
609
+ with patch("subprocess.Popen") as mock_popen:
610
+ import marshal as _m, io as _io
611
+ buf = _io.BytesIO()
612
+ _m.dump(error_dict, buf)
613
+ buf.seek(0)
614
+ mock_popen.return_value.__enter__ = lambda s: s
615
+ mock_popen.return_value.__exit__ = MagicMock(return_value=False)
616
+ mock_popen.return_value.stdout = buf
617
+ mock_popen.return_value.kill = MagicMock()
618
+ with pytest.raises(P4CommandError, match="file\\(s\\) not on client"):
619
+ p4client.run_cmd("sync", file_list=["//depot/file.txt"], raise_on_errors=True)
620
+
621
+
622
+ def test_run_cmd_no_raise_without_errors(p4client):
623
+ """run_cmd doesn't raise when raise_on_errors=True but no errors are present."""
624
+ ok_dict = {b"code": b"stat", b"depotFile": b"//depot/file.txt"}
625
+ with patch("subprocess.Popen") as mock_popen:
626
+ import marshal as _m, io as _io
627
+ buf = _io.BytesIO()
628
+ _m.dump(ok_dict, buf)
629
+ buf.seek(0)
630
+ mock_popen.return_value.__enter__ = lambda s: s
631
+ mock_popen.return_value.__exit__ = MagicMock(return_value=False)
632
+ mock_popen.return_value.stdout = buf
633
+ mock_popen.return_value.kill = MagicMock()
634
+ result = p4client.run_cmd("sync", file_list=["//depot/file.txt"], raise_on_errors=True)
635
+ assert result == [ok_dict]
636
+
637
+
638
+ def test_run_cmd_handles_string_key_error_dicts(p4client, caplog):
639
+ """run_cmd handles error dicts with string keys (from ValueError fallback)."""
640
+ # String-keyed error dicts come from the ValueError handler in run_cmd
641
+ error_dict = {b"code": b"error", b"data": b"some error\n"}
642
+ with patch("subprocess.Popen") as mock_popen:
643
+ import marshal as _m, io as _io
644
+ buf = _io.BytesIO()
645
+ _m.dump(error_dict, buf)
646
+ buf.seek(0)
647
+ mock_popen.return_value.__enter__ = lambda s: s
648
+ mock_popen.return_value.__exit__ = MagicMock(return_value=False)
649
+ mock_popen.return_value.stdout = buf
650
+ mock_popen.return_value.kill = MagicMock()
651
+ with caplog.at_level(logging.WARNING):
652
+ p4client.run_cmd("sync", file_list=["//depot/file.txt"])
653
+ assert "some error" in caplog.text
654
+
655
+
580
656
  def test_get_local_paths_missing_path(p4client):
581
657
  """get_local_paths skips entries where path is missing (e.g. unmapped paths from p4 where)."""
582
658
  info_dicts = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes