Package: treepath
ThetreepathPackage.Thetreepathpackage offers adeclarative programmingapproach to extracting data from ajsondata structure.  The expressions
are aquery languagesimilar tojsonpath, andXpath, but are
written in native python syntax.Note python 3.6 is supported in version earlier that 1.0.0.Quick startAll the treepath components should be imported as follows:fromtreepathimportpath,pathd,wc,wildcard,set_,get,get_match,find,find_matches,has,has_all,has_any,has_not,MatchNotFoundError,Match,log_to,Document,attr,attr_typed,attr_iter_typed,attr_list_typed,JsonArgTypesA treepath example that fetches the value 1 from data.data={"a":{"b":[{"c":1},{"c":2}]}}value=get(path.a.b[0].c,data)assertvalue==1A treepath example that fetches the values 1 and 2 from data.value=[valueforvalueinfind(path.a.b[wc].c,data)]assertvalue==[1,2]Solar System Json DocumentThe examples shown in this README use the following json document.  It describes our solar system. Click to expand.solar_system = {...}{"star":{"name":"Sun","diameter":1391016,"age":null,"planets":{"inner":[{"name":"Mercury","Number of Moons":"0","diameter":4879,"has-moons":false},{"name":"Venus","Number of Moons":"0","diameter":12104,"has-moons":false},{"name":"Earth","Number of Moons":"1","diameter":12756,"has-moons":true},{"name":"Mars","Number of Moons":"2","diameter":6792,"has-moons":true}],"outer":[{"name":"Jupiter","Number of Moons":"79","diameter":142984,"has-moons":true},{"name":"Saturn","Number of Moons":"82","diameter":120536,"has-moons":true},{"name":"Uranus","Number of Moons":"27","diameter":51118,"has-moons":true},{"name":"Neptune","Number of Moons":"14","diameter":49528,"has-moons":true}]}}}Quick comparison between Imperative and Declarative Solution.The following problem is solved using a Imperative Solution and a Declarative Solution to try to illustrate the
differences between the two approaches.The problem is fetch the planet by name from the given solar system json document.Imperative SolutionThe first example uses flow control statements to define aImperative Solution.   This is a
very common approach to solving problems.defget_planet_by_name(name,the_solar_system):try:planets=the_solar_system['star']['planets']forarcinplanets.values():forplanetinarc:ifname==planet.get('name',None):returnplanetexceptKeyError:passreturnNoneactual=get_planet_by_name('Earth',solar_system)expected={'Number of Moons':'1','diameter':12756,'has-moons':True,'name':'Earth'}assertactual==expectedDeclarative  SolutionThe second example uses treepath to define adeclarative solution.
It solves the same problem without defining any flow control statements.    This keeps the Cyclomatic and
Cognitive Complexity low.defget_planet_by_name(name:str,the_solar_system):returnget(path.star.planets.wc[wc][has(path.name==name)],the_solar_system,default=None)actual=get_planet_by_name('Earth',solar_system)expected={'Number of Moons':'1','diameter':12756,'has-moons':True,'name':'Earth'}assertactual==expectedquery examples.DescriptionXpathjsonpathtreepathFind planet earth./star/planets/inner[name='Earth']$.star.planets.inner[?(@.name=='Earth')]path.star.planets.inner[wc][has(path.name == 'Earth')]List the names of all inner planets./star/planets/inner[*].name$.star.planets.inner[*].namepath.star.planets.inner[wc].nameList the names of all planets./star/planets/*/name$.star.planets.[].namepath.star.planets.wc[wc].nameList the names of all celestial bodies//name$..namepath.rec.nameList all nodes in the tree Preorder//*$..path.recGet the third rock from the sun/star/planets/inner[3]$.star.planets.inner[2]path.star.planets.inner[2]List first two inner planets/star/planets.inner[position()<3]$.star.planets.inner[:2]path.star.planets.inner[0:2]$.star.planets.inner[0, 1]path.star.planets.inner[0, 2]List planets smaller than earth/star/planets/inner[diameter < 1]$.star.planets.inner[?(@.diameter < 12756)]path.star.planets.inner[wc][has(path.diameter < 12756)]List celestial bodies that have planets.//*[planets]/name$..[?(@.planets)].namepath.rec[has(path.planets)].nameList the planets with more than 50 moons$..[?(int(@['Number of Moons']) > 50)].namepath.rec[wc][has(path['Number of Moons'] > 50, int)].nameTraversal FunctionsgetThegetfunction returns the first value the path leads to.Get the star name from the solar_systemsun=get(path.star.name,solar_system)assertsun=='Sun'When there is no match, MatchNotFoundError is thrown.try:get(path.star.human_population,solar_system)assertFalse,"Not expecting humans on the sun"exceptMatchNotFoundError:passOr if preferred, a default value can be given.human_population=get(path.star.human_population,solar_system,default=0)asserthuman_population==0In addition to a constant, the default value may also be a callabledefpopulation():return0human_population=get(path.star.human_population,solar_system,default=population)asserthuman_population==0The default value can be automatically injected in to json documenthuman_population=get(path.star.human_population,solar_system,default=1,store_default=True)asserthuman_population==solar_system["star"]["human_population"]The data source can be a json data structure or a Match object.parent_match=get_match(path.star.planets.inner,solar_system)name=get(path[2].name,parent_match)assertname=="Earth"set_Theset_function modifies the json document.Use the set_ function to modify the star name.sun=get(path.star.name,solar_system)assertsun=='Sun'set_(path.star.name,"RedSun",solar_system)sun=get(path.star.name,solar_system)assertsun=='RedSun'assertsolar_system["star"]["name"]=='RedSun'Use the set_ to add planet9.   This example creates multiple objects in one step.name=get(path.star.planets.outer[4].name,solar_system,default=None)assertnameisNoneplanets_count=len(list(find(path.star.planets.wc[wc].name,solar_system)))assertplanets_count==8set_(path.star.planets.outer[4].name,'planet9',solar_system,cascade=True)name=get(path.star.planets.outer[4].name,solar_system,default=None)assertname=='planet9'planets_count=len(list(find(path.star.planets.wc[wc].name,solar_system)))assertplanets_count==9findThefindfunction returns an Iterator that iterates to each value the path leads to.  Each value is
determine on its iteration.Find All the planet names.inner_planets=[planetforplanetinfind(path.star.planets.inner[wc].name,solar_system)]assertinner_planets==['Mercury','Venus','Earth','Mars']The data source can be a json data structure or a Match object.parent_match=get_match(path.star.planets.inner,solar_system)inner_planets=[planetforplanetinfind(path[wc].name,parent_match)]assertinner_planets==['Mercury','Venus','Earth','Mars']get_matchTheget_matchfunction returns the first Match the path leads to.Get the star name from the solar_systemmatch=get_match(path.star.name,solar_system)assertmatch.data=='Sun'When there is no match, MatchNotFoundError is thrown.try:get_match(path.star.human_population,solar_system)assertFalse,"Not expecting humans on the sun"exceptMatchNotFoundError:passOr if preferred,Noneis returned if must_match is set to False.match=get_match(path.star.human_population,solar_system,must_match=False)assertmatchisNoneThe data source can be a json data structure or a Match object.parent_match=get_match(path.star.planets.inner,solar_system)earth_match=get_match(path[2].name,parent_match)assertearth_match.path_as_str=="$.star.planets.inner[2].name"assertearth_match.data=="Earth"find_matchesThefind_matchesfunction returns an Iterator that iterates to each match the path leads to.  Each match is
determine on its iteration.Find the path to each of the inner planets.formatchinfind_matches(path.star.planets.inner[wc],solar_system):assertmatch.path_as_strin['$.star.planets.inner[0]','$.star.planets.inner[1]','$.star.planets.inner[2]','$.star.planets.inner[3]',]The data source can be a json data structure or a Match object.parent_match=get_match(path.star.planets.inner,solar_system)formatchinfind_matches(path[wc],parent_match):assertmatch.path_as_strin['$.star.planets.inner[0]','$.star.planets.inner[1]','$.star.planets.inner[2]','$.star.planets.inner[3]',]The Match ClassTheMatchclass provides metadata about the match.match=get_match(path.star.name,solar_system)The explicit path to the matchexplicit_path=match.pathassertexplicit_path==path.star.nameThe string representation of the match including the value: "path=value"assertrepr(match)=="$.star.name=Sun"assertstr(match)=="$.star.name=Sun"The string representation of the match, but with just the path component.assertmatch.path_as_str=="$.star.name"A list containing each match in the path.assertmatch.path_match_list==[match.parent.parent,match.parent,match]The key that points to the match value.  The data_name is a dictionary key if the parent is a dict or an index if
the parent is a list.assertmatch.data_name=="name"andmatch.parent.data[match.data_name]==match.dataThe value the path matched.assertmatch.data=="Sun"The parent match.assertmatch.parent.path_as_str=="$.star"The match can modify the valuematch.data="Soleil"assertrepr(match)=="$.star.name=Soleil"delmatch.dataassertrepr(match)=="$.star.name=None"match.data="Sun"assertrepr(match)=="$.star.name=Sun"match.pop()assertrepr(match)=="$.star.name=None"Tracing DebuggingAll the functions: get, find, get_match and find_matchesm, support tracing.   An option, when enabled,
records the route the algorithm takes to determine a match.This example logs the route the algorithm takes to find the inner planets.  Theprintfunction is give to capture the logs, but any single argument function can be used.inner_planets=[planetforplanetinfind(path.star.planets.inner[wc].name,solar_system,trace=log_to(print))]assertinner_planets==['Mercury','Venus','Earth','Mars']The results"""at $.star got {'name': 'Sun', 'dia...at $.star.planets got {'inner': [{'name': ...at $.star.planets.inner got [{'name': 'Mercury',...at $.star.planets.inner[*] got {'name': 'Mercury', ...at $.star.planets.inner[0].name got 'Mercury'at $.star.planets.inner[*] got {'name': 'Venus', 'N...at $.star.planets.inner[1].name got 'Venus'at $.star.planets.inner[*] got {'name': 'Earth', 'N...at $.star.planets.inner[2].name got 'Earth'at $.star.planets.inner[*] got {'name': 'Mars', 'Nu...at $.star.planets.inner[3].name got 'Mars'"""PathThe rootThepathpoint to root of the tree.match=get_match(path,solar_system)assertmatch.data==solar_systemIn a filter, thepathpoint to the current element.match=get_match(path.star.name[has(path=='Sun')],solar_system)assertmatch.data=='Sun'DictionariesKeysThe dictionary keys are referenced as dynamic attributes on a path.inner_from_attribute=get(path.star.planets.inner,solar_system)inner_from_string_keys=get(path["star"]["planets"]["inner"],solar_system)assertinner_from_attribute==inner_from_string_keys==solar_system["star"]["planets"]["inner"]Keys With Special CharactersDictionary keys that are not valid python syntax can be referenced as quoted strings.sun_equatorial_diameter=get(path.star.planets.inner[0]["Number of Moons"],solar_system)assertsun_equatorial_diameter==solar_system["star"]["planets"]["inner"][0]["Number of Moons"]Dictionaries that have alot of keys with a dash in the name can can usepathdinstead.  It will interpret
path attributes with underscore as dashes.mercury_has_moons=get(pathd.star.planets.inner[0].has_moons,solar_system)assertmercury_has_moons==solar_system["star"]["planets"]["inner"][0]["has-moons"]Wildcard as a Key.Thewildcardattribute specifies all sibling keys.   It is useful for iterating over attributes.star_children=[childforchildinfind(path.star.wildcard,solar_system)]assertstar_children==[solar_system["star"]["name"],solar_system["star"]["diameter"],solar_system["star"]["age"],solar_system["star"]["planets"],]Thewcis the short version of wildcard.star_children=[childforchildinfind(path.star.wc,solar_system)]assertstar_children==[solar_system["star"]["name"],solar_system["star"]["diameter"],solar_system["star"]["age"],solar_system["star"]["planets"],]The dictionary wildcard is declared using dot notation and cannot be used to iterator over a list.  The list
wildcard is declared using index notation and cannot be used to iterate over dictionary keys.all_planets=[pforpinfind(path.star.planets.wc[wc].name,solar_system)]assertall_planets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']The generic_wildcard, also known as gwc, can be declared in either notations and supports iterating over both
list and dictionaries.all_planets=[pforpinfind(path.star.planets.gwc.gwc.name,solar_system)]assertall_planets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']Comma Delimited KeysMultiple dictionary keys can be specified using a comma delimited list.last_and_first=[planetforplanetinfind(path.star["diameter","name"],solar_system)]assertlast_and_first==[1391016,"Sun"]ListIndexesList can be access using index.earth=get(path.star.planets.inner[2],solar_system)assertearth==solar_system["star"]["planets"]["inner"][2]List the third inner and outer planet.last_two=[planetforplanetinfind(path.star.wc.wc[2].name,solar_system)]assertlast_two==['Earth','Uranus']Comma Delimited Indexes.List indexes can be specified as a comma delimited list.last_and_first=[planetforplanetinfind(path.star.planets.outer[3,0].name,solar_system)]assertlast_and_first==["Neptune","Jupiter"]SlicesList can be access using slices.List the first two planets.first_two=[planetforplanetinfind(path.star.planets.outer[:2].name,solar_system)]assertfirst_two==["Jupiter","Saturn"]List the last two planets.last_two=[planetforplanetinfind(path.star.planets.outer[-2:].name,solar_system)]assertlast_two==["Uranus","Neptune"]List all outer planets in reverse.last_two=[planetforplanetinfind(path.star.planets.outer[::-1].name,solar_system)]assertlast_two==["Neptune","Uranus","Saturn","Jupiter"]List the last inner and outer planets.last_two=[planetforplanetinfind(path.star.wc.wc[-1:].name,solar_system)]assertlast_two==["Mars","Neptune"]Wildcard as an Index.Thewildcardword can be used as a list index.   It is useful for iterating over attributes.all_outer=[planetforplanetinfind(path.star.planets.outer[wildcard].name,solar_system)]assertall_outer==["Jupiter","Saturn","Uranus","Neptune"]Thewcis the short version of wildcard.all_outer=[planetforplanetinfind(path.star.planets.outer[wc].name,solar_system)]assertall_outer==["Jupiter","Saturn","Uranus","Neptune"]The list wildcard is declared using index notation and cannot be used to iterate over dictionary keys.  The
dictionary wildcard is declared using dot notation and cannot be used to iterator over a list.all_planets=[pforpinfind(path.star.planets.wc[wc].name,solar_system)]assertall_planets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']The generic_wildcard, also known as gwc, can be declared in either notations and supports iterating over both
list and dictionaries.all_planets=[pforpinfind(path.star.planets[gwc][gwc].name,solar_system)]assertall_planets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']RecursionTherecursiveword implies recursive search.  It executes a preorder tree traversal.  The search algorithm
descends the tree hierarchy evaluating the path on each vertex until a match occurs.  On each iteration it
continues where it left off. This is an example that finds all the planets names.all_planets=[pforpinfind(path.star.planets.recursive.name,solar_system)]assertall_planets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']Therecis the short version of recursive.all_planets=[pforpinfind(path.star.planets.rec.name,solar_system)]assertall_planets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']Here is another example that finds all the celestial bodies names.all_celestial_bodies=[pforpinfind(path.rec.name,solar_system)]assertall_celestial_bodies==['Sun','Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']FiltersFilters are use to add additional search criteria.has filterThehasfunction is a filter that evaluates a branched off path relative to its parent path.  This example
finds all celestial bodies that have planets.sun=get(path.rec[has(path.planets)].name,solar_system)assertsun=="Sun"This search finds all celestial bodies that have a has-moons attribute.all_celestial_bodies_moon_attribute=[planetforplanetinfind(path.rec[has(pathd.has_moons)].name,solar_system)]assertall_celestial_bodies_moon_attribute==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']This search finds all celestial bodies that have moons. Note theoperator.truthis used to exclude planets
that don't have moons.all_celestial_bodies_moon_attribute=[planetforplanetinfind(path.rec[has(pathd.has_moons,operator.truth)].name,solar_system)]assertall_celestial_bodies_moon_attribute==['Earth','Mars','Jupiter','Saturn','Uranus','Neptune']has filter comparison operatorsFilters can be specified with a comparison operator.earth=[planetforplanetinfind(path.rec[has(path.diameter==12756)].name,solar_system)]assertearth==['Earth']earth=[planetforplanetinfind(path.rec[has(path.diameter!=12756)].name,solar_system)]assertearth==['Sun','Mercury','Venus','Mars','Jupiter','Saturn','Uranus','Neptune']earth=[planetforplanetinfind(path.rec[has(path.diameter>12756)].name,solar_system)]assertearth==['Sun','Jupiter','Saturn','Uranus','Neptune']earth=[planetforplanetinfind(path.rec[has(path.diameter>=12756)].name,solar_system)]assertearth==['Sun','Earth','Jupiter','Saturn','Uranus','Neptune']earth=[planetforplanetinfind(path.rec[has(path.diameter<12756)].name,solar_system)]assertearth==['Mercury','Venus','Mars']earth=[planetforplanetinfind(path.rec[has(path.diameter<=12756)].name,solar_system)]assertearth==['Mercury','Venus','Earth','Mars']has filter type conversionSometimes the value is the wrong type for the comparison operator. In this example the attribute
"Number of Moons" is str type.planets=[planetforplanetinfind(path.rec[has(path["Number of Moons"]>"5")].name,solar_system)]assertplanets==['Jupiter','Saturn']This is how to convert the type to an int before applying the comparison operator.planets=[planetforplanetinfind(path.rec[has(path["Number of Moons"]>5,int)].name,solar_system)]assertplanets==['Jupiter','Saturn','Uranus','Neptune']has filter comparison operators as single argument functionsA filter operator can be specified as a single argument function.  Here an example that searches for planets that
have the same diameter as earth.earths_diameter=partial(operator.eq,12756)earth=[planetforplanetinfind(path.rec[has(path.diameter,earths_diameter)].name,solar_system)]assertearth==['Earth']Any single argument function can be used as an operator.  This example uses a Regular Expression to finds
planets that end with the letter s.name_ends_with_s=re.compile(r"\w+s").matchearth=[planetforplanetinfind(path.rec[has(path.name,name_ends_with_s)].name,solar_system)]assertearth==['Venus','Mars','Uranus']This example uses a closure to find planets that have the same diameter as earth.defsmaller_than_earth(value):returnvalue<12756earth=[planetforplanetinfind(path.rec[has(path.diameter,smaller_than_earth)].name,solar_system)]assertearth==['Mercury','Venus','Mars']logical and, or and not filtershas_allA regular express to test if second letter in the value is 'a'.second_letter_is_a=re.compile(r".a.*").fullmatchThehas_allfunction evaluates as the logicalandoperator.   It is equivalent to: (arg1 and arg2 and ...)found=[planetforplanetinfind(path.rec[has_all(path.diameter<10000,(path.name,second_letter_is_a))].name,solar_system)]assertfound==['Mars']has_anyThehas_anyfunction evaluates as the logicaloroperator.   It is equivalent to: (arg1 and arg2 and ...)found=[planetforplanetinfind(path.rec[has_any(path.diameter<10000,(path.name,second_letter_is_a))].name,solar_system)]assertfound==['Mercury','Earth','Mars','Saturn']has_notThehas_notfunction evaluates as the logicalnotoperator.   It is equivalent to: (not arg)
This example find all the planets names not not equal to Earth.  Note the double nots.found=[planetforplanetinfind(path.rec[has_not(path.name!='Earth')].name,solar_system)]assertfound==['Earth']Combining has, has_all, has_any, and has_not filters.Each of thehasfunction can be passed as arguments to any of the otherhasfunction to construct complex
boolean equation.  This example is equivalent to:
(10000 > diameter  or diameter > 20000) and second_letter_is_a(name))found=[planetforplanetinfind(path.rec[has_all(has_any(path.diameter<10000,path.diameter>20000),(path.name,second_letter_is_a))].name,solar_system)]assertfound==['Mars','Saturn']has.theseThe decoratorhas.thesecan be used to construct the boolean equations more explicitly.  This example shows
to use python built in and, or and not operators.@has.these(path.diameter<10000,path.diameter>20000,(path.name,second_letter_is_a))defpredicate(parent_match:Match,small_diameter,large_diameter,name_second_letter_is_a):return(small_diameter(parent_match)orlarge_diameter(parent_match))andname_second_letter_is_a(parent_match)found=[planetforplanetinfind(path.rec[predicate].name,solar_system)]assertfound==['Mars','Saturn']A custom filter.A predicate is a single argument function that returns anything. The argument is the current match.   The has
function is a fancy predicate.This example writes a custom predicate that find all of Earth's neighbours.defmy_neighbor_is_earth(match:Match):i_am_planet=get_match(path.parent.parent.parent.planets,match,must_match=False)ifnoti_am_planet:returnFalseindex_before_planet=match.data_name-1before_planet=get_match(path[index_before_planet][has(path.name=="Earth")],match.parent,must_match=False)ifbefore_planet:returnTrueindex_after_planet=match.data_name+1before_planet=get_match(path[index_after_planet][has(path.name=="Earth")],match.parent,must_match=False)ifbefore_planet:returnTruereturnFalseearth=[planetforplanetinfind(path.rec[my_neighbor_is_earth].name,solar_system)]assertearth==['Venus','Mars']Class Descriptorsbasic path descriptorpaths can be added as properties to a class using the path_descriptor function.planets=path.star.planets.wc[wc]classSolarSystem(Document):jupiter_name=attr(path.star.planets.outer[0].name)saturn_name=attr(path.star.planets.outer[1].name)big_planets=attr(planets[has(path.diameter>25000)].name,getter=find)small_planets=attr(planets[has(path.diameter<=25000)].name,getter=find,to_wrapped_value=list)number_of_planets=attr(planets,getter=find,to_wrapped_value=lambdaitr:len(list(itr)))The property support both gets and sets and delsss=SolarSystem(solar_system)assertss.jupiter_name=='Jupiter'assertss.saturn_name=='Saturn'Rename Jupiter to Planet 5ss.jupiter_name='Planet 5'assertss.jupiter_name=='Planet 5'The assignment operation alters the original document.assertsolar_system["star"]["planets"]["outer"][0]["name"]=='Planet 5'remove Jupiter's namedelss.jupiter_namewithpytest.raises(MatchNotFoundError):print(ss.jupiter_name)assert"name"notinsolar_system["star"]["planets"]["outer"][0]There are still 8 planets because only Jupiter's name was deleteassertss.number_of_planets==8list all the big planets.  Remember Jupiter was deleted.big_planets=[namefornameinss.big_planets]assertbig_planets==['Saturn','Uranus','Neptune']list all the small planets.assertss.small_planets==['Mercury','Venus','Earth','Mars']document typed path descriptorA descriptor support wrapping json types with an adaptor class. This example wraps the json that represents a
planet with the planet class.   The Planet class extends the Document class which provides the marshalling
methods.classPlanet(Document):name=attr(path.name)classSolarSystem(Document):jupiter=attr_typed(Planet,path.star.planets.outer[0])planets=attr_iter_typed(Planet,path.star.planets.wc[wc])outer_planets=attr_list_typed(Planet,path.star.planets.outer)The getter returns the planet typess=SolarSystem(solar_system)planet=ss.jupiterassertplanet.name=='Jupiter'rename Jupiter to Planet 5planet.name='Planet 5'assertplanet.name=='Planet 5'The assignment operation alters the original document.assertsolar_system["star"]["planets"]["outer"][0]["name"]=='Planet 5'Jupiter can be renamed by replacing the planet with an imposter.impostor_planet=Planet({})impostor_planet.name='Imposter Jupiter'ss.jupiter=impostor_planetassertss.jupiter.name=='Imposter Jupiter'The imposter planet also alters the original document.assertsolar_system["star"]["planets"]["outer"][0]["name"]=='Imposter Jupiter'An attribute descriptor can return an iterator where each element is converted to the correct type.planets=[planet.nameforplanetinss.planets]assertplanets==['Mercury','Venus','Earth','Mars','Imposter Jupiter','Saturn','Uranus','Neptune']An attribute descriptor can return an list where each element is converted to the correct type.assertss.outer_planets[0].name=='Imposter Jupiter'The list can be modified and the underline document is modified too.jupiter=Planet({})jupiter.name='Jupiter'ss.outer_planets[0]=jupiterplanets=[planet.nameforplanetinss.planets]assertplanets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']custom typed path descriptorA descriptor support wrapping json types with an adaptor class. This example wraps the json that represents a
planet with the planet class.   This Planet class defines its own marshalling methods.classPlanet:def__init__(self,name:str=None):self._name=name@propertydefname(self)->str:returnself._name@name.setterdefname(self,name:str):self._name=name@staticmethoddefto_wrapped_value(json_:JsonArgTypes)->Planet:returnPlanet(json_["name"])@staticmethoddefto_json_value(planet_:Planet)->JsonArgTypes:return{"name":planet_.name}classSolarSystem(Document):jupiter=attr_typed(Planet,path.star.planets.outer[0],to_wrapped_value=Planet.to_wrapped_value,to_json_value=Planet.to_json_value)planets=attr_iter_typed(Planet,path.star.planets.wc[wc],to_wrapped_value=Planet.to_wrapped_value)outer_planets=attr_list_typed(Planet,path.star.planets.outer,to_wrapped_value=Planet.to_wrapped_value,to_json_value=Planet.to_json_value)The getter returns the planet typess=SolarSystem(solar_system)planet=ss.jupiterassertplanet.name=='Jupiter'rename Jupiter to Planet 5planet.name='Planet 5'assertplanet.name=='Planet 5'Jupiter can be renamed by replacing the planet with an imposter.impostor_planet=Planet()impostor_planet.name='Imposter Jupiter'ss.jupiter=impostor_planetassertss.jupiter.name=='Imposter Jupiter'The imposter planet also alters the original document.assertsolar_system["star"]["planets"]["outer"][0]["name"]=='Imposter Jupiter'An attribute descriptor can return an iterator where each element is converted to the correct type.planets=[planet.nameforplanetinss.planets]assertplanets==['Mercury','Venus','Earth','Mars','Imposter Jupiter','Saturn','Uranus','Neptune']An attribute descriptor can return an list where each element is converted to the correct type.assertss.outer_planets[0].name=='Imposter Jupiter'The list can be modified and the underline document is modified too.jupiter=Planet()jupiter.name='Jupiter'ss.outer_planets[0]=jupiterplanets=[planet.nameforplanetinss.planets]assertplanets==['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune']