Coverage for src/P4OO/_P4PythonSchema.py: 83%

189 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-09-07 17:17 +0000

1###################################################################### 

2# Copyright (c)2024 David L. Armstrong. 

3# 

4# P4OO._P4PythonSchema.py 

5# 

6###################################################################### 

7 

8""" 

9Provide a set of objects that allow for interaction and translation 

10between P4Python output and our internally maintained Python-friendly 

11versions of Spec objects and Command Output. 

12""" 

13 

14import os 

15import re 

16from datetime import datetime 

17 

18import yaml 

19from P4OO._SpecObj import _P4OOSpecObj 

20from P4OO.Exceptions import P4OOFatal 

21 

22 

23class _P4OOP4PythonSchema(): 

24 """ Class to abstract the contents of our p4Config.yml file, 

25 separate from the execution of the p4 commands done in 

26 P4OO._P4Python._P4OOP4Python 

27 """ 

28 

29 def __init__(self, configFile=None): 

30 self.configFile = configFile 

31 if self.configFile is None: 

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

33 

34 self.schemaCommands = self.readSchema(self.configFile) 

35 

36 def readSchema(self, configFile): 

37 """ Read in our YAML p4Config.yml file and return a dict of 

38 supported commands as _P4OOP4PythonCommand objects 

39 """ 

40 

41 with open(configFile, 'r', encoding="utf-8") as stream: 

42 data = yaml.load(stream, Loader=yaml.Loader) 

43 

44 return {command: _P4OOP4PythonCommand(command=command, 

45 commandDict=commandDict) 

46 for (command, commandDict) in data["COMMANDS"].items()} 

47 

48 def getCmd(self, cmdName): 

49 """ Return the schema definition for a given command 

50 """ 

51 

52 if cmdName in self.schemaCommands: 

53 return self.schemaCommands[cmdName] 

54 

55 raise P4OOFatal("Unsupported Command " + cmdName) 

56 

57 def getSpecCmd(self, specType): 

58 """ Return the schema definition for a given command if it's 

59 a Perforce Spec type 

60 """ 

61 

62 cmdObj = self.getCmd(specType) 

63 

64 if cmdObj.isSpecCommand(): 

65 return self.schemaCommands[specType] 

66 

67 raise P4OOFatal("Unsupported Spec type %s" % specType) 

68 

69 

70class _P4OOP4PythonCommand(): 

71 

72 def __init__(self, command=None, commandDict=None): 

73 self.command = command 

74 self.commandDict = commandDict 

75 

76 def isSpecCommand(self): 

77 if 'specCmd' in self.commandDict: 

78 return True 

79 

80 return False 

81 

82 def isForcible(self): 

83 if 'forceOption' in self.commandDict: 

84 return True 

85 

86 return False 

87 

88 def isIdRequired(self): 

89 

90 return self.commandDict['idRequired'] 

91 

92 def getSpecCmd(self): 

93 

94 if self.isSpecCommand(): 

95 return self.commandDict['specCmd'] 

96 

97 raise P4OOFatal("Unsupported Spec type %s" % self.command) 

98 

99 def getOutputType(self): 

100 if 'output' in self.commandDict and 'p4ooType' \ 

101 in self.commandDict['output']: 

102 return self.commandDict['output']['p4ooType'] 

103 

104 return None 

105 

106 def getOutputIdAttr(self): 

107 if 'output' in self.commandDict and 'idAttr' \ 

108 in self.commandDict['output']: 

109 return self.commandDict['output']['idAttr'] 

110 

111 return None 

112 

113 def getPyIdAttribute(self): 

114 

115 if self.isSpecCommand(): 

116 return self.commandDict['idAttr'] 

117 

118 raise P4OOFatal("Unsupported Spec type %s" % self.command) 

119 

120 def getP4IdAttribute(self): 

121 

122 pyIdAttr = self.getPyIdAttribute() 

123 

124 return self.commandDict['specAttrs'][pyIdAttr] 

125 

126 def translateP4SpecToPython(self, p4OutputSpec): 

127 """ Translate a P4Python provided dictionary into something more 

128 Python-friendly. 

129 

130 P4Python is pretty lazy and only outputs string versions of 

131 spec attributes. We'll do integer and datetime conversion of 

132 attributes specified by the translation configuration. 

133 

134 As defined in the config, we'll rename attributes to internally 

135 consistent names, and we'll do any necessary type conversion. 

136 """ 

137 

138 if not self.isSpecCommand(): 

139 raise P4OOFatal("Unsupported Spec type %s" % self.command) 

140 

141 pythonSpec = {} 

142 

143 # Selectively copy p4OutputSpec attrs to mutable spec 

144 if 'specAttrs' in self.commandDict: 

145 for specAttr in self.commandDict['specAttrs']: 

