cosmogonies.net Blog of cosmogonies.net

18Apr/130

Shotgun (DAM) python object wrapper

shotgun

Asset Manager is a great way to fluidify your workflow.

As a DAM (Digital Asset Manager) has a fuzzy definition, I will quickly describe how I see Shotgun.
Shotgun does the boring thing you do not want to do when dealing with digital assets.
Shotgun types them, calling them Entity, record them and all kind of metadatas attached to them (string, float, date, entites, etc.. and lists), and call that Fields.
In facts, Shotgun is a web-page (internet or intranet), displaying in a very user-friendly way those datas (a lot more elaborated and cgi-oriented than a phpMyAdmin for example).
So basically, it is a back-end database (postGre), and a front-end browsing page.

What it does NOT, is storing files, or have any further connection with a filesystem but some string fields where you can put paths.
Now the new TANK extension of shotgun could do that, I will talk about it later.

So the method I present here is how deal very easily with shotgun entities as objects, manipulating very semantically with calls like

print myAsset.description, myScene.created_at, myScene.created_by

Using the analogy: Entity=Object, Field=Member of python class instances.

This is how I organized the package (ALK_ prefix stands for the studio I currently work, Alkymia):
ALK_Entity\ (Package name)
ALK_Entity\__init__.py (Main superclass to inherit from shotgun API)
ALK_Entity\Scenes.py (sub-classes definition, one representing the exact shotgun entity, the other to simply add your additional stuff. So the module name is the Entity name in plural.)
ALK_Entity\Shots.py
ALK_Entity\Assets.py
... #and so on

ShotgunWrapperUML

From this UML diagram on the left, you can see how the package hierarchy works.

With that in mind, you can graph easily a real network of all your assets.
You will obtain a dependency graph, of relationships as "needs" or "is a part of" that is really appreciated in object-oriented scripting. It is really powerful.

The dark side of it, as you may notice parsing my code, is that every entity is requested with all fields, which is not very performance.
Good usability versus good performance is often a very tricky balance to handle.
(Mistake already done: In my first code iteration, I had put the request of filed as entity into the constructor, not the attribute getter.
Result: any simple entity was bouncing all over dependencies and links, putting the whole f** database into RAM for one single asset^^.)

So, this is the final result looks like, and this is how I deal with our metadatas in the pipeline tools I made:

###
import ALK_Entity.Scenes

myScene = ALK_Entity.Scenes.ALK_Scene("S004_M001")
print type(myScene)  # <class 'ALK_Entity.Scenes.ALK_Scene'>
print myScene.id, myScene.code  # 794 S004_M001

print myScene.sg_sequence  # [<ALK_Entity.Sequences.ALK_Sequence object at 0x0000000038BADE80>]
print myScene.sg_sequence[0].code  # S004

mySceneById = ALK_Entity.Scenes.ALK_Scene(794)
print mySceneById.code  # S004_M001
###

If you are interested, here is an example of python scripts I wrote.
I shared them "as it is", and with quick mods to be more clear and subject-centric.
This modifications could have altered functionality, you have my apologies if so.
( In another blog post, I will describe how I setup my production versioning system linked to shotgun entities. )

""" File: ...\ALK_Entity\__init__.py  """

# -*- coding: utf-8 -*-
""" Contains all connections to the Asset Management (Shotgun API wrapper). """
import os
import shotgun_api3.shotgun
_PROJECT_ID = 00  # We only worked on one project so I made very few cases of multi-project instances.

class ShotgunEntity(object):
  """Representation in python object of an entity in shotgun."""

  def getShotgunHandle(self):
    """ Default method that needs to be overloaded in sub-classes """
    import shotgun_api3.shotgun
    SERVER_PATH = "http://your_shotgun_url"
    SCRIPT_NAME = 'Framework_Connector'
    SCRIPT_KEY = '##########################################'
    return shotgun_api3.shotgun.Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY)

  def getEntityType(self):
    """ Default method that needs to be overloaded in sub-classes """
    return 'DefaultValue'

  def getNameFilter(self,_name):
    """ Default method that needs to be overloaded in sub-classes """
    #Must overload that, for example tasks has no 'code' but 'content'...
    return ['code','contains',_name]

  def getProjectFilter(self):
    """ Default method that needs to be overloaded in sub-classes """
    return ['project','is',{'type':'Project','id':_PROJECT_ID}]

  def updateFromDataBase(self,_sg_Id):
    """Looking for the shotgun Asset and retrieve its metadatas.
    Be aware that you can use shotgun Id or Name to find the Asset."""

    shotgunHandle = self.getShotgunHandle()
    filters =[]

    if self.getProjectFilter()!=None:
      filters.append(self.getProjectFilter())

    if type(_sg_Id) == str:
      filters.append(self.getNameFilter(_sg_Id))
    elif type(_sg_Id) == int:
      filters.append(['id','is',_sg_Id])
    else:
      pass
      #put here your throwing error system

    fields = self.__dict__.keys()
    #All fields, that does not fit well for high performance or big requests on huge database of course.
    Assetdata = shotgunHandle.find_one(self.getEntityType(), filters, fields)
    shotgunHandle.close()

    if not Assetdata:
      print("ERROR, no entry in shotgun DB for entity with id = "+str(_sg_Id)+" ("+str(self.getEntityType())+")  ",eVerboseLevel.kError)
      print("Filters were ="+str(filters),eVerboseLevel.kError)
      raise ALK_InputOutput.Debug.ShotGunEntryMissing("ERROR, no entry in shotgun DB for Asset !")
    else:
      for currentmember in fields:
        if not Assetdata.has_key(currentmember):
          print("ERROR, no entry in shotgun DB for member :"+currentmember,eVerboseLevel.kError)
        else:
          setattr(self, currentmember, Assetdata[currentmember] )

  def __getattribute__(self,name):
    """ When a member of the instance of the class is queried, we get its data. """

    myValue = object.__getattribute__(self, name)  # getting a member value given its name as a string.

    if(isinstance(myValue, list)):

      entityList=[]
      for currentSubObj in myValue:
        newEntity = self.__castFromDictToEntity(currentSubObj)
        entityList.append(newEntity)
      setattr(self, name, entityList)
      return entityList

    else:
      if(isinstance(myValue, dict)):
        newEntity = self.__castFromDictToEntity(myValue)
        setattr(self, name, newEntity)
        return newEntity
      else:
        return myValue

    return None

  def __castFromDictToEntity(self, _data):
    """ With the native python's shotgun wrapper, every entity is represented as a dict, this will cast them """

    #every shotgun dict has an id and a type (every id is unique based on its type, often show as entity name)
    if(isinstance(_data, dict)):
      if(_data.has_key('id') and _data.has_key('type')):

        myType = _data['type']
        myALK_Entity = _createEntityByName(myType, _data['id'])
        return myALK_Entity
    return _data

  def updateIntoDataBase(self):
    """" Publish all members from python RAM into shotgun Database."""
    #import shotgun_api3.shotgun

    shotgunHandle = self.getShotgunHandle()
    fields = self.__dict__.keys()
    memberDict = {}
    for currentmember in fields:
      memberDict[currentmember] = getattr(self, currentmember)

    AssetdataNew = shotgunHandle.update(self.getEntityType(), self.id, memberDict)
    shotgunHandle.close()

  def updateMemberIntoDataBase(self,_memberNameToUpdate):
    """" Publish given member from python RAM into shotgun Database."""

    shotgunHandle = self.getShotgunHandle()

    memberDict={}
    memberDict[_memberNameToUpdate] = getattr(self, _memberNameToUpdate)
    AssetdataNew = shotgunHandle.update(self.getEntityType(), self.id, memberDict)

    print("Asset Updated in shotgun :" + str(AssetdataNew))
    shotgunHandle.close()

  def _createEntityByName(_TypeName, _Id):
    """ Cast from a type as string into a ALK_Entity wrapped instance. """

    if _Id==None:
      return None

    if _TypeName == 'Asset':
      import ALK_Entity.Assets
      return ALK_Entity.Assets.ALK_Asset(_Id)

    if _TypeName=='Scene':
      import ALK_Entity.Scenes
      return ALK_Entity.Scenes.ALK_Scene(_Id)

    if _TypeName=='Version':
      import ALK_Entity.Versions
      return ALK_Entity.Versions.ALK_Version(_Id)

    #And so on for all your managed entities.
    printt("Un-managed shogun type = "+_TypeName)

