Surveyors: Traverse Structures¶
Surveyors chart nested data, using chained Compass
objects to build
(Course
, value) pairs
Surveyor Class¶
-
class
gemma.
Surveyor
(compasses=None, compasses_extra=None, end_points=None, end_points_extra=None, course_type=<class 'gemma._course.Course'>)[source]¶ -
__init__
(compasses=None, compasses_extra=None, end_points=None, end_points_extra=None, course_type=<class 'gemma._course.Course'>)[source]¶ Charts courses through data structure
- Parameters
compasses (
Optional
[List
[Compass
]]) – available compasses to navigate bearingscompasses_extra (
Optional
[List
[Compass
]]) – compasses to use in addition to defaultsend_points (
Union
[Tuple
[Type
, …],Type
,None
]) – types where courses should terminateend_points_extra (
Union
[Tuple
[Type
, …],Type
,None
]) – end points to use in addition to defaultscourse_type (
Type
[Course
]) – course class to use for course creation
The main functionality for this course is through
Surveyor.chart_iter()
andSurveyor.chart()
See below documentation for usage.
-
chart
(target, exceptions=True)[source]¶ As
Surveyor.chart_iter()
, but returns all result pairs as list.- Parameters
target (
Any
) – data structure to chartexceptions (
bool
) –True
: Exceptions will be raised during processFalse
: Exceptions will be suppressed and aSuppressedErrors
Error will be raised at the end.
- Return type
List
[Tuple
[Course
,Any
]]- Returns
list
of (Course
, value) pairs- Raises
NonNavigableError – If a target is found that can’t be navigated. This error will be suppressed if
exceptions
is set toFalse
SuppressedErrors – Raised at end if
NonNavigableError
occurs andexceptions
is set toFalse`. Partial chart can be recovered from ``SuppressedErrors.chart_partial
See examples below.
-
chart_iter
(target, exceptions=True)[source]¶ Charts data structure, yielding each (
Course
, value) pair one at a time- Parameters
target (
Any
) – data structure to chartexceptions (
bool
) –True
: Exceptions will be raised during processFalse
: Exceptions will be suppressed and aSuppressedErrors
Error will be raised at the end.
- Return type
Generator
[Tuple
[Course
,Any
],None
,None
]- Returns
yields
Course
, value pairs- Raises
NonNavigableError – If a target is found that can’t be navigated. This error will be suppressed if
exceptions
is set toFalse
SuppressedErrors – Raised at end if
NonNavigableError
occurs andexceptions
is set toFalse
See examples below.
-
Get all Courses in Object¶
>>> from gemma import Surveyor
>>> from gemma.test_objects import test_objects
...
... simple, data_dict, data_list, structured, target = test_objects()
>>> default = Surveyor()
>>>
>>> for course, value in default.chart_iter(data_dict):
... print(repr(course), '|', value)
...
<Course: <Item: 'a'>> | a dict
<Course: <Item: 'b'>> | b dict
<Course: <Item: 1>> | one dict
<Course: <Item: 2>> | two dict
<Course: <Item: 'nested'>> | {'one key': 1, 'two key': 2}
<Course: <Item: 'nested'> / <Item: 'one key'>> | 1
<Course: <Item: 'nested'> / <Item: 'two key'>> | 2
<Course: <Item: 'simple'>> | DataSimple(text='string value', number=40)
<Course: <Item: 'simple'> / <Attr: 'text'>> | string value
<Course: <Item: 'simple'> / <Attr: 'number'>> | 40
example object refs: data_dict
Charting with Custom Compasses¶
>>> keys = ['a', 'b', 'nested']
>>> dict_compass = Compass(target_types=dict, items=keys)
>>>
>>> custom = Surveyor(compasses=[dict_compass])
>>> for course, value in custom.chart_iter(data_dict):
... print(repr(course), value)
...
<Course: <Item: 'a'>> a dict
<Course: <Item: 'b'>> b dict
<Course: <Item: 'nested'>> {'one key': 1, 'two key': 2}
Only "a"
, "b"
, and "nested"
are returned. Why did the :class:’Surveyor’ not
recurse into "nested"
, instead returning only the first layer?
Because we have supplied only one Compass -- *tied to all ``dict``objects* --
that returns keys for `
”a”, ``"b"
, and "nested"
only. Lets override
Compass
and set a better filter for when it should be used, something
more granular than type.
>>> class DictDataCompass(Compass):
... def __init__(self):
... keys = ['a', 'b', 'nested']
... super().__init__(target_types=dict, items=keys)
...
... def is_navigable(self, target):
... if not super().is_navigable(other):
... return False
... if 'nested' in other:
... return True
... else:
... return False
...
We override Compass.is_navigable()
, checking that target
is a dict
through
the super()
call, then confirm the "nested"
key exists – the defining
characteristic of our target dict
.
We also override __init__
to restrict our return keys rather than doing it every
time this compass is initialized.
Lets load it into a Surveyor
and try to chart dict_data
again.
>>> dict_data_compass = DictDataCompass()
>>> custom = Surveyor(compasses=[dict_data_compass])
>>>
>>> for course, value in custom.chart_iter(data_dict):
... print(repr(course), value)
...
<Course: <Item: 'a'>> a dict
<Course: <Item: 'b'>> b dict
<Course: <Item: 'nested'>> {'one key': 1, 'two key': 2}
Traceback (most recent call last):
...
gemma._exceptions.NonNavigableError: could not find compass for f{'one ...
A NonNavigableError
once the surveyor hits 'nested'
. Why?
Because we have only supplied a Compass
that can handle dict_data
,
but no Compass
that can handle a generic dict
.
Lets use the default Compass
. We list it after DictDataCompass
, so
the default is not chosen first for every object. Surveyor uses the first compatible
Compass
it finds.
>>> default_compass = Compass()
>>> custom = Surveyor(compasses=[dict_data_compass, default_compass])
>>> for course, value in custom.chart_iter(data_dict):
... print(repr(course), value)
...
<Course: <Item: 'a'>> a dict
<Course: <Item: 'b'>> b dict
<Course: <Item: 'nested'>> {'one key': 1, 'two key': 2}
<Course: <Item: 'nested'> / <Item: 'one key'>> 1
<Course: <Item: 'nested'> / <Item: 'two key'>> 2
Now we get the full chart.
We can use compasses_extra
to add our special Compass
before the default
automatically.
>>> custom = Surveyor(compasses_extra=[dict_data_compass])
>>>
>>> simple, data_dict, data_list, structured, target = test_objects()
>>> for course, value in custom.chart_iter(data_dict):
... print(repr(course), value)
...
<Course: <Item: 'a'>> a dict
<Course: <Item: 'b'>> b dict
<Course: <Item: 'nested'>> {'one key': 1, 'two key': 2}
<Course: <Item: 'nested'> / <Item: 'one key'>> 1
<Course: <Item: 'nested'> / <Item: 'two key'>> 2
The default Compass
is used for any objects compasses_extra
doesn’t catch.
example object refs: data_dict
Adding Additional End Points¶
The surveyor needs to know how it recognizes where a course should terminate, and has a tuple of types it will not traverse into.
The default types are: (str, int, float, type)
. Additional types can be anything
capable of an isinstance()
check.
In the last example, data_dict
contains a Dataclass, “DataSimple”. Lets say we don’t
wish to recurse into DataSimple, instead treating it as primary data type.
We can add it as an additional end points type as so:
>>> from gemma.test_objects import DataSimple
>>> more_end_points = Surveyor(end_points_extra=(DataSimple,))
>>> for course, value in more_end_points.chart_iter(data_dict):
... print(repr(course), value)
...
<Course: <Item: 'a'>> a dict
<Course: <Item: 'b'>> b dict
<Course: <Item: 1>> one dict
<Course: <Item: 2>> two dict
<Course: <Item: 'nested'>> {'one key': 1, 'two key': 2}
<Course: <Item: 'nested'> / <Item: 'one key'>> 1
<Course: <Item: 'nested'> / <Item: 'two key'>> 2
<Course: <Item: 'simple'>> DataSimple(text='string value', number=40)
The resulting courses do not dig further then the “simple” key.
Like compasses
and compasses_extra
, you can change or remove the default end
points using the end_points
keyword argument.
To see why endpoints are important, try removing str
as an endpoint
and chart a dict with string values.