# -*- coding: utf-8 -*-
"""
perforce.models
~~~~~~~~~~~~~~~
This module implements the main data models used by perforce
:copyright: (c) 2015 by Brett Dixon
:license: MIT, see LICENSE for more details
"""
import subprocess
import datetime
import traceback
import os
import marshal
import logging
import re
from collections import namedtuple
from functools import wraps
import path
import six
from perforce import errors
LOGGER = logging.getLogger('Perforce')
CHAR_LIMIT = 8000
DATE_FORMAT = "%Y/%m/%d %H:%M:%S"
FORMAT = """Change: {change}
Client: {client}
User: {user}
Status: {status}
Description:
\t{description}
Files:
{files}
"""
NEW_FORMAT = """Change: new
Client: {client}
Status: new
Description:
\t{description}
"""
#: Error levels enum
ErrorLevel = namedtuple('ErrorLevel', 'EMPTY, INFO, WARN, FAILED, FATAL')(*range(5))
#: Connections status enum
ConnectionStatus = namedtuple('ConnectionStatus', 'OK, OFFLINE, NO_AUTH, INVALID_CLIENT')(*range(4))
#: File spec http://www.perforce.com/perforce/doc.current/manuals/cmdref/filespecs.html
FileSpec = namedtuple('FileSpec', 'depot,client')
RE_FILESPEC = re.compile('^"?(//[\w\d\_\/\.\s]+)"?\s')
[docs]def split_ls(func):
"""Decorator to split files into manageable chunks as not to exceed the windows cmd limit
:param func: Function to call for each chunk
:type func: :py:class:Function
"""
@wraps(func)
def wrapper(self, files, silent=True, exclude_deleted=False):
if not isinstance(files, (tuple, list)):
files = [files]
counter = 0
index = 0
results = []
while files:
if index >= len(files):
results += func(self, files, silent, exclude_deleted)
break
length = len(str(files[index]))
if length + counter > CHAR_LIMIT:
# -- at our limit
runfiles = files[:index]
files = files[index:]
counter = 0
index = 0
results += func(self, runfiles, silent, exclude_deleted)
runfiles = None
del runfiles
else:
index += 1
counter += length
return results
return wrapper
[docs]def camel_case(string):
"""Makes a string camelCase
:param string: String to convert
"""
return ''.join((string[0].lower(), string[1:]))
[docs]class Connection(object):
"""This is the connection to perforce and does all of the communication with the perforce server"""
def __init__(self, port=None, client=None, user=None, executable='p4', level=ErrorLevel.FAILED):
self._executable = executable
self._level = level
self._port = port
self._client = client
self._user = user
self.__getVariables()
# -- Make sure we can even proceed with anything
if self._port is None:
raise errors.ConnectionError('Perforce host could not be found, please set P4PORT or provide the hostname\
and port')
if self._user is None:
raise errors.ConnectionError('No user could be found, please set P4USER or provide the user')
def __repr__(self):
return '<Connection: {0}, {1}, {2}>'.format(self._port, str(self._client), self._user)
def __getVariables(self):
"""Parses the P4 env vars using 'set p4'"""
try:
startupinfo = None
if os.name == 'nt':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
output = subprocess.check_output(['p4', 'set'], startupinfo=startupinfo)
if six.PY3:
output = str(output, 'utf8')
except subprocess.CalledProcessError as err:
LOGGER.error(err)
return
p4vars = {}
for line in output.splitlines():
if not line:
continue
try:
k, v = line.split('=', 1)
except ValueError:
continue
p4vars[k.strip()] = v.strip().split(' (')[0]
if p4vars[k.strip()].startswith('(config'):
del p4vars[k.strip()]
self._port = self._port or os.getenv('P4PORT', p4vars.get('P4PORT'))
self._user = self._user or os.getenv('P4USER', p4vars.get('P4USER'))
self._client = self._client or os.getenv('P4CLIENT', p4vars.get('P4CLIENT'))
@property
def client(self):
"""The client used in perforce queries"""
if isinstance(self._client, six.string_types):
self._client = Client(self._client, self)
return self._client
@client.setter
def client(self, value):
if isinstance(value, Client):
self._client = value
elif isinstance(value, six.string_types):
self._client = Client(value, self)
else:
raise TypeError('{} not supported for client'.format(type(value)))
@property
def user(self):
"""The user used in perforce queries"""
return self._user
@property
def level(self):
"""The current exception level"""
return self._level
@level.setter
def level(self, value):
"""Set the current exception level"""
self._level = value
@property
def status(self):
"""The status of the connection to perforce"""
try:
# -- Check client
res = self.run(['info'])
if res[0]['clientName'] == '*unknown*':
return ConnectionStatus.INVALID_CLIENT
# -- Trigger an auth error if not logged in
self.run(['user', '-o'])
except errors.CommandError as err:
if 'password (P4PASSWD) invalid or unset' in str(err.args[0]):
return ConnectionStatus.NO_AUTH
if 'Connect to server failed' in str(err.args[0]):
return ConnectionStatus.OFFLINE
return ConnectionStatus.OK
[docs] def run(self, cmd, stdin=None, marshal_output=True, **kwargs):
"""Runs a p4 command and returns a list of dictionary objects
:param cmd: Command to run
:type cmd: list
:param stdin: Standard Input to send to the process
:type stdin: str
:param marshal_output: Whether or not to marshal the output from the command
:type marshal_output: bool
:param kwargs: Passes any other keyword arguments to subprocess
:raises: :class:`.error.CommandError`
:returns: list, records of results
"""
records = []
args = [self._executable, "-u", self._user, "-p", self._port]
if self._client:
args += ["-c", str(self._client)]
if marshal_output:
args.append('-G')
if isinstance(cmd, six.string_types):
raise ValueError('String commands are not supported, please use a list')
args += cmd
command = ' '.join(args)
startupinfo = None
if os.name == 'nt':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
proc = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
**kwargs
)
if stdin:
proc.stdin.write(six.b(stdin))
if marshal_output:
try:
while True:
record = marshal.load(proc.stdout)
if record.get(b'code', '') == b'error' and record[b'severity'] >= self._level:
proc.stdin.close()
proc.stdout.close()
raise errors.CommandError(record[b'data'], record, command)
if isinstance(record, dict):
if six.PY2:
records.append(record)
else:
records.append({str(k, 'utf8'): str(v) if isinstance(v, int) else str(v, 'utf8', errors='ignore') for k, v in record.items()})
except EOFError:
pass
stdout, stderr = proc.communicate()
else:
records, stderr = proc.communicate()
if stderr:
raise errors.CommandError(stderr, command)
return records
@split_ls
[docs] def ls(self, files, silent=True, exclude_deleted=False):
"""List files
:param files: Perforce file spec
:type files: list
:param silent: Will not raise error for invalid files or files not under the client
:type silent: bool
:param exclude_deleted: Exclude deleted files from the query
:type exclude_deleted: bool
:raises: :class:`.errors.RevisionError`
:returns: list<:class:`.Revision`>
"""
try:
cmd = ['fstat']
if exclude_deleted:
cmd += ['-F', '^headAction=delete ^headAction=move/delete']
cmd += files
results = self.run(cmd)
except errors.CommandError as err:
if silent:
results = []
elif "is not under client's root" in str(err):
raise errors.RevisionError(err.args[0])
else:
raise
return [Revision(r, self) for r in results if r.get('code') != 'error']
[docs] def findChangelist(self, description=None):
"""Gets or creates a Changelist object with a description
:param description: The description to set or lookup
:type description: str
:returns: :class:`.Changelist`
"""
if description is None:
change = Default(self)
else:
if isinstance(description, six.integer_types):
change = Changelist(description, self)
else:
pending = self.run(['changes', '-l', '-s', 'pending', '-c', str(self._client), '-u', self._user])
for cl in pending:
if cl['desc'].strip() == description.strip():
LOGGER.debug('Changelist found: {}'.format(cl['change']))
change = Changelist(int(cl['change']), self)
break
else:
LOGGER.debug('No changelist found, creating one')
change = Changelist.create(description, self)
change.client = self._client
change.save()
return change
[docs] def add(self, filename, change=None):
"""Adds a new file to a changelist
:param filename: File path to add
:type filename: str
:param change: Changelist to add the file to
:type change: int
:returns: :class:`.Revision`
"""
try:
if not self.canAdd(filename):
raise errors.RevisionError('File is not under client path')
if change is None:
self.run(['add', filename])
else:
self.run(['add', '-c', str(change.change), filename])
data = self.run(['fstat', filename])[0]
except errors.CommandError as err:
LOGGER.debug(err)
raise errors.RevisionError('File is not under client path')
rev = Revision(data, self)
if isinstance(change, Changelist):
change.append(rev)
return rev
[docs] def canAdd(self, filename):
"""Determines if a filename can be added to the depot under the current client
:param filename: File path to add
:type filename: str
"""
try:
result = self.run(['add', '-n', '-t', 'text', filename])[0]
except errors.CommandError as err:
LOGGER.debug(err)
return False
if result.get('code') not in ('error', 'info'):
return True
LOGGER.warn('Unable to add {}: {}'.format(filename, result['data']))
return False
@six.python_2_unicode_compatible
[docs]class PerforceObject(object):
"""Abstract class for dealing with the dictionaries coming back from p4 commands
This is a simple descriptor for the incoming P4Dict
"""
def __init__(self, connection=None):
self._connection = connection or Connection()
self._p4dict = {}
def __str__(self):
return self.__unicode__()
def __unicode__(self):
return u'<{}>'.format(self.__class__.__name__)
def __repr__(self):
return self.__unicode__()
[docs]class Changelist(PerforceObject):
"""
A Changelist is a collection of files that will be submitted as a single entry with a description and
timestamp
"""
def __init__(self, changelist=None, connection=None):
connection = connection or Connection()
super(Changelist, self).__init__(connection=connection)
self._files = None
self._dirty = False
self._reverted = False
self._change = changelist
self.query(files=False)
def __repr__(self):
return '<Changelist {}>'.format(self._change)
def __int__(self):
return int(self._change)
def __nonzero__(self):
return True
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_type:
LOGGER.debug(traceback.format_exc())
raise errors.ChangelistError(exc_value)
self.save()
def __contains__(self, other):
if not isinstance(other, Revision):
raise TypeError('Value needs to be a Revision instance')
if self._files is None:
self.query()
names = [f.depotFile for f in self._files]
return other.depotFile in names
def __getitem__(self, name):
if self._files is None:
self.query()
return self._files[name]
def __len__(self):
if self._files is None:
self.query()
return len(self._files)
def __iadd__(self, other):
if self._files is None:
self.query()
if isinstance(other, list):
currentfiles = self._files[:]
try:
files = [str(f) for f in other]
cmd = ['edit', '-c', str(self.change)]
self._connection.run(cmd + files)
self._files += other
self.save()
except errors.CommandError:
self._files = currentfiles
raise
return self
def __eq__(self, other):
return int(self) == int(other)
def __format__(self, *args, **kwargs):
if self._files is None:
self.query()
kwargs = {
'change': self._p4dict['change'],
'client': str(self._p4dict['client']),
'user': self._p4dict['user'],
'status': self._p4dict['status'],
'description': self._p4dict['description'].replace('\n', '\n\t'),
'files': '\n'.join(['\t{}'.format(f.depotFile) for f in self._files])
}
return FORMAT.format(**kwargs)
[docs] def query(self, files=True):
"""Queries the depot to get the current status of the changelist"""
if self._change:
cl = str(self._change)
self._p4dict = {camel_case(k): v for k, v in six.iteritems(self._connection.run(['change', '-o', cl])[0])}
if files:
self._files = []
if self._p4dict.get('status') == 'pending' or self._change == 0:
change = self._change or 'default'
data = self._connection.run(['opened', '-c', str(change)])
self._files = [Revision(r, self._connection) for r in data]
else:
data = self._connection.run(['describe', str(self._change)])[0]
depotfiles = []
for k, v in six.iteritems(data):
if k.startswith('depotFile'):
depotfiles.append(v)
self._files = self._connection.ls(depotfiles)
[docs] def append(self, rev):
"""Adds a :py:class:Revision to this changelist and adds or checks it out if needed
:param rev: Revision to add
:type rev: :class:`.Revision`
"""
if not isinstance(rev, Revision):
results = self._connection.ls(rev)
if not results:
self._connection.add(rev, self)
return
rev = results[0]
if not rev in self:
if rev.isMapped:
rev.edit(self)
self._files.append(rev)
rev.changelist = self
self._dirty = True
[docs] def remove(self, rev, permanent=False):
"""Removes a revision from this changelist
:param rev: Revision to remove
:type rev: :class:`.Revision`
:param permanent: Whether or not we need to set the changelist to default
:type permanent: bool
"""
if not isinstance(rev, Revision):
raise TypeError('argument needs to be an instance of Revision')
if rev not in self:
raise ValueError('{} not in changelist'.format(rev))
self._files.remove(rev)
if not permanent:
rev.changelist = self._connection.default
[docs] def revert(self, unchanged_only=False):
"""Revert all files in this changelist
:param unchanged_only: Only revert unchanged files
:type unchanged_only: bool
:raises: :class:`.ChangelistError`
"""
if self._reverted:
raise errors.ChangelistError('This changelist has been reverted')
change = self._change
if self._change == 0:
change = 'default'
cmd = ['revert', '-c', str(change)]
if unchanged_only:
cmd.append('-a')
files = [f.depotFile for f in self._files]
if files:
cmd += files
self._connection.run(cmd)
self._files = []
self._reverted = True
[docs] def save(self):
"""Saves the state of the changelist"""
self._connection.run(['change', '-i'], stdin=format(self), marshal_output=False)
self._dirty = False
[docs] def submit(self):
"""Submits a chagelist to the depot"""
if self._dirty:
self.save()
self._connection.run(['submit', '-c', str(self._change)], marshal_output=False)
[docs] def delete(self):
"""Reverts all files in this changelist then deletes the changelist from perforce"""
try:
self.revert()
except errors.ChangelistError:
pass
self._connection.run(['change', '-d', str(self._change)])
@property
def change(self):
return int(self._change)
@property
def client(self):
"""Perforce client this changelist is under"""
return self._p4dict['client']
@client.setter
def client(self, client):
self._p4dict['client'] = client
self._dirty = True
@property
def description(self):
"""Changelist description"""
return self._p4dict['description'].strip()
@description.setter
def description(self, desc):
self._p4dict['description'] = desc.strip()
self._dirty = True
@property
def status(self):
return self._p4dict['status']
@property
def user(self):
return self._p4dict['user']
@property
def isDirty(self):
"""Does this changelist have unsaved changes"""
return self._dirty
@property
def time(self):
"""Creation time of this changelist"""
return datetime.datetime.strptime(self._p4dict['date'], DATE_FORMAT)
@staticmethod
[docs] def create(description='<Created by Python>', connection=None):
"""Creates a new changelist
:param connection: Connection to use to create the changelist
:type connection: :class:`.Connection`
:param description: Description for new changelist
:type description: str
:returns: :class:`.Changelist`
"""
connection = connection or Connection()
description = description.replace('\n', '\n\t')
form = NEW_FORMAT.format(client=str(connection.client), description=description)
result = connection.run(['change', '-i'], stdin=form, marshal_output=False)
return Changelist(int(result.split()[1]), connection)
class Default(Changelist):
def __init__(self, connection):
super(Default, self).__init__(None, connection)
data = self._connection.run(['opened', '-c', 'default'])
for f in data:
if self._files is None:
self._files = []
self._files.append(Revision(f, self._connection))
data = self._connection.run(['change', '-o'])[0]
self._change = 0
self._description = data['Description']
self._client = connection.client
self._time = None
self._status = 'new'
self._user = connection.user
def save(self):
"""Saves the state of the changelist"""
files = [f.depotFile for f in self._files]
cmd = ['reopen', '-c', 'default']
self._connection.run(cmd + files)
self._dirty = False
[docs]class Revision(PerforceObject):
"""A Revision represents a file on perforce at a given point in it's history"""
def __init__(self, data, connection=None):
connection = connection or Connection()
super(Revision, self).__init__(connection=connection)
if isinstance(data, six.string_types):
self._p4dict = {'depotFile': data}
self.query()
else:
self._p4dict = data
self._head = HeadRevision(self._p4dict)
self._changelist = None
self._filename = None
def __len__(self):
if 'fileSize' not in self._p4dict:
self._p4dict = self._connection.run(['fstat', '-m', '1', '-Ol', self.depotFile])[0]
return int(self._p4dict['fileSize'])
def __unicode__(self):
return self.depotFile
def __repr__(self):
return '<%s: %s#%s>' % (self.__class__.__name__, self.depotFile, self.revision)
def __int__(self):
return self.revision
[docs] def query(self):
"""Runs an fstat for this file and repopulates the data"""
self._p4dict = self._connection.run(['fstat', '-m', '1', self._p4dict['depotFile']])[0]
self._head = HeadRevision(self._p4dict)
self._filename = self.depotFile
[docs] def edit(self, changelist=0):
"""Checks out the file
:param changelist: Optional changelist to checkout the file into
:type changelist: :class:`.Changelist`
"""
command = 'reopen' if self.action in ('add', 'edit') else 'edit'
if int(changelist):
self._connection.run([command, '-c', str(changelist.change), self.depotFile])
else:
self._connection.run([command, self.depotFile])
self.query()
[docs] def lock(self, lock=True, changelist=0):
"""Locks or unlocks the file
:param lock: Lock or unlock the file
:type lock: bool
:param changelist: Optional changelist to checkout the file into
:type changelist: :class:`.Changelist`
"""
cmd = 'lock' if lock else 'unlock'
if changelist:
self._connection.run([cmd, '-c', changelist, self.depotFile])
else:
self._connection.run([cmd, self.depotFile])
self.query()
[docs] def sync(self, force=False, safe=True, revision=0, changelist=0):
"""Syncs the file at the current revision
:param force: Force the file to sync
:type force: bool
:param safe: Don't sync files that were changed outside perforce
:type safe: bool
:param revision: Sync to a specific revision
:type revision: int
:param changelist: Changelist to sync to
:type changelist: int
"""
cmd = ['sync']
if force:
cmd.append('-f')
if safe:
cmd.append('-s')
if revision:
cmd.append('{}#{}'.format(self.depotFile, revision))
elif changelist:
cmd.append('{}@{}'.format(self.depotFile, changelist))
else:
cmd.append(self.depotFile)
self._connection.run(cmd)
self.query()
[docs] def revert(self, unchanged=False):
"""Reverts any file changes
:param unchanged: Only revert if the file is unchanged
:type unchanged: bool
"""
cmd = ['revert']
if unchanged:
cmd.append('-a')
wasadd = self.action == 'add'
cmd.append(self.depotFile)
self._connection.run(cmd)
if 'movedFile' in self._p4dict:
self._p4dict['depotFile'] = self._p4dict['movedFile']
if not wasadd:
self.query()
if self._changelist:
self._changelist.remove(self, permanent=True)
[docs] def shelve(self, changelist=None):
"""Shelves the file if it is in a changelist
:param changelist: Changelist to add the move to
:type changelist: :class:`.Changelist`
"""
if changelist is None and self.changelist.description == 'default':
raise errors.ShelveError('Unabled to shelve files in the default changelist')
cmd = ['shelve']
if changelist:
cmd += ['-c', str(changelist)]
cmd.append(self.depotFile)
self._connection.run(cmd)
self.query()
[docs] def move(self, dest, changelist=0, force=False):
"""Renames/moves the file to dest
:param dest: Destination to move the file to
:type dest: str
:param changelist: Changelist to add the move to
:type changelist: :class:`.Changelist`
:param force: Force the move to an existing file
:type force: bool
"""
cmd = ['move']
if force:
cmd.append('-f')
if changelist:
cmd += ['-c', str(changelist)]
if not self.isEdit:
self.edit(changelist)
cmd += [self.depotFile, dest]
self._connection.run(cmd)
self._p4dict['depotFile'] = dest
self.query()
[docs] def delete(self, changelist=0):
"""Marks the file for delete
:param changelist: Changelist to add the move to
:type changelist: :class:`.Changelist`
"""
cmd = ['delete']
if changelist:
cmd += ['-c', str(changelist)]
cmd.append(self.depotFile)
self._connection.run(cmd)
self.query()
@property
def hash(self):
"""The hash value of the current revision"""
if 'digest' not in self._p4dict:
self._p4dict = self._connection.run(['fstat', '-m', '1', '-Ol', self.depotFile])[0]
return self._p4dict['digest']
@property
def clientFile(self):
"""The local path to the revision"""
return path.path(self._p4dict['clientFile'])
@property
def depotFile(self):
"""The depot path to the revision"""
return path.path(self._p4dict['depotFile'])
@property
def isMapped(self):
"""Is the file mapped to the current workspace"""
return 'isMapped' in self._p4dict
@property
def isShelved(self):
"""Is the file shelved"""
return 'shelved' in self._p4dict
@property
def revision(self):
"""Revision number"""
rev = self._p4dict.get('haveRev', -1)
if rev == 'none':
rev = 0
return int(rev)
@property
def description(self):
return self._p4dict.get('desc')
@property
def action(self):
"""The current action: add, edit, etc."""
return self._p4dict.get('action')
@property
def changelist(self):
"""Which :class:`.Changelist` is this revision in"""
if self._changelist:
return self._changelist
if self._p4dict['change'] == 'default':
return Default(connection=self._connection)
else:
return Changelist(str(self._p4dict['change']), self._connection)
@changelist.setter
def changelist(self, value):
if not isinstance(value, Changelist):
raise TypeError('argument needs to be an instance of Changelist')
if self not in value:
value.append(self)
self._changelist = value
@property
def type(self):
"""Best guess at file type. text or binary"""
if self.action == 'edit':
return self._p4dict['type']
return None
@property
def isResolved(self):
"""Is the revision resolved"""
return self.unresolved == 0
@property
def resolved(self):
"""The number, if any, of resolved integration records"""
return int(self._p4dict.get('resolved', 0))
@property
def unresolved(self):
"""The number, if any, of unresolved integration records"""
return int(self._p4dict.get('unresolved', 0))
@property
def openedBy(self):
"""Who has this file open for edit"""
return self._p4dict.get('otherOpen', [])
@property
def lockedBy(self):
"""Who has this file locked"""
return self._p4dict.get('otherLock', [])
@property
def isLocked(self):
"""Is the file locked by anyone excluding the current user"""
return 'ourLock' in self._p4dict or 'otherLock' in self._p4dict
@property
def head(self):
"""The :class:`.HeadRevision` of this file"""
return self._head
@property
def isSynced(self):
"""Is the local file the latest revision"""
return self.revision == self.head.revision
@property
def isEdit(self):
"""Is the file open for edit"""
return self.action == 'edit'
[docs]class HeadRevision(object):
"""The HeadRevision represents the latest version on the Perforce server"""
def __init__(self, filedict):
self._p4dict = filedict
@property
def action(self):
return self._p4dict['headAction']
@property
def change(self):
return int(self._p4dict['headChange']) if self._p4dict['headChange'] else 0
@property
def revision(self):
return int(self._p4dict['headRev'])
@property
def type(self):
return self._p4dict['headType']
@property
def time(self):
return datetime.datetime.fromtimestamp(int(self._p4dict['headTime']))
@property
def modifiedTime(self):
return datetime.datetime.fromtimestamp(int(self._p4dict['headModTime']))
[docs]class Client(FormObject):
"""Represents a client(workspace) for a given connection"""
COMMAND = 'client'
def __init__(self, client, connection=None):
super(Client, self).__init__(connection=connection)
assert client is not None
results = self._connection.run(['client', '-o', client])[0]
self._p4dict = {camel_case(k): v for k, v in six.iteritems(results)}
def __unicode__(self):
return self.client
@property
def root(self):
"""Root path fo the client"""
return path.Path(self._p4dict['root'])
@property
def client(self):
return self._p4dict['client']
@property
def description(self):
return self._p4dict['description'].strip()
@description.setter
def description(self, value):
self._p4dict['description'] = value.strip()
self._dirty = True
@property
def host(self):
return self._p4dict['host']
@host.setter
def host(self, value):
self._p4dict['host'] = value
self._dirty = True
@property
def lineEnd(self):
return self._p4dict['lineEnd']
@lineEnd.setter
def lineEnd(self, value):
self._p4dict['lineEnd'] = value
self._dirty = True
@property
def owner(self):
return self._p4dict['owner']
@owner.setter
def owner(self, value):
self._p4dict['owner'] = value
self._dirty = True
@property
def submitOptions(self):
return self._p4dict['submitOptions']
@submitOptions.setter
def submitOptions(self, value):
self._p4dict['submitOptions'] = value
self._dirty = True
@property
def view(self):
"""A list of view specs"""
spec = []
for k, v in six.iteritems(self._p4dict):
if k.startswith('view'):
match = RE_FILESPEC.search(v)
if match:
spec.append(FileSpec(v[:match.end() - 1], v[match.end():]))
return spec
@property
def access(self):
"""The date and time last accessed"""
return datetime.datetime.strptime(self._p4dict['access'], DATE_FORMAT)
@property
def update(self):
"""The date and time the client was updated"""
return datetime.datetime.strptime(self._p4dict['update'], DATE_FORMAT)
@property
def stream(self):
"""Which stream, if any, the client is under"""
stream = self._p4dict.get('stream')
if stream:
return Stream(stream, self._connection)
[docs]class Stream(PerforceObject):
"""An object representing a perforce stream"""
def __init__(self, stream, connection=None):
super(Stream, self).__init__(connection=connection)
assert stream is not None
results = self._connection.run(['stream', '-o', '-v', stream])[0]
self._p4dict = {camel_case(k): v for k, v in six.iteritems(results)}
def __unicode__(self):
return self._p4dict['stream']
@property
def description(self):
"""Stream description tha thas been trimmed"""
return self._p4dict.get('description', '').strip()
@property
def view(self):
"""A list of view specs"""
spec = []
for k, v in six.iteritems(self._p4dict):
if k.startswith('view'):
match = RE_FILESPEC.search(v)
if match:
spec.append(FileSpec(v[:match.end() - 1], v[match.end():]))
return spec
@property
def access(self):
"""The date and time last accessed"""
return datetime.datetime.strptime(self._p4dict['access'], DATE_FORMAT)
@property
def update(self):
"""The date and time the client was updated"""
return datetime.datetime.strptime(self._p4dict['update'], DATE_FORMAT)