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

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###################################################################### 

8 

9import os 

10import re 

11from dataclasses import dataclass, field 

12 

13# P4Python 

14from P4 import P4, P4Exception, Spec 

15 

16from P4OO.Exceptions import P4OOFatal, P4Fatal, P4Warning 

17from P4OO._Connection import _P4OOConnection 

18from P4OO._SpecObj import _P4OOSpecObj 

19from P4OO._P4PythonSchema import _P4OOP4PythonSchema 

20 

21@dataclass 

22class _P4OOP4Python(_P4OOConnection): 

23 

24 def __post_init__(self): 

25 self._ownP4PythonObj = None 

26 

27 def readCounter(self, counterName): 

28 """ Read the named counter from Perforce and return the value. """ 

29 

30 # Make sure we've read in the config file 

31 self._initialize() 

32 

33 p4Output = self._execCmd("counter", counterName) 

34 try: 

35 return int(p4Output[0]['value']) 

36 except ValueError: 

37 return p4Output[0]['value'] 

38 

39 def setCounter(self, counterName, newValue): 

40 """ Set the named counter in Perforce. """ 

41 

42 # Make sure we've read in the config file 

43 self._initialize() 

44 

45 p4Output = self._execCmd("counter", counterName, newValue) 

46 try: 

47 return int(p4Output[0]['value']) 

48 except ValueError: 

49 return p4Output[0]['value'] 

50 

51 def refreshSpec(self, specObj): 

52 """ Clear the cached objects and modifiedSpec and re-read spec 

53 from Perforce. 

54 

55 Any changes made via _setSpecAttr will be lost! 

56 """ 

57 

58 specObj._p4SpecObj = None 

59 specObj._modifiedSpec = None 

60 self.readSpec(specObj) 

61 

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. 

66 

67 If the spec has already been read and is present, no action 

68 is taken. 

