194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
# Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
import types
|
|
import marshal
|
|
import threading
|
|
import subprocess as sp
|
|
from pathlib import PurePath
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class _P4Result(object):
|
|
def __init__(self, result):
|
|
super().__setattr__("_result", result)
|
|
|
|
def __str__(self): return str(self._result)
|
|
def __contains__(self, key): return (key in self._result) or (key + "0" in self._result)
|
|
def as_dict(self): return {k.decode():v.decode() for k,v in self._result.items()}
|
|
def __setattr__(self, key, value): self._result[key.encode()] = str(value).encode()
|
|
|
|
def __getattr__(self, key):
|
|
if (key + "0").encode() in self._result:
|
|
def as_list():
|
|
index = 0;
|
|
while (key + str(index)).encode() in self._result:
|
|
indexed_key = (key + str(index)).encode()
|
|
yield self._result[indexed_key].decode()
|
|
index += 1
|
|
return as_list()
|
|
|
|
ret = self._result.get(str.encode(key))
|
|
if ret == None: raise AttributeError(key)
|
|
return ret.decode(errors="replace")
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class _P4Command(object):
|
|
@staticmethod
|
|
def _read_args(*args, **kwargs):
|
|
for k,v in kwargs.items():
|
|
if isinstance(v, bool):
|
|
if v:
|
|
yield "-" + k
|
|
elif v != None:
|
|
yield f"-{k}={v}" if len(k) > 1 else f"-{k}{v}"
|
|
|
|
for arg in args:
|
|
if isinstance(arg, str) or isinstance(arg, PurePath):
|
|
yield str(arg)
|
|
|
|
def __init__(self, **options):
|
|
opt_iter = _P4Command._read_args(**options)
|
|
self._command = ["p4", "-Qutf8", "-G"]
|
|
self._command += (x for x in opt_iter)
|
|
|
|
def start(self, command, *args, **kwargs):
|
|
self._stdin_args = []
|
|
for arg in args:
|
|
if isinstance(arg, str) or isinstance(arg, PurePath):
|
|
continue
|
|
if not hasattr(arg, "__iter__"):
|
|
raise TypeError("P4 arguments can be only strings or sequences")
|
|
self._stdin_args.append(arg)
|
|
|
|
self._proc = None
|
|
|
|
arg_iter = _P4Command._read_args(*args, **kwargs)
|
|
|
|
if self._stdin_args:
|
|
self._command.append("-x-")
|
|
self._command.append(command)
|
|
self._command += (x for x in arg_iter)
|
|
|
|
def __del__(self): self._close_proc()
|
|
def __str__(self): return " ".join(self._command)
|
|
def __iter__(self): yield from self._iter()
|
|
def __getattr__(self, name): return getattr(self.run(), name)
|
|
|
|
def run(self, **kwargs):
|
|
return next(self._iter(**kwargs), None)
|
|
|
|
def read(self, **kwargs):
|
|
yield from self._iter(**kwargs)
|
|
|
|
def _close_proc(self):
|
|
if self._proc:
|
|
if self._proc.stdin:
|
|
self._proc.stdin.close()
|
|
self._proc.stdout.close()
|
|
self._proc = None
|
|
|
|
def _iter(self, input_data=None, on_error=True, on_info=None, on_text=None):
|
|
stdin = None
|
|
if input_data != None:
|
|
if self._stdin_args:
|
|
raise _P4.Error("It is unsupported to have both generator-type arguments and input data")
|
|
|
|
if isinstance(input_data, dict):
|
|
input_data = {str(k).encode():str(v).encode() for k,v in input_data.items()}
|
|
else:
|
|
raise _P4.Error("Unsupported input data type; " + type(input_data).__name__)
|
|
stdin = sp.PIPE
|
|
|
|
if self._stdin_args:
|
|
stdin = sp.PIPE
|
|
|
|
proc = sp.Popen(self._command, stdout=sp.PIPE, stdin=stdin)
|
|
self._proc = proc
|
|
|
|
if stdin:
|
|
def stdin_thread_entry():
|
|
try:
|
|
if input_data:
|
|
marshal.dump(input_data, proc.stdin, 0)
|
|
for args in self._stdin_args:
|
|
for arg in args:
|
|
arg = str(arg).encode() + b"\n"
|
|
proc.stdin.write(arg)
|
|
except (BrokenPipeError, OSError):
|
|
pass
|
|
finally:
|
|
try: proc.stdin.close()
|
|
except: pass
|
|
|
|
stdin_thread = threading.Thread(target=stdin_thread_entry)
|
|
stdin_thread.start()
|
|
|
|
while True:
|
|
try: result = marshal.load(proc.stdout)
|
|
except EOFError: break
|
|
|
|
code = result[b"code"]
|
|
del result[b"code"]
|
|
|
|
if code == b"error":
|
|
if isinstance(on_error, bool):
|
|
if on_error:
|
|
raise _P4.Error(result[b"data"].decode()[:-1])
|
|
continue
|
|
|
|
try:
|
|
on_error(_P4Result(result))
|
|
except:
|
|
proc.terminate()
|
|
raise
|
|
continue
|
|
|
|
if code == b"stat":
|
|
yield _P4Result(result)
|
|
continue
|
|
|
|
if code == b"text" and on_text:
|
|
try:
|
|
data = result.get(b"data", b"")
|
|
on_text(data)
|
|
continue
|
|
except:
|
|
proc.terminate()
|
|
raise
|
|
|
|
if code == b"info" and on_info:
|
|
try:
|
|
on_info(_P4Result(result))
|
|
continue
|
|
except:
|
|
proc.terminate()
|
|
raise
|
|
|
|
if stdin:
|
|
stdin_thread.join()
|
|
self._close_proc()
|
|
|
|
#-------------------------------------------------------------------------------
|
|
class _P4(object):
|
|
class Error(Exception):
|
|
def __init__(self, msg):
|
|
super().__init__("Perforce: " + msg)
|
|
|
|
def __init__(self, **options):
|
|
self._options = options
|
|
|
|
def __getattr__(self, command):
|
|
if command == "Error":
|
|
return _P4.Error
|
|
|
|
def inner(*args, **kwargs):
|
|
instance = _P4Command(**self._options)
|
|
instance.start(command, *args, **kwargs)
|
|
return instance
|
|
|
|
return inner
|
|
|
|
def __call__(self, **options):
|
|
return _P4(**options)
|
|
|
|
P4 = _P4()
|