11import mgear .pymaya as pm
22from mgear .core import attribute
33from maya import cmds
4+ import sys
5+ import importlib
6+ import os
47
58
69class customShifterMainStep (object ):
7- '''
10+ """
811 Main Class for shifter custom steps
9- '''
12+ """
1013
1114 def __init__ (self , stored_dict ):
12- """Constructor
13- """
15+ """Constructor"""
1416 self ._step_dict = stored_dict
1517 # init local setup root var
1618 # this should be a group to organize the objects of a local setup
@@ -19,14 +21,37 @@ def __init__(self, stored_dict):
1921
2022 @property
2123 def mgear_run (self ):
22- """Returns the resulting object of the 'mgearRun' step.
23- """
24+ """Returns the resulting object of the 'mgearRun' step."""
2425 if "mgearRun" not in self ._step_dict :
2526 raise Exception (
2627 "Can't access the 'mgearRun' in pre steps \
27- or when running individual steps." )
28+ or when running individual steps."
29+ )
30+
31+ return self ._step_dict .get ("mgearRun" )
32+
33+ @property
34+ def rig (self ):
35+ """Alias for mgear_run for more intuitive access.
36+
37+ Returns:
38+ mgearRun: The shifter rig object
39+
40+ Example:
41+ global_ctl = self.rig.global_ctl
42+ """
43+ return self .mgear_run
44+
45+ @property
46+ def guide (self ):
47+ """Access to the guide if available.
2848
29- return self ._step_dict .get ('mgearRun' )
49+ Returns:
50+ Guide: The guide object from the rig build
51+ """
52+ if hasattr (self .mgear_run , "guide" ):
53+ return self .mgear_run .guide
54+ return None
3055
3156 def component (self , name ):
3257 """Access to components from the current build process.
@@ -41,6 +66,41 @@ def component(self, name):
4166 raise KeyError ("Could not find the '{}' component." .format (name ))
4267 return self .mgear_run .components [name ]
4368
69+ def components_by_type (self , comp_type ):
70+ """Get all components of a specific type.
71+
72+ Args:
73+ comp_type (str): The component type (e.g., "control_01", "arm_2jnt_01")
74+
75+ Returns:
76+ list: List of matching components
77+
78+ Example:
79+ all_controls = self.components_by_type("control_01")
80+ for ctrl in all_controls:
81+ print(ctrl.fullName)
82+ """
83+ matching = []
84+ for comp_name , comp in self .mgear_run .components .items ():
85+ if comp .type == comp_type :
86+ matching .append (comp )
87+ return matching
88+
89+ def has_component (self , name ):
90+ """Check if a component exists without raising an error.
91+
92+ Args:
93+ name (str): The name of the component
94+
95+ Returns:
96+ bool: True if component exists, False otherwise
97+
98+ Example:
99+ if self.has_component("arm_L0"):
100+ arm = self.component("arm_L0")
101+ """
102+ return name in self .mgear_run .components
103+
44104 def custom_step (self , name ):
45105 """Allows access to custom steps that have already ran in the past.
46106
@@ -53,25 +113,211 @@ def custom_step(self, name):
53113 if name not in self ._step_dict :
54114 raise KeyError (
55115 "The custom step '{}' does not exist, or \
56- did not run yet." .format (name ))
116+ did not run yet." .format (
117+ name
118+ )
119+ )
57120
58121 return self ._step_dict [name ]
59122
123+ def has_custom_step (self , name ):
124+ """Check if a custom step exists without raising an error.
125+
126+ Args:
127+ name (str): The name of the custom step
128+
129+ Returns:
130+ bool: True if custom step exists, False otherwise
131+
132+ Example:
133+ if self.has_custom_step("proxy_setup"):
134+ proxy = self.custom_step("proxy_setup")
135+ """
136+ return name in self ._step_dict
137+
138+ def run_sub_step (self , module_name , step_path = None ):
139+ """Load and run a sub custom step from an external module.
140+
141+ This method allows you to dynamically load and execute another custom step
142+ from a separate Python file. The sub-step will have access to the same
143+ step dictionary, components, and mgear_run context as the parent step.
144+
145+ The method will automatically:
146+ - Check for MGEAR_SHIFTER_CUSTOMSTEP_PATH environment variable if no step_path is provided
147+ - Add the step_path to sys.path if provided and not already present
148+ - Import and reload the module to get the latest changes
149+ - Find the class that inherits from customShifterMainStep
150+ - Instantiate it with the same step dictionary
151+ - Run its setup() and run() methods
152+ - Register it in the step dictionary for later access
153+
154+ Args:
155+ module_name (str): The name of the Python module/file without the .py extension.
156+ Example: "my_other_step" for file "my_other_step.py"
157+ step_path (str, optional): The absolute directory path where the module is located.
158+ If None, will check MGEAR_SHIFTER_CUSTOMSTEP_PATH environment variable.
159+ If neither is provided, assumes the module is already in Python's path.
160+ Example: "w:/my_steps" or "C:/pipeline/custom_steps"
161+
162+ Returns:
163+ customShifterMainStep: The instantiated and executed sub-step object.
164+ You can use this to access attributes from the sub-step
165+ immediately after execution.
166+
167+ Raises:
168+ ImportError: If the module cannot be found or imported
169+ AttributeError: If the module does not contain a class that inherits
170+ from customShifterMainStep
171+
172+ Examples:
173+ # Run a sub-step from a specific path
174+ self.run_sub_step("apply_colors", "w:/my_steps")
175+
176+ # Run a sub-step using MGEAR_SHIFTER_CUSTOMSTEP_PATH env var
177+ self.run_sub_step("apply_colors")
178+
179+ # Run a sub-step that's already in the Python path
180+ self.run_sub_step("mirror_controls")
181+
182+ # Access the sub-step immediately after running
183+ color_step = self.run_sub_step("apply_colors", "w:/my_steps")
184+ print(color_step.colors_applied)
185+
186+ # Access the sub-step later using custom_step()
187+ self.run_sub_step("apply_colors", "w:/my_steps")
188+ # ... other code ...
189+ colors = self.custom_step("apply_colors").colors_applied
190+
191+ Note:
192+ - The module must contain exactly one class that inherits from
193+ customShifterMainStep (excluding the base class itself)
194+ - The sub-step's name (set in its setup() method) determines how
195+ it can be accessed later via self.custom_step("name")
196+ - The module is reloaded each time to ensure you get the latest version
197+ during development
198+ - If step_path is not provided, the method will check for the
199+ MGEAR_SHIFTER_CUSTOMSTEP_PATH environment variable
200+ """
201+ self .log ("Running sub-step: '{}'" .format (module_name ))
202+
203+ # If no step_path provided, check for environment variable
204+ if step_path is None :
205+ step_path = os .environ .get ("MGEAR_SHIFTER_CUSTOMSTEP_PATH" )
206+
207+ # Add path if provided and not already in sys.path
208+ if step_path and step_path not in sys .path :
209+ sys .path .insert (0 , step_path )
210+
211+ # Import the module
212+ try :
213+ module = importlib .import_module (module_name )
214+ # Reload to ensure we get the latest version
215+ importlib .reload (module )
216+ except ImportError as e :
217+ raise ImportError (
218+ "Could not import module '{}': {}" .format (module_name , e )
219+ )
220+
221+ # Find the CustomShifterStep class in the module
222+ step_class = None
223+ for item_name in dir (module ):
224+ item = getattr (module , item_name )
225+ # Check if it's a class and a subclass of customShifterMainStep
226+ # but not customShifterMainStep itself
227+ if (
228+ isinstance (item , type )
229+ and issubclass (item , customShifterMainStep )
230+ and item is not customShifterMainStep
231+ ):
232+ step_class = item
233+ break
234+
235+ if not step_class :
236+ raise AttributeError (
237+ "Module '{}' has no class that inherits from customShifterMainStep" .format (
238+ module_name
239+ )
240+ )
241+
242+ # Instantiate with the same step dictionary
243+ other_step = step_class (self ._step_dict )
244+
245+ # Run setup
246+ other_step .setup ()
247+
248+ # Register it in the step dictionary
249+ if other_step .name :
250+ self ._step_dict [other_step .name ] = other_step
251+
252+ # Run the step
253+ other_step .run ()
254+
255+ self .log ("Sub-step '{}' completed successfully" .format (module_name ))
256+
257+ return other_step
258+
259+ def get_or_create_setup_root (self , name = None ):
260+ """Get or create the setup root group for organizing custom step objects.
261+
262+ Args:
263+ name (str, optional): Custom name for the setup root.
264+ If None, uses "{self.name}_setup_grp"
265+
266+ Returns:
267+ PyNode: The setup root group
268+
269+ Example:
270+ setup_grp = self.get_or_create_setup_root()
271+ pm.parent(my_object, setup_grp)
272+ """
273+ if not self .setup_root or not pm .objExists (self .setup_root ):
274+ grp_name = name or "{}_setup_root" .format (self .name )
275+ self .setup_root = pm .group (empty = True , name = grp_name )
276+
277+ # Try to parent under rig setup group if it exists
278+ if pm .objExists (
279+ self .mgear_run .setupWS
280+ ):
281+ pm .parent (self .setup_root , self .mgear_run .setupWS )
282+
283+ return self .setup_root
284+
285+ def log (self , message , level = "info" ):
286+ """Log a message with the step name prefix.
287+
288+ Args:
289+ message (str): The message to log
290+ level (str): Log level - "info", "warning", or "error"
291+
292+ Example:
293+ self.log("Processing controls")
294+ self.log("Missing component", level="warning")
295+ """
296+ prefix = "[{}]" .format (self .name ) if hasattr (self , "name" ) else "[CustomStep]"
297+ full_message = "{} {}" .format (prefix , message )
298+
299+ if level == "warning" :
300+ pm .warning (full_message )
301+ elif level == "error" :
302+ pm .error (full_message )
303+ else :
304+ print (full_message )
305+
60306 def setup (self ):
61- """This function mus be re implemented for each custom step.
307+ """This function must be re implemented for each custom step.
62308
63309 Raises:
64- Exception: Description
310+ NotImplementedError: If not implemented in subclass
65311 """
66- raise NotImplemented ("'setup' must be implemented" )
312+ raise NotImplementedError ("'setup' must be implemented" )
67313
68314 def run (self ):
69- """This function mus be re implemented for each custom step.
315+ """This function must be re implemented for each custom step.
70316
71317 Raises:
72- Exception: Description
318+ NotImplementedError: If not implemented in subclass
73319 """
74- raise NotImplemented ("'run' must be implemented" )
320+ raise NotImplementedError ("'run' must be implemented" )
75321
76322 def dup (self , source , name = None ):
77323 """Duplicate the source object and rename it
0 commit comments