Search code examples
pythonpython-importnvidia-isaac

Python class-level "global" imports


I am writing a robotics simulation using IsaacSim which I would like to encapsulate in a custom class to keep the simulation bits separate from the rest of my logic. However, the IsaacSim python API has an "extension" mechanism that requires the following import structure:

from isaacsim import SimulationApp
app = SimulationApp(some_params)
# other isaacsim imports
import omni.isaacsim.whatever

Where all the further imports (omni.isaacsim.whatever) are only findable once the SimulationApp instance has been created. Creating this instance takes a while (as it launches the IsaacSim simulator), and requires parameters that I need to set dynamically. My current code structure is as follows:

class Sim(object):
  def __init__(self, params):
      from isaacsim import SimulationApp
      self.app = SimulationApp(params)
      import omni.isaacsim.whatever
      # etc.
  
  def some_function(self):
      # using omni.XY import

  def other_func(self):
      # also using omni.XY import

if __name__=='__main__':
  # figure out initialization and params
  ...
  sim = Sim(params)
  # rest of the logic
  sim.some_function()
  ...

My issue is in figuring out where to properly put the further omni.XY imports. I need them in multiple functions of the Sim class, however importing them in __init__ with import omni.XY doesn't expose them in the other functions of Sim, and putting the import statement for every omni.whatever module in every function of Sim doesn't seem right. Is there some pattern that would respect the following constraints:

  1. all omni.whatever imports must only happen after the SimulationApp instance is created (ideally in Sim.__init__)
  2. these omni.whatever imports need to be accessible by every function in the Sim class
  3. (edit) SimulationApp instance is only created when needed - i.e., when Sim is created

It is possible that creating Sim as a class is not the correct solution and I'm open to other suggestions, although do keep in mind that I need to process some parameters to create the SimulationApp instance.


Edit: the parameters for SimulationApp are set in code based on other parts of the system (the module won't necessarily be run as the main script) and may not be known at the module level. Creating the SimulationApp launches a heavy simulator and I need some control over when that happens as some other initialization needs to happen first. Creating this instance at the module level is impractical and would require to import the containing module in the middle of initialization code.


Solution

  • Python has somewhat simple mechanisms to control its variable and method scoping, and having access to them dynamically.

    Once one gets a grasp of those, it is easy to deal with them. I guess the best thing to to here, if __init__ will always run first than the other methods, is to expose then as module-global names so that they can be used from everywhere else in the same module, just like if it were done in a top-level import.

    As it turns out, the import statement just does a variable assignment in the scope where it occurs. Either import modulename (equivalent to modulename = __import__("modulename") ) or from module import name (name = __import__("module").name)

    So, all you need to have the imports inside __init__ to behave just like toplevel imports is to declare the top-most module name as a global variable, prior to the import.

    I.E.:

    class Sim(object):
      def __init__(self, params):
          global omni
          from isaacsim import SimulationApp
          self.app = SimulationApp(params)
          import omni.isaacsim.whatever
    

    Just adding the global omni declaration there will make the omni variable, as assigned by the import statement further down, available for all functions and methods in the same module. The inner modules and packages are inner attriutes to the omni object, so they will just work.

    Showing another example with plain stdlib modules in a Python REPL session:

    
    
    In [67]: def blah():
        ...:     import math
        ...:     ...
        ...:
    
    In [68]: math
    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[68], line 1
    ----> 1 math
    
    NameError: name 'math' is not defined
    
    In [69]: blah()
    
    In [70]: math
    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[70], line 1
    ----> 1 math
    
    NameError: name 'math' is not defined
    
    In [71]: def blah():
        ...:     global math
        ...:     import math
        ...:     ...
        ...:
    
    In [72]: blah()
    
    In [73]: math
    Out[73]: <module 'math' from '/home/jsbueno/.pyenv/versions/3.11.9/lib/python3.11/lib-dynload/math.cpython-311-x86_64-linux-gnu.so'>
    
    

    Restricting the availability of omni to an instance attributes:

    Otherwise, if you find interesting that in the other methods you can get to the imported modules by doing self.omni.XY, it is possible to trick the import statement to populate the self namespace by running it inside an exec - which allows one to especify the namespace where a command block is executed. So:

    class Sim(object):
      def __init__(self, params):
          from isaacsim import SimulationApp
          self.app = SimulationApp(params)
          exec("import omni.isaacsim.whatever", vars(self))
          ...
      
      def some_function(self):
          # using omni.XY import:
          self.omni.isaacsim.whatever.func(...)
      ...