And now this is an entity wrap of a scene:
""" File: ...\ALK_Entity\Scenes.py  """

# -*- coding: utf-8 -*-
""" Wrapper for Scenes into the Asset Manager Database. """
import os
import ALK_InputOutput.Debug
import ALK_Entity

_SERVER_PATH = "http://your_shotgun_url" # make sure to change this to https if your studio uses it.
_SCRIPT_NAME = 'ALK_Scene' #In your administration panel, be sure to create as many scripts as you have entities wrapper.
_SCRIPT_KEY = '############################'

class __Shotgun_Scene(ALK_Entity.ShotgunEntity):
    """Representation in python object of a Scene entity in shotgun.
    We assume that all members of this class is a field with a same name."""

    def __init__(self,_sg_Id):
        """Constructor of the class with defaultValues and next updated them from database."""

        self.addressings_cc = 'DefautValue'
        self.assets = 'DefautValue'
        self.code = 'DefautValue'
        self.created_at = 'DefautValue'
        self.created_by = 'DefautValue'
        self.description = 'DefautValue'
        self.id = 'DefautValue'
        self.image = 'DefautValue'
        self.notes = 'DefautValue'
        self.open_notes = 'DefautValue'
        self.open_notes_count = 'DefautValue'
        self.project = 'DefautValue'
        self.sg_scene_type = 'DefautValue'
        self.sg_status_list = 'DefautValue'
        self.shoot_days = 'DefautValue'
        self.shots = 'DefautValue'
        self.step_0 = 'DefautValue'
        self.tag_list = 'DefautValue'
        self.tasks = 'DefautValue'
        self.task_template = 'DefautValue'
        self.updated_at = 'DefautValue'
        self.updated_by = 'DefautValue'
        #Custom fields
        self.sg_cameras = 'DefautValue'
        self.sg_sequence = 'DefautValue'#Warning it is a list of dictionnaries

        self.updateFromDataBase(_sg_Id)

    def getShotgunHandle(self):
        import shotgun_api3.shotgun
        return shotgun_api3.shotgun.Shotgun(_SERVER_PATH, _SCRIPT_NAME, _SCRIPT_KEY)

    def getEntityType(self):
        return 'Scene'

    def getNameFilter(self,_name):
        return ['code','contains',_name]

class ALK_Scene(__Shotgun_Scene):
    """Custom class to implement metadatas of Scenes without shotgun synchronisation."""

    def __init__(self,_sg_Id):
        """ Constructor. """
        #First calling the inherited constructor:
        super(ALK_Scene,self).__init__(_sg_Id)

        #TODO verif if sequence.code is in scene.code

    def getFolder(self,_task):
        import ALK_Entity.Projects
        import ALK_Entity.Sequences

        if len(self.sg_sequence)>=1:
            mySeq = self.sg_sequence[0]
            #print mySeq.code
            #print _task.step.code
            if _task!=None:
                myFilePath = ALK_Entity.Projects.getZeProject().getProjectDirectory()+"\\Exploitation\\"+mySeq.code+"\\scenes\\"+_task.step.code+"\\"+_task.content
            #maybe replace myFilePath = ALK_Entity.Projects.getZeProject().getProjectDirectory()+"\\Exploitation\\"+mySeq.code+"     by ALK_Seq.getFolder()
                return myFilePath
            else:
                printt("No task associated to this scene !", eVerboseLevel.kError)
                raise ALK_InputOutput.Debug.ShotGunEntryMissing("ERROR, no entry in shotgun DB for this task !"+self.code)
        else:
            printt("No sequence associated to this scene !",eVerboseLevel.kError)

            #raise

    def getRange(self):
        import sys
        import ALK_Entity.Versions

        StartFrame = sys.maxint
        EndFrame = 0

        for (currentVersion,currentShot,currentCamera) in ALK_Entity.Versions.getAllValidated(self):
            print currentVersion.code , currentCamera.sg_short_name, "In=" ,currentVersion.sg_cut_in,"Out=",currentVersion.sg_cut_out

            if currentVersion.sg_cut_inEndFrame:
                EndFrame = currentVersion.sg_cut_out

        return (StartFrame, EndFrame)

def getSceneList():
    import shotgun_api3.shotgun
    myShotgunHandle = shotgun_api3.shotgun.Shotgun(_SERVER_PATH, _SCRIPT_NAME, _SCRIPT_KEY)

    allItems = myShotgunHandle.find('Scene', [['project','is',{'type':'Project','id':ALK_Entity._PROJECT_ID}]])
    result=[]
    for currentItem in allItems:
        newItem = ALK_Scene(currentItem['id'])
        result.append(newItem)
    return result

store
guidelines
18Mar/130

How to handle the unexpected

Thanks to a french blog, I just discovered that python allow you to handle the unexecpected python errors.
It is called "excepthook" and it is a must have in a studio framework !
So I urged myself to put a routine into the userSetup.py of anyone in the studio that starts Maya or MotionBuilder:
 

import sys

def handleCrash(_ErrorType, _Value, _TraceBack):
  """ do your stuff """
  sys.__excepthook__(_ErrorType, _Value, _TraceBack) #as Sam says, be polite.

sys.excepthook = handleCrash
 
With that, you can send you an email with a HTML-formated text, with all the details, but be careful about the possible lag when sending email.
My best guest right now is to insert an sql request into a debug.db file using sqlite3.
With a little additional efforts (or a database reading app), you can track anything happening in the studio.
My advice is to get exact time, the user name, machine name and its opened-file's name to fill the logging.
Here is the final snippet:

import sys

def handleCrash(_ErrorType, _Value, _TraceBack):
  import traceback
  import sys
  import datetime
  import os

  UserName = os.getenv('USERNAME')
  MachineName = os.getenv('COMPUTERNAME')

  FileName = "" # could be, regarding your current environment,
                #maya.cmds.file(query=True,sceneName=True,shortName=False)
                #pyfbsdk.FBApplication().FBXFileName

  HtmlBuffer="<body>"
  HtmlBuffer+="UNEXPECTED:"+str(_ErrorType) +" ==> "+ str(_Value)+"<br>"
  HtmlBuffer+= UserName+" on "+MachineName+"<br>"
  HtmlBuffer+= "on File ="+FileName+"<br>"

  dt = datetime.datetime.now()
  DateNiceName = dt.strftime("%A, %d. %B %Y %I:%M%p")
  HtmlBuffer+= "At "+DateNiceName+"<br>"

  formatted_lines = traceback.format_tb(_TraceBack)
  for cur in  formatted_lines:
    HtmlBuffer+=str(cur)+"<br>"
  HtmlBuffer+="</body>"

  #do your stuff

  sys.__excepthook__(_ErrorType, _Value, _TraceBack)

