Trees Using Autovivification with Default Dict

Create trees using autovivification in Python with defaultdict.

Resources

In [1]:
from collections import defaultdict
import operator as op
from functools import reduce, partial
from pprint import pprint

from bs4 import BeautifulSoup
In [2]:
_pprint = partial(pprint, indent=4)
In [3]:
def tree():
    return defaultdict(tree)
In [4]:
def dicts(t):
    return {k: dicts(t[k]) for k in t}
In [5]:
def get(item, path):
    return reduce(op.getitem, path, item)
In [6]:
def add(t, path):
    for node in path:
        t = t[node]
In [7]:
taxonomy = tree()  # root
path = "Animalia>Chordata>Mammalia>Cetacea>Balaenopteridae>Balaenoptera>blue whale".split(
    ">"
)
path
Out[7]:
['Animalia',
 'Chordata',
 'Mammalia',
 'Cetacea',
 'Balaenopteridae',
 'Balaenoptera',
 'blue whale']
In [8]:
add(
    taxonomy, path,
)
None  # prevent output

❝Flat is better than nested.❞

A dict with keys that are the path to a node.

In [9]:
{
    path_ or ("root",): dicts(get(taxonomy, path_))
    for path_ in (tuple(path[:i]) for i in range(len(path)))
}  # order is outermost to innermost
Out[9]:
{('root',): {'Animalia': {'Chordata': {'Mammalia': {'Cetacea': {'Balaenopteridae': {'Balaenoptera': {'blue whale': {}}}}}}}},
 ('Animalia',): {'Chordata': {'Mammalia': {'Cetacea': {'Balaenopteridae': {'Balaenoptera': {'blue whale': {}}}}}}},
 ('Animalia',
  'Chordata'): {'Mammalia': {'Cetacea': {'Balaenopteridae': {'Balaenoptera': {'blue whale': {}}}}}},
 ('Animalia',
  'Chordata',
  'Mammalia'): {'Cetacea': {'Balaenopteridae': {'Balaenoptera': {'blue whale': {}}}}},
 ('Animalia',
  'Chordata',
  'Mammalia',
  'Cetacea'): {'Balaenopteridae': {'Balaenoptera': {'blue whale': {}}}},
 ('Animalia',
  'Chordata',
  'Mammalia',
  'Cetacea',
  'Balaenopteridae'): {'Balaenoptera': {'blue whale': {}}},
 ('Animalia',
  'Chordata',
  'Mammalia',
  'Cetacea',
  'Balaenopteridae',
  'Balaenoptera'): {'blue whale': {}}}
In [10]:
from chamelboots import ChameleonTemplate as CT
from chamelboots import TalStatement as TS
from chamelboots.constants import Join

ATTRIBUTE_CONTENT = (
    TS("attributes", "attrib"),
    TS("content", "structure content"),
)
In [11]:
ID = "query"
groups = (
    (("form", tuple({"method": "GET", "action": "#", "id": "search-form"}.items()),),),
    (("div", tuple({"class": "form-group",}.items()),),),
    (
        ("label", tuple({"class": "col-sm-2 col-form-label col-form-label-lg", "for": ID,}.items()),),
        (
            "input",
            tuple({
                "type": "text",
                "class": "form-control",
                "id": ID,
                "placeholder": "Enter search term",
            }.items()),
        ),
        ("button", tuple({"type": "submit", "class": "btn btn-primary"}.items()),),
        ("button", tuple({"type": "button", "class": "btn btn-secondary"}.items()),),
    ),
)
root = tree()
add(root, groups)
dom = dicts(root)
_pprint(dom)
{   (('form', (('method', 'GET'), ('action', '#'), ('id', 'search-form'))),): {   (('div', (('class', 'form-group'),)),): {   (('label', (('class', 'col-sm-2 col-form-label col-form-label-lg'), ('for', 'query'))), ('input', (('type', 'text'), ('class', 'form-control'), ('id', 'query'), ('placeholder', 'Enter search term'))), ('button', (('type', 'submit'), ('class', 'btn btn-primary'))), ('button', (('type', 'button'), ('class', 'btn btn-secondary')))): {   }}}}

Convert the dom into HTML.