146 p4SpecAttr = self.commandDict['specAttrs'][specAttr] 

147 if p4SpecAttr in p4OutputSpec: 

148 if p4SpecAttr == "Change": 

149 pythonSpec[specAttr] = int(p4OutputSpec[p4SpecAttr]) 

150 else: 

151 pythonSpec[specAttr] = p4OutputSpec[p4SpecAttr] 

152 

153 # Reformat date strings in Perforce objects to be more useful 

154 # datetime objects. 

155 # Date attrs cannot be modified, so don't need to be selectively copied 

156 if 'dateAttrs' in self.commandDict: 

157 for dateAttr in self.commandDict['dateAttrs']: 

158 p4DateAttr = self.commandDict['dateAttrs'][dateAttr] 

159 

160 if p4DateAttr in p4OutputSpec: 

161 if re.match(r'^\d+$', p4OutputSpec[p4DateAttr]): 

162 # some query commands return epoch seconds output 

163 # (e.g. clients) 

164 pythonSpec[dateAttr] = datetime.fromtimestamp( 

165 float(p4OutputSpec[p4DateAttr])) 

166 else: 

167 # spec commands return formatted date strings 

168 # local to the server, not the client 

169 pythonSpec[dateAttr] = datetime.strptime( 

170 p4OutputSpec[p4DateAttr], '%Y/%m/%d %H:%M:%S') 

171 

172 return pythonSpec 

173 

174 def translatePySpecToP4(self, pythonSpec, p4SpecDict): 

175 """ Copy any modified non-date attributes to the Perforce-generated 

176 dictionary ignoring any date attribtues we don't modify 

177 """ 

178 

179 if p4SpecDict is None: 

180 p4SpecDict = {} 

181 

182 if pythonSpec is not None and 'specAttrs' in self.commandDict: 

183 

184 for (specAttr, p4SpecAttr) \ 

185 in self.commandDict['specAttrs'].items(): 

186 if specAttr in pythonSpec: 

187 if pythonSpec[specAttr] is None: 

188 if p4SpecAttr in p4SpecDict: 

189 del p4SpecDict[p4SpecAttr] 

190 else: 

191 p4SpecDict[p4SpecAttr] = pythonSpec[specAttr] 

192 

193 return p4SpecDict 

194 

195 def getAllowedFilters(self): 

196 if 'queryOptions' in self.commandDict: 

197 return self.commandDict['queryOptions'] 

198 

199 return None 

200 

201 def getAllowedConfigs(self): 

202 if 'configOptions' in self.commandDict: 

203 return self.commandDict['configOptions'] 

204 

205 return None 

206 

207 def validateQuery(self, queryDict): 

208 """ Take a dict of name=[list] args and separate out p4 config 

209 arguments from command arguments. 

210 

211 Validate each argument and its list of values, convert the 

212 list of values to p4 commandline string arguments as needed (if 

213 they are P4OO_ objects), and return the validated configuration 

214 and commandline arguments. 

215 """ 

216 

217 allowedFilters = self.getAllowedFilters() 

218 if allowedFilters is None: 

219 raise P4OOFatal("Querying not supported for Command " 

220 + self.command) 

221 

222 allowedConfigs = self.getAllowedConfigs() or {} 

223 

224 execArgs = [] 

225 p4Config = {} 

226 

227 for (origFilterKey, queryValue) in queryDict.items(): 

228 

229 # None is used to remove options 

230 if queryValue is None: 

231 continue 

232 

233 

234 lcFilterKey = origFilterKey.lower() 

235 

236 # Separate global configuration options from query options 

237 optionConfig = None 

238 isConfigOpt = False 

239 if lcFilterKey in allowedConfigs: 

240 optionConfig = allowedConfigs[lcFilterKey] 

241 isConfigOpt = True 

242 elif lcFilterKey in allowedFilters: 

243 optionConfig = allowedFilters[lcFilterKey] 

244 isConfigOpt = False 

245 else: 

246 raise P4OOFatal("Invalid Filter key: " + origFilterKey) 

247 

248 optionArgs = [] 

249 if isinstance(queryValue, (_P4OOSpecObj, int, str)): 

250 optionArgs.append(queryValue) 

251 else: 

252 optionArgs.extend(queryValue) 

253 

254 # Check option argument types, and replace option args with 

255 # IDs for P4::OO objects passed in. 

256 # Take the opportunity to expand any Set objects we find. 

257 cmdOptionArgs = self.getOptionArgs(optionConfig=optionConfig, 

258 optionArgs=optionArgs, 

259 origFilterKey=origFilterKey) 

260 

261# print("optionConfig: ", optionConfig) 

262 # defined cmdline options go at the front 

263 if 'multiplicity' in optionConfig: 

264 

265 if len(cmdOptionArgs) != optionConfig['multiplicity']: 