sys.excepthook = handleCrash

By the way, in Maya you will find that some of your errors will not be caught by that system.
Cyrille Fauvel wrote about that in the awesome "Around the corner" dev blog.
http://around-the-corner.typepad.com/adn/2013/03/my-entry.html
import maya.utils

def handleCrash_Maya(etype, value, tb, detail=2):
handleCrash(etype, value, tb)
  return maya.utils._formatGuiException(etype, value, tb, detail)

maya.utils.formatGuiException = handleCrash_Maya

PS: Coming back from GDC2013, I attended to an awesome conference called "Spend Time Where it Matters: Friction Free Bug Reporting" by Raphael Saint-Pierre (Ubisoft) , talking about auto-BugReporting.
I learned here a very great tips to identify and index a bug into a database without creating some clones of similar ones everytime a new occurence pop. How ? Very easy: give an Id coming from a hash of the call stack ! Tested... and approved !

faq
5Mar/133

How to batch Motionbuilder: FBX SDK to the rescue.

When I suddently discovered that MotionBuilder does not have a batch mode, I fainted, as I consider doing my job is half the time coding batch tools.

As a reminder, the Batch version of a software is the access of this one in command-line; means without interfaces, but more importantly it is also scriptable so you can process infinite auto-tasks management with it.
Maya comes with a mayabatch.exe, and xsi has its xsibatch.exe, but you can just start MotionBuilder.exe with a -console flag, which allow you having an external output, nothing really fancy.
This is a real issue here: How can building a python framework, multi-environment, and keep tools doing full-auto pipeline processes.
Thankful to Autodesk policy, they gave us a solution : FBX SDK.

  • Explanations:

FBX SDK is written in c++, but hopefully Autodesk managed to wrapped it with SWIG to propose a binding python (if the link is dead, try a search with fbx sdk python binding in Area)
The full doc is actually here, but it is the c++ one. Do not worry about it, python equivalent are found easily (remember dir statement).

So, Is this another environement to my framework: FBX ? Not quite.
We are still in standalone python, but we are using a library to PARSE a file format.
As Maya uses .MA as a 3d structure of succession of MEL Commands, and .MB as a binay, FBX provides both ascii and binary, but ascii fbx are very hard to parse.
FBX SDK help you to read, modify and save any file of FBX, but require only python, ANY python installed on your computer.
For example, I use FBX SDK to open, read, and manage a fbx file, running into a mayabatch.exe (and its mayapy python).
You can also used FBX SDK into MotionBuilder UI, but that is REALLY the snake eating its own tail !

  • Several snippets:

Here I share with you several usefull snippets:


def getHandle(_FilePath):
  import PyFBX
  import PyFBX.fbx

  lSdkManager = PyFBX.fbx.KFbxSdkManager.Create()
  ios = PyFBX.fbx.KFbxIOSettings.Create(lSdkManager, PyFBX.fbx.IOSROOT)
  lImporter = PyFBX.fbx.KFbxImporter.Create(lSdkManager, "")
  lImporter.Initialize(_FilePath, -1, lSdkManager.GetIOSettings())
  MobuFileHandle = PyFBX.fbx.KFbxScene.Create(lSdkManager, "myScene")
  lImporter.Import(MobuFileHandle)
  return MobuFileHandle

If you use that to parse a lot of files, be really sure to free the RAM from
.Import here will literally copy the file into the RAM of your computer, and as the reference is still alive, the garbage collector will not free the RAM.
My advice here is to explicitly calling lSdkManager.Destroy() and recreate a fileHandle from scratch every time you switch from a file to another.


def saveAs(_FBXHandle, _FilePath):
  import PyFBX
  import PyFBX.fbx
  import os.path
  lSdkManager = PyFBX.fbx.KFbxSdkManager.Create()
  ios = PyFBX.fbx.KFbxIOSettings.Create(lSdkManager,PyFBX.fbx.IOSROOT)
  lExporter = PyFBX.fbx.KFbxExporter.Create(lSdkManager,"")
  lExporter.Initialize(_FilePath,-1,lSdkManager.GetIOSettings())
  res = lExporter.Export(_FBXHandle)

  if res and os.path.exists(_FilePath):
    print("File was exported succesfully at ="+_FilePath)

Remember how Motionbuilder manage their types of objects in Navigator Models aside, and all objects ordered by types?
Same thing here in FBX, everything is stored by types first, and you have a Method of Scene for everyone:
Finally, notice how most of them are only accessible with an index, GetCharacter, GetControlSetPlug, GetCharacterPose, GetPose , GetMaterial, GetTexture, GetVideo
Here are some snippets for parsing:


def getVideoFiles(_Scene):
  VideoList=[]
  for currentIdx in range(_Scene.GetVideoCount()):
    VideoList.append( _Scene.GetVideo(currentIdx).GetFileName() )
  return VideoList

def getMaterials(_Scene):
  MaterialList=[]
  for currentIdx in range(_Scene.GetMaterialCount()):
    MaterialList.append( _Scene.GetMaterial(currentIdx) )
  return MaterialList

def getTextures(_Scene):
  TextureList=[]
  for currentIdx in range(_Scene.GetTextureCount()):
    TextureList.append( _Scene.GetTexture(currentIdx) )
  return TextureList

def getChildren(_Node):
  ChildrenList=[]
  for currentIdx in range(_Node.GetChildCount()):
    ChildrenList.append( _Node.GetChild(currentIdx) )
  return ChildrenList

So you can easily explore the graph of a top node:


def getAllNodes(_TopNode):
  import PyFBX
  import PyFBX.fbx
  theNodeList=[]

  def recurseExplore(currentNode):
    print currentNode.GetTypeName(), currentNode.GetName()

    for i in range(currentNode.GetChildCount()):
      cur = currentNode.GetChild(i)
      theNodeList.append(cur)
    recurseExplore(cur)

  recurseExplore(_TopNode)
  return theNodeList
fd = getHandle(r"\\...\Your\Path\FileName.fbx")
test = getAllNodes(fd.GetRootNode())

  • Avoiding traps and shortcuts:

Finally, you can of course look for a specific property.
You have in the doc all the methods for getting rotation or translation in Nodes.
Here is some snippets to get a value of a property by name, for example your own custom ones.


#currentNode= fd.GetRootNode()
value = currentNode.FindProperty(_PropertyName).Get()

But be very careful about not mixing both SDK in your mind!
FBX is NOT Motionbuilder, and experienced python coders in Mobu will find some similarities, but it differs a lot.
For example, forget about .Data member of a node to get its value, but use .Get() method.

Precisely, python in Mobu is called "Open Reality SDK" and is not FBX SDK, but the python package name in mobu, pyfbsdk, can be really confusing (FB prefix comes from FilmBox).
Here are some others examples:


if type(currentNode) == PyFBX.fbx.KFbxNode:
  value = currentNode.Translation.Get()
  print type(value), type(value[0]),value[0]
#<class 'fbx.fbxDouble3'> <type 'float'> 0.0
  value = currentNode.GetPreRotation(0)
  print type(value), value, value[0]
