Custom Classes

Writing custom classes using SANE workflows will mostly depend on which type of workflow object you wish to customize. However, each class is always based off of the sane.options.OptionLoader base class. If you plan on modifying the class to expose configurable options with the idea of allowing both Python and JSON interfacing then you should update the load_extra_options() function for that class.

As the principle is the same for any derivable workflow option, the main concepts are presented here before the specifics of any individual class.

Custom Option Loading

The key idea behind sane.options.OptionLoader is that it provides a consistent and auditable contract for loading configuration dictionaries into an object. This is the same pattern used by sane.Action, sane.Host, sane.Environment, and other workflow objects.

The primary benefits to using the provided functions:

  • It standardizes option loading across workflow objects.

  • It encourages safe handling of configuration dictionaries by consuming supported keys and warning about unused keys.

  • It preserves the ability to trace where options were loaded from with origins.

Primary User Interface Function

A custom sane.options.OptionLoader class (beyond the workflow objects in SANE) is expected to implement or extend load_extra_options()

Quick Reference (click to open/close)
OptionLoader.load_extra_options(options, origin=None)[source]

Load any extra options after load_core_options().

Any processed field should be removed from the options dict, with everything else ignored.

See load_options() for parameters.

Parameters:

This function is always called after load_core_options() during the processing of load_options():

Quick Reference (click to open/close)
OptionLoader.load_options(options, origin=None)[source]

Base class implementation for loading of dict-based attributes into instance

Take a options dict of relevant attributes and load them via load_core_options() then load_extra_options(). The options dict should be modified in each call to remove processed fields so that at the very end of this method, any unused keys in the options dict may be logged.

The load_extra_options() is meant as a user-overwritable method so that load_core_options() may retain core underlying base class implementation details without the risk of base class loading not being called.

To keep track of every time this function is called and potentially modifying this instance an origin may be provided, noting where the change is coming from.

Parameters:
  • options (dict) –

    A dict of class-specific attributes.

    Important

    The options dict is modified such that only unused values are left in it at the end of this method

  • origin (str) – A string identifier of where this load is coming from


Warning

When using ANY load_*_options() method, the most important rule that any key that your implementation handles should be removed from the options dict.

This is because at the end of the call, any remaining keys will be recognized as unused and will be logged.

Python Example

The following simple example defines a custom Action object, and demonstrates some common use patterns for option loading. Use in Python is purely for convenience if configuring from a dict works best for your workflow.

import sane

class MyAction( sane.Action ):
  def __init__( self, id ):
    super().__init__( id )
    self.max_items = 20
    self.prefix    = "memo"

  def load_extra_options( self, options, origin ):
    # Pop from the dictionary using a default value if not found allows this
    # key to remain optional
    self.max_items = options.pop( "max_items", self.max_items )
    self.prefix    = options.pop( "prefix", self.prefix )
    # Continue to load any extra options - good practice for inheritance
    super().load_extra_options(options, origin)

@sane.register
def workflow( orch ):
  action = MyAction( "foo" )
  action.load_options( { "max_items" : 10 } )
  orch.add_action( action )

Note

When the workflow is loaded, action.load_options(...) is the only call to be called as it will under the hood call the derived methods.

JSON Interfacing

When a JSON file declares an object with a "type" field, the sane.Orchestrator will resolve that type using sane.options.OptionLoader.search_type().

Quick Reference (click to open/close)
OptionLoader.search_type(type_str, noexcept=False)[source]

Match a type (as an input string) to an actualy python type

If at any point a search is successful, the function immediately returns the found type.

Search priority:

  1. type_str using pydoc locate() (effectively search current context for type of that fully qualified name )

  2. Split type_str on last . in name and search any user-loaded module that contains the prefix for an attribute matching the suffix. If no split occurs all user modules are searched.

Valid type examples:

import sane
import user_mod.nested.foo # module foo has CustomType

# ... in the context of this class ...
self.search_type( "sane.Action" )
self.search_type( "sane.host.Host" )
self.search_type( "user_mod.nested.foo.CustomType" )
# Using search method (2) if foo was loaded into the user modules by the workflow
# since "foo" is a substring of "user_mod.nested.foo"
self.search_type( "foo.CustomType" )
Returns:

type corresponding to the type_str

Parameters:

type_str (str)


This means that your custom Python module must be available to the workflow before the JSON file is interpreted. When using the sane_runner entry point, this is done for you by default - loading your Python files first before any JSON files. The exact order of operations is outlined in Orchestrator.load_paths():

Quick Reference (click to open/close)
Orchestrator.load_paths()[source]

Load workflow definitions from current search paths and filters

This is the primary load call after all necessary paths and filters have been set. The order of operations is as follows:

  1. Add all search paths to sys.path

  2. All valid files matching at least one search filter across all paths are gathered.

  3. Files are sorted based on file extension into .py and .json[c]

  4. All .py files are loaded via load_py_files()

  5. All registered calls (via @sane.register) are invoked in priority order via process_registered()

  6. All .json[c] files are then loaded via load_config_files() (.json first, then .jsonc)

  7. All patches are processed in priority order via process_patches()

Return type:

None


The JSON type can be the Python fully qualified name or a shortened type name that uniquely identifies the type from the loaded Python modules.

{
  "actions":
  {
    "bar":
    {
      "type": "where_MyAction_is.MyAction",
      "max_items": 50,
    }
  }
}

Hint

Recall from the Tutorial Structure that the path we provide to the runner functions as the root namespace package so our type will be named based off of that. If MyAction is found in .sane/proj/custom_actions/acts.py and run with sane_runner -p .sane/ ... then "type" : "proj.custom_actions.acts.MyAction" is the full type name.

Summary

Setting up the load_extra_options() is a good idea for custom workflow classes that plan to use load_options() either directly from Python or via JSON configs. When adding your own options keep in mind to:

  • Only override load_extra_options() (core options are reserved for internal classes)

  • Always remove each processed key from the options dictionary

  • Always call super.load_extra_options() at some point in your derived method to ensure proper inheritance setup

Specific Classes