Package: sections
[ s e | c t | i o | n s ]Python package providing flexible tree data structures for organizing lists and dicts into sections.Sections is designed to be:Intuitive: Start quickly, spend less time reading the docs.Scalable: Grow arbitrarily complex trees as your problem scales.Flexible: Rapidly build nodes with custom attributes, properties, and methods on the fly.Fast: Made with performance in mind - access lists and sub-lists/dicts in Θ(1) time in many cases. See the Performance section for the full details.Reliable: Contains an exhaustive test suite and 100% code coverage.LinksGitHubDocumentationUsage$pipinstallsectionsimportsectionsmenu=sections('Breakfast','Dinner',main=['Bacon&Eggs','Burger'],side=['HashBrown','Fries'],)$print(menu)_________________________│_____________________│││'Breakfast'││││main='Bacon&Eggs'││││side='HashBrown'│││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯││_________________│││'Dinner'││││main='Burger'││││side='Fries'│││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯│¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯# menu's API with the expected results:assertmenu.mains==['Bacon&Eggs','Burger']assertmenu.sides==['HashBrown','Fries']assertmenu['Breakfast'].main=='Bacon&Eggs'assertmenu['Breakfast'].side=='HashBrown'assertmenu['Dinner'].main=='Burger'assertmenu['Dinner'].side=='Fries'assertmenu('sides',list)==['HashBrown','Fries']assertmenu('sides',dict)=={'Breakfast':'HashBrown','Dinner':'Fries'}# root section/node:assertisinstance(menu,sections.Section)# child sections/nodes:assertisinstance(menu['Breakfast'],sections.Section)assertisinstance(menu['Dinner'],sections.Section)Scale in size:library=sections("My Bookshelf",sections({'Fiction'},'LOTR','Harry Potter',author=['JRR Tolkien','JK Rowling'],topic=[{'Fantasy'},'Hobbits','Wizards'],),sections({'Non-Fiction'},'General Relativity','A Brief History of Time',author=['Albert Einstein','Steven Hawking'],topic=[{'Physics'},'Time, Gravity','Black Holes'],),)$print(menu)________________________________________│'My Bookshelf'││______________________________│││'Fiction'││││topic='Fantasy'││││________________________│││││'LOTR'││││││author='JRR Tolkien'││││││topic='Hobbits'│││││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯││││_______________________│││││'Harry Potter'││││││author='JK Rowling'││││││topic='Wizards'│││││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯│││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯││__________________________________│││'Non-Fiction'││││topic='Physics'││││____________________________│││││'General Relativity'││││││author='Albert Einstein'││││││topic='Time, Gravity'│││││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯││││___________________________│││││'A Brief History of Time'││││││author='Steven Hawking'││││││topic='Black Holes'│││││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯│││¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯│¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯Attrs: Plural/singular hybrid attributes and moreSpend less time deciding between using the singular or plural form for an attribute name:tasks=sections('pay bill','clean',status=['completed','started'])asserttasks.statuses==['completed','started']asserttasks['pay bill'].status=='completed'asserttasks['clean'].status=='started'If you don’t like this feature, simply turn it off as shown in theDetails - Plural/singular attributessection.Properties: Easily add on the flyProperties and methods are automatically added to all nodes in a structure returned from asections()call when passed as keyword arguments:schedule=sections('Weekdays','Weekend',hours_per_day=[[8,8,6,10,8],[4,6]],hours=property(lambdaself:sum(self.hours_per_day)),)assertschedule['Weekdays'].hours==40assertschedule['Weekend'].hours==10assertschedule.hours==50Adding properties and methods this way doesn’t affect the class definitions of Sections/nodes from other structures. See theDetails - Properties/methodssection for how this works.Construction: Build gradually or all at onceConstruct section-by-section, section-wise, attribute-wise, or other ways:defdemo_different_construction_techniques():"""Example construction techniques for producing the same structure."""# Building section-by-sectionbooks=sections()books['LOTR']=sections(topic='Hobbits',author='JRR Tolkien')books['Harry Potter']=sections(topic='Wizards',author='JK Rowling')demo_resulting_object_api(books)# Section-wise constructionbooks=sections(sections('LOTR',topic='Hobbits',author='JRR Tolkien'),sections('Harry Potter',topic='Wizards',author='JK Rowling'))demo_resulting_object_api(books)# Attribute-wise constructionbooks=sections('LOTR','Harry Potter',topics=['Hobbits','Wizards'],authors=['JRR Tolkien','JK Rowling'])demo_resulting_object_api(books)# setattr post-constructionbooks=sections('LOTR','Harry Potter',)books.topics=['Hobbits','Wizards']books['LOTR'].author='JRR Tolkien'books['Harry Potter'].author='JK Rowling'demo_resulting_object_api(books)defdemo_resulting_object_api(books):"""Example Section structure API and expected results."""assertbooks.names==['LOTR','Harry Potter']assertbooks.topics==['Hobbits','Wizards']assertbooks.authors==['JRR Tolkien','JK Rowling']assertbooks['LOTR'].topic=='Hobbits'assertbooks['LOTR'].author=='JRR Tolkien'assertbooks['Harry Potter'].topic=='Wizards'assertbooks['Harry Potter'].author=='JK Rowling'demo_different_construction_techniques()DetailsSection namesThe non-keyword arguments passed into asections()call define the section names and are accessed through the attributename. The names are used likekeysin adictto access each child section of the root section node:books=sections('LOTR','Harry Potter',topic=['Hobbits','Wizards'],author=['JRR Tolkien','JK Rowling'])assertbooks.names==['LOTR','Harry Potter']assertbooks['LOTR'].name=='LOTR'assertbooks['Harry Potter'].name=='Harry Potter'Names are optional, and by default, children names are assigned as integer values corresponding to indices in an array, while a root has a default keyvalue ofsections.SectionNone:sect=sections(x=['a','b'])assertsect.sections.names==[0,1]assertsect.nameissections.SectionNone# the string representation of sections.SectionNone is 'section':assertstr(sect.name)=='sections'Parent names and attributesA parent section name can optionally be provided as the first argument in asections()call by defining it in a set (surrounding it with curly brackets). This strategy avoids an extra level of braces when instantiating Section objects. This idea applies also for defining parent attributes:library=sections({"My Bookshelf"},[{'Fantasy'},'LOTR','Harry Potter'],[{'Academic'},'Advanced Mathematics','Physics for Engineers'],topic=[{'All my books'},[{'Imaginary things'},'Hobbits','Wizards'],[{'School'},'Numbers','Forces']],)assertlibrary.name=="My Bookshelf"assertlibrary.sections.names==['Fantasy','Academic']assertlibrary['Fantasy'].sections.names==['LOTR','Harry Potter']assertlibrary['Academic'].sections.names==['Advanced Mathematics','Physics for Engineers']assertlibrary['Fantasy']['Harry Potter'].name=='Harry Potter'assertlibrary.topic=='All my books'assertlibrary['Fantasy'].topic=='Imaginary things'assertlibrary['Academic'].topic=='School'Return attributes as a list, dict, or iterableAccess the data in different forms with thegettypeargument inSection.__call__()as follows:menu=sections('Breakfast','Dinner',sides=['HashBrown','Fries'])# return as list always, even if a single element is returnedassertmenu('sides',list)==['HashBrown','Fries']assertmenu['Breakfast']('side',list)==['HashBrown']# return as dictassertmenu('sides',dict)=={'Breakfast':'HashBrown','Dinner':'Fries'}assertmenu['Breakfast']('side',dict)=={'Breakfast':'HashBrown'}# return as iterator over elements in list (fastest method, theoretically)fori,valueinenumerate(menu('sides',iter)):assertvalue==['HashBrown','Fries'][i]fori,valueinenumerate(menu['Breakfast']('side',iter)):assertvalue==['HashBrown'][i]See theSection.__call__()method in the References section of the docs for more options.Set the default return type when accessing structure attributes by changingSection.default_gettypeas follows:menu=sections('Breakfast','Dinner',sides=['HashBrown','Fries'])menu['Breakfast'].default_gettype=dict# set for only 'Breakfast' nodeassertmenu.sides==['HashBrown','Fries']assertmenu['Breakfast']('side')=={'Breakfast':'HashBrown'}menu.cls.default_gettype=dict# set for all nodes in `menu`assertmenu('sides')=={'Breakfast':'HashBrown','Dinner':'Fries'}assertmenu['Breakfast']('side')=={'Breakfast':'HashBrown'}sections.Section.default_gettype=dict# set for all structurestasks1=sections('pay bill','clean',status=['completed','started'])tasks2=sections('pay bill','clean',status=['completed','started'])asserttasks1('statuses')=={'pay bill':'completed','clean':'started'}asserttasks2('statuses')=={'pay bill':'completed','clean':'started'}The above will also work for accessing attributes in the formobject.attrbut only if the node does not contain the attributeattr, otherwise it will return the non-iterable raw value forattr. Therefore, for consistency, access attributes usingSection.__call__()like above if you wish toalways receive an iterableform of the attributes.Plural/singular attributesWhen an attribute is not found in a Section node, both the plural and singular
forms of the word are then checked to see if the node contains the attribute
under those forms of the word. If they are still not found, the node will
recursively repeat the same search on each of its children, concatenating the
results into a list or dict. The true attribute name in each node supplied a
corresponding value is whatever name was given in the keyword argument’s key
(i.e.statusin the example below).If you don’t like this feature, simply turn it off using the following:import pytest
tasks = sections('pay bill', 'clean', status=['completed', 'started'])
assert tasks.statuses == ['completed', 'started']
# turn off for all future structures:
sections.Section.use_pluralsingular = False
tasks = sections('pay bill', 'clean', status=['completed', 'started'])
with pytest.raises(AttributeError):
    tasks.statuses  # this now raises an AttributeErrorNote, however, that this will still traverse descendant nodes to see if they
