List of paths to nested dictionary

4

1

Given a list of nested menu items,

items = [
    '3D/Axis',
    '3D/CameraTracker',

    'Color/Invert',
    'Color/Log2Lin',

    'Color/Math/Add',
    'Color/Math/Multiply',

    'Other/Group',
    'Other/NoOp',

    'Views/JoinViews',
    'Views/ShuffleViews',

    'Views/Stereo/Anaglyph',
    'Views/Stereo/ReConverge',
]

..and a dummy function to be triggered by each item:

def msg(path):
    """Dummy function used in menu
    """
    return path.lower()

Programatically create a hierarchal collection of dictionaries, which is equivalent to manually typing this:

menu = {
    '3D': {'Axis':                     lambda: msg('3D/Axis'),
           'CameraTracker':            lambda: msg("3D/CameraTracker")},

    'Color': {'Invert':                lambda: msg('Color/Invert'),
              'Log2Lin':               lambda: msg('Color/Log2Lin'),

              'Math': {'Add':          lambda: msg('Color/Add'),
                       'Multiply':     lambda: msg('Color/Multiply')}},

    'Other': {'Group':                 lambda: msg('Other/Group'),
              'NoOp':                  lambda: msg('Other/NoOp')},

    'Views': {'JoinViews':             lambda: msg('Views/JoinViews'),
              'ShuffleViews':          lambda: msg('Views/ShuffleViews'),
              'Stereo': {'Anaglyph':   lambda: msg('Views/Stereo/Anaglyph'),
                         'ReConverge': lambda: msg('Views/Stereo/ReConverge')}}},

..which could be tested as follows:

assert menu['3D']['Axis']() == '3d/axis'
assert menu['Color']['Invert']() == 'color/invert'
assert menu['Color']['Math']['Add']() == 'color/math/add'
assert menu['Views']['Stereo']['Anaglyph']() == 'views/stereo/anaglyph'

dbr

Posted 2012-01-01T07:34:39.543

Reputation: 157

2Code golf questions should go to [codegolf.se], it was decided that they don't belong here anymore (AFAIK). – None – 2012-01-01T07:51:42.773

@dbr If you'd like this migrated, go ahead and flag for it. – None – 2012-01-02T04:08:09.807

2...except we usually don't take gratuitously language-specific questions here :-/ – J B – 2012-01-02T13:03:33.693

Answers

2

A simple recursion will do the work

def build_nested_helper(path, text, container):
    segs = path.split('/')
    head = segs[0]
    tail = segs[1:]
    if not tail:
        container[head] = lambda: msg(text)
    else:
        if head not in container:
            container[head] = {}
        build_nested_helper('/'.join(tail), text, container[head])

def build_nested(paths):
    container = {}
    for path in paths:
        build_nested_helper(path, path, container)
    return container

menu = build_nested(items)

squashed code: 159 chars

def f(p,t,c):
 s=p.split('/');a=s[0];b=s[1:]
 if b:
  if a not in c:c[a]={}
  f('/'.join(b),t,c[a])
 else:c[a]=lambda:msg(t)
menu={}
for i in items:f(i,i,menu)

qiao

Posted 2012-01-01T07:34:39.543

Reputation: 121

3squash s=p.split('/');a=s[0];b=s[1:] to a,b=p.split('/',1) – None – 2012-01-01T10:54:42.010

@eumiro nice one! didn't know the second argument before. – None – 2012-01-01T13:01:13.033

0

As a starting point,

def populate_dict(item, existing_dict, fullpath = None):
    if len(item) == 1:
        existing_dict[item[0]] = lambda p=item[0]: msg(fullpath + "/" + item[0])
    else:
        head, tail = item[0], item[1:]
        existing_dict.setdefault(head, {})
        populate_dict(
            tail,
            existing_dict[head],
            fullpath = "/".join([x for x in (fullpath, head) if x is not None]))

menu = {}
for i in items:
    populate_dict(i.split("/"), menu)

..which can be squashed down to:

def p(i,e,fp=""):
 if len(i)<2:e[i[0]]=lambda p=i[0]:msg(fp+"/"+i[0])
 else:p(i[1:], e.setdefault(i[0], {}),"/".join([fp, i[0]]).lstrip("/"))
menu = {}
for i in items:p(i.split("/"),menu)

dbr

Posted 2012-01-01T07:34:39.543

Reputation: 157

0

#!/usr/bin/env python3

from itertools import groupby
from pprint import pprint

items = [
    '3D/Axis',
    '3D/CameraTracker',

    'Color/Invert',
    'Color/Log2Lin',

    'Color/Math/Add',
    'Color/Math/Multiply',

    'Other/Group',
    'Other/NoOp',

    'Views/JoinViews',
    'Views/ShuffleViews',

    'Views/Stereo/Anaglyph',
    'Views/Stereo/ReConverge',
]

def fun(group, items, path):
    sep = lambda i:i.split('/', 1)
    head = [i for i in items if len(sep(i))==2]
    tail = [i for i in items if len(sep(i))==1]
    gv = groupby(sorted(head), lambda i:sep(i)[0])
    return group, dict([(i, path+i) for i in tail] + [fun(g, [sep(i)[1] for i in v], path+g+'/') for g,v in gv])

menu = dict([fun('menu', items, '')])['menu']
pprint(menu)

{'3D': {'Axis': '3D/Axis', 'CameraTracker': '3D/CameraTracker'},
 'Color': {'Invert': 'Color/Invert',
           'Log2Lin': 'Color/Log2Lin',
           'Math': {'Add': 'Color/Math/Add',
                    'Multiply': 'Color/Math/Multiply'}},
 'Other': {'Group': 'Other/Group', 'NoOp': 'Other/NoOp'},
 'Views': {'JoinViews': 'Views/JoinViews',
           'ShuffleViews': 'Views/ShuffleViews',
           'Stereo': {'Anaglyph': 'Views/Stereo/Anaglyph',
                      'ReConverge': 'Views/Stereo/ReConverge'}}}

fun() takes 6 lines
Just change path+i to (path+i).lower() to get what you want.

kev

Posted 2012-01-01T07:34:39.543

Reputation: 121