Bearings: Access Data Generically

A bearing details how to fetch or place data of a given name on a target object.

E.g., you would fetch name from a dict using dict[name], but for a dataclass using data.name.

Bearings are the building blocks of a Course objects, which fetch/place a sequence of bearings on a complex data structure.

BearingAbstract Base Type

BearingAbstract outlines the methods and attributes that must/may be overridden by implementations.

name

type

required?

description

BearingAbstract.fetch()

method

yes

Gets data from object at name

BearingAbstract.place()

method

yes

Sets data on object at name

__str__

method

yes

String shorthand

NAME_TYPES

cls attribute

encouraged

Compatible name type (s)

REGEX

cls attribute

encouraged

String shorthand pattern

BearingAbstract.is_compatible()

method

no

Can name be cast to bearing?

BearingAbstract.name_from_str()

method

no

Converts string to name value

BearingAbstract.init_factory()

method

no

Returns initialized factory_type

class gemma.BearingAbstract(name, factory=None)[source]
__init__(name, factory=None)[source]

Abstract base class for Course bearings.

inherits from: Generic[_NameType]

Parameters
  • name (Union[~_NameType, BearingAbstract[~_NameType]]) – name of key/index/attribute/method/etc to act on.

  • factory (Optional[Type[~FactoryType]]) – type to be used as default value if Course.place() hits empty or non-existent value so structure can be built

Class-level Attributes
  • REGEX: ( re.Pattern ) - Regex pattern to match string shorthand

  • NAME_TYPES ( List[Union[Type, Any]] ) - type ( or tuple of types ) that name can be.

property factory_type

Read-only property.

Return type

Optional[Type[~FactoryType]]

Returns

factory passed to __init__.

>>> from gemma import Attr
>>> attribute = Attr('a', factory=dict)
>>> attribute.factory_type
dict
fetch(target)[source]

MUST BE IMPLEMENTED

Fetches value from target object by BearingAbstract.name()

Parameters

target (Any) – object to fetch value from

Return type

Any

Returns

value to be fetched

Raises
  • NullNameError – generic error when bearing cannot be found

  • TypeError – when target is wrong type for bearing

See documentation of the default Bearing implementations for examples:

init_factory()[source]

MAY BE IMPLEMENTED

Returns an initialized version of BearingAbstract.factory().

Return type

~FactoryType

Returns

initialized data object

Raises

TypeError – is factory is not callable

DEFAULT IMPLEMENTATION: initializes object with no parameters.

>>> from gemma import Attr
>>> attribute = Attr('a', factory=dict)
>>> attribute.init_factory()
{}
classmethod is_compatible(name)[source]

MAY BE IMPLEMENTED

Checks whether the name can be cast to current type.

Parameters

name (Any) – value to be cast

Return type

bool

Returns

  • True: Can be cast.

  • False: Cannot be cast

DEFAULT IMPLEMENTATION: checks whether the type of name is in cls.NAME_TYPES.

Most Bearing implementations will not need to override this method, instead supplying a list of acceptable types to cls.NAME_TYPES.

However, some Bearings may depend on criteria other than type and override this method, possibly making cls.NAME_TYPES irrelevant.

For example, a bearing used to fetch and set data from the dataclasses module might re-implement BearingAbstract.is_compatible() as a wrapper for dataclasses.is_dataclass().

property name

Read-only property.

Return type

~_NameType

Returns

name passed to __init__.

>>> from gemma import Attr
>>> attribute = Attr('a')
>>> attribute.name
a
classmethod name_from_str(text)[source]

MAY BE IMPLEMENTED

Casts string to appropriate type for name param of __init__.

Parameters

text (str) – text to cast

Return type

Any

Returns

cast value.

Raises

ValueError – if text is not cast-able.

DEFAULT IMPLEMENTATION: Tests if the text matches to the regex pattern in BearingAbstract.REGEX, then extracts name from pattern.

Raises ValueError if no match.

place(target, value, **kwargs)[source]

MUST BE IMPLEMENTED

Sets value at BearingAbstract.name() of target object.

Parameters
  • target (Any) – target object to set value

  • value (Any) – value to set

Returns None

method should not return anything

Raises
  • NullNameErrorNullNameError should be raised if bearing cannot be placed

  • TypeError – When target is wrong type for Bearing

