Python API 2.0 Reference
python/api2/modules/JSONPatternCreator.py
1 from builtins import object
2 import sys
3 import json
4 import maya.cmds as cmds
5 import maya.api.OpenMaya as omAPI
6 from pyJsonAttrPatternInfo import PyJsonAttrPatternInfo as JsonKeys
7 from pyJsonAttrPatternInfo import jsonDebug as jsonDebug
8 
9 #======================================================================
10 def attributeTypeName(node, attr):
11  """
12  Find the name of an attribute's type. Some translation is required since
13  some construction types don't exactly match the class type as returned
14  by the attributeQuery command.
15 
16  Returns None if the type is either unknown or not one of the ones this
17  script currently supports.
18  """
19  attributeType = cmds.attributeQuery( attr, node=node, attributeType=True )
20 
21  if attributeType in JsonKeys.kNumericTypes:
22  return attributeType
23 
24  if attributeType in JsonKeys.kTypeMatrix:
25  return attributeType
26 
27  if attributeType == JsonKeys.kTypeEnum:
28  return attributeType
29 
30  if attributeType == JsonKeys.kTypeMessage:
31  return attributeType
32 
33  if attributeType == JsonKeys.kTypeString:
34  return attributeType
35 
36  return None
37 
38 #======================================================================
39 class JSONPatternCreator(object):
40  """
41  Utility class to build JSON pattern objects from a set of attributes.
42  The most common use is to create a set of patterns from a node and
43  print them out using:
44  import json
45  import JSONPatternCreator
46  patternCreator = JSONPatternCreator.JSONPatternCreator()
47  jsonNodePattern = patternCreator.nodeAsJSON( mayaNodeName )
48  json.dumps( jsonNodePattern, sort_keys=True, indent=4, separators=(',', ': ') )
49  """
50 
51  def __init__(self):
52  """
53  Initialize the pattern creator.
54  """
55  self.node = None
56  self.dgNode = None
57  self.nodeType = None
58 
59  #======================================================================
60  def numericAttributeAsJSON(self, attr):
61  """
62  Find the numeric-specific attribute parameters
63  attr = Attribute belonging to the pattern
64  RETURN = JSON object representing the numeric-attribute-specific parameters
65  It's best to merge these into the main object one by one, rather
66  than making it a sub-object.
67  """
68  pattern = {}
69 
70  # Set default value(s)
71  defaultValue = cmds.attributeQuery( attr, node=self.node, listDefault=True )
72  if defaultValue != None and len(defaultValue) > 0:
73  pattern[JsonKeys.kKeyDefault] = defaultValue[0]
74 
75  # Set min/max values
76  if cmds.attributeQuery( attr, node=self.node, minExists=True ):
77  value = cmds.attributeQuery( attr, node=self.node, min=True )
78  if value != None:
79  if len(value) == 1:
80  pattern[JsonKeys.kKeyMin] = value[0]
81  elif len(value) > 1:
82  pattern[JsonKeys.kKeyMin] = value
83 
84  if cmds.attributeQuery( attr, node=self.node, maxExists=True ):
85  value = cmds.attributeQuery( attr, node=self.node, max=True )
86  if value != None:
87  if len(value) == 1:
88  pattern[JsonKeys.kKeyMax] = value[0]
89  elif len(value) > 1:
90  pattern[JsonKeys.kKeyMax] = value
91 
92  if cmds.attributeQuery( attr, node=self.node, softMinExists=True ):
93  value = cmds.attributeQuery( attr, node=self.node, softMin=True )
94  if value != None:
95  pattern[JsonKeys.kKeySoftMin] = value[0]
96 
97  if cmds.attributeQuery( attr, node=self.node, softMaxExists=True ):
98  value = cmds.attributeQuery( attr, node=self.node, softMax=True )
99  if value != None:
100  pattern[JsonKeys.kKeySoftMax] = value[0]
101 
102  return pattern
103 
104  #======================================================================
105  def compoundAttributeAsJSON(self, attr):
106  """
107  Find the compound-specific attribute parameters
108  attr = Attribute belonging to the pattern
109  RETURN = JSON object representing the compound-attribute-specific parameters
110  It's best to merge these into the main object one by one, rather
111  than making it a sub-object.
112  """
113  pattern = {}
114  # TODO:
115  return pattern
116 
117  #======================================================================
118  def lightDataAttributeAsJSON(self, attr):
119  """
120  Find the lightData-specific attribute parameters
121  attr = Attribute belonging to the pattern
122  RETURN = JSON object representing the lightData-attribute-specific parameters
123  It's best to merge these into the main object one by one, rather
124  than making it a sub-object.
125  """
126  pattern = {}
127  # TODO:
128  return pattern
129 
130  #======================================================================
131  def stringAttributeAsJSON(self, attr):
132  """
133  Find the string-specific attribute parameters
134  attr = Attribute belonging to the pattern
135  RETURN = JSON object representing the string-attribute-specific parameters
136  It's best to merge these into the main object one by one, rather
137  than making it a sub-object.
138  """
139  pattern = {}
140  stringDefault = cmds.attributeQuery( attr, node=self.node, listDefault=True )
141  if stringDefault != None and len(stringDefault) > 0:
142  pattern[JsonKeys.kKeyDefault] = stringDefault[0]
143  return pattern
144 
145  #======================================================================
146  def matrixAttributeAsJSON(self, attr):
147  """
148  Find the matrix-specific attribute parameters
149  attr = Attribute belonging to the pattern
150  RETURN = JSON object representing the matrix-attribute-specific parameters
151  It's best to merge these into the main object one by one, rather
152  than making it a sub-object.
153  """
154  pattern = {}
155  matrixDefault = cmds.attributeQuery( attr, node=self.node, listDefault=True )
156  if matrixDefault != None and len(matrixDefault) > 0:
157  pattern[JsonKeys.kKeyDefault] = matrixDefault[0]
158  return pattern
159 
160  #======================================================================
161  def typedAttributeAsJSON(self, attr):
162  """
163  Find the typed-specific attribute parameters
164  attr = Attribute belonging to the pattern
165  RETURN = JSON object representing the typed-attribute-specific parameters
166  It's best to merge these into the main object one by one, rather
167  than making it a sub-object.
168  """
169  pattern = {}
170  # TODO:
171  return pattern
172 
173  #======================================================================
174  def enumAttributeAsJSON(self, attr):
175  """
176  Find the enum-specific attribute parameters
177  attr = Attribute belonging to the pattern
178  RETURN = JSON object representing the enum-attribute-specific parameters
179  It's best to merge these into the main object one by one, rather
180  than making it a sub-object.
181  """
182  pattern = {}
183  enumDefault = cmds.attributeQuery( attr, node=self.node, listDefault=True )
184  if enumDefault != None and len(enumDefault) > 0:
185  pattern[JsonKeys.kKeyDefault] = int(enumDefault[0])
186  enumList = cmds.attributeQuery( attr, node=self.node, listEnum=True)
187  if enumList != None and len(enumList) > 0:
188  pattern[JsonKeys.kKeyEnumNames] = enumList[0].split(':')
189  return pattern
190 
191  #======================================================================
192  def attributeAsJSON(self, attr):
193  """
194  Convert the attribute into its JSON attribute pattern equivalent.
195  attr = Attribute belonging to the pattern
196  RETURN = JSON object representing the attribute
197  """
198  jsonDebug( 'attributeAsJSON(%s)' % attr)
199  jsonAttr = {}
200  jsonAttr['name'] = attr
201 
202  shortName = cmds.attributeQuery( attr, node=self.node, shortName=True )
203  # No need to write it if they two names are the same
204  if shortName != attr: jsonAttr['shortName'] = shortName
205 
206  niceName = cmds.attributeQuery( attr, node=self.node, niceName=True )
207  jsonAttr['niceName'] = niceName
208 
209  attributeType = attributeTypeName(self.node, attr)
210  jsonAttr['attributeType'] = attributeType
211  jsonDebug( '... type %s' % attributeType )
212 
213  categories = cmds.attributeQuery( attr, node=self.node, categories=True )
214  if categories != None and len(categories) > 0:
215  jsonAttr['categories'] = categories
216 
217  # Some flags default to false, some default to true, some are
218  # code-dependent. Force setting of the ones who have changed
219  # from their defaults, or whose defaults are unknown.
220  #
221  # Keep the list alphabetical for convenience
222  flagList = []
223  if cmds.attributeQuery( attr, node=self.node, affectsAppearance=True ):
224  flagList.append( 'affectsAppearance' )
225  if cmds.attributeQuery( attr, node=self.node, affectsWorldspace=True ):
226  flagList.append( 'affectsWorldspace' )
227  if not cmds.attributeQuery( attr, node=self.node, cachedInternally=True ):
228  flagList.append( '!cached' )
229  if not cmds.attributeQuery( attr, node=self.node, writable=True ):
230  flagList.append( '!canConnectAsDst' )
231  isReadable = True
232  if not cmds.attributeQuery( attr, node=self.node, readable=True ):
233  isReadable = False
234  flagList.append( '!canConnectAsSrc' )
235  if cmds.attributeQuery( attr, node=self.node, multi=True ):
236  flagList.append( 'array' )
237  # You can't set the indexMatters flag unless the attribute is an
238  # unreadable array attribute. (It might be set otherwise, but just
239  # by accident and the API doesn't support setting it incorrectly.)
240  if cmds.attributeQuery( attr, node=self.node, indexMatters=True ) and not isReadable:
241  flagList.append( 'indexMatters' )
242  if cmds.attributeQuery( attr, node=self.node, channelBox=True ):
243  flagList.append( 'channelBox' )
244  if not cmds.attributeQuery( attr, node=self.node, connectable=True ):
245  flagList.append( '!connectable' )
246  if cmds.attributeQuery( attr, node=self.node, hidden=True ):
247  flagList.append( 'hidden' )
248  if cmds.attributeQuery( attr, node=self.node, indeterminant=True ):
249  flagList.append( 'indeterminant' )
250  if cmds.attributeQuery( attr, node=self.node, internalSet=True ):
251  flagList.append( 'internal' )
252  if cmds.attributeQuery( attr, node=self.node, keyable=True ):
253  flagList.append( 'keyable' )
254  else:
255  flagList.append( '!keyable' )
256  if cmds.attributeQuery( attr, node=self.node, renderSource=True ):
257  flagList.append( 'renderSource' )
258  if not cmds.attributeQuery( attr, node=self.node, storable=True ):
259  flagList.append( '!storable' )
260  if cmds.attributeQuery( attr, node=self.node, usedAsColor=True ):
261  flagList.append( 'usedAsColor' )
262  if cmds.attributeQuery( attr, node=self.node, usedAsFilename=True ):
263  flagList.append( 'usedAsFilename' )
264  if cmds.attributeQuery( attr, node=self.node, usesMultiBuilder=True ):
265  flagList.append( 'usesArrayDataBuilder' )
266  if cmds.attributeQuery( attr, node=self.node, worldspace=True ):
267  flagList.append( 'worldspace' )
268 
269  # Write out the flag collection
270  if len(flagList) > 0:
271  jsonAttr['flags'] = flagList
272 
273  # Get attribute-type specific JSON parameters
274  extraInfo = None
275  if attributeType == 'enum':
276  jsonDebug( '... decoding enum attribute parameters' )
277  extraInfo = self.enumAttributeAsJSON(attr )
278  elif attributeType in JsonKeys.kNumericTypes:
279  jsonDebug( '... decoding numeric attribute parameters' )
280  extraInfo = self.numericAttributeAsJSON(attr )
281  elif attributeType in JsonKeys.kTypeMatrix:
282  jsonDebug( '... decoding matrix attribute parameters' )
283  extraInfo = self.matrixAttributeAsJSON(attr )
284  elif attributeType == 'string':
285  jsonDebug( '... decoding string attribute parameters' )
286  extraInfo = self.stringAttributeAsJSON(attr )
287  elif attributeType == 'message':
288  jsonDebug( '... decoding message attribute parameters' )
289  elif attributeType == 'compound':
290  jsonDebug( '... decoding compound attribute parameters' )
291  extraInfo = self.compoundAttributeAsJSON(attr )
292  elif attributeType == 'lightData':
293  jsonDebug( '... decoding lightData attribute parameters' )
294  extraInfo = self.lightDataAttributeAsJSON(attr )
295  elif attributeType == 'typed':
296  jsonDebug( '... decoding typed attribute parameters' )
297  extraInfo = self.typedAttributeAsJSON(attr )
298 
299  if extraInfo != None:
300  for extraKey in extraInfo:
301  jsonAttr[extraKey] = extraInfo[extraKey]
302 
303  return jsonAttr
304 
305  #======================================================================
306  def attributeListAsJSON(self, patternName, attrs):
307  """
308  Convert the list of attributes into a JSON attribute pattern object.
309  Any attributes not supported will be written out as an (ignored) JSON property
310  "unsupportedAttributes".
311  patternName = Name of the new pattern
312  attrs = Attributes belonging to the pattern
313  RETURN = JSON object containing the pattern for the attribute list
314  """
315  # Firewall to ignore bad input
316  if patternName == None or len(patternName) == 0:
317  raise ValueError( 'Pattern name cannot be empty' )
318  if attrs == None or len(attrs) == 0:
319  return None
320 
321  unsupportedAttrs = []
322  supportedAttrs = []
323  pattern = {}
324  pattern["name"] = patternName
325 
326  for attr in attrs:
327  attrType = attributeTypeName( self.node, attr )
328  if attrType != None:
329  supportedAttrs.append( attr )
330  else:
331  unsupportedAttrs.append( '%s:%s' % (attr, cmds.attributeQuery( attr, node=self.node, attributeType=True )) )
332  unsupportedAttrs.sort()
333  supportedAttrs.sort()
334 
335  if len(unsupportedAttrs) > 0:
336  pattern["unsupportedAttributes"] = unsupportedAttrs
337 
338  attrPatternList = []
339  for attr in supportedAttrs:
340  attrPatternList.append( self.attributeAsJSON( attr ) )
341  pattern["attributes"] = attrPatternList
342 
343  return pattern
344 
345  #======================================================================
346  def nodeAsJSON(self, node):
347  """
348  node = Node based on which the pattern is to be created
349  RETURN = JSON object containing patterns for all node attributes
350 
351  Method to walk through the list of attributes in a node and return the
352  supported types out in a JSON attribute pattern format. The returned
353  object can then be written out to a file or processed directly as a
354  JSON attribute pattern.
355 
356  There will be up to three attribute patterns specified in the object,
357  one for each of static, dynamic, and extension attributes. Each of the
358  patterns is only created if there is at least one attribute of that type.
359 
360  The names of the patterns for a node named "NODE" will be:
361  dynamic_NODE
362  static_NODE
363  extension_NODE
364  You really don't need me to explain which is which do you?
365  """
366  self.node = node # Need this local for getting attribute info
367  self.dgNode = omAPI.MSelectionList().add(node).getDependNode(0)
368  jsonDebug( 'Getting node information from %s' % str(self.dgNode) )
369  patternList = []
370 
371  try:
372  dynamicAttributes = cmds.listAttr( node, userDefined=True )
373  extensionAttributes = cmds.listAttr( node, extension=True )
374  allAttributes = cmds.listAttr( node )
375  staticAttributes = [attr for attr in allAttributes if not attr in extensionAttributes and not attr in dynamicAttributes]
376  #----------------------------------------------------------------------
377  # Add the static attribute pattern to the string if there are any
378  newPattern = self.attributeListAsJSON( "static_%s" % node, staticAttributes )
379  if newPattern != None:
380  patternList.append( newPattern )
381 
382  #----------------------------------------------------------------------
383  # Add the dynamic attribute pattern to the string if there are any
384  newPattern = self.attributeListAsJSON( "dynamic_%s" % node, dynamicAttributes )
385  if newPattern != None:
386  patternList.append( newPattern )
387 
388  #----------------------------------------------------------------------
389  # Add the extension attribute pattern to the string if there are any
390  newPattern = self.attributeListAsJSON( "extension_%s" % node, extensionAttributes )
391  if newPattern != None:
392  patternList.append( newPattern )
393 
394  except Exception as e:
395  print('ERR: Failed pattern creation on node %s (%s)' % (node, str(e)))
396 
397  # The pattern should be all built up now
398  return patternList
399 
400  #======================================================================
401  def test(self):
402  """
403  Run an internal consistency test on the pattern creator to verify its
404  functions are operating correctly.
405  """
406  factoryIsLoaded = cmds.pluginInfo('pyJsonAttrPatternFactory.py', query=True, loaded=True)
407  if not factoryIsLoaded:
408  try:
409  cmds.loadPlugin('pyJsonAttrPatternFactory.py', quiet=True)
410  except:
411  # Repotest environment won't find it so skip it then
412  return False
413  factoryIsLoaded = cmds.pluginInfo('pyJsonAttrPatternFactory.py', query=True, loaded=True)
414  # If the environment isn't set up to find the plugin there's
415  # nothing we can do about it. It's not a failure of the test so
416  # don't report anything other than a warning that the test could
417  # not be run.
418  if not factoryIsLoaded:
419  print('Warning: JSON attribute pattern factory could not be loaded, test aborted')
420  return False
421 
422  patterns = """
423  [
424  {
425  "name": "testPattern",
426  "attributes": [
427  {
428  "name" : "floatWithRanges",
429  "shortName" : "fwr",
430  "defaultValue" : 0.5,
431  "min" : -10.0,
432  "max" : 20.0,
433  "softMin" : 1.0,
434  "softMax" : 10.0,
435  "attributeType" : "float"
436  } ,
437  {
438  "name" : "float3WithRanges",
439  "shortName" : "ftwr",
440  "defaultValue" : [7.5, 7.6, 7.7],
441  "min" : [-17.0, -17.1, -17.2],
442  "max" : [27.0, 27.1, 27.2],
443  "attributeType" : "float3"
444  }
445  ]
446  }
447  ]
448  """
449  cmds.createAttrPatterns( patternType='json', patternDefinition=patterns )
450  cmds.file( force=True, new=True )
451  node = cmds.createNode( 'addMatrix' )
452  cmds.applyAttrPattern( node, patternName='testPattern' )
453 
454  jsonString = self.nodeAsJSON( node )
455  print(json.dumps(jsonString, indent=4, separators=(',', ': ')))
456