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
« 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######################################################################
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"""
14import os
15import re
16from datetime import datetime
18import yaml
19from P4OO._SpecObj import _P4OOSpecObj
20from P4OO.Exceptions import P4OOFatal
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 """
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"
34 self.schemaCommands = self.readSchema(self.configFile)
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 """
41 with open(configFile, 'r', encoding="utf-8") as stream:
42 data = yaml.load(stream, Loader=yaml.Loader)
44 return {command: _P4OOP4PythonCommand(command=command,
45 commandDict=commandDict)
46 for (command, commandDict) in data["COMMANDS"].items()}
48 def getCmd(self, cmdName):
49 """ Return the schema definition for a given command
50 """
52 if cmdName in self.schemaCommands:
53 return self.schemaCommands[cmdName]
55 raise P4OOFatal("Unsupported Command " + cmdName)
57 def getSpecCmd(self, specType):
58 """ Return the schema definition for a given command if it's
59 a Perforce Spec type
60 """
62 cmdObj = self.getCmd(specType)
64 if cmdObj.isSpecCommand():
65 return self.schemaCommands[specType]
67 raise P4OOFatal("Unsupported Spec type %s" % specType)
70class _P4OOP4PythonCommand():
72 def __init__(self, command=None, commandDict=None):
73 self.command = command
74 self.commandDict = commandDict
76 def isSpecCommand(self):
77 if 'specCmd' in self.commandDict:
78 return True
80 return False
82 def isForcible(self):
83 if 'forceOption' in self.commandDict:
84 return True
86 return False
88 def isIdRequired(self):
90 return self.commandDict['idRequired']
92 def getSpecCmd(self):
94 if self.isSpecCommand():
95 return self.commandDict['specCmd']
97 raise P4OOFatal("Unsupported Spec type %s" % self.command)
99 def getOutputType(self):
100 if 'output' in self.commandDict and 'p4ooType' \
101 in self.commandDict['output']:
102 return self.commandDict['output']['p4ooType']
104 return None
106 def getOutputIdAttr(self):
107 if 'output' in self.commandDict and 'idAttr' \
108 in self.commandDict['output']:
109 return self.commandDict['output']['idAttr']
111 return None
113 def getPyIdAttribute(self):
115 if self.isSpecCommand():
116 return self.commandDict['idAttr']
118 raise P4OOFatal("Unsupported Spec type %s" % self.command)
120 def getP4IdAttribute(self):
122 pyIdAttr = self.getPyIdAttribute()
124 return self.commandDict['specAttrs'][pyIdAttr]
126 def translateP4SpecToPython(self, p4OutputSpec):
127 """ Translate a P4Python provided dictionary into something more
128 Python-friendly.
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.
134 As defined in the config, we'll rename attributes to internally
135 consistent names, and we'll do any necessary type conversion.
136 """
138 if not self.isSpecCommand():
139 raise P4OOFatal("Unsupported Spec type %s" % self.command)
141 pythonSpec = {}
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]
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]
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')
172 return pythonSpec
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 """
179 if p4SpecDict is None:
180 p4SpecDict = {}
182 if pythonSpec is not None and 'specAttrs' in self.commandDict:
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]
193 return p4SpecDict
195 def getAllowedFilters(self):
196 if 'queryOptions' in self.commandDict:
197 return self.commandDict['queryOptions']
199 return None
201 def getAllowedConfigs(self):
202 if 'configOptions' in self.commandDict:
203 return self.commandDict['configOptions']
205 return None
207 def validateQuery(self, queryDict):
208 """ Take a dict of name=[list] args and separate out p4 config
209 arguments from command arguments.
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 """
217 allowedFilters = self.getAllowedFilters()
218 if allowedFilters is None:
219 raise P4OOFatal("Querying not supported for Command "
220 + self.command)
222 allowedConfigs = self.getAllowedConfigs() or {}
224 execArgs = []
225 p4Config = {}
227 for (origFilterKey, queryValue) in queryDict.items():
229 # None is used to remove options
230 if queryValue is None:
231 continue
234 lcFilterKey = origFilterKey.lower()
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)
248 optionArgs = []
249 if isinstance(queryValue, (_P4OOSpecObj, int, str)):
250 optionArgs.append(queryValue)
251 else:
252 optionArgs.extend(queryValue)
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)
261# print("optionConfig: ", optionConfig)
262 # defined cmdline options go at the front
263 if 'multiplicity' in optionConfig:
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)))
271 if isConfigOpt:
272 p4Config[optionConfig['option']] = True
273 continue
275 if optionConfig['multiplicity'] == 0:
276 execArgs.insert(0, optionConfig['option'])
277 continue
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
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
293 if 'option' in optionConfig:
294# TODO - ignoring p4Config here because it won't be needed... I think
295 execArgs.append(optionConfig['option'])
297 execArgs.extend(cmdOptionArgs)
299 return (execArgs, p4Config)
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 """
307 cmdOptionArgs = []
308 if optionConfig is None:
309 return cmdOptionArgs
311# 'queryOptions': { 'user': { 'type': [ 'string',
312# 'P4OO.User.User',
313# ],
314# 'option': '-u',
315# 'multiplicity': 1,
316# }
318 for optionArg in optionArgs:
319 matchedType = False
321 if 'type' not in optionConfig:
322 matchedType = True
323 continue
325 for checkType in optionConfig['type']:
327 if checkType == "string":
328 if isinstance(optionArg, str):
329 cmdOptionArgs.append(optionArg)
330 matchedType = True
331 break
332 continue
334 if checkType == "integer":
335 if isinstance(optionArg, int):
336 cmdOptionArgs.append(optionArg)
337 matchedType = True
338 break
339 continue
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
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']))
355 return cmdOptionArgs
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 """
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"
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)
383 specClass = getattr(specModule, "P4OO" + specType)
384 setClass = getattr(specModule, "P4OO" + setType)
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()
391 if checkType == specType and isinstance(optionArg, specClass):
392 # Wrap it in a list just to return something consistent
393 return [ optionArg._uniqueID() ]
395 return None