#<class 'fbx.KFbxVector4'> fbx.KFbxVector4(0.000000, 0.000000, 0.000000, 1.000000) 0.0

  • The bright and dark sides

You can open the scenes quickly, automatically.
You can safely open/close a lot of files like this.

I encountered some issues while saving cleaned file, so be sure to increment and not overwrite (as usual of course!)
You cannot blast, cannot plot, etc. ; well basically, you are not in mobu, nor using pyfbsdk module, but you can go very far (edit keys for example)

In my present experience with MotionBuilder, I used FBX SDK to:

- Scan all characters assets to parse a specific PreRotation joint value.
- Scan all scenes to list their content, sync it with asset management system.
- Done some stats about the artists worked, draw dependencies graph and find obsolete assets.

international
23Aug/120

APyTA_17 : OBJECT Part III = Static tactics

APyTA_17 : OBJECT Part III = Static tactics

Cosmogonies_APyTA_17

Illustation of Lesson APYTA#17:
http://cosmogonies.net/Blog/category/apyta/

Well, this snippet is just a more elaborate version of the latest post, about creating an object-oriented skeleton:
Because my credo here is to illustrate how objects is all about meaning, giving sense (and some reality) to our lines of code, I propose here to add some arguments to give some consistence to our skeleton, a little how we create a character in role playing game^^. So, this is my prototype of my Avatar creation:

#
def __init__(self, _Name="DefaultName", _Age=30, _Size=1.70, _Sex=eSexe.kFemale, _NbArm=2, _NbLeg=2, _hasTail=False):
#

See how we are dealing with real stuff, and also giving more flexibility than previous snippet.

By the way, you have now to discover another concept, static members and methods.
I talked previously how a class is instantiated into a variable: myInstance = myClass().
The instance will benefits to all members and methods (functions) bound to the self keyword in class definition.

Well sometimes you don't want to bind members to instances. You want a variable to be linked to the scope of the class itself !
These are called static members and static methods, and examples are explained bellow.
Again, rather than cutting the long snippet, I placed my comments through the lines inside the code itself:

import math
import maya.OpenMaya
import pymel.core
import maya.cmds

maya.cmds.currentUnit(linear='m')  # Unless you're a gridsnap-lover, I prefer working with meter units that make more sense for me.
maya.cmds.jointDisplayScale(5)

class eSexe():
    """ An enumeration of all sex status """
    (kMale, kFemale) = ("Male",  "Female")  # This snippet is useful when you want to manage an very simple-object with several enumeration (as struct in c)

class Avatar(object):  # Also when desribing your mother-of-all class, always inherit from the python class-object (you will benefits of some built-ins)
    """ This object is a rigged-skeleton representation of a character """
    NbSpine = 8  # Arbitrary, we decide that all spinal chain will have 8 joints. So it is in a static Member of Avatar class.
    HipsRatio = 0.4  # Percentage of hips height. Arbitrary also !

    def __init__(self, _Name="DefaultName", _Age=30, _Size=1.70, _Sex=eSexe.kFemale, _NbArm=2, _NbLeg=2, _hasTail=False):
        self.Age = _Age  # With age,  bones are stacked and incurved,  int ,  in years
        self.Size = _Size  # How tall it is. Float,  in meters
        self.Sex = _Sex  # Sexe influence Hips Width and Shoulder width

        self.LimbList = []   # Limb Lists
        self.ArticulationList = []   # All Joints Lists
        self.Hips = None  # Quick Access shortcut
        self.NeckBase = None  # Quick Access shortcut

        self._createTrunk()  # We can call an method into the inialisation process of our Avatar instance.

        ArmLength = self.NeckBase.getTranslation(space='world').y - self.Hips.getTranslation(space='world').y
        for i in range(_NbArm):
            angle = (float(i) / float(_NbArm)) * 2 * math.pi
            RelativePosition = maya.OpenMaya.MVector(math.cos(angle), 0, math.sin(angle))
            newArm = Arm(chr(65 + i), self.NeckBase, RelativePosition * Limb.getGenderOffset(Arm, self.Sex), ArmLength)  # Here we create a new instance of class Arm, and Arm-Object.
            newArm.createIK()  # Notice here we call the method from the newly created instance. For Leg it is done differently.
            self.LimbList.append(newArm)

        for i in range(_NbLeg):
            angle = (float(i) / float(_NbLeg)) * 2 * math.pi
            RelativePosition = maya.OpenMaya.MVector(math.cos(angle),  0,  math.sin(angle))
            newLeg = Leg(chr(65 + i), self.Hips,  RelativePosition * Limb.getGenderOffset(Leg, self.Sex))  # Notice also the call of a static-method of class Limb.
            self.LimbList.append(newLeg)

        if _hasTail:
            self.LimbList.append(Tail(self.Hips, 10, 2))

        RootLocator = pymel.core.circle(normal=(0, 1, 0), center=(0, 0, 0), radius=0.5)[0]
        pymel.core.annotate(RootLocator, text=str(_Name), point=(0, 0, 0.5))
        pymel.core.parent(self.Hips, RootLocator)

    def _createTrunk(self):
        """ Will create the trunk of the body """
        HipsPos = maya.OpenMaya.MVector(0, self.Size * Avatar.HipsRatio, 0)  # Notice here the use of static member of Avatar, treat that like a constant value.
        SpineOffset = (self.Size - HipsPos.y) / float(Avatar.NbSpine)
        SenescenceOffset = float(self.Age) / 100.0  # Factor of spine curve due to age (Cheating because Height is conserved)
        SenescenceOffset *= 0.01

        for increment in range(Avatar.NbSpine):
            pymel.core.general.select(clear=True)   # We do not want Maya to create fuzzy stuff with active selection. So before each creation we clear the selection.
            currrentArticulation = pymel.core.nodetypes.Joint(name="Spine_" + str(increment + 1).zfill(2))
            currrentArticulation.setTranslation(maya.OpenMaya.MVector(0,  HipsPos.y + increment * SpineOffset, (increment ** 2) * SenescenceOffset))

            if self.ArticulationList:  # Not the first joint, we have to connect it to the previous one...
                pymel.core.connectJoint(currrentArticulation,  self.ArticulationList[-1],  parentMode=True)  # Remember,  -1 is the index of the last member of a list.
            else:
                self.Hips = currrentArticulation

            if increment == Avatar.NbSpine - 2:  # The last two bones are Neck and Head, so Arms takes their root from bellow.
                self.NeckBase = currrentArticulation

            self.ArticulationList.append(currrentArticulation)

class Limb(object):
    def __init__(self, _Name):
        self.ArticulationList = []  # Same name as in Avatar member, but 'self.' indicates the ownership of this class instance. We're deadling with two very different variables so.
        self.Name = _Name
        self.Effector = None  # Every Limb MUST have an effector. But right now it is purely virtual. The member will be set when Ik will be constructed.

    @staticmethod
    def getGenderOffset(_Limb, _Sex):  # Notive there is no 'self' first argument.
        """ Static Method that return offset of Base position for given Limb """
        if _Limb is Arm:  # Notice also the first expected argument here is the class itself, not one of their instances: That's where is operator fit better than ==
            if _Sex == eSexe.kFemale:
                return 0.25
            elif _Sex == eSexe.kMale:  # Male have larger shoulder...
                return 0.3
        elif _Limb is Leg:
            if _Sex == eSexe.kFemale:  # Male have larger hips...
                return 0.3
            elif _Sex == eSexe.kMale:
                return 0.25
        else:
            raise  # Unknown Limb ! (Not the purpose here but why not raise a custom Exception... as a new class of course)

