Usage¶
The aim of audobject
is to provide
a base class, namely audobject.Object
,
which allows it to convert an object to a YAML string
and re-instantiate the object from it.
Object class¶
Let’s create a class that derives from audobject.Object
.
import audobject
__version__ = '1.0.0' # pretend we have a package version
class MyObject(audobject.Object):
def __init__(
self,
string: str,
*,
num_repeat: int = 1,
):
self.string = string
self.num_repeat = num_repeat
def __str__(self) -> str:
return ' '.join([self.string] * self.num_repeat)
Now we instantiate an object and print it.
o = MyObject('hello object!', num_repeat=2)
print(o)
hello object! hello object!
As expected we see that string
is repeated num_repeat
times.
But since we derived from audobject.Object
we get some additional functionality.
For instance, we can get a dictionary with the arguments the object was initialized with.
o.arguments
{'string': 'hello object!', 'num_repeat': 2}
Or a dictionary that also stores module, object name and package version.
o_dict = o.to_dict()
print(o_dict)
{'$__main__.MyObject==1.0.0': {'string': 'hello object!', 'num_repeat': 2}}
And we can re-instantiate the object from it.
o2 = audobject.from_dict(o_dict)
print(o2)
hello object! hello object!
We can also convert it to YAML.
o_yaml = o.to_yaml_s()
print(o_yaml)
$__main__.MyObject==1.0.0:
string: hello object!
num_repeat: 2
And create the object from YAML.
o3 = audobject.from_yaml_s(o_yaml)
print(o3)
hello object! hello object!
If we want, we can override arguments when we instantiate an object.
o4 = audobject.from_yaml_s(
o_yaml,
override_args={
'string': 'I was set to a different value!'
}
)
print(o4)
I was set to a different value! I was set to a different value!
Or save an object to disk and re-instantiate it from there.
file = 'my.yaml'
o.to_yaml(file)
o5 = audobject.from_yaml(file)
print(o5)
hello object! hello object!
Object ID¶
Every object has an ID.
o = MyObject('I am unique!', num_repeat=2)
print(o.id)
10e6cf43-5246-94cd-04d2-35d51ac22bb2
Objects with exact same arguments share the same ID.
o2 = MyObject('I am unique!', num_repeat=2)
print(o.id == o2.id)
True
When an object is serialized the ID does not change.
o3 = audobject.from_yaml_s(o.to_yaml_s())
print(o3.id == o.id)
True
Objects with different arguments get different IDs.
o4 = MyObject('I am different!', num_repeat=2)
print(o.id == o4.id)
False
Malformed objects¶
In the constructor of MyObject
we have assigned
every argument to an attribute with the same name.
This ensures that we can re-instantiate the object from YAML.
Let’s create a class where we don’t follow this rule.
class MyBadObject(audobject.Object):
def __init__(
self,
string: str,
*,
num_repeat: int = 1,
):
self.msg = string
self.repeat = num_repeat
def __str__(self) -> str:
return ' '.join([self.msg] * self.repeat)
At a first glance, everything works as expected.
bad = MyBadObject('test', num_repeat=2)
print(bad)
test test
But if we try to instantiate the object from YAML, we’ll get an error.
bad_yaml = bad.to_yaml_s()
bad2 = audobject.from_yaml_s(bad_yaml)
print(bad2)
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[17], line 1
----> 1 bad_yaml = bad.to_yaml_s()
2 bad2 = audobject.from_yaml_s(bad_yaml)
3 print(bad2)
File ~/work/audobject/audobject/audobject/core/object.py:366, in Object.to_yaml_s(self, include_version)
342 def to_yaml_s(
343 self,
344 *,
345 include_version: bool = True,
346 ) -> str:
347 r"""Convert object to YAML string.
348
349 Args:
(...)
364
365 """ # noqa: E501
--> 366 return yaml.dump(self.to_dict(include_version=include_version))
File ~/work/audobject/audobject/audobject/core/object.py:309, in Object.to_dict(self, include_version, flatten, root)
275 r"""Converts object to a dictionary.
276
277 Includes items from :attr:`audobject.Object.arguments`.
(...)
298
299 """ # noqa: E501
300 name = utils.create_class_key(self.__class__, include_version)
302 d = {
303 key: self._encode_variable(
304 key,
305 value,
306 include_version,
307 root,
308 )
--> 309 for key, value in self.arguments.items()
310 }
312 return Object._flatten(d) if flatten else {name: d}
File ~/work/audobject/audobject/audobject/core/object.py:80, in Object.arguments(self)
78 missing.append(name)
79 if len(missing) > 0:
---> 80 raise RuntimeError(
81 'Arguments '
82 f'{missing} '
83 'of '
84 f'{self.__class__} '
85 'not assigned to attributes of same name.'
86 )
88 # check borrowed attributes
89 for key, value in borrowed.items():
RuntimeError: Arguments ['string', 'num_repeat'] of <class '__main__.MyBadObject'> not assigned to attributes of same name.
However, in the next section we’ll learn that it’s possible to hide arguments. If we hide an argument, we don’t have to set it to an attribute of the same name.
Borrowed arguments¶
It is possible to borrow arguments from an instance attribute.
For instance, here we borrow the attributes x
, y
, and z
from self.point
and a dictionary self.d
.
Using borrowed arguments it becomes possible
to add properties with the name of an argument.
class Point:
def __init__(
self,
x: int,
y: int,
):
self.x = x
self.y = y
class ObjectWithBorrowedArguments(audobject.Object):
@audobject.init_decorator(
borrow={
'x': 'point',
'y': 'point',
'z': 'd',
},
)
def __init__(
self,
x: int,
y: int,
z: int,
):
self.point = Point(x, y)
self.d = {'z': z}
@property
def z(self): # property sharing the name of an argument
return self.d['z']
o = ObjectWithBorrowedArguments(0, 1, 2)
print(o.to_yaml_s())
$__main__.ObjectWithBorrowedArguments==1.0.0:
x: 0
y: 1
z: 2
Object with kwargs¶
If the __init__
function accepts **kwargs
,
we have to pass them to the base class.
This is necessary,
since we cannot figure out
the names of additional keyword arguments
from the signature of the function.
class MyObjectWithKwargs(audobject.Object):
def __init__(
self,
string: str,
**kwargs,
):
super().__init__(**kwargs) # inform base class about keyword arguments
self.string = string
self.num_repeat = kwargs['num_repeat'] if 'num_repeat' in kwargs else 1
def __str__(self) -> str:
return ' '.join([self.string] * self.num_repeat)
o = MyObjectWithKwargs('I have kwargs', num_repeat=3)
print(o)
I have kwargs I have kwargs I have kwargs
When we serialize the object,
we see that keyword argument
num_repeat
will be included.
o_yaml = o.to_yaml_s()
print(o_yaml)
$__main__.MyObjectWithKwargs==1.0.0:
string: I have kwargs
num_repeat: 3
Object as argument¶
It is possible to have arguments of
type audobject.Object
.
For instance, we can define the following class.
class MySuperObject(audobject.Object):
def __init__(
self,
obj: MyObject,
):
self.obj = obj
def __str__(self) -> str:
return f'[{str(self.obj)}]'
And initialize it with an instance of MyObject
.
o = MyObject('eat me!')
w = MySuperObject(o)
print(w)
[eat me!]
This translates to the following YAML string.
w_yaml = w.to_yaml_s()
print(w_yaml)
$__main__.MySuperObject==1.0.0:
obj:
$__main__.MyObject==1.0.0:
string: eat me!
num_repeat: 1
From which we can re-instantiate the object.
w2 = audobject.from_yaml_s(w_yaml)
print(w2)
[eat me!]
Value resolver¶
As long as the type of an arguments is one of:
bool
datetime.datetime
dict
float
int
list
None
Object
str
it is ensured that we get a clean YAML file.
Other types may be encoded using the !!python/object
tag
and clutter the YAML syntax.
To illustrate this, let’s use an instance of timedelta.
from datetime import timedelta
class MyDeltaObject(audobject.Object):
def __init__(
self,
delta: timedelta,
):
self.delta = delta
def __str__(self) -> str:
return str(self.delta)
As before, we can create an instance and print it.
delta = timedelta(
days=50,
seconds=27,
microseconds=10,
milliseconds=29000,
minutes=5,
hours=8,
weeks=2
)
d = MyDeltaObject(delta)
print(d)
64 days, 8:05:56.000010
But if we convert it to YAML, we’ll see a warning.
d_yaml = d.to_yaml_s()
print(d_yaml)
$__main__.MyDeltaObject==1.0.0:
delta: !!python/object/apply:datetime.timedelta
- 64
- 29156
- 10
/home/runner/work/audobject/audobject/audobject/core/object.py:414: RuntimeWarning: No default encoding exists for type '<class 'datetime.timedelta'>'. Consider to register a custom resolver. warnings.warn(
And in fact, we can see that
the delta
value is encoded
with a !!python/object
tag,
which is followed by some plain numbers.
Only after looking into the documentation of
timedelta we can guess that
they probably encode days
, seconds
,
and microseconds
.
We can avoid this by providing a custom resolver that defines how a timedelta object should be encoded and decoded.
class DeltaResolver(audobject.resolver.Base):
def decode(self, value: dict) -> timedelta:
return timedelta(
days=value['days'],
seconds=value['seconds'],
microseconds=value['microseconds'],
)
def encode(self, value: timedelta) -> dict:
return {
'days': value.days,
'seconds': value.seconds,
'microseconds': value.microseconds,
}
def encode_type(self):
return dict
To apply our custom resolver to the
delta
argument, we pass it to the
audobject.init_decorator()
decorator of the __init__
function.
class MyResolvedDeltaObject(audobject.Object):
@audobject.init_decorator(
resolvers={'delta': DeltaResolver},
)
def __init__(
self,
delta: timedelta,
):
self.delta = delta
def __str__(self) -> str:
return str(self.delta)
Now, we don’t get a warning
and the !!python/object
tag has disappeared.
d = MyResolvedDeltaObject(delta)
d_yaml = d.to_yaml_s()
print(d_yaml)
$__main__.MyResolvedDeltaObject==1.0.0:
delta:
days: 64
seconds: 29156
microseconds: 10
Resolve file paths¶
Portability is a core feature of
audobject
.
Assume we have an object
that takes as argument the path to a file.
When we serialize the object
we want to make sure that:
we store the file path relative to the YAML file
the path is correctly expanded when we re-instantiate the object
This can be achieved using
audobject.resolver.FilePath
.
class MyObjectWithFile(audobject.Object):
@audobject.init_decorator(
resolvers={
'path': audobject.resolver.FilePath, # ensure portability
}
)
def __init__(
self,
path: str,
):
self.path = path
def read(self): # print path and content
print(self.path)
with open(self.path, 'r') as fp:
print(fp.readlines())
Here, we create a file and pass it to the object.
import os
import audeer
root = 'root'
res_path = os.path.join(root, 're', 'source.txt') # root/re/source.txt
audeer.mkdir(os.path.dirname(res_path))
with open(res_path, 'w') as fp:
fp.write('You found me!')
o = MyObjectWithFile(res_path)
o.read()
root/re/source.txt
['You found me!']
When we serialize the object, the path is stored relative to the directory of the YAML file.
import yaml
yaml_path = os.path.join(root, 'yaml', 'object.yaml') # root/yaml/object.yaml
o.to_yaml(yaml_path)
with open(yaml_path, 'r') as fp:
content = yaml.load(fp, Loader=yaml.Loader)
content
{'$__main__.MyObjectWithFile==1.0.0': {'path': '../re/source.txt'}}
When we re-instantiate the object the path gets expanded again.
o2 = audobject.from_yaml(yaml_path)
o2.read()
/home/runner/work/audobject/audobject/docs/tmp/root/re/source.txt
['You found me!']
This will also work from another location. Note that we have to move all referenced files as well, as their relative location to the YAML file must not change.
import shutil
new_root = os.path.join('some', 'where', 'else')
shutil.move(root, new_root)
yaml_path_new = os.path.join(new_root, 'yaml', 'object.yaml')
o3 = audobject.from_yaml(yaml_path_new)
o3.read()
/home/runner/work/audobject/audobject/docs/tmp/some/where/else/re/source.txt
['You found me!']
Serialize functions¶
To serialize functions,
a special resolver
audobject.resolver.Function
can be used.
It encodes the source code of the function
and dynamically creates the function during decoding.
The following class takes as arguments a function with two parameters.
import typing
class MyObjectWithFunction(audobject.Object):
@audobject.init_decorator(
resolvers={
'func': audobject.resolver.Function,
}
)
def __init__(
self,
func: typing.Callable[[int, int], int],
):
self.func = func
def __call__(self, a: int, b: int):
return self.func(a, b)
Here, we initialize an object with a function that sums up the two parameters.
def add(a, b):
return a + b
o = MyObjectWithFunction(add)
o(1, 2)
3
When we serialize the object, the definition of our function is stored in plain text.
o_yaml = o.to_yaml_s()
print(o_yaml)
$__main__.MyObjectWithFunction==1.0.0:
func: "def add(a, b):\n return a + b\n"
From which the function can be dynamically initialized when the object is recreated.
o2 = audobject.from_yaml_s(o_yaml)
o2(2, 3)
5
It also works for lambda expressions.
o3 = MyObjectWithFunction(lambda a, b: a * b)
o3_yaml = o3.to_yaml_s()
print(o3_yaml)
$__main__.MyObjectWithFunction==1.0.0:
func: 'lambda a, b: a * b'
o4 = audobject.from_yaml_s(o3_yaml)
o4(2, 3)
6
Instead of a function,
we can also pass a callable object
that derives from
audobject.Object
.
class MyCallableObject(audobject.Object):
def __init__(
self,
n: int,
):
self.n = n
def __call__(self, a: int, b: int):
return (a + b) * self.n
a_callable_object = MyCallableObject(2)
o5 = MyObjectWithFunction(a_callable_object)
o5(4, 5)
18
In that case, the YAML representation is store instead of the function code.
o5_yaml = o5.to_yaml_s()
print(o5_yaml)
$__main__.MyObjectWithFunction==1.0.0:
func:
$__main__.MyCallableObject==1.0.0:
n: 2
And we can still restore the original object.
o6 = audobject.from_yaml_s(o5_yaml)
o6(4, 5)
18
Warning
Since the described mechanism offers a way to execute arbitrary Python code, you should never load objects from a source you do not trust!
Flat dictionary¶
Let’s create a class that takes as input a string, a list and a dictionary.
class MyListDictObject(audobject.Object):
def __init__(
self,
a_str: str,
a_list: list,
a_dict: dict,
):
self.a_str = a_str
self.a_list = a_list
self.a_dict = a_dict
And initialize an object.
o = MyListDictObject(
a_str='test',
a_list=[1, '2', o],
a_dict={'pi': 3.1416, 'e': 2.71828},
)
o.to_dict()
{'$__main__.MyListDictObject==1.0.0': {'a_str': 'test',
'a_list': [1,
'2',
{'$__main__.MyObjectWithFunction==1.0.0': {'func': 'def add(a, b):\n return a + b\n'}}],
'a_dict': {'pi': 3.1416, 'e': 2.71828}}}
As expected, the dictionary of the object
looks pretty nested.
This is not always handy,
e.g. if we try to store the object to a
audfactory.Lookup
table,
this would not work.
Therefore, in can sometimes be useful to
get a flatten version of the dictionary.
o.to_dict(flatten=True)
{'a_str': 'test',
'a_list.0': 1,
'a_list.1': '2',
'a_list.2.func': 'def add(a, b):\n return a + b\n',
'a_dict.pi': 3.1416,
'a_dict.e': 2.71828}
However, it’s important to note that it’s not possible to re-instantiate an object from a flattened dictionary.
Versioning¶
When an object is converted to YAML the package version is stored. But what happens if we later load the object with a different package version?
Let’s create another instance of MyObject
.
o = MyObject('I am a 1.0.0!', num_repeat=2)
print(o)
I am a 1.0.0! I am a 1.0.0!
And convert it to YAML.
o_yaml = o.to_yaml_s()
print(o_yaml)
$__main__.MyObject==1.0.0:
string: I am a 1.0.0!
num_repeat: 2
Loading it with a newer version of the package works without problems.
__version__ = '1.1.0'
o2 = audobject.from_yaml_s(o_yaml)
print(o2)
I am a 1.0.0! I am a 1.0.0!
But if we load it with an older version,
a warning will be shown.
We can force it to show no warning at all,
or always show a warning
when the package version does not match
by adjusting
audobject.config.PACKAGE_MISMATCH_WARN_LEVEL
.
__version__ = '0.9.0'
o3 = audobject.from_yaml_s(o_yaml)
print(o3)
I am a 1.0.0! I am a 1.0.0!
/home/runner/work/audobject/audobject/audobject/core/utils.py:90: RuntimeWarning: Instantiating __main__.MyObject from version '1.0.0' when using version '0.9.0'. warnings.warn(
Now we pretend that we update the package to 2.0.0
.
It includes a new version of MyObject
,
with a slightly changed __str__
function.
__version__ = '2.0.0'
class MyObject(audobject.Object):
def __init__(
self,
string: str,
*,
num_repeat: int = 1,
):
self.string = string
self.num_repeat = num_repeat
def __str__(self) -> str:
return ','.join([self.string] * self.num_repeat)
Since the signature of the constructor has not changed, the object will be created without problems. However, when we print the object the strings are now separated by comma.
o3 = audobject.from_yaml_s(o_yaml)
print(o3)
I am a 1.0.0!,I am a 1.0.0!
In the next release, we decide to introduce an argument that let the user set a custom delimiter.
__version__ = '2.1.0'
class MyObject(audobject.Object):
def __init__(
self,
string: str,
delimiter: str,
*,
num_repeat: int = 1,
):
self.string = string
self.delimiter = delimiter
self.num_repeat = num_repeat
def __str__(self) -> str:
return ' '.join([self.string] * self.num_repeat)
If we now instantiate the object, we will get an error, because we are missing a value for the new argument.
audobject.from_yaml_s(o_yaml)
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[62], line 1
----> 1 audobject.from_yaml_s(o_yaml)
File ~/work/audobject/audobject/audobject/core/api.py:176, in from_yaml_s(yaml_string, auto_install, override_args, **kwargs)
173 for key, value in kwargs.items():
174 override_args[key] = value
--> 176 return from_dict(
177 yaml.load(yaml_string, yaml.Loader),
178 auto_install=auto_install,
179 override_args=override_args,
180 )
File ~/work/audobject/audobject/audobject/core/api.py:69, in from_dict(d, root, auto_install, override_args, **kwargs)
62 for key, value in d[name].items():
63 params[key] = _decode_value(
64 value,
65 auto_install,
66 override_args,
67 )
---> 69 object, params = utils.get_object(
70 cls,
71 version,
72 installed_version,
73 params,
74 root,
75 override_args,
76 )
78 if isinstance(object, Object):
79 # create attribute to signal that object was loaded
80 object.__dict__[define.OBJECT_LOADED] = None
File ~/work/audobject/audobject/audobject/core/utils.py:152, in get_object(cls, version, installed_version, params, root, override_args)
150 missing_required_params = list(required_params - set(params))
151 if len(missing_required_params) > 0:
--> 152 raise RuntimeError(
153 f"Missing mandatory arguments "
154 f"{missing_required_params} "
155 f"while instantiating {cls} from "
156 f"version '{version}' when using "
157 f"version '{installed_version}'."
158 )
160 # check for missing optional arguments
161 optional_params = set([
162 p.name for p in signature.parameters.values()
163 if p.default != inspect.Parameter.empty and p.name not in [
164 'self', 'args', 'kwargs',
165 ]
166 ])
RuntimeError: Missing mandatory arguments ['delimiter'] while instantiating <class '__main__.MyObject'> from version '1.0.0' when using version '2.1.0'.
Since we want to be backward compatible, we decide to release a bug fix, where we initialize the new argument with a default value.
__version__ = '2.1.1'
class MyObject(audobject.Object):
def __init__(
self,
string: str,
delimiter: str = ',',
*,
num_repeat: int = 1,
):
self.string = string
self.delimiter = delimiter
self.num_repeat = num_repeat
def __str__(self) -> str:
return ' '.join([self.string] * self.num_repeat)
And in fact, it successfully creates the object again. It works, because it now has a default value for the missing argument.
o4 = audobject.from_yaml_s(o_yaml)
print(o4)
I am a 1.0.0! I am a 1.0.0!
Finally, we will do it the other way round.
Create an object with version 2.1.1
.
o5 = MyObject('I am a 2.1.1!', num_repeat=2)
print(o5)
I am a 2.1.1! I am a 2.1.1!
Convert it to YAML.
o5_yaml = o5.to_yaml_s()
print(o5_yaml)
$__main__.MyObject==2.1.1:
string: I am a 2.1.1!
delimiter: ','
num_repeat: 2
And load it with 1.0.0
.
__version__ = '1.0.0'
class MyObject(audobject.Object):
def __init__(
self,
string: str,
*,
num_repeat: int = 1,
):
self.string = string
self.num_repeat = num_repeat
def __str__(self) -> str:
return ' '.join([self.string] * self.num_repeat)
o6 = audobject.from_yaml_s(o5_yaml)
print(o6)
I am a 2.1.1! I am a 2.1.1!
/home/runner/work/audobject/audobject/audobject/core/utils.py:90: RuntimeWarning: Instantiating __main__.MyObject from version '2.1.1' when using version '1.0.0'. warnings.warn( /home/runner/work/audobject/audobject/audobject/core/utils.py:186: RuntimeWarning: Ignoring arguments ['delimiter'] while instantiating <class '__main__.MyObject'> from version '2.1.1' when using version '1.0.0'. warnings.warn(
In fact, it works, too. However, a warning is given that an argument was ignored.
Dictionary¶
audobject.Dictionary
implements a
audobject.Object
that can used like a dictionary.
d = audobject.Dictionary(
string='I am a dictionary!',
pi=3.14159265359,
)
print(d)
$audobject.core.dictionary.Dictionary:
string: I am a dictionary!
pi: 3.14159265359
We can use []
notation to access the
values of the dictionary.
d['string'] = 'Still a dictionary!'
d['new'] = None
print(d)
$audobject.core.dictionary.Dictionary:
string: Still a dictionary!
pi: 3.14159265359
new: null
And update from another dictionary.
d2 = audobject.Dictionary(
string='I will be a dictionary forever!',
object=MyObject('Hey, I am an object.'),
)
d.update(d2)
print(d)
$audobject.core.dictionary.Dictionary:
string: I will be a dictionary forever!
pi: 3.14159265359
new: null
object:
$__main__.MyObject:
string: Hey, I am an object.
num_repeat: 1
And we can read/write the dictionary from/to a file.
file = 'dict.yaml'
d.to_yaml(file)
d3 = audobject.from_yaml(file)
print(d3)
$audobject.core.dictionary.Dictionary:
string: I will be a dictionary forever!
pi: 3.14159265359
new: null
object:
$__main__.MyObject:
string: Hey, I am an object.
num_repeat: 1
Parameters¶
You have probably used argparse before. It is a package to write user-friendly command-line interfaces that allows the user to define what arguments are required, what are the expected types, default values, etc.
The idea behind audobject.Parameters
is similar
(in fact, we will see that i even has an interface to argparse).
audobject.Parameters
is basically a collection of
named values that control the behaviour of an object.
Each value is wrapped in a audobject.Parameter
object and has a specific type and default value,
possibly one of a set of choices.
And it can be bound a parameter to a specific versions.
Let’s pick up the previous example and define two parameters. A parameter that holds a string.
string = audobject.Parameter(
value_type=str,
description='the string we want to repeat',
value='bar',
choices=['bar', 'Bar', 'BAR'],
)
print(string)
$audobject.core.parameter.Parameter:
value_type: str
description: the string we want to repeat
value: bar
default_value: null
choices:
- bar
- Bar
- BAR
version: null
And a parameter that defines how many times we want to repeat the string.
repeat = audobject.Parameter(
value_type=int,
description='the number of times we want to repeat',
default_value=1,
)
print(repeat)
$audobject.core.parameter.Parameter:
value_type: int
description: the number of times we want to repeat
value: 1
default_value: 1
choices: null
version: null
Now we combine the two parameters into a list.
params = audobject.Parameters(
string=string,
num_repeat=repeat,
)
print(params)
Name Value Default Choices Description Version
---- ----- ------- ------- ----------- -------
string bar None ['bar', 'Bar', 'BAR'] the string we want to repeat None
num_repeat 1 1 None the number of times we want to repeat None
If we call the list, we get a dictionary of parameter names and values.
params()
{'string': 'bar', 'num_repeat': 1}
We can access the values of the parameters using .
notation.
params.string = 'BAR'
params.num_repeat = 2
print(params)
Name Value Default Choices Description Version
---- ----- ------- ------- ----------- -------
string BAR None ['bar', 'Bar', 'BAR'] the string we want to repeat None
num_repeat 2 1 None the number of times we want to repeat None
If we try to assign a value that is not in choices, we will get an error.
params.string = 'par'
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[77], line 1
----> 1 params.string = 'par'
File ~/work/audobject/audobject/audobject/core/parameter.py:302, in Parameters.__setattr__(self, name, value)
300 def __setattr__(self, name: str, value: typing.Any): # noqa: D105
301 p = self.__dict__[name]
--> 302 p.set_value(value)
File ~/work/audobject/audobject/audobject/core/parameter.py:126, in Parameter.set_value(self, value)
113 def set_value(self, value: typing.Any):
114 r"""Sets a new value.
115
116 Applies additional checks, e.g. if value is of the expected type.
(...)
124
125 """
--> 126 self._check_value(value)
127 self.value = value
File ~/work/audobject/audobject/audobject/core/parameter.py:137, in Parameter._check_value(self, value)
132 raise TypeError(
133 f"Invalid type '{type(value)}', "
134 f"expected {self.value_type}."
135 )
136 if self.choices is not None and value not in self.choices:
--> 137 raise ValueError(
138 f"Invalid value '{value}', "
139 f"expected one of {self.choices}."
140 )
ValueError: Invalid value 'par', expected one of ['bar', 'Bar', 'BAR'].
It is possible to assign a version (or a range of versions) to a parameter.
delim = audobject.Parameter(
value_type=str,
description='defines the delimiter',
default_value=',',
version='>=2.0.0,<3.0.0'
)
params['delimiter'] = delim
print(params)
Name Value Default Choices Description Version
---- ----- ------- ------- ----------- -------
string BAR None ['bar', 'Bar', 'BAR'] the string we want to repeat None
num_repeat 2 1 None the number of times we want to repeat None
delimiter , , None defines the delimiter >=2.0.0,<3.0.0
We can check if a parameter is available for a specific version.
'1.0.0' in delim, '2.4.0' in delim
(False, True)
We can also filter a list of parameters by version.
params_v3 = params.filter_by_version('3.0.0')
print(params_v3)
Name Value Default Choices Description Version
---- ----- ------- ------- ----------- -------
string BAR None ['bar', 'Bar', 'BAR'] the string we want to repeat None
num_repeat 2 1 None the number of times we want to repeat None
Or add them to a command line interface.
import argparse
parser = argparse.ArgumentParser()
params.to_command_line(parser)
print(parser.format_help())
usage: ipykernel_launcher.py [-h] [--string {bar,Bar,BAR}]
[--num_repeat NUM_REPEAT] [--delimiter DELIMITER]
optional arguments:
-h, --help show this help message and exit
--string {bar,Bar,BAR}
the string we want to repeat
--num_repeat NUM_REPEAT
the number of times we want to repeat
--delimiter DELIMITER
defines the delimiter (version: >=2.0.0,<3.0.0)
Or update the values from a command line interface.
args = parser.parse_args(
args=['--string=Bar', '--delimiter=;']
)
params.from_command_line(args)
print(params)
Name Value Default Choices Description Version
---- ----- ------- ------- ----------- -------
string Bar None ['bar', 'Bar', 'BAR'] the string we want to repeat None
num_repeat 1 1 None the number of times we want to repeat None
delimiter ; , None defines the delimiter >=2.0.0,<3.0.0
It is possible to convert it into a file path that keeps track of the parameters.
params.to_path(sort=True)
'delimiter[;]/num_repeat[1]/string[Bar]'
Last but not least, we can read/write the parameters from/to a file.
file = 'params.yaml'
params.to_yaml(file)
params2 = audobject.from_yaml(file)
print(params2)
Name Value Default Choices Description Version
---- ----- ------- ------- ----------- -------
string Bar None ['bar', 'Bar', 'BAR'] the string we want to repeat None
num_repeat 1 1 None the number of times we want to repeat None
delimiter ; , None defines the delimiter >=2.0.0,<3.0.0