Courses: Data as Paths¶
Courses are the main way gemma
fetches and places data.
Course Class¶
-
class
gemma.
Course
(*bearings)[source]¶ -
__init__
(*bearings)[source]¶ Sequence of bearings that lead to data in a structure.
- Parameters
bearings (
Union
[Tuple
[Union
[Course
,BearingAbstract
,str
,Any
], …],Union
[Course
,BearingAbstract
,str
,Any
]]) –Bearing
orCourse
objects to be combined into new course.
The core functionality of a course are its
Course.fetch()
andCourse.place()
methods. See their documentation below.
-
property
end_point
¶ Last bearing of current course
- Return type
BearingAbstract
- Returns
end point of course
>>> from gemma import Course >>> example = Course() / "one" / "two" / "three" >>> example <Course: <Fallback: 'one'> / <Fallback: 'two'> / <Fallback: 'three'>> >>> example.end_point <Fallback: 'three'>
-
ends_with
(other)[source]¶ Checks if course ends with
other
Bearing
orCourse
.- Parameters
other (
Union
[BearingAbstract
,Course
]) – Bearing or Sub-Course to check- Return type
bool
- Returns
True: Course ends with
other
>>> from gemma import Course(), Fallback >>> >>> example = Course() / "one" / "two" / "three" / "four" >>> tail = Course() / "three" / "four" >>> middle = Course() / "two" / "three" >>> >>> example.ends_with(Fallback("four")) True >>> example.ends_with(Fallback("three")) False >>> >>> example.ends_with(tail) True >>> example.ends_with(middle) False
For more detail, see the Checking for a Sub-Course or Bearing section below.
-
fetch
(target, *, default=<object object>)[source]¶ Traverses
target
, getting data at end point of course.- Parameters
target (
Any
) – data structure to get data from.default (
Any
) – Optional parameter to pass default value. When passed this value will be used if the course does not exist ontarget
.
- Return type
Any
- Returns
Value at end of course.
- Raises
NullNameError – if any bearing cannot be found in
target
Data is fetched by iterating through a course, running each bearing’s
BearingAbstract.fetch()
method on the previously fetched object.Lets use the data_dict object from
test_objects()
as an example.>>> from gemma import Course(), Attr >>> from gemma.test_objects import test_objects >>> >>> simple, data_dict, data_list, structured, target = test_objects() >>> example_course = Course() / "[nested]" / "[one key]" >>> >>> example_course.fetch(data_dict) 1
What is happening in the example above? This course contains two
Item
bearings. TheItem.fetch()
method gets a key or index. When the course executes a fetch, it first fetches the"nested"
key from thedata_dict
object. This returns a sub_dictionary with the data:{"one key": 1, "two key": 2}
The next bearing’s fetch method is called on
"one key"
of the sub-dict, returning1
. This is the end of the course, so1
is returned as the final value of the fetch.Each fetch down the chain follows the rules established in Bearings: Access Data Generically.
If a bearing cannot be found at any point in the chain,
NullNameError
is raised.>>> bad_course = Course() / "[non_existent]" / "[one key]" >>> bad_course.fetch(data_dict) Traceback (most recent call last): ... gemma._exceptions.NullNameError: [non_existent]
We can supply a default value for non-existent targets like so:
>>> bad_course.fetch(data_dict, default="default value fetched!") default value fetched!
-
property
parent
¶ Parent course of current course
- Return type
Course
- Returns
parent
>>> from gemma import Course >>> example = Course() / "one" / "two" / "three" >>> example <Course: <Fallback: 'one'> / <Fallback: 'two'> / <Fallback: 'three'>> >>> example.parent <Course: <Fallback: 'one'> / <Fallback: 'two'>>
-
place
(target, value)[source]¶ Traverses
target
to place data atCourse.end_point()
.- Parameters
target (
Any
) – data structure to place data on.value (
Any
) – value to place.
- Return type
None
- Returns
None. Changes are made in-place.
- Raises
NullNameError – if any bearing cannot be found in
target
Course.fetch()
is called on the acting course’sCourse.parent()
, returning the object that needs to be modified.BearingAbstract.place()
ofCourse.end_point()
is called on the object to placevalue
.Lets look at an example using the target object from
test_objects()
.>>> from gemma import Course >>> from gemma.test_objects import test_objects >>> >>> simple, data_dict, data_list, structured, target = test_objects() >>> >>> example_course = Course() / "dict_target" / "new key" >>> >>> example_course.place(target, 1) >>> target.dict_target["new key"] 1
See examples from the
Course.fetch
method in the documentation below.Course.place
abides by the same rules, except final bearing, which usesBearingAbstract.place()
instead ofBearingAbstract.fetch()
, and always returnsNone
-
replace
(index, replacement)[source]¶ Replace bearings at index / slice with
replacement
- Parameters
index (
Union
[int
,slice
]) – item(s) to replacereplacement (
Union
[Course
,BearingAbstract
,str
,Any
]) – Replacement bearing / course
- Return type
Course
- Returns
>>> course = PORT / "thing" / 10 / "key" >>> course.replace(1, 11) <Course: <Fallback: 'thing'> / <Item: 11> / <Fallback: 'key'>>
>>> course.replace(slice(1, 3), 14) <Course: <Fallback: 'thing'> / <Item: 14> / <Fallback: 'sub-key'>>
>>> course.replace(slice(1, 3), PORT / 14 / "key2") <Course: <Fallback: 'thing'> / <Item: 14> / <Fallback: 'key2'> / ...>>
-
starts_with
(other)[source]¶ Checks if course starts with
other
Bearing
orCourse
.- Parameters
other (
Union
[BearingAbstract
,Course
]) – Bearing or Sub-Course to check- Return type
bool
- Returns
True: Course starts with
other
>>> from gemma import Course, Fallback >>> >>> example = Course() / "one" / "two" / "three" / "four" >>> head = Course() / "one" / "two" >>> middle = Course() / "two" / "three" >>> >>> example.starts_with(Fallback("one")) True >>> example.starts_with(Fallback("two")) False >>> >>> example.starts_with(head) True >>> example.starts_with(middle) False
For more detail, see the Checking for a Sub-Course or Bearing section below.
-
with_end_point
(end_point)[source]¶ Returns course with new end point.
- Parameters
end_point (
Union
[Course
,BearingAbstract
,str
,Any
]) – bearing to use as new end point.- Return type
Course
- Returns
New Course.
>>> from gemma import Course >>> example = Course() / "one" / "two" / "three" >>> example <Course: <Fallback: 'one'> / <Fallback: 'two'> / <Fallback: 'three'>> >>> example.with_end_point("new") <Course: <Fallback: 'one'> / <Fallback: 'two'> / <Fallback: 'new'>>
-
Making a New Course Object¶
Each *arg
becomes one bearing in the new course:
>>> from gemma import Course, Item, Attr, Fallback
>>>
>>> new_course = Course(Item("a"), Item(2))
>>> new_course
<Course: <Item: 'a'> / <Item: 2>>
Course supports pathlib-like syntax to declare bearings via the /
operator.
>>> path_like = Course() / Item("a") / Item(2)
>>> path_like
<Course: <Item: 'a'> / <Item: 2>>
Bearings will be automatically cast from other values using the bearing()
factory.
>>> auto_cast = Course() / 5 / "method()" / "[item]" / "@attr"
>>> auto_cast
<Course: <Item: 5> / <Call: 'method'> / <Item: 'item'> / <Attr: 'attr'>
bearing()
is passed the list of courses in Course.BEARINGS_EXTENSION
added to
Course.BEARINGS
.
By default, Course.BEARINGS
contains Item
, Call
, Attr
,
and Fallback
in that order, and Course.BEARING_EXTENSION
is empty, a
placeholder for custom bearings when Course
is inherited to extend
functionality.
Appending Courses¶
Courses are immutable. Whenever an item is added to the end of a course, a new course object is returned.
>>> original = Course() / Item("a") / Item(2)
>>> extended = original / Item("additional")
>>>
>>> original
<Course: <Item: 'a'> / <Item: 2>>
>>> extended
<Course: <Item: 'a'> / <Item: 2> / <Item: 'additional'>>
original
is not modified when Item("additional")
is added to it; it returns a
new Course
object.
Courses can be appended to other Courses:
>>> course_one = Course() / 1 / 2
>>> course_two = Course() / 3 / 4
>>> course_one / course_two
<Course: <Item: 1> / <Item: 2> / <Item: 3> / <Item: 4>>
The PORT Course¶
gemma
comes with a blank course called PORT
( all good courses start in
port! ), which can be imported and used as the root of pathlib-like courses
instead of using Course()
:
>>> new_course = PORT / "@a" / 2
>>> new_course
<Course: <Attr: 'a'> / <Item: 2>>
>>> second_course = PORT / "[key]" / 10
>>> second_course
<Course: <Item: 'key'> / <Item: 10>>
Since PORT
is immutable, it can be used over and over to indicate the
declaration of a new course with fewer keystrokes.
Course Length¶
Check the number of bearings in a Course
using len()
:
>>> two_long = PORT / "first" / "second"
>>> two_long
<Course: <Fallback: 'first'> / <Fallback: 'second'>>
>>> len(two_long)
2
>>> three_long = two_long / "third"
>>> three_long
<Course: <Fallback: 'first'> / <Fallback: 'second'> / <Fallback: 'third'>>
>>> len(three_long)
3
PORT is an empty Course
used for initialization, so appending two bearings
results in a Course
of length 2
, not 3
.
Iterating Through a Course¶
You can easily iterate through a course’s bearings like any other iterable.
>>> example = PORT / 1 / "[two]" / "@three"
>>> for this_bearing in example:
... print(repr(this_bearing))
...
<Item: 1>
<Item: 'two'>
<Attr: 'three'>
Course Equality¶
Course
objects are equal when each’s BearingAbstract.fetch()
or BearingAbstract.place()
would have the same effect.
- Courses are equal if the bearings they contain at each index are equal.
>>> course_one = PORT / Item(-1) / Attr("text") >>> course_two = PORT / -1 / "@text" >>> course_one == course_two True
Because they would get the same data:
>>> from gemma.test_objects import test_objects >>> simple, data_dict, data_list, structured, target = test_objects() >>> >>> course_one.fetch(data_list) 'in a list' >>> course_two.fetch(data_list) 'in a list'
example object refs: simple
- Courses of different lengths are unequal.
>>> course_three = course_one / Attr("additional") >>> print( ... len(course_one), ... len(course_three), ... course_one == course_three ... ) 2 3 False
They would fetch different data.
- Courses with different bearing names are not equal.
>>> course_one = PORT / Item(-1) / Attr("text") >>> course_two = PORT / Item(-1) / Attr("number") >>> course_one == course_two False
They fetch different data.
>>> course_one = PORT / Item(-1) / Attr("text") >>> course_two = PORT / Item(-1) / Attr("number") >>> course_one.fetch(data_list) 'in a list' >>> course_two.fetch(data_list) 20
- Courses with different bearing types are not equal.
>>> course_items = PORT / Item("nested") / Item('one key') >>> course_attrs = PORT / Attr("nested") / Attr("one key") >>> course_attrs == course_items False
They fetch different data.
>>> course_items.fetch(data_dict) 1 >>> course_attrs.fetch(data_dict) Traceback (most recent call last): ... gemma._exceptions.NullNameError: @nested
Fallback
can be equal to other bearing types.… depending on what other bearing types are loaded into it’s
.BEARING_CLASSES
field.>>> course_items = PORT / Item("nested") / Item("one key") >>> course_bearings = PORT / Fallback("nested") / Fallback("one key") >>> course_items == course_bearings True
Both these courses would fetch the same data on the same objects, so they are equal.
>>> course_items.fetch(data_dict) 1 >>> course_bearings.fetch(data_dict) 1
Slicing and Indexing¶
- You can get a bearing at a specific Index like so:
>>> example = PORT / 0 / "[one]" / "@two" / "three()" >>> example[1] <Item: 'one'> >>> example[-1] <Call: 'three'>
- Slices will return a new
Course
with the requested bearings: >>> example[1:3] <Course: <Item: 'one'> / <Attr: 'two'>>
- Slices will return a new
Course.parent()
will return a course to the parent.>>> example.parent <Course: <Item: 0> / <Item: 'one'> / <Attr: 'two'>>
Course.end_point()
will return the last bearing.>>> example.end_point <Call: 'three'>
Checking for a Sub-Course or Bearing¶
- Check if one course contains another.
>>> course_example = PORT / "[one]" / "[two]" / "[three]" / "[four]" >>> course_middle = PORT / "[two]" / "[three]" >>> course_middle in course_example True >>> course_other = PORT / "[one]" / "[three]" >>> course_other in course_example False
- Check if a course contains a bearing.
>>> Item("two") in course_example True >>> >>> Item("five") in course_example False
- Check if a course begins with a Sub-Course or Bearing.
>>> course_start = PORT / "[one]" / "[two]" >>> course_example.starts_with(course_start) True >>> course_example.starts_with(Item("one")) True >>> course_example.starts_with(course_middle) False >>> course_example.starts_with(Item("three")) False
- Check if a course ends with a Sub-Course or Bearing.
>>> course_end = PORT / "[three]" / "[four]" >>> course_example.ends_with(course_end) True >>> course_example.ends_with(Item("four")) True >>> course_example.ends_with(course_middle) False >>> course_example.ends_with(Item("three")) False
- Checking for Sub-Courses follows the same rules as equality above.
Bearings can be equal to other types.
>>> example_is_bearings = PORT / "one" / "two" / "three" / "four"
The above uses all bearings so will contain courses of different types.
>>> example_is_bearings = PORT / "one" / "two" / "three" / "four" >>> >>> test_course_attrs = PORT / Attr("two") / Attr("three") >>> test_course_items = PORT / Item("two") / Item("three") >>> >>> test_course_attrs in example_is_bearings True >>> test_course_items in example_is_bearings True
But
test_course_items
would not containtest_course_attrs
because the bearings are not of equal types.>>> test_course_attrs in test_course_items False
Fetching Default Values¶
Courses can return a default value if the course does not exist on a target object.
>>> data = {"nested": {"one": 1}}
>>> course = PORT / "nested" / "three"
>>>
>>> course.fetch(data)
Traceback (most recent call last):
...
gemma._exceptions.NullNameError: [3]
>>>
>>> course.fetch(data, default=3)
3
Creating Missing Bearings¶
By passing a type to the factory
of a bearing, you can generate missing types during
a Course.place()
operation.
>>> from gemma import PORT, Item, Attr
>>>
>>> data = {}
>>> has_factory = PORT / Item("list", factory=list) / 0
>>> has_factory.place(data, "value")
>>> data
{'list': ['value']}
Item("list")
would normally throw NullNameError
, but when factory
is not
set to None, the error is caught. A new instance of the factory
type is placed
at the missing bearing. Traversal then continues as normal.
Factories can be chained:
>>> data = {}
>>> chained = PORT / Item("list", factory=list) / Item(0, factory=dict) / "key"
>>> chained.place(data, "value")
>>> data
{'list': [{'key': 'value'}]}
This does not work with Course.fetch()
. Factories cannot be used with shorthand.
Factories do not only catch NullNameError
, but also replace values of the wrong
type.
>>> data = {"list": None}
>>> replaces_none = PORT / Item("list", factory=list) / 0
>>> replaces_none.place(data, "value")
>>> data
{'list': ['value']}
This allows for arbitrary stand-ins to represent no data: None, Null, etc. Some caution
is required, as factory
may replace valid data if the type is incorrect.
>>> data = {"nested": ["zero", "one", "two"]}
>>> wrong_factory = PORT / "nested" / Item(0, factory=dict)
>>> wrong_factory.place(data, "whoops")
>>> data
{'nested': {0: 'whoops'}}
The existing list
under “nested” is replaced with a new dict
. Factories do not
replace bearings of the same type, so to fix this:
>>> data = {"nested": ["zero", "one", "two"]}
>>> right_factory = PORT / "nested" / Item(0, factory=list)
>>> right_factory.place(data, "yay!")
>>> data
{'nested': ['yay!', 'one', 'two']}
BearingAbstract.init_factory()
can be overridden to alter how a factory is
initialized.
More fetch() Examples¶
- Additional example 1a:
>>> from gemma import Attr, Call, Fallback, Item, PORT >>> from gemma.test_objects import test_objects >>> >>> simple, data_dict, data_list, structured, target = test_objects() >>> >>> example_one = ( ... PORT / Attr("list_data") / Item(-3) / Item("simple") / Attr("text") ... ) >>> example_one.fetch(structured) 'string value'
example object refs: structured
- Additional example 1b:
Remember that course can auto-cast bearings based on their string cues, so the above can be rewritten as:
>>> example_cast = PORT / "@list_data" / -3 / "[simple]" / "@text" >>> example_cast.fetch(structured) 'string value'
example object refs: structured
- Additional example 1c:
Even simpler, as:
>>> example_fallback = PORT / "list_data" / -3 / "simple" / "text" >>> example_fallback.fetch(structured) 'string value'
Comparing
example_cast
andexample_fallback
you will see the second has cast to theFallback
type.>>> example_cast <Course: <Attr: 'list_data'> / <Item: -3> / <Item: 'simple'> / <Attr: 'text'>> >>> example_fallback <Course: <Fallback: 'list_data'> / <Item: -3> / <Fallback: 'simple'> / <Fallback: 'text'>>
The exact types cannot be determined through shorthand. In most cases, this is acceptable, and trades a slight performance hit for more readable, faster to create code.
example object refs: structured
- Additional example 2:
A
BearingAbstract
type of the wrong kind will raise aNullNameError
exception.>>> wrong_type = ( ... PORT / Attr("list_data") / Item(-3) / Attr("simple") / Attr("text") ... ) >>> wrong_type.fetch(structured) Traceback (most recent call last): ... gemma._exceptions.NullNameError: @simple
The object
Attr("simple")
is to act on has a key called"simple"
; It does not have an attribute calledsimple
, whichAttr.fetch()
is looking for.example object refs: structured
- Additional example 3a:
Lets fetch data from the
keys()
method of a dictionary.>>> with_method = PORT / Attr("dict_data") / Call("keys") >>> with_method.fetch(structured) dict_keys(['a', 'b', 1, 2, 'nested', 'simple'])
example object refs: structured
- Additional example 3b:
The above can be auto-cast like so:
>>> with_method_cast = PORT / "dict_data" / "keys" dict_keys(['a', 'b', 1, 2, 'nested', 'simple'])
Be careful.
Item.fetch()
is attempted beforeCall.fetch()
, so if the dict has a key calledkeys
there may be odd results:>>> data = {"nested": {"keys": "uh-oh"}} >>> ambiguous = PORT / "nested" / "keys" >>> ambiguous.fetch(data) 'uh-oh'
To fix this, you can use a string-cue to indicate the last bearing should be cast to
Call
instead ofItem
.>>> more_clear = PORT / "nested" / "keys()" >>> more_clear.fetch(data) dict_keys(['keys'])
Now, what if there is actually a KEY we want to get called
"keys()"
, parentheses and all?>>> confusing_data = {"nested": {"keys()": "butwhytho?"}}
In this case, we should cast to
Item
explicitly, to avoid the detection of the()
convention thatCall
uses as shorthand.>>> most_clear = PORT / "nested" / Item("keys()") >>> most_clear.fetch(confusing_data) 'butwhytho?'
Auto-casting is a powerful feature for saving time, but one must keep in mind the ambiguity it can introduce in certain edge-cases.