Coverage for src/P4OO/_P4Python.py: 83%
232 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-09-07 17:17 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-09-07 17:17 +0000
1######################################################################
2# Copyright (c)2011-2012,2024 David L. Armstrong.
3# Copyright (c)2012-2013, Cisco Systems, Inc.
4#
5# P4OO._P4Python.py
6#
7######################################################################
9import os
10import re
11from dataclasses import dataclass, field
13# P4Python
14from P4 import P4, P4Exception, Spec
16from P4OO.Exceptions import P4OOFatal, P4Fatal, P4Warning
17from P4OO._Connection import _P4OOConnection
18from P4OO._SpecObj import _P4OOSpecObj
19from P4OO._P4PythonSchema import _P4OOP4PythonSchema
21@dataclass
22class _P4OOP4Python(_P4OOConnection):
24 def __post_init__(self):
25 self._ownP4PythonObj = None
27 def readCounter(self, counterName):
28 """ Read the named counter from Perforce and return the value. """
30 # Make sure we've read in the config file
31 self._initialize()
33 p4Output = self._execCmd("counter", counterName)
34 try:
35 return int(p4Output[0]['value'])
36 except ValueError:
37 return p4Output[0]['value']
39 def setCounter(self, counterName, newValue):
40 """ Set the named counter in Perforce. """
42 # Make sure we've read in the config file
43 self._initialize()
45 p4Output = self._execCmd("counter", counterName, newValue)
46 try:
47 return int(p4Output[0]['value'])
48 except ValueError:
49 return p4Output[0]['value']
51 def refreshSpec(self, specObj):
52 """ Clear the cached objects and modifiedSpec and re-read spec
53 from Perforce.
55 Any changes made via _setSpecAttr will be lost!
56 """
58 specObj._p4SpecObj = None
59 specObj._modifiedSpec = None
60 self.readSpec(specObj)
62 def readSpec(self, specObj):
63 """ Query Perforce for the specified object's spec and load it
64 into the provided object doing any appropriate data conversions
65 along the way.
67 If the spec has already been read and is present, no action
68 is taken.
69 """
71 # Make sure we've read in the config file
72 self._initialize()
74 specType = specObj._SPECOBJ_TYPE
75 specID = specObj.id
76 p4SpecObj = specObj._p4SpecObj
77 modifiedSpec = specObj._modifiedSpec
79 specCmdObj = self._p4PythonSchema.getSpecCmd(specType=specType)
80 idAttr = specCmdObj.getPyIdAttribute()
82 # We only read if not already read. Use refreshSpec to re-read.
83 if p4SpecObj is None:
84 if specID is None and specCmdObj.isIdRequired():
85 if modifiedSpec is not None and idAttr in modifiedSpec:
86 specID = specObj.id = modifiedSpec[idAttr]
87 else:
88 # Nothing to do here, no id in any form from caller
89 raise P4OOFatal("Cannot identify %s object" %
90 (specType,))
92 specCmd = specCmdObj.getSpecCmd()
93 p4Output = self._execCmd(specCmd, "-o", specID)
95 # Since we muck with the Spec replacing date fields with
96 # datetime objects, we just flatten the objects.
97 p4SpecObj = p4Output[0]
98 specObj._p4SpecObj = p4SpecObj
100 p4Spec = dict(p4SpecObj)
101 return self._generateModifiedSpec(specCmdObj, specObj, p4Spec)
103 def _generateModifiedSpec(self, specCmdObj, specObj, specDict):
104 # Perforce will return an "empty" spec when a specified object isn't
105 # found so it can be handily created.
106 # We don't want that behavior here, so we throw an exception instead.
107 # We'll want to have a "createSpec" method for that kind of thing.
109# HACK - Specs that don't have 'Update' timestamp are specs that don't exist yet.
110# HACKHACK - change is exceptional in this regard. :)
111# HACKHACKHACK want to comment this out to leverage default specs, but other stuff breaks right now.
112# TODO fix this somehow
113# if specType is not 'change' and 'Update' not in p4Spec:
114# raise P4OOFatal(specType + ": " + str(specID) + " does not exist")
115# return None
117 specID = specObj.id
119 # Here we take the spec from P4 and make it something useful
120 modifiedSpec = specObj._modifiedSpec
121 if modifiedSpec is None:
122 modifiedSpec = {}
124 # merge specDict from P4Python and modifiedSpec
125 pythonSpecDict = specCmdObj.translateP4SpecToPython(specDict)
127 for specAttr in pythonSpecDict:
128 # ignore attributes already set by caller
129 if specAttr not in modifiedSpec:
130 modifiedSpec[specAttr] = pythonSpecDict[specAttr]
132 specObj._modifiedSpec = modifiedSpec
134 # We'll set the object's specID if it was not defined..if we can.
135 if specID is None:
136 idAttr = specCmdObj.getPyIdAttribute()
137 specObj.id = modifiedSpec[idAttr]
139 return modifiedSpec
141 def saveSpec(self, specObj, force=False):
142# TODO Document this...
144 # Start with readSpec to make sure we're in sync with the server
145 self.readSpec(specObj)
147 specType = specObj._SPECOBJ_TYPE
148 specID = specObj.id
150 specCmdObj = self._p4PythonSchema.getSpecCmd(specType=specType)
151 specCmd = specCmdObj.getSpecCmd()
153 p4SpecObj = specObj._p4SpecObj
154 modifiedSpec = specObj._modifiedSpec
156 # If there's no modified spec or ID, then there's nothing to save
157 if modifiedSpec is None and specID is None:
158 raise P4OOFatal("Cannot save %s object: Nothing to save" %
159 (specType,))
161 if p4SpecObj is None:
162 # This must be a brand new spec
163 p4SpecObj = Spec()
165 specCmdObj.translatePySpecToP4(modifiedSpec, p4SpecObj)
167 # If we need a specID, we need a specID...
168 p4IdAttr = specCmdObj.getP4IdAttribute()
170 if p4IdAttr in p4SpecObj:
171 # We'll set the object's specID if it was not defined..if we can.
172 if specID is None:
173 # At this point the SpecObj is initialized enough for
174 # this to work...
175 specObj.id = p4SpecObj[p4IdAttr]
176 else:
177 if specCmdObj.isIdRequired():
178 if specID is not None:
179 p4SpecObj[p4IdAttr] = specID
180 else:
181 p4SpecObj[p4IdAttr] = "new"
183 p4Output = None
184 if force:
185 if not specCmdObj.isForcible():
186 raise P4OOFatal("Command %s doesn't support force" %
187 (specCmd,))
189# TODO hardcoded forceoption should be config produced..
190 p4Output = self._execCmd(specCmd, "-i", p4SpecObj, "-f")
191 else:
192 p4Output = self._execCmd(specCmd, "-i", p4SpecObj)
194 # Since we know we're saving a spec, we can take some liberties
195 # with hardcoded parsing here.
196 if specID is None:
197 if specType == "change":
198 # parse p4Output for new change#
199 # ['Change 1 created.']
200 specID = re.search(r'Change (\d+) created',
201 p4Output[0]).group(1)
202# TODO...
203# print("specID: ", specID)
204 specObj.id = specID
205# TODO... other spec types that accept new?
207 # refresh our object against the freshly saved spec to get updated
208 # timestamps and so on
209 self.refreshSpec(specObj)
211 return True
213 def deleteSpec(self, specObj, force=False):
214 specType = specObj._SPECOBJ_TYPE
215 specID = specObj.id
217 # Make sure we've read in the config file
218 self._initialize()
220 specCmdObj = self._p4PythonSchema.getSpecCmd(specType=specType)
221 idAttr = specCmdObj.getPyIdAttribute()
222 specCmd = specCmdObj.getSpecCmd()
224 p4SpecObj = specObj._p4SpecObj
225 modifiedSpec = specObj._modifiedSpec
227 # If there's no modified spec or ID, then there's nothing to save
228 if modifiedSpec is None and specID is None:
229# TODO throw an exception?
230 return False
232 # If specObj isn't already initialized (it should be), then
233 # initialize it.
234 if p4SpecObj is None:
235 # We need specID first here to initialize empty spec properly..
236 # see if we have it in modifiedSpec
237 if specID is None and modifiedSpec is not None:
238 if idAttr in modifiedSpec:
239 specID = specObj.id = modifiedSpec[idAttr]
241 try:
242 self.readSpec(specObj)
243 except P4Exception:
244 # Ignore exceptions for objects that don't exist, we might
245 # be creating them here
246 pass
248 # refresh the local variables for spec after read
249 specID = specObj.id
250 p4SpecObj = specObj._p4SpecObj
251 modifiedSpec = specObj._modifiedSpec
253 if p4SpecObj is None:
254 # This must be a brand new spec... nothing to delete
255 return False
257 # If we need a specID, we need a specID...
258 p4IdAttr = specCmdObj.getP4IdAttribute()
260 if specID is None:
261 # We'll set the object's specID if it was not defined..if we can.
262 if p4IdAttr in p4SpecObj:
263 # At this point the SpecObj is initialized enough for this
264 # to work...
265 specObj.id = p4SpecObj[p4IdAttr]
266 specID = p4SpecObj[p4IdAttr]
267# TODO be throwing exceptions...
269 p4Output = None
270 if force:
271 if not specCmdObj.isForcible():
272 raise P4OOFatal("Command %s doesn't support force" %
273 (specCmd,))
275# TODO hardcoded forceoption should be config produced..
276 p4Output = self._execCmd(specCmd, "-d", "-f", specID)
277 else:
278 p4Output = self._execCmd(specCmd, "-d", specID)
280 # If we made it this far, nothing fatal happened inside Perforce,
281 # but spec was not necessarily deleted.
282# TODO I'm just guessing that all spec deletions follow this format
283# TODO of "^p4IdAttr specID (can't be )?deleted.$"
284 m = re.match(r'^%s %s (.*)deleted.$' % (re.escape(p4IdAttr),
285 re.escape(specID)),
286 p4Output[0])
287 if not m or m.group(1) != '':
288 raise P4OOFatal(p4Output)
290 return True
292 def runCommand(self, cmdName, rawOutput=False, **kwargs):
293 """ Wrapper around _execCmd that orchestrates validating the
294 commandline arguments from P4OO Spec/Set objects, executing
295 the command through P4Python, and parsing the returned output
296 back into P4OO Spec/Set objects.
297 """
298 query = dict(kwargs)
300 # Make sure we've read in the config file
301 self._initialize()
303 cmdObj = self._p4PythonSchema.getCmd(cmdName=cmdName)
305 (execArgs, p4Config) = cmdObj.validateQuery(query)
307 if rawOutput:
308 # We also turn off tagged output when raw is requested!
309 p4Config['tagged'] = 0
311# print("p4Config: ", p4Config )
312 p4Out = self._execCmd(cmdName, execArgs, **p4Config)
314# TODO... subcommands?
315# 'counter' => { 'specCmd' => 'counter',
316# 'singularID' => 'counter',
317# 'queryCmd' => 'counters',
318# 'pluralID' => 'counter',
319# 'idAttr' => 'counter',
320# p4 counter name
321# p4 counter [-f] name value
322# p4 counter [-f] -d name
323# p4 counter [-i] name
324#
325# subcommands:
326# increment
327# delete
328# set
329# },
331 # If no special output massaging is needed, we're done!
333 if rawOutput or cmdObj.getOutputType() is None:
334 return p4Out
336 return self._parseOutput(cmdName, p4Out)
338 def _parseOutput(self, cmdName, p4Out):
340 # Make sure we've read in the config file
341 self._initialize()
342 cmdObj = self._p4PythonSchema.getCmd(cmdName=cmdName)
344 p4ooType = cmdObj.getOutputType()
345 setType = p4ooType + "Set"
346 idAttr = cmdObj.getOutputIdAttr()
347# singularID = _P4Python._P4PYTHON_COMMAND_TRANSLATION[cmdName]['output']['singularID']
349 # Make sure the caller is properly equipped to use any objects
350 # we construct here.
351# specModule = __import__("P4OO." + p4ooType, globals(), locals(),
352# ["P4OO" + p4ooType, "P4OO" + setType], -1)
353 specModule = __import__("P4OO." + p4ooType, globals(), locals(),
354 ["P4OO" + p4ooType, "P4OO" + setType], 0)
355# setModule = __import__("P4OO." + setType, globals(), locals(),
356# ["P4OO" + setType], -1)
357 specClass = getattr(specModule, "P4OO" + p4ooType)
358 setClass = getattr(specModule, "P4OO" + setType)
360 objectList = []
362 # Don't really care about the content of the output, just the specIDs.
363 for p4OutHash in p4Out:
364 if idAttr not in p4OutHash:
365 raise P4OOFatal("Unexpected output from Perforce.")
367 # Copy the idAttr output value to the id attribute
368 # if they aren't one and the same already
369# if singularID is not idAttr:
370# p4OutHash[singularID] = p4OutHash[idAttr]
372 # HACK - Instead of eval'ing this through the type's
373 # constructor, we'll just use the base class and bless
374# # Construct the new spec dictionary, copying our own _p4Conn attribute
375# specAttrs = { 'p4Spec': p4OutHash,
376# 'id': p4OutHash[idAttr],
377# '_p4Conn': self,
378# }
379# specObj = eval( singularType + "(specAttrs)" )
381 specObj = specClass()
383# TODO - figure out P4.Spec objects
384# specObj._setAttr('p4Spec', Spec(p4OutHash))
385# TODO - need to fix this special case for Change - need better construction logic
386 if p4ooType == 'Change':
387 specObj.id = int(p4OutHash[idAttr])
388 else:
389 specObj.id = p4OutHash[idAttr]
391# TODO - This is a little awkward...
392 if isinstance(specObj, _P4OOSpecObj):
393 specCmdObj = self._p4PythonSchema.getSpecCmd(
394 specType=specObj._SPECOBJ_TYPE)
395 self._generateModifiedSpec(specCmdObj, specObj, p4OutHash)
396# specObj._setAttr('modifiedSpec', p4OutHash)
397# self._logDebug( "id: ", p4OutHash[idAttr])
399 # Make sure each of these objects can reuse this connection too
400 specObj._p4Conn = self
401 objectList.append(specObj)
403 # Wrap it with a bow
404 setObj = setClass()
406 setObj._p4Conn = self
407 setObj.addObjects(objectList)
408 return setObj
410 ######################################################################
411 # Internal Methods
412 #
413 def _execCmd(self, p4SubCmd, *args, **p4Config):
415 # We want this pretty much right from the start
416 p4PythonObj = self._connect()
418 # copy the input tuple to a mutable list first.
419 listArgs = list(args)
421# TODO - listArgs is an immutable tuple in P4Python...
422 if len(listArgs) > 0:
423 # First strip undef args from the tail, P4PERL don't like them
424 while listArgs[-1] is None or listArgs[-1] == "":
425 del listArgs[-1]
427 # Next look for a '-i' arg for setting input and extract the arg
428 try:
429 inputIndex = listArgs.index("-i")
430 p4PythonObj.input = listArgs[inputIndex+1]
431 listArgs = listArgs[:inputIndex+1]+listArgs[inputIndex+2:]
432 except ValueError:
433 pass
435# if listArgs[0] == "-i":
436# p4PythonObj.input = listArgs[1]
437# self._logDebug("Setting Input:", p4PythonObj.input)
438# listArgs = listArgs[2:]
440 # override p4Python settings for this command as applicable
441 origConfig = {}
442 for (var, value) in p4Config.items():
443 origConfig[var] = p4PythonObj.__getattribute__(var)
444 p4PythonObj.__setattr__(var, value)
445 self._logDebug("overriding p4Config['%s'] = %s with %s"
446 % (var, str(origConfig[var]), str(value)))
448# TODO ping server before each command?
449 self._logDebug("Executing:", p4SubCmd, listArgs)
450 p4Out = p4PythonObj.run(p4SubCmd, listArgs)
451 self._logDebug("p4Out: ", p4Out)
453 # restore p4Python settings changed for this command only
454 for var in p4Config:
455 p4PythonObj.__setattr__(var, origConfig[var])
456 self._logDebug("resetting p4Config['%s'] = %s"
457 % (var, str(origConfig[var])))
459# TODO Should do something to detect disconnects, etc.
461 # If we have errors and warnings, we want to give both to caller
462 if len(p4PythonObj.errors) > 0:
463 errMsg = "ERROR: " + "".join(p4PythonObj.errors)
465 if len(p4PythonObj.warnings) > 0:
466 errMsg += "\nWARNING: " + "".join(p4PythonObj.warnings)
468 raise P4Fatal("P4 Command Failed:\n" + errMsg)
470 if len(p4PythonObj.warnings) > 0:
471 warnMsg = "WARNING: " + "".join(p4PythonObj.warnings)
472 raise P4Warning("P4 Command Warned:\n" + warnMsg)
474 return p4Out
476 def _connect(self):
477 p4PythonObj = self.p4PythonObj
479 if p4PythonObj is None:
480 p4PythonObj = P4()
481 try:
482 p4PythonObj.connect()
483 p4PythonObj.exception_level = 0
484 except P4Exception as exc:
485 raise P4Fatal("P4 Connection Failed") from exc
487 self.p4PythonObj = p4PythonObj
488 self._ownP4PythonObj = 1
490 return p4PythonObj
492 def _disconnect(self):
493 ownP4PythonObj = self._ownP4PythonObj
495 if ownP4PythonObj:
496 # We instantiated the connection, so we'll tear it down too
497 p4PythonObj = self.p4PythonObj
499 if p4PythonObj is not None:
500 p4PythonObj.disconnect()
502 self._ownP4PythonObj = None
503 self.p4PythonObj = None
504 return True
506 def _initialize(self):
507 configFile = os.path.dirname(__file__) + "/p4Config.yml"
509 self._p4PythonSchema = _P4OOP4PythonSchema(configFile=configFile)
510 return True
512 def __del__(self):
513 self._disconnect()
514 return True