class Arm(Limb):
    NbArticulation = 3  # We iterate through a max joint define in the class member, not the instance member. It is call a static member. Useful for storing scoped-constants and convert factor.

    def __init__(self, _Name, _NeckBase,  _Offset, _Length):
        super(Arm, self).__init__("Arm_" + _Name)  # Inheritance in python is not automatic. Super (Mother) class of Arm is Limb, and its methods are callable form Arm instance, but it needs the keywords super to do that.
        LastPosition = _NeckBase.getTranslation(space='world')
        _Offset.normalize()

        for i in range(Arm.NbArticulation):
            pymel.core.general.select(clear=True)
            currentArticulation = pymel.core.nodetypes.Joint(name=self.Name + "_" + str(i + 1).zfill(2))
            NewPosition = LastPosition + _Offset * (_Length / float(Arm.NbArticulation))
            currentArticulation.setTranslation(NewPosition)
            if not self.ArticulationList:  # The first arm joint must be connected to the Base.
                pymel.core.connectJoint(currentArticulation,  _NeckBase,  parentMode=True)
            else:
                pymel.core.connectJoint(currentArticulation,  self.ArticulationList[-1],  parentMode=True)
            self.ArticulationList.append(currentArticulation)
            LastPosition = NewPosition

    def createIK(self):
        pymel.core.general.select(clear=True)
        result = pymel.core.ikHandle(name=self.Name + "_IKEffector", startJoint=self.ArticulationList[0], endEffector=self.ArticulationList[-1],  solver=pymel.core.nodetypes.IkSCsolver)
        self.Effector = result[0]  # Filling Limb inherited member with the newly created effector.

class Leg(Limb):
    NbArticulation = 3

    def __init__(self, _Name, _HipsBase,  _Offset):
        super(Leg, self).__init__("Leg_" + _Name)
        BasePosition = _HipsBase.getTranslation(space='world')
        previouslyCreated = _HipsBase
        for i in range(0, Leg.NbArticulation):
            pymel.core.general.select(clear=True)
            currentArticulation = pymel.core.nodetypes.Joint(name=self.Name + "_" + str(i + 1).zfill(2))

            if(previouslyCreated == _HipsBase):  # First joint
                currentArticulation.setTranslation(BasePosition + _Offset, space='world')
            else:
                InverseLerp = (1 - (i / float(Arm.NbArticulation - 1)))   # Lerp is the transpose of a range FROM [min = >max] TO [0 = >1]   # Arm.NbArticulation-1 because we do not care about the first Articulation
                currentArticulation.setTranslation(maya.OpenMaya.MVector(previouslyCreated.getTranslation(space='world').x, previouslyCreated.getTranslation(space='world').y * InverseLerp, previouslyCreated.getTranslation(space='world').z), space='world')

            pymel.core.connectJoint(currentArticulation,  previouslyCreated,  parentMode=True)
            previouslyCreated = currentArticulation
            self.ArticulationList.append(currentArticulation)
        self.createIK()  # Instead of Arm, here we decided that it is into the __init__ process to create IK, so we call the methods from here.

    def createIK(self):
        pymel.core.general.select(clear=True)
        result = pymel.core.ikHandle(name=self.Name + "_IKEffector", startJoint=self.ArticulationList[0], endEffector=self.ArticulationList[-1],  solver=pymel.core.nodetypes.IkSCsolver)  # IkRPsolver TOASK
        self.Effector = result[0]
        return result[0]

class Tail(Limb):
    def __init__(self,  _HipsBase,  _NbSubdiv, _Size=1):
        super(Tail, self).__init__("Tail")
        BasePosition = _HipsBase.getTranslation(space='world')
        for i in range(1, _NbSubdiv):
            pymel.core.general.select(clear=True)
            currentArticulation = pymel.core.nodetypes.Joint(name="Tail_" + str(i + 1).zfill(2))
            offSet = i / float(_NbSubdiv)
            currentArticulation.setTranslation(maya.OpenMaya.MVector(BasePosition.x, BasePosition.y, BasePosition.z - (offSet * _Size)),  space='world')
            if not self.ArticulationList:  # The first arm joint must be connected to the Base.
                pymel.core.connectJoint(currentArticulation, _HipsBase,  parentMode=True)
            else:
                pymel.core.connectJoint(currentArticulation, self.ArticulationList[-1],  parentMode=True)
            self.ArticulationList.append(currentArticulation)
        self.createSplineIK()

    def createSplineIK(self):
        pymel.core.ikHandle(name="TailIKEffector", startJoint=self.ArticulationList[0], endEffector=self.ArticulationList[-1],  solver=pymel.core.nodetypes.IkSplineSolver)

JohnDoe = Avatar("John Doe", _Sex=eSexe.kMale)
TentacleMonster = Avatar("Weirdo", _Size=2.5, _NbArm=5, _NbLeg=3)
GrandMaMouse = Avatar("Grand'Ma Mouse", _Size=1.2, _Age=86, _hasTail=True)

forum
Tagged as: , , , , No Comments
21Aug/121

APyTA_16 : OBJECT Part II, How to inherit a skeleton

APyTA_16 : OBJECT Part II, How to inherit a skeleton

I promise you that object-oriented coding will be meaningful.
For a more accurate example, I have a biggest snippet to show you , dedicaced to rig team.

First we have to introduce a new concept between objects : the inheritance.
You can also create a hierarchy between objects, indicating explicitely in a class a super-class.
Not like parenting, telling a class is issued from another object (could be your classes or another built-in type) is about describing an object which is a more complex version of this class.
The super-class is a generalisation of your class.
Here is a quick example of hierarchy into classes:

class Animal:
  pass
class Mamal(Animal):
  pass
class Cat(Mamal):
  pass

Well, so you can create objects with inheritance of other classes
and you can create members (self-owned variables, which are objects too).
Sometimes it is very hard to DESIGN when contain, and when inherit.
This will help you in most of the case:
"inherits" means " is a sort of "
"contains (members)" means "have some "

Not as before, I will comment the code directly inside the snippet, as there is a lot to say!
So, as I want to create a skeleton, how object-orientation can change my way of coding ?
This is my design:
I propose to create an Avatar object. It contains Limbs.
There are two sort of limbs, arms and legs.
Each of those are basically a list of joints.
Here's the snippet (do not care for lame-algorithms, a more completed version will follow in next post)

import math
import maya.OpenMaya
import pymel.core