See documentation of the default Bearing implementations for examples.

Return type

None

Bearing Shorthand

The default implementations of BearingAbstract checks for the following patterns when determining if a string can be cast to it’s type:

class

shorthand

note

Attr

@name

Item

[name]

Call

name()

Fallback

name

accepts all values

>>> from gemma import Attr
>>>
>>> attribute = Attr.from_string('name')
Traceback (most recent call last):
    ...
ValueError: text does not match regex
>>> attribute = Attr.from_string('@name')
>>> attribute.name
'name'

This is slightly different from instantiating the class directly, which does not check/parse the input value of a string, instead using whatever it is fed.

>>> attribute = Attr('name')
>>> attribute.name
'name'
>>> attribute = Attr('@name')
>>> attribute.name
'@name'

Likewise, str(BearingAbstract) should return the properly formatted shorthand through BearingAbstract.__str__

>>> from gemma import Attr, Item, Call, Bearing
>>>
>>> str(Attr("a"))
'@a'
>>> str(Item("a"))
'[a]'
>>> str(Call("a"))
'a()'
>>> str(Fallback("a"))
'a'

This allows for string-encoded bearings which can be easily cast by Course objects. For example:

>>> from gemma import Course
>>>
>>> data = {'nested': {'a': 'a value', 'b': 'b value'}}
>>>
>>> to_fetch = Course() / '[nested]' / 'keys()'
>>> to_fetch.fetch(data)
dict_keys(['a', 'b'])

Course auto-casts strings using a configurable factory method.

BearingAbstract.from_string() can be overridden when implementing a custom Bearing if more granularity is needed than setting the class’ REGEX field.

Implementations

gemma comes with four implementations of BearingAbstract, covering most basic data object API’s.

Attr Type

class gemma.Attr(name, factory=None)[source]

Attr fetches and places data on a target’s attribute.

inherits from: BearingAbstract

name types: Attr only accepts str as name value.

shorthand: "@name"

fetch(target)[source]

Fetches attribute of target.

Parameters

target (Any) – Object to fetch attribute from

Return type

Any

Returns

Value of attribute

Raises

NullNameError – if attribute does not exist

Equivalent to:

>>> getattr(target, self.name)

Example:

>>> from gemma import Attr
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class Data:
...     a: str = 'value'
...
>>> data = Data()
>>> Attr('a').fetch(data)
'value'

If target does not have an attribute of bearing.name:

>>> Attr('b').fetch(data)
Traceback (most recent call last):
  ...
gemma._exceptions.NullNameError: @b>
place(target, value, **kwargs)[source]

Sets Attribute of target to value.

Parameters
  • target (Any) – Object to set attribute on

  • value (Any) – Value to set on attribute

Return type

None

Returns

None

Raises

NullNameError – if attribute does not exist

Equivalent to:

>>> setattr(target, self.name, value)

Example, using data from the fetch example above:

>>> data.a
'value'
>>> Attr('a').place(data, 'changed')
>>> data.a
'changed'

If target does not have an attribute of bearing.name:

>>> Attr('b').place(data, 'changed again')
Traceback (most recent call last):
  ...
gemma._exceptions.NullNameError: @b

Unlike setattr(), Attr.place() cannot be used to declare arbitrary attributes. Non-existent attributes will raise a NullNameError.

Item Type

class gemma.Item(name, factory=None)[source]

Item fetches and places data on a target’s index or mapping

inherits from: BearingAbstract

name types: Any.

shorthand: "[name]"

fetch(target)[source]

Fetches data at index or key of target

Parameters

target (Any) – object to fetch data from

Return type

Any

Returns

value of index/key

Raises
  • NullNameError – if index/key does not exist

  • TypeError – if Target does not have a valid __getitem__ method

Fetch data from a list:

>>> from gemma import Item
>>>
>>> data_list = ["zero", "one", "two"]
>>> item = Item(0)
>>> item.fetch(data_list)
'zero'

Fetch data from a dict:

>>> data_dict = {"a": "a value", "b": "b value"}
>>> item = Item("b")
>>> item.fetch(data_dict)
'b value'

Fetching an Index / Item that does not exist raises NullNameError

>>> out_of_index = Item(5)
>>> out_of_index.fetch(data_list)
Traceback (most recent call last):
    ...