In [12]:
groups = (
    (("form", (("method", "GET"), ("action", "#"), ("id", "search-form")),),),
    (("div", (("class", "form-group"),),),),
    (
        (
            "label",
            (("class", "col-sm-2 col-form-label col-form-label-lg",), ("for", ID,),),
        ),
        (
            "input",
            (
                ("type", "text"),
                ("class", "form-control"),
                ("id", ID),
                ("placeholder", "Enter search term"),
            ),
        ),
        ("button", (("type", "submit",), ("class", "btn btn-primary"),),),
        ("button", (("type", "submit",), ("class", "btn btn-secondary"),),),
    ),
)
root = tree()
add(root, groups)
dom = dicts(root)

start = len(groups) - 1
leaf = Join.LINES(
    CT(tag, ATTRIBUTE_CONTENT).render(attrib=dict(attrib), content="")
    for items in get(dom, groups[:start]).keys()
    for tag, attrib in items
)

for i in reversed(range(start)):
    leaf = Join.LINES(
        CT(tag, ATTRIBUTE_CONTENT).render(attrib=dict(attrib), content=leaf)
        for items in get(dom, groups[:i]).keys()
        for tag, attrib in items
    )
print(BeautifulSoup(leaf, "html.parser").prettify())
<form action="#" id="search-form" method="GET">
 <div class="form-group">
  <label class="col-sm-2 col-form-label col-form-label-lg" for="query">
  </label>
  <input class="form-control" id="query" placeholder="Enter search term" type="text"/>
  <button class="btn btn-primary" type="submit">
  </button>
  <button class="btn btn-secondary" type="submit">
  </button>
 </div>
</form>

Avoid using nested dicts.

In [13]:
groups = (
    (("form", (("method", "GET"), ("action", "#"), ("id", "search-form",)),),),
    (("div", (("class", "form-group"),),),),
    (
        (
            "label",
            (("class", "col-sm-2 col-form-label col-form-label-lg",), ("for", ID,),),
        ),
        (
            "input",
            (
                ("type", "text"),
                ("class", "form-control"),
                ("id", ID),
                ("placeholder", "Enter search term"),
            ),
        ),
        ("button", (("type", "submit",), ("class", "btn btn-primary"),),),
        ("button", (("type", "button",), ("class", "btn btn-secondary"),),),
    ),
)
groups_ = groups[::-1]  # reverse
leaf = Join.LINES(
    CT(tag, ATTRIBUTE_CONTENT).render(attrib=dict(attrib), content=content)
    for (tag, attrib), content in zip(groups_[0], ("Search", "", "search", "clear"))
)
for i in range(1, len(groups)):
    leaf = Join.LINES(
        CT(tag, ATTRIBUTE_CONTENT).render(attrib=dict(attrib), content=leaf)
        for tag, attrib in groups_[i]
    )
print(BeautifulSoup(leaf, "html.parser").prettify())
<form action="#" id="search-form" method="GET">
 <div class="form-group">
  <label class="col-sm-2 col-form-label col-form-label-lg" for="query">
   Search
  </label>
  <input class="form-control" id="query" placeholder="Enter search term" type="text"/>
  <button class="btn btn-primary" type="submit">
   search
  </button>
  <button class="btn btn-secondary" type="button">
   clear
  </button>
 </div>
</form>
In [14]:
from IPython.display import HTML
HTML(leaf)
Out[14]:
In [15]:
from chamelboots.constants import FAKE

string = CT(
    "button",
    (TS("repeat", "button buttons"), TS("attributes", "attrib")),
    inner_content="${button}",
).render(
    buttons=(FAKE.name() for _ in range(5)),
    attrib=dict((("type", "button",), ("class", "btn btn-success"),)),
)

display(HTML(string))
print(BeautifulSoup(string, "html.parser").prettify())
<button class="btn btn-success" type="button">
 John Howell
</button>
<button class="btn btn-success" type="button">
 Anthony Stevenson
</button>
<button class="btn btn-success" type="button">
 Zachary Williams
</button>
<button class="btn btn-success" type="button">
 Chelsea Wilson
</button>
<button class="btn btn-success" type="button">
 Scott Reyes
</button>