Create complex Python objects from dicts and yaml files.
device-spinner is a "no-framework" implementation of the inversion of control pattern.
It faciliates stitching together modular classes with dependency injection with no
performance overhead and without requiring custom decorators, base classes, or any alterations to your code.
Building complex objects from a yaml file:
- write self-contained modular code and wire it together with a spec in a yaml file.
- implements inversion of control pattern enabling you to write modular code without worrying about how to stitch it together.
- simplifies simulation, where some objects can be mocked or stubbed out with a specific config.
- produces a flat view of complex hierarchical objects that take other objects as input (dependency injection).
To use the software, in the root directory, run
pip install -e .To develop the code, run
pip install -e .[dev]which will install extra dependencies for linting.
To create an object from a yaml file, annotate it like this:
devices:
my_list:
module: builtins
class: dict
kwds:
Peach: 10,
Mario: 5,
Samus: 12Then, in Python
import yaml
from pathlib import Path
cfg_file = Path("/path/to/yaml/cfg.yaml")
cfg_content = cfg_file.open("r").read()
device_specs = yaml.safe_load(cfg_content)
devices = factory.create_devices_from_specs(device_specs["devices"])Sometimes, you need to pass an object instance into another objects instance's __init__ (aka: dependency injection).
DeviceSpinner handles this by matching instance names.
Let's say we have a robot that requires an arm and a leg.
# robot_parts.py
class Arm:
pass
class Leg:
pass# robots.py
class JumpingRobot
def __init__(arm: Arm, leg: Leg):
self.arm = arm
self.leg = legIn the yaml file, under my_robot kwds, we pass in the instance names
as they are named in the parent dict (devices).
When DeviceSpinner sees these names that match other object intances in the
parent dictionay, it will first build these dependencies, and then pass them in
as parameters.
devices:
my_robot:
module: robot_lib.robots
class: JumpingRobot
kwds:
arm: my_arm
leg: my_leg
my_arm:
module: robot_lib.robot_parts
class: Arm
my_leg:
module: robot_lib.robot_parts
class: Leg
This works for *args and **kwds also.
Sometimes you don't want the above behavior, and you want to treat strings as strings.
To do so, mark them with the skip_args or skip_kwds field.
By default, DeviceSpinner knows not to insert an instance of itself into itself during __init__.
So if you have have a yaml like this:
devices:
my_robot:
module: robot_lib.robots
class: TurtleBot
kwds:
name: my_robotDeviceSpinner will not try to insert the TurtleBot instance into itself--even though the name parameter matches the outer name of the instance.
This library has a workaround for creating lists that require dependency injection.
Here's an example that doesn't do dependency injection:
devices
my_legs:
module: builtins
class: list
args:
- - left_leg # <-- treated as a string!
- right_leg # <-- treated as a string!
left_leg:
module: robot_lib.robot_parts
class: Leg
right_leg:
module: robot_lib.robot_parts
class: Leg
The reason for this is because the list constructor takes an iterable, (usually a tuple).
The fix is a custom factory function from this library that accepts any number
of arguments (i.e: *args) and returns a list instance.
devices
my_legs:
module: device_spinner.factory_utils
factory: to_list # returns a list insance
args:
- left_leg # <-- will be replaced by the object instance of the same name
- right_leg # <-- will be replaced by the object instance of the same name
left_leg:
module: robot_lib.robot_parts
class: Leg
right_leg:
module: robot_lib.robot_parts
class: Leg
For internal members, please create a branch. For external members, please fork the repository and open a pull request from the fork. We'll primarily use Angular style for commit messages. Roughly, they should follow the pattern:
<type>(<scope>): <short summary>
where scope (optional) describes the packages affected by the code changes and type (mandatory) is one of:
- build: Changes that affect build tools or external dependencies (example scopes: pyproject.toml, setup.py)
- ci: Changes to our CI configuration files and scripts (examples: .github/workflows/ci.yml)
- docs: Documentation only changes
- feat: A new feature
- fix: A bugfix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- test: Adding missing tests or correcting existing tests
The table below, from semantic release, shows which commit message gets you which release type when semantic-release runs (using the default configuration):
| Commit message | Release type |
|---|---|
fix(pencil): stop graphite breaking when too much pressure applied |
|
feat(pencil): add 'graphiteWidth' option |
|
perf(pencil): remove graphiteWidth optionBREAKING CHANGE: The graphiteWidth option has been removed.The default graphite width of 10mm is always used for performance reasons. |
(Note that the BREAKING CHANGE: token must be in the footer of the commit) |
To generate the rst files source files for documentation, run
sphinx-apidoc -o doc_template/source/ src Then to create the documentation HTML files, run
sphinx-build -b html doc_template/source/ doc_template/build/htmlMore info on sphinx installation can be found here.