gemma._exceptions.NullNameError: [5]
>>>
>>> bad_key = Item("c")
>>> bad_key.fetch(data_dict)
Traceback (most recent call last):
    ...
gemma._exceptions.NullNameError: [c]

An invalid name raises a NullNameError regardless of whether the target object would normally raise a KeyError or IndexError.

Fetching from a target which does not support __getitem__ raises a TypeError.

>>> no_get_item = int(1)
>>> bad_key.fetch(no_get_item)
Traceback (most recent call last):
    ...
TypeError: 'int' object is not subscriptable
place(target, value, **kwargs)[source]

Sets value at Index/Key of target

Parameters
  • target (Any) – object to set value on.

  • value (Any) – value to set.

Return type

None

Returns

None

Raises
  • NullNameError – When Index/Key cannot be set

  • TypeError – When target does not support __setitem__

Changing an existing dict key:

>>> from gemma import Item
>>>
>>> data_dict = {"a": "a value", "b": "b value"}
>>> existing_item = Item("b")
>>>
>>> existing_item.place(data_dict, "changed")
>>> data_dict
{'a': 'a value', 'b': 'changed'}

Setting a new dict key:

>>> new_item = Item("c")
>>> new_item.place(data_dict, "new")
>>> data_dict
{'a': 'a value', 'b': 'changed', 'c': 'new'}

Changing an existing list index:

>>> data_list = ["zero", "one", "two"]
>>> item = Item(0)
>>>
>>> item.place(data_list, "changed")
>>> data_list
['changed', 'one', 'two']

Changing an index out of range does not result in NullNameError, as it does with Item.fetch().

>>> out_of_index = Item(5)
>>> out_of_index.place(data_list, "new value")
>>> data_list
['changed', 'one', 'two', None, None, 'new value']

None is inserted in any missing indexes between the last existing index and the new index.

Attempting to place a value on a target that does not support __setitem__ raises a TypeError:

>>> data_tuple = ("zero", "one", "two")
>>> cannot_set = Item(0)
>>>
>>> cannot_set.place(data_tuple, "changed")
Traceback (most recent call last):
  ...
TypeError: 'tuple' object does not support item assignment

If the object would normally raise a KeyError or IndexError, it is cast to a NullNameError.

Let us create a dict class that raises a KeyError when attempting to set any key that is not present upon initialization:

>>> class StrictDict(dict):
...     def __setitem__(self, item, value):
...         if not item in self:
...             raise KeyError
...         super().__setitem__(item, value)
...
>>> strict = StrictDict({"a": "a value", "b": "b value"})
>>> strict["c"] = "changed"
Traceback (most recent call last):
    ...
KeyError

Item ‘s place method will cast the KeyError to a NullNameError.

>>> raises_key = Item("c")
>>> raises_key.place(strict, "changed")
Traceback (most recent call last):
    ...
gemma._exceptions.NullNameError: [c]

Call Type

class gemma.Call(name, func_args=None, func_kwargs=None)[source]

Call fetches and sets data through a target’s target.name() method.

inherits from: BearingAbstract

name types: Call only accepts str as name values.

shorthand: "name()"

__init__(name, func_args=None, func_kwargs=None)[source]
Parameters
  • name (str) – method name to act on

  • func_args (Optional[Iterable]) – *args to pass to method when fetching or placing

  • func_kwargs (Optional[Dict[str, Any]]) – **kwargs to pass to method when fetching or placing

Class Attributes:
  • VALUE_ARG ( Any ): object to act as placeholder in func_args and func_kwargs

fetch(target)[source]

Fetches value from method of target

Parameters

target (Any) – Object to call method on.

Return type

Any

Returns

return of method

Getting the keys of a dict:

>>> from gemma import Call
>>>
>>> data_dict = {"a": "a value", "b": "b value"}
>>> method = Call("keys")
>>>
>>> method.fetch(data_dict)
dict_keys(['a', 'b'])

If the method does not exist, a NullNameError is raised.

>>> invalid_method = Call("does_not_exist")
>>> invalid_method.fetch(data_dict)
Traceback (most recent call last):
    ...
gemma._exceptions.NullNameError: does_not_exist()

When fetching a value, call will pass the func_args and func_kwargs passed to __init__ as *args and *kwargs