69 """ 

70 

71 # Make sure we've read in the config file 

72 self._initialize() 

73 

74 specType = specObj._SPECOBJ_TYPE 

75 specID = specObj.id 

76 p4SpecObj = specObj._p4SpecObj 

77 modifiedSpec = specObj._modifiedSpec 

78 

79 specCmdObj = self._p4PythonSchema.getSpecCmd(specType=specType) 

80 idAttr = specCmdObj.getPyIdAttribute() 

81 

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,)) 

91 

92 specCmd = specCmdObj.getSpecCmd() 

93 p4Output = self._execCmd(specCmd, "-o", specID) 

94 

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 

99 

100 p4Spec = dict(p4SpecObj) 

101 return self._generateModifiedSpec(specCmdObj, specObj, p4Spec) 

102 

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. 

108 

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 

116 

117 specID = specObj.id 

118 

119 # Here we take the spec from P4 and make it something useful 

120 modifiedSpec = specObj._modifiedSpec 

121 if modifiedSpec is None: 

122 modifiedSpec = {} 

123 

124 # merge specDict from P4Python and modifiedSpec 

125 pythonSpecDict = specCmdObj.translateP4SpecToPython(specDict) 

126 

127 for specAttr in pythonSpecDict: 

128 # ignore attributes already set by caller 

129 if specAttr not in modifiedSpec: 

130 modifiedSpec[specAttr] = pythonSpecDict[specAttr] 

131 

132 specObj._modifiedSpec = modifiedSpec 

133 

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] 

138 

139 return modifiedSpec 

140 

141 def saveSpec(self, specObj, force=False): 

142# TODO Document this... 

143 

144 # Start with readSpec to make sure we're in sync with the server 

145 self.readSpec(specObj) 

146 

147 specType = specObj._SPECOBJ_TYPE 

148 specID = specObj.id 

149 

150 specCmdObj = self._p4PythonSchema.getSpecCmd(specType=specType) 

151 specCmd = specCmdObj.getSpecCmd() 

152 

153 p4SpecObj = specObj._p4SpecObj 

154 modifiedSpec = specObj._modifiedSpec 

155 

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,)) 

160 

161 if p4SpecObj is None: 

162 # This must be a brand new spec 

163 p4SpecObj = Spec() 

164 

165 specCmdObj.translatePySpecToP4(modifiedSpec, p4SpecObj) 

166 

167 # If we need a specID, we need a specID... 

168 p4IdAttr = specCmdObj.getP4IdAttribute() 

169 

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" 

182 

183 p4Output = None 

184 if force: 

185 if not specCmdObj.isForcible(): 

186 raise P4OOFatal("Command %s doesn't support force" % 

187 (specCmd,)) 

188 

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) 

193 

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? 

206 

207 # refresh our object against the freshly saved spec to get updated 

208 # timestamps and so on 

209 self.refreshSpec(specObj) 

210 

211 return True 

212 

213 def deleteSpec(self, specObj, force=False): 

214 specType = specObj._SPECOBJ_TYPE 

215 specID = specObj.id 

216 

217 # Make sure we've read in the config file 

218 self._initialize() 

219 

220 specCmdObj = self._p4PythonSchema.getSpecCmd(specType=specType) 

221 idAttr = specCmdObj.getPyIdAttribute() 

222 specCmd = specCmdObj.getSpecCmd() 

223 

224 p4SpecObj = specObj._p4SpecObj 

225 modifiedSpec = specObj._modifiedSpec 

226 

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 

231 

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] 

240 

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 

247 

248 # refresh the local variables for spec after read 

249 specID = specObj.id 

250 p4SpecObj = specObj._p4SpecObj 

251 modifiedSpec = specObj._modifiedSpec 

252 

253 if p4SpecObj is None: 

254 # This must be a brand new spec... nothing to delete 

255 return False 

256 

257 # If we need a specID, we need a specID... 

258 p4IdAttr = specCmdObj.getP4IdAttribute() 

259 

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... 

268 

269 p4Output = None 

270 if force: 

271 if not specCmdObj.isForcible(): 

272 raise P4OOFatal("Command %s doesn't support force" % 

273 (specCmd,)) 

274 

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) 

279 

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) 

289 

290 return True 

291 

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) 

299 

300 # Make sure we've read in the config file 

301 self._initialize() 

302 

303 cmdObj = self._p4PythonSchema.getCmd(cmdName=cmdName) 

304 

305 (execArgs, p4Config) = cmdObj.validateQuery(query) 

306 

307 if rawOutput: 

308 # We also turn off tagged output when raw is requested! 

309 p4Config['tagged'] = 0 

310 

311# print("p4Config: ", p4Config ) 

312 p4Out = self._execCmd(cmdName, execArgs, **p4Config) 

313 

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# }, 

330 

331 # If no special output massaging is needed, we're done! 

332 

333 if rawOutput or cmdObj.getOutputType() is None: 

334 return p4Out 

335 

336 return self._parseOutput(cmdName, p4Out) 

337 

338 def _parseOutput(self, cmdName, p4Out): 

339 

340 # Make sure we've read in the config file 

341 self._initialize() 

342 cmdObj = self._p4PythonSchema.getCmd(cmdName=cmdName) 

343 

344 p4ooType = cmdObj.getOutputType() 

345 setType = p4ooType + "Set" 

346 idAttr = cmdObj.getOutputIdAttr() 

347# singularID = _P4Python._P4PYTHON_COMMAND_TRANSLATION[cmdName]['output']['singularID'] 

348 

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) 

359 

360 objectList = [] 

361 

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.") 

366 

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] 

371 

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)" ) 

380 

381 specObj = specClass() 

382 

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] 

390 

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]) 

398 

399 # Make sure each of these objects can reuse this connection too 

400 specObj._p4Conn = self 

401 objectList.append(specObj) 

402 

403 # Wrap it with a bow 

404 setObj = setClass() 

405 

406 setObj._p4Conn = self 

407 setObj.addObjects(objectList) 

408 return setObj 

409 

410 ###################################################################### 

411 # Internal Methods 

412 # 

413 def _execCmd(self, p4SubCmd, *args, **p4Config): 

414 

415 # We want this pretty much right from the start 

416 p4PythonObj = self._connect() 

417 

418 # copy the input tuple to a mutable list first. 

419 listArgs = list(args) 

420 

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] 

426 

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 

434 

435# if listArgs[0] == "-i": 

436# p4PythonObj.input = listArgs[1] 

437# self._logDebug("Setting Input:", p4PythonObj.input) 

438# listArgs = listArgs[2:] 

439 

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))) 

447 

448# TODO ping server before each command? 

449 self._logDebug("Executing:", p4SubCmd, listArgs) 

450 p4Out = p4PythonObj.run(p4SubCmd, listArgs) 

451 self._logDebug("p4Out: ", p4Out) 

452 

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]))) 

458 

459# TODO Should do something to detect disconnects, etc. 

460 

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) 

464 

465 if len(p4PythonObj.warnings) > 0: 

466 errMsg += "\nWARNING: " + "".join(p4PythonObj.warnings) 

467 

468 raise P4Fatal("P4 Command Failed:\n" + errMsg) 

469 

470 if len(p4PythonObj.warnings) > 0: 

471 warnMsg = "WARNING: " + "".join(p4PythonObj.warnings) 

472 raise P4Warning("P4 Command Warned:\n" + warnMsg) 

473 

474 return p4Out 

475 

476 def _connect(self): 

477 p4PythonObj = self.p4PythonObj 

478 

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 

486 

487 self.p4PythonObj = p4PythonObj 

488 self._ownP4PythonObj = 1 

489 

490 return p4PythonObj 

491 

492 def _disconnect(self): 

493 ownP4PythonObj = self._ownP4PythonObj 

494 

495 if ownP4PythonObj: 

496 # We instantiated the connection, so we'll tear it down too 

497 p4PythonObj = self.p4PythonObj 

498 

499 if p4PythonObj is not None: 

500 p4PythonObj.disconnect() 

501 

502 self._ownP4PythonObj = None 

503 self.p4PythonObj = None 

504 return True 

505 

506 def _initialize(self): 

507 configFile = os.path.dirname(__file__) + "/p4Config.yml" 

508 

509 self._p4PythonSchema = _P4OOP4PythonSchema(configFile=configFile) 

510 return True 

511 

512 def __del__(self): 

513 self._disconnect() 

514 return True