266 raise P4OOFatal("Filter key: %s accepts %d arguments. %d provided.\n" 

267 % (origFilterKey, 

268 optionConfig['multiplicity'], 

269 len(cmdOptionArgs))) 

270 

271 if isConfigOpt: 

272 p4Config[optionConfig['option']] = True 

273 continue 

274 

275 if optionConfig['multiplicity'] == 0: 

276 execArgs.insert(0, optionConfig['option']) 

277 continue 

278 

279 if optionConfig['multiplicity'] == 1: 

280 if 'bundledArgs' in optionConfig and optionConfig['bundledArgs'] is not None: 

281 # join the option and its args into one string ala "-j8" 

282 bundledArg = optionConfig['option'] + "".join(cmdOptionArgs) 

283 execArgs.insert(0, bundledArg) 

284 continue 

285# TODO - ignoring p4Config here because it won't be needed... I think 

286 

287 # "unshift" one at a time in reverse order 

288 for arg in reversed(cmdOptionArgs): 

289 execArgs.insert(0, arg) 

290 execArgs.insert(0, optionConfig['option']) 

291 continue 

292 

293 if 'option' in optionConfig: 

294# TODO - ignoring p4Config here because it won't be needed... I think 

295 execArgs.append(optionConfig['option']) 

296 

297 execArgs.extend(cmdOptionArgs) 

298 

299 return (execArgs, p4Config) 

300 

301 

302 def getOptionArgs(self, optionConfig, optionArgs, origFilterKey): 

303 """ For a given option, verify all passed in arguments match 

304 valid types for the option according to the schema config. 

305 """ 

306 

307 cmdOptionArgs = [] 

308 if optionConfig is None: 

309 return cmdOptionArgs 

310 

311# 'queryOptions': { 'user': { 'type': [ 'string', 

312# 'P4OO.User.User', 

313# ], 

314# 'option': '-u', 

315# 'multiplicity': 1, 

316# } 

317 

318 for optionArg in optionArgs: 

319 matchedType = False 

320 

321 if 'type' not in optionConfig: 

322 matchedType = True 

323 continue 

324 

325 for checkType in optionConfig['type']: 

326 

327 if checkType == "string": 

328 if isinstance(optionArg, str): 

329 cmdOptionArgs.append(optionArg) 

330 matchedType = True 

331 break 

332 continue 

333 

334 if checkType == "integer": 

335 if isinstance(optionArg, int): 

336 cmdOptionArgs.append(optionArg) 

337 matchedType = True 

338 break 

339 continue 

340 

341 # Must be a P4OO type! To check P4OO types, we need to import. 

342 p4ooOptionArgs = self.getP4ooTypeOptionArgs(checkType, optionArg) 

343 if p4ooOptionArgs is not None: 

344 cmdOptionArgs.extend(p4ooOptionArgs) 

345 matchedType = True 

346 break 

347 

348 if not matchedType: 

349 # Looped through all types, didn't find a match 

350 raise P4OOFatal("Got %r, but filter key '%s'" 

351 % (optionArg, origFilterKey) 

352 + " accepts arguments of only these types: " 

353 + ", ".join(optionConfig['type'])) 

354 

355 return cmdOptionArgs 

356 

357 

358 def getP4ooTypeOptionArgs(self, checkType, optionArg): 

359 """ If an option type that might be a P4OO type, do the following: 

360 - dynamically import the appropriate P4OO module 

361 - validate that the type matches the expected type 

362 - return the enumeration of the optionArg's objects 

363 """ 

364 

365 # First, break down setType/SpecType from the checkType to perform the import 

366 m = re.match(r'^(.+)Set$', checkType) 

367 if m: 

368 specType = m.group(1) 

369 setType = checkType 

370 else: 

371 specType = checkType 

372 setType = checkType + "Set" 

373 

374 # Second, import specType and SetType 

375# TODO need to look into this issue 

376# specModule = __import__("P4OO." + specType, 

377# globals(), locals(), 

378# ["P4OO" + specType, "P4OO" + setType], -1) 

379 specModule = __import__("P4OO." + specType, 

380 globals(), locals(), 

381 ["P4OO" + specType, "P4OO" + setType], 0) 

382 

383 specClass = getattr(specModule, "P4OO" + specType) 

384 setClass = getattr(specModule, "P4OO" + setType) 

385 

386 # Third, do the actual type check and append optionArgs as appropriate 

387 if checkType == setType and isinstance(optionArg, setClass): 

388 # Special Set expansion...this gets weird, eh? 

389 return optionArg.listObjectIDs() 

390 

391 if checkType == specType and isinstance(optionArg, specClass): 

392 # Wrap it in a list just to return something consistent 

393 return [ optionArg._uniqueID() ] 

394 

395 return None