>>> data_list = ["repeat", "one", "two", "repeat"]
>>> method = Call("index", func_args=("repeat", 1))
>>> method.fetch(data_list)
3

This is equivalent to

>>> data_list.index("repeat", 1)
3
place(target, value, **kwargs)[source]

Places value with method of target.

Parameters
  • target (Any) – Object to call method on

  • value (Any) – value to pass

Return type

None

Returns

None

By default, value is passed as the first argument to the given method.

Appending value at end of list:

>>> from gemma import Call
>>>
>>> data_list = ["zero", "one", "two"]
>>>
>>> adds_value = Call("append")
>>> adds_value.place(data_list, "three")
>>>
>>> data_list
['zero', 'one', 'two', 'three']

Any additional args are passed as positional arguments, after value. Let us create a list class with a method that will attempt to cast a value to a given type before appending it to the list:

>>> class MultiArg(list):
...     def cast_append(self, value, cast_type=None):
...         try:
...             value = cast_type(value)
...         except (TypeError, ValueError):
...             pass
...         self.append(value)
...
...     def args_reversed(self, cast_type=None, value=None):
...         self.cast_append(value, cast_type)
...

We can pass str to the cast_type parameter by setting it as the first additional argument to Call.

>>> data_list = MultiArg(('zero', 'one', 'two'))
>>> data_list
['zero', 'one', 'two']
>>> casts_str = Call('cast_append', func_args=(str,))
>>> casts_str.place(data_list, 3)
>>> data_list
['zero', 'one', 'two', '3']

This is equivalent to:

>>> data_list.cast_append(3, str)

We can also pass cast_type as a keyword argument.

>>> casts_str = Call('cast_append', func_kwargs={'cast_type': str})
>>> casts_str.place(data_list, 4)
>>> data_list
['zero', 'one', 'two', '3', '4']

For most setter methods, it is likely that the value will be the first argument passed. However, this is not always the case. Take list.insert() – the first argument is the index where the value is inserted.

>>> data_list = ["zero", "one", "two"]
>>> data_list.insert(0, "new")
>>> data_list
['new', 'zero', 'one', 'two']

In this case, if we want a bearing that places value at the head of a list, we need to pass value as the second argument.

We can use the Call.VALUE_ARG object to indicate that instead of being placed as the first argument, value should replace the Call.VALUE_ARG whenever a new value is passed to the bearing.

>>> data_list = ["zero", "one", "two"]
>>>
>>> inserts_head = Call("insert", func_args=(0, Call.VALUE_ARG))
>>> inserts_head.place(data_list, "new")
>>>
>>> data_list
['new', 'zero', 'one', 'two']

This also works with **kwarg values. Using the MultiArg, class from above:

>>> data_list = MultiArg(('zero', 'one', 'two'))
>>>
>>> kwargs = {'value': Call.VALUE_ARG, 'cast_type': str}
>>> inserts_head = Call("args_reversed", func_kwargs=kwargs)
>>>
>>> inserts_head.place(data_list, 3)
>>> inserts_head
['zero', 'one', 'two', '3']

Fallback Type

class gemma.Fallback(name)[source]
__init__(name)[source]

Attempts to fetch or place data on a target using other bearing class’ methods.

Parameters

name (Any) – name of bearing.

inherits from: BearingAbstract

name types: Any.

shorthand: "name"

Class Attributes:
  • BEARING_CLASSES ( List[Type[BearingAbstract]] ): bearing classes to cycle through when attempting to fetch or place data.

The available methods and order of attempts is determined by the list of types in Fallback.BEARING_CLASSES. This makes bearing less performant than invoking one of its test classes directly, but does make setting up Course objects easier and less tedious. The performance hit may or may not be worth it depending on your workflow.

The default BEARING_CLASSES are Item, Call, and Attr, though that can be extended by inheriting this class

Meant as a generic class when the bearing type is not well defined in a string ( Allows for more compact, generic Course declarations ).

fetch(target)[source]

Attempts to fetch data from target object.

Parameters

target (Any) – object to fetch data from.

Return type

Any

Returns

value

Cycles through the classes in Fallback.BEARING_CLASSES, casting the current Bearing to each type, and invoking their fetch() method.

If the cast or fetch results in a :class:NullNameError, TypeError, or ValueError, the exception is caught and the next class is tried.