class Avatar(object):  # Also when desribing your mother-of-all class, always inherit from the python class-object (you will benefits of some built-ins)
    """ This object is a rigged-skeleton representation of a character """

    def __init__(self, _Size=1.70, _NbArm=2, _NbLeg=2, _hasTail=False):
        self.Size = _Size  # How tall it is. Float,  in meters

        self.LimbList = []   # Limb Lists
        self.ArticulationList = []   # All Joints Lists
        self.Hips = None  # Quick Access shortcut
        self.NeckBase = None  # Quick Access shortcut

        self._createTrunk()  # We can call an method into the inialisation process of our Avatar instance.

        RelativePosition = maya.OpenMaya.MVector(math.cos(0), 0, math.sin(0))
        currentArm = Arm("Left", self.NeckBase,  RelativePosition * 0.2)  # Here we create a new instance of class Arm, and Arm-Object.
        self.LimbList.append(currentArm)

        RelativePosition = maya.OpenMaya.MVector(math.cos(math.pi), 0, math.sin(math.pi))
        currentArm = Arm("Right", self.NeckBase,  RelativePosition * 0.2)
        self.LimbList.append(currentArm)

        RelativePosition = maya.OpenMaya.MVector(math.cos(0), 0, math.sin(0))
        currentLeg = Leg("Left", self.Hips,  RelativePosition * 0.2)
        self.LimbList.append(currentLeg)

        RelativePosition = maya.OpenMaya.MVector(math.cos(math.pi), 0, math.sin(math.pi))
        currentLeg = Leg("Right", self.Hips,  RelativePosition * 0.2)
        self.LimbList.append(currentLeg)

    def _createTrunk(self):
        """ Will create the trunk of the body """
        HipsPos = maya.OpenMaya.MVector(0, self.Size * 0.4, 0)
        SpineOffset = (self.Size - HipsPos.y) / float(8)

        for increment in range(8):
            pymel.core.general.select(clear=True)   # We do not want Maya to create fuzzy stuff with active selection. So before each creation we clear the selection.
            currrentArticulation = pymel.core.nodetypes.Joint(name="Spine_" + str(increment + 1).zfill(2))
            currrentArticulation.setTranslation(maya.OpenMaya.MVector(0,  HipsPos.y + increment * SpineOffset, 0))

            if self.ArticulationList:  # Not the first joint, we have to connect it to the previous on...
                pymel.core.connectJoint(currrentArticulation,  self.ArticulationList[-1],  parentMode=True)  # Remember,  -1 is the index of the last member of a list.
            else:
                self.Hips = currrentArticulation

            if increment == 6:  # The last two bones are Neck and Head, so Arms takes their root from bellow.
                self.NeckBase = currrentArticulation

            self.ArticulationList.append(currrentArticulation)


class Limb(object):
    def __init__(self, _Name):
        self.ArticulationList = []  # Same name as in Avatar member, but 'self.' indicates the ownership of this class instance. We're deadling with two very different variables so.
        self.Name = _Name


class Arm(Limb):
    def __init__(self, _Name, _NeckBase,  _Offset):
        super(Arm, self).__init__("Arm_" + _Name)  # Inheritance in python is not automatic. Super (Mother) class of Arm is Limb, and its methods are callable form Arm instance, but it needs the keywords super to do that.
        BasePosition = _NeckBase.getTranslation(space='world')

        for i in range(3):
            pymel.core.general.select(clear=True)
            currentArticulation = pymel.core.nodetypes.Joint(name=self.Name + "_" + str(i + 1).zfill(2))
            currentArticulation.setTranslation(BasePosition + _Offset * (i + 1))
            if not self.ArticulationList:  # The first arm joint must be connected to the Base.
                pymel.core.connectJoint(currentArticulation,  _NeckBase,  parentMode=True)
            else:
                pymel.core.connectJoint(currentArticulation,  self.ArticulationList[-1],  parentMode=True)
            self.ArticulationList.append(currentArticulation)


class Leg(Limb):
    NbArticulation = 3

    def __init__(self, _Name, _HipsBase,  _Offset):
        super(Leg, self).__init__("Leg_" + _Name)
        BasePosition = _HipsBase.getTranslation(space='world')
        previouslyCreated = _HipsBase
        for i in range(3):
            pymel.core.general.select(clear=True)
            currentArticulation = pymel.core.nodetypes.Joint(name=self.Name + "_" + str(i + 1).zfill(2))

            if(previouslyCreated == _HipsBase):  # First joint
                currentArticulation.setTranslation(BasePosition + _Offset, space='world')
            else:
                InverseLerp = (1 - (i / float(2)))   # Lerp is the transpose of a range FROM [min = >max] TO [0 = >1]   # Arm.NbArticulation-1 because we do not care about the first Articulation
                currentArticulation.setTranslation(maya.OpenMaya.MVector(previouslyCreated.getTranslation(space='world').x, previouslyCreated.getTranslation(space='world').y * InverseLerp, previouslyCreated.getTranslation(space='world').z), space='world')

            pymel.core.connectJoint(currentArticulation,  previouslyCreated,  parentMode=True)
            previouslyCreated = currentArticulation
            self.ArticulationList.append(currentArticulation)

I hope you had catch it. Now let's manipulate that:


JohnDoe = Avatar()  # This variable aim to a newly created object, typed from class Avatar.

#As in pymel, you no longer need to find Hips by name and fit a naming-convention.

JohnDoe.Hips.setTranslation(maya.OpenMaya.MVector(1,2,3)) #Move hips

#Now, let's select both hands:
pymel.core.general.select(clear=True)  
for currentLimb in JohnDoe.LimbList:
    if type(currentLimb) is Arm:
        pymel.core.general.select( currentLimb.ArticulationList[-1],add=True )

See how Object-Orientation is more about giving sense to a bunch of integers and strings ?
But this backside of the medal is this design thing. Sometimes you don't need to deeply create a very precise hierarchy of objects.
Worse, you can have a big change in your framework (like a new Software in the pipeline), which needs you to re-think your all classes...
As this concise example lacks of real interest, the next post will go (a little) further into Avatar creation.

store
guidelines
20Jul/120

APyTA_15 : OBJECT Part I, Everything is an object, and Object is everything, and…

APyTA_15 : OBJECT Part I, Everything is an OBJECT is Everything...

The golden rule in python is this. Everything is an object, so without fully understand it, you already use it a lot.
You may also already known that python is an object-oriented language.
It is time to define this notion right now.

A class is a definition of a template, describing a structure. A type.
An object is an instance of this class, its application into a 'real' case

MyVar = "lalo"
type(MyVar) # Result: <type 'str'> #
MyVar.__len__() # Result: 4 #
id(MyVar)# Result: 875904576L #
MyVar.replace("la","ri") # Result: 'rilo' #

Looking at this little piece of code, you have to understand that the type statement will return the name of the class, given the object.
Every time you create a variable, using the = operator, python will store somewhere in RAM (indexed by id() ) a copy of the class definition, with your given values.
This is the key concept here, the instantiation. And forget about Maya Instances, the concept here is different.
To take a more accurate example, Instantiate a class into a variable is like copy/paste a template folder, where you already put some subFolder/subFiles to avoid to do that every time.
Storing a named variable with a typed-value is precisely instantiate a class, given its template as a type, to create a new object.

The next notion, and sorry again, some vocabulary to learn, is to know what classes are made of.
Basically, they are a list of Members and Methods.
Members are local variable, created in the scope of the instance of the class, and refers to a value (could be a built-in, as int or string, but could be any other classes too).
Methods are like members, created locally in the object-scope, but they are functions defined with a def statement, they have a scope,  and can return value.

Python does not comes with built-in variables type to manage Vectors. Maya does, but forget that and focus on how to do some calculus with vectors.
First, you can create a variable, setting it with a tuple of three floats, like MyVector = (1.2,3.4,5.6). But you will very soon have readability issues to access the Y axis by MyVector[1] ...
Secondly, you will have to create a lot of calculus functions, and every time you will need to assert your arguments, because they will work only for your specific tuple.

import math
class APYTA_Vector():

  def __init__(self,_x=0.0,_y=0.0,_z=0.0):
    self.x=_x
    self.y=_y
    self.z=_z

  def getLength(self):
    return math.sqrt( self.x*self.x + self.y*self.y + self.z*self.z)

  def Normalize(self):
    InvLength = 1 / self.getLength()
    self.x *= InvLength
    self.y *= InvLength
    self.z *= InvLength

print APYTA_Vector() #<__main__.APYTA_Vector instance at 0x000000000CF23B88>
MyVector = APYTA_Vector(1,2,3)
print MyVector.y #2
print MyVector.getLength()#3.74165738677
MyVector.Normalize()
print MyVector.x, MyVector.y, MyVector.z #0.267261241912 0.534522483825 0.801783725737
print MyVector.getLength()#1.0

First, we can see here the class is defined by the keyword class. I usually had a prefix to my classes, because clashes can occurs, and that signed them explicitly.
The members of the class are defined into a very specific function (a Method) __init__(self). Here I setup three members, initialized at 0.0
Remember well the self , which is mandatory to be the first argument, and it explicitly refers to the Instance of the class (the object) .
This __init__ function is not really a constructor like in other languages (if you omit it, your object still exist), but it is used for initialization, declaration of members.

Like __init__, you can define any functions, with def statement, into the scope of the class. It is exactly like a function, living in the scope class, and having a first mandatory argument self.
Here, the method getLength can return an evaluation of all the object members, and see how referring to it is so neat : MyVector.getLength() .
This is the "object revolution" in coding. You will start to forget about processes that take "that and that" and return its calculus.
You will begin to manage object of meaning. Define functions (Methods) that just make sense.

Finally, see how I can modify the members of my object into a method, like in Normalize() ?
Nothing is returned, but the values (members) of my current instance are updated according to the processes of Normalization.
It is the very same thing of a list.sort() for example.

Tagged as: , , No Comments
faq
27May/120

APyTA_14: Most used modules, Exp Files management.

APyTA_14: Most used modules, Exp Files management.

The os module, for Operating System, is a module that helps you deal with your filesystem.
A sub-module, called os.path, will be dedicated to the path resolving and manipulation.
http://docs.python.org/library/os.html and http://docs.python.org/library/os.path.html

You will see in the os doc a lot of low-level functions, because python handle very well the kernel of the file system.
But we do not need that depth, often so let's have a look first about how dealing with files easily:

FILEPATH_ASSTRING=r"F:\PythonCourses\PYTHON_TEST01.txt" #For this test please enter here a path where you have the right to create files.
myFile = open(FILEPATH_ASSTRING,'w')
print type(myFile)#<type 'file'>  Another python object, called a File Descriptor
myFile.write("line1\n")
myFile.writelines(["line2\n","line3\n"])
myFile.close()
print open(FILEPATH_ASSTRING,'r').readlines() #returned as a list of string (lines)
#['line1\n', 'line2\n', 'line3\n'] (to check use REAL notepad, like notepad++, forget the crappy one in windows)

Manage a file with the open built-in function, giving a path (remember the string format for path).
Your variable will not be the string stream, it is an handle, a file - descriptor. Take that object like a cursor into your text editor.
Notice also the second argument. You can open a file in ONE MODE ONLY. 'w' is for write, and if anything in the path already exists, it will purely vanish into the warp !
So be very careful in which mode you have to deal with your file ('w' is write, 'r' for read, 'a' for append).

I hardly show you a full example here, because it will require involving your filesystem, so let's enumerate the most useful functions:
os.mkdir(path) #Create the directory at path (but the sub-path must exist)
os.makedirs(path)#Create the directory at path, and create all parent folder if the sub-path does not exist !
os.rename(OldNamePath, NewNamePath) #Rename the file (but can not change its path).
os.listdir(path) #Get all direct children of current Folder. Could be files or sub-folders !
os.remove(path) #Delete a file, do not work if path is a directory!
os.rmdir(path) #This remove a directory, but only if it is empty
os.chmod(path,mode) #Change the attribute of the file at path (use to put a file in read-only)
os.chown(path, uid, gid) #Change the owner of the file at path (give it to root for example).

import os,os.path
print os.environ #Will display all you need to know about your current machine (USERNAME,COMPUTERNAME,USERDOMAIN, etc.)
myTempFolder = os.environ['TEMP'] #For me it is : 'C:\\Users\\Username\\AppData\\Local\\Temp'
if not os.path.exists(myTempFolder):
  os.makedirs(myTempFolder)
for currentItem in os.listdir(myTempFolder):
  if os.path.isfile(myTempFolder+"\\"+currentItem): #Warning currentItem is JUST the file name.
    print currentItem

The os.path module is a tool to analyze the string as a path one, and the exists function is really mandatory to safely consider the file or dir.
FILEPATH_ASSTRING=r"F:\PythonCourses\PYTHON_TEST01.txt" #remember the r to express the string as RAW ?
os.path.exists(FILEPATH_ASSTRING) # Result: True #
os.path.basename(FILEPATH_ASSTRING) # Result: PYTHON_TEST01.txt #
os.path.dirname(FILEPATH_ASSTRING) # Result: F:\PythonCourses #Returned value is full path of the directory!
os.path.getsize(FILEPATH_ASSTRING) # Result: 18L #  In Bytes
os.path.isdir(FILEPATH_ASSTRING) # Result: False #
os.path.splitdrive(FILEPATH_ASSTRING) # Result: ('F:', '\\PythonCourses\\PYTHON_TEST01.txt') #
os.path.splitext(FILEPATH_ASSTRING) # Result: ('F:\\PythonCourses\\PYTHON_TEST01', '.txt') #
os.path.splitunc(FILEPATH_ASSTRING) # Result: ('', 'F:\\PythonCourses\\PYTHON_TEST01.txt') #   The first tuple will be the start of a UNC path like \\ServerName\...

If you want to copy a file very easily, use the shutil module which can do that for you:
http://docs.python.org/library/shutil.html

import shutil
shutil.copy(SourcePath, DestinationPath) #Copy the file from source to destination
shutil.copy2(SourcePath, DestinationPath) #Idem, but also copy the stat ! (owner, creation date, etc.) A perfect clone!
shutil.copytree(SourcePath, DestinationPath) #Can duplicate recursively a whole filesystem hierarchy.

At last, but not least, os.walk is a very powerful function to explore recursively your filesystem.
Here is the snippet:

fileList=[]
rootdir = os.environ['TEMP']
for root, subFolders, files in os.walk(rootdir):
  for currentFile in files:
    fileList.append(os.path.join(root,currentFile)) #You can use os.path.join instead of the operator +"/"+
import pprint
pprint.pprint(fileList) #Lots of crap right ?
international
26May/120

APyTA_13: Most used modules, Diving into the system

APyTA_13: Most used modules, Diving into the system
Let's talk about the most famous modules.
Theses are not external, they comes as built-in into your python, all you have to do to start playing with them is an import.

pprint (pretty print) is a very handy module to replace the standard print. http://docs.python.org/library/pprint.html
Use it when you need a visual feedback on a variable that is a dictionary, or some nested lists.

The module sys (for system) is a module very helpful to prompt python about itself and its mechanics. http://docs.python.org/library/sys.html
We already view the sys.path which return a string list of all the filesystem directory where python will look for when importing.
You can nicely browse all the modules in ram with .modules (example given with pprint):

import sys
import pprint
pprint.pprint(sys.modules)

Another cool feature of this module is the getsizeof function.
Managing HUGE collections of datas ? Big batch loops ? Well then, keep track of what you are really storing.

import sys
sys.getsizeof("a") #41
sys.getsizeof("") #40
sys.getsizeof(None) #16  So None is 2.5 lighter than an empty list, see what I mean ?
sys.getsizeof(False) #24

Sys help also to manipulate its own buffer stream. Python have a three level of buffer : standard In, Out, and Error (coming from linux world).
For us, what is matter is just standard output (error is the same thing), and that we can redirect it:

userInput = sys.stdin.read() #This just open a prompt
print userInput
sys.stdout.write("This is standard output") #See ? Like a print, but without the newline
sys.stderr.write("This is standard error")

sys.stdout = open("F:\PythonCourses\MayaBuffer.txt",'w') #Insert here an already exsisting file
sys.stdout.write("This is standard output") #Nothing is printed in the Maya script editor !

Notice that the file will not be readable instantaneously, but will be flushed when Maya is closed (or if you close it intentionally with .close() ).
We will see above a better way to log your infos. Eventually, know that you also can access to the starting buffer, initialized at python launch.
But now let's have a look at sys.__stdout__ which is a recorded value of default output for printing at python startup.
This helps that a lot because by default it is the output window of Maya ! (Which have better performance, and can isolate your verbosity from usual commands feedback):

sys.__stdout__.write("I am written in Output window, and I like that.")

I use the module time when I need to name my unique files, or when I upload tracking into the Asset Manager.
http://docs.python.org/library/time.html

import time
print time.strftime("%Y-%m-%d") #2012-05-26
print time.strftime("%H:%M:%S") #12:45:47
print time.strftime("%YY-%mM-%dD_%Hh%Mm%Ss") #2012Y-05M-26D_12h46m07s  This is how I format the time personally

The module logging is dedicated to help you keeping trace of your outputs, and I recommend heavily to do that.
CGI Applications crashes. It happens. You need to keep a log of what the user was doing, that is on top of the application. In a file.
It help you a lot, in AGILE development, when frequently users ask you to come to their desk for an "error".
What error ? They do not read it, or they forgot. But the log know.
And you will not have to reproduce the manipulation (which often requires loading a huge file).

import time
#Setuping of the logger:
import logging
logger = logging.getLogger('ToolName')
logger.setLevel(logging.DEBUG)
LogFileFullPath = LOG_FOLDER+"\\"+time.strftime("%YY-%mM-%dD")+'.log' #I record a log in a daily file
logFileHandle = logging.FileHandler(LogFileFullPath)
logFileHandle.setLevel(logging.DEBUG) #This Level could be change in a menu of yours, if the users wants to be in "quiet mode" for example (speed up huge ierations).
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') #This is how you setup the prefix of all your lines.
logFileHandle.setFormatter(formatter)
logger.addHandler(logFileHandle)
#Example of usage:
logger.info("This is a normal message") #Use that for classic feedback, resolved variables.
logger.warn("This is a warning message") #Use when something is wrong but the process can continue. Show must go on.
logger.error("This is an ERROR !") #Ok, this is Unexpected context, a non-sense argument. You can not continue. Function will stop and user must know it.
logger.critical("This is the end of the WORLD!!!") #Without the feature in scope (connection to the DB, internet acess for examples), the tool is USELESS. Please contact your administrator and give him a coffee.
forum
Tagged as: , , , , No Comments
6May/120

APyTA_12: Handling exceptions, just in case

APyTA_12: Handling exceptions, just in case

Let's see now, how python will help you to manage errors and failure! Yes it can do that too!
try #Test if the next instruction is valid
except #Coupled with try, create a scope in case of try failure
finally #Coupled with try, open a scope independent of try success
raise
#Throw a custom-exception, defined as a class

No full explanation needed here, just see the magic by yourself:

aList=[]
try:
  Doomed = aList[56]  # Error: IndexError: list index out of range #
except :
  print("ERROR")
else:
  print("OK")
finally:
  print("Anyway...")

try:
  Doomed = aList[56]
except IndexError:
  print("ERROR On Index")

try:
  print("toto="+45)
except TypeError: # Error: TypeError: cannot concatenate 'str' and 'int' objects #
  print("ERROR On Type")

Because you sometimes (often?) can't manage all the cases combination that will define your context when computing,
these tools helps you to handle an unexpected and behave accordingly.
You can just let the except keyword alone, or differentiate your scopes to execute, depending on your error type
(You can find all errors types at the address : http://docs.python.org/tutorial/errors.html)

By the way , with raise keyword, you will be able to create your own type of errors, using classes.
(cf further about classes and object creation).

import pymel.core.general
class UnitarySelectionError(BaseException):
  pass

def validSelection():
  currentSelection = pymel.core.general.ls(selection=True, transforms=True)
  if(len(currentSelection)>1):
    raise UnitarySelectionError()
  else:
    print currentSelection[0].name()

try:
  validSelection()
except UnitarySelectionError:
  print("The user have selected more than one transform")
except:
  print("The user have selected nothing!!") #Reached because we do not handle an empty selection, so it is an IndexError at line17
else:
  print("It's ok, you can go on") #Reached if your selection is one transform object)
store
guidelines
6May/122

Acronym is EVIL

I really enjoy all my experience in python-coding because I find it very readable and transparent.
Yes we're math guys, but honestly 80% of our time is dedicated to one single process:
BECAME UNDERSTANDABLE by theses f***ing stupid machines.

Python is really great for that, re-usability, factoring stuff,
building framework, and all your applications can be read like a book.
BUT, and I mean it, all theses efforts are useless if we are fuzzy, imprecise or the worst of it: concise.
I'm pretty sure that nobody will advice to call a variable "a, b , toto", but better to stick to "currentRootJoint", right ?

So why I see sooooo many acronyms along the internet snippets ? Around templates or also in python mechanics ? (Cf my APyTA post on python referencing)
Python does not recommend using import *, to avoid collapsing names.
We're writing to machines, who only recognize keywords, so please do NOT complicates things and shorcut your code.

Yes, "pmc"  is shorter that "pymel.modeling.core" but do you well remember it ? One year later ?
How can you be sure that you will not require an external module, in a distant future, who will have pmc as acronyms  too?
You have "dof" everywhere for your "degree of freedom" in your character-rigging tollkit,
and six month later, you are dealing with rendering and "depth of field", so.... "dof" also ? Is this readable really ?
Same thing in c# and Unity3d, for Random for example (Do you mean UnityEngine.Random  or  System.Random ?)

Therefore, I strictly forbid any acronyms in my personal naming convention. And I mean it.
So yes, my code is wider. But I was taught to not make functions too high, not too large !
We code now on the same workstation that infographists, so 24inches is not an issue at all !

Long import, long call of function (I always write pymel.core.modeling.polyCube() , but like that I am precise. I know where I come from (notion of namespace like in c#).
And when I am dealing with my own framework (which is multi-contextual, and manage several workflows, as Mobu;Maya, etc., with an external asset manager), I really need this "full package path".

On top of that, when you are tinying your names (function, var, import) you just keeping your knowledge for yourself.
(And for the yourself of 'right now'). That's selfish.
A future reader, coming from a non-CGI world, would most probably do not understand you. From the "not so different math-world" there is many ways of splitting our point of view (like the Y-axis definition)...
I know that I 'm quite alone on this fight, and my coding style appears weird or preposterous, and are not like all others API, but I kept this philosophy which suits me.

Tagged as: , , , 2 Comments
faq