contain the requested attribute. To stop using this feature also, access
attributes using theSection.get_node_attr()method instead.Properties/methodsEachsections()call returns a structure containing nodes of a unique class created in a class factory function, where the unique class definition contains no logic except that it inherits from the Section class. This allows properties/methods added to one structure’s class definition to not affect the class definitions of nodes from other structures.SubclassingInheriting Section is easy, the only requirement is to callsuper().__init__(**kwds)at some point in__init__()like below if you override that method:classLibrary(sections.Section):"""My library class."""def__init__(self,price="Custom default value",**kwds):"""Pass **kwds to super."""super().__init__(**kwds)self.price=price@propertydefgenres(self):"""A synonym for sections."""ifself.isroot:returnself.sectionselse:raiseAttributeError('This library has only 1 level of genres')@propertydefbooks(self):"""A synonym for leaves."""returnself.leaves@propertydeftitles(self):"""A synonym for names."""returnself.leaves.namesdefcritique(self,review="Haven't read it yet",rating=0):"""Set the book price based on the rating."""self.review=reviewself.price=rating*2library=Library([{'Fantasy'},'LOTR','Harry Potter'],[{'Academic'},'Advanced Math.','Physics for Engineers'])assertlibrary.genres.names==['Fantasy','Academic']assertlibrary.books.titles==['LOTR','Harry Potter','Advanced Math.','Physics for Engineers']library.books['LOTR'].critique(review='Good but too long',rating=7)library.books['Harry Potter'].critique(review="I don't like owls",rating=4)assertlibrary.books['LOTR'].review=='Good but too long'assertlibrary.books['LOTR'].price==14assertlibrary.books['Harry Potter'].review=="I don't like owls"assertlibrary.books['Harry Potter'].price==8importpytestwithpytest.raises(AttributeError):library['Fantasy'].genresSection.__init__()assigns the kwds values passed to it to the object attributes, and the passed kwds are generated during instantiation by a metaclass.PerformanceEach non-leaf Section node keeps a cache containing quickly readable references to attribute dicts previously parsed from manually traversing through descendant nodes in an earlier read. The caches are invalidated accordingly for modified nodes and their ancestors when the tree structure or node attribute values change.The caches allow instant reading of sub-lists/dicts in Θ(1) time and can often
make structure attribute reading faster by 5x, or even much more when the
structure is rarely being modified.
If preferred, turn this feature off to avoid the extra memory consumption it causes by modifying the node or structure’s class attributeuse_cachetoFalseas follows:sect=sections(*[[[[[42]*10]*10]*10]*10])sect.use_cache=False# turn off for just the root nodesect.cls.use_cache=False# turn off for all nodes in `sect`sections.Section.use_cache=False# turn off for all structures