By default, Fallback cycles through fetch() on Item, Attr, and Call, in that order.

With the default classes, if an attribute exists, it will be fetched.

>>> from gemma import Fallback
>>>
>>> class TestData:
...     pass
...
>>> test_data = TestData()
>>> test_data.a = 'a value'
>>>
>>> to_fetch = Fallback('a')
>>> to_fetch.fetch(test_data)
'a value'

Same with an Index.

>>> data_dict = {"a": "a dict"}
>>> to_fetch.fetch(data_dict)
'a dict'

Same with a method.

>>> class FetchMethod:
...     def a(self):
...         return 'a method'
...
>>> test_data = FetchMethod()
>>> to_fetch.fetch(test_data)
'a method'

When an name has multiple compatible bearings, it takes the first method’s value that does not throw an exception.

>>> class TwoValid(dict):
...     def a(self):
...         return 'a method'
...
>>> data_dict = TwoValid({'a': 'a item'})
>>> to_fetch.fetch(data_dict)
'a item'

Both Item('a') and Call('a') would return valid values – "a method" and "a item", respectively – but since Item is tried first and gets a valid response, the key value is returned.

place(target, value, **kwargs)[source]

Attempts to set value on target.

Parameters
  • target (Any) – Object to set value on.

  • value (Any) – Value to set.

Return type

None

Returns

None

Attempts to place data on a given name of target by cycling through the classes in Fallback.BEARING_CLASSES, casting the current Bearing to each type, and invoking their place() method.

If the cast or place results in a NullNameError, TypeError, or ValueError, the exception is caught and the next class is tried.

By default, Fallback cycles through place() on Item, Attr, and Call, in that order.

With the default classes, if an attribute exists, it will be placed.

>>> from gemma import Fallback
>>>
>>> class TestData:
...     a = None
...
>>> test_data = TestData()
>>>
>>> to_place = Fallback("a")
>>> to_place.place(test_data, "a attr")
>>>
>>> test_data.a
'a attr'

Same with an Index.

>>> data_dict = dict()
>>> to_place.place(data_dict, "a dict")
>>> data_dict
{'a': 'a dict'}

Same with a method.

>>> class FetchMethod:
...     def a(self, value):
...         self.a = value
...
>>> test_data = FetchMethod()
>>> to_place.place(test_data, "a method")
>>> test_data.a
'a method'

When an name has multiple compatible bearings, it takes the first method’s value that does not throw an exception.

>>> class TwoValid(dict):
...     def a(self, value):
...         self.a = value
...
>>> data_dict = TwoValid()
>>> to_place.place(data_dict, 'a value')
>>> data_dict
{'a': 'a value'}
>>> data_dict.a
<Item: 'a'>

Both Item('a') and Call('a') would set valid values – (to a key and attribute respectively) – but since Item is tried first and does not return an error, it is set to the dict’s key rather than overriding it’s a method.

Casting Bearings

When casting a bearing directly, formatting is not parsed, unlike the method that bearing() below uses.

>>> from gemma import Item
>>>
>>> Item("[has_brackets]")
<Item: '[has_brackets]'>
>>>
>>> Item("no_brackets")
<Item: 'no_brackets'>

Values must pass the class’ BearingAbstract.is_compatible(), if it does not, TypeError is raised.

>>> from gemma import Attr
>>>
>>> Attr(10)
Traceback (most recent call last):
 ...
TypeError: type <class 'int'> not allowed as <class 'gemma._bearings.Attr'>

When passing a bearing to another bearing, the original’s BearingAbstract.name() value is extracted before casting.

>>> original = Item('key_name')
>>> Attr(original)
<Attr: 'key_name'>

Sorting Bearings

Bearings are sorted in the following order:

  • By class type:

  • Custom classes are sorted by class __name__

  • Each class group is sorted by it’s value type’s __name__

  • Within each value type, values are sorted by their respective sort methods.

Lets look as how the following list of bearings is sorted:

>>> from gemma import Item, Attr, Call, Fallback, BearingAbstract

We’re going to make a couple custom bearings to demonstrate how they sort:

>>> class Alpha(BearingAbstract):
...     pass
...
>>> class Beta(BearingAbstract):
...     pass
...

Here are our unsorted values:

>>> bearing_unsorted = [
...     Fallback("b"),
...     Call("b"),
...     Alpha("b"),
...     Attr("b"),
...     Fallback("a"),
...     Item("b"),
...     Call("a"),
...     Beta("b"),
...     Item(2),
...     Alpha("a"),
...     Attr("a"),
...     Item("a"),
...     Attr("a"),
...     Beta("a"),
...     Item(1)
... ]
...

Sort them:

>>> bearings_sorted = sorted(bearing_unsorted)

Print sorted list to check:

>>> for this_bearing in bearings_sorted:
...     print(repr(this_bearing))
...
<Item: 1>
<Item: 2>
<Item: 'a'>
<Item: 'b'>
<Attr: 'a'>
<Attr: 'a'>
<Attr: 'b'>
<Call: 'a'>
<Call: 'b'>
<Alpha: 'a'>
<Alpha: 'b'>
<Beta: 'a'>
<Beta: 'b'>
<Fallback: 'a'>
<Fallback: 'b'>

Checking Bearing Equality

  • Bearings of the same type and BearingAbstract.name() value are considered equal:
    >>> Attr('a') == Attr('a')
    True
    
  • Bearings with different BearingAbstract.name() values are unequal.
    >>> Attr('a') == Attr('b')
    False
    
  • Bearings of different types are always unequal.
    >>> Attr('a') == Item('a')
    False
    
  • Except the Fallback class… sometimes

    The Fallback class is equal to any type loaded into its BEARING_CLASSES field. By default, this makes it equal to any Item, Attr, Call or Fallback with the same BearingAbstract.name() value.

    >>> Fallback('a') == Attr('a')
    True
    >>>
    >>> Fallback('a') == Item('a')
    True
    >>>
    >>> Fallback('a') == Call('a')
    True
    

    However, if we define an arbitrary class named Alpha, it will not be equal to a bearing with the same name.

    >>> class Alpha(BearingAbstract):
    ...     pass
    ...
    >>> Fallback('a') == Alpha('a')
    False
    

    If we create a Bearing with the bearing() factory method detailed below, using Alpha as one of its possibilities, the two values will be equal.

    >>> from gemma import bearing
    >>>
    >>> new_bearing = Fallback('a', bearing_classes=[Attr, Fallback, Alpha])
    >>> new_bearing
    <Fallback: 'a'>
    >>> new_bearing == Alpha("a")
    True
    

    This is because bearing() loads any fallback Fallback objects .BEARING_CLASSES field with the classes passed to bearing_classes.

Bearing Factory Method

The factory method that most default gemma objects use when casting name values to a bearing is:

gemma.bearing(name, bearing_classes=None, bearing_classes_extra=None)[source]

Factory function for bearing classes.

Parameters
  • name (Any) – value for Bearing.name

  • bearing_classes (Optional[List[Type[BearingAbstract]]]) – list and order of bearing classes to attempt casting

  • bearing_classes_extra (Optional[List[Type[BearingAbstract]]]) – additional classes to add to bearing classes. Used to add to defaults when bearing_classes is set to None.

Return type

BearingAbstract

Returns

Bearing object

If name is a string, bearing will attempt to cast using each class’ BearingAbstract.from_string() method.

Otherwise, bearing attempts to cast to each class normally, passing name as a single argument to each __init__ method.

The default list of classes to attempt are: Item, Call, Attr, and Fallback – in that order.

Any bearing types passed to bearing_classes_extra are put ahead of the bearings in bearing_classes.

Casting formatted strings:

>>> from gemma import bearing
>>>
>>> bearing("[item_name]")
<Item: 'item_name'>
>>>
>>> bearing("@attr_name")
<Attr: 'attr_name'>
>>>
>>> bearing("call_name()")
<Call: 'call_name'>

Passing a string which does not match any of the above class’ string conventions will result in a generic Fallback.

>>> bearing("unknown_name")
<Fallback: 'unknown_name'>

This Fallback class will be loaded with the methods of the other classes, if bearing_classes includes custom Bearings, those class’ methods will be added to the list. No need to make a custom subclass to extend the pool methods available to Fallback when using this factory.

Passing a non-string will always result in an Item object with the default list, since Item accepts all types, and is attempted first.

>>> bearing(5)
<Item: 5>
>>>
>>> bearing((5, 4))
<Item: (5, 4)>