I have this middle-sized data structure that I want to maintain in a format that does not contain any extra parentheses or curly braces. Here is a small sample:

_rules = """
left:
    type: leading
    target:
        attribute: x
        leading: value
    source:
        regular: view.x
        container: view.bounds.x
right:
    type: trailing
    target:
        attribute: x
        and_so_on: ...
"""

This is nice to edit as Pythonista editor helps with the indentation.

Then I of course want to turn it into the corresponding Python data structure for usage, i.e. something like this:

{
    "left": {
        "type": "leading",
        "target": {
            "attribute": "x",
            "leading": "value",
            "trailing": "value + gap",
            "flex": etc...

I did not want to bring in yaml as a dependency, so instead wrote the very fancy parser below, shared here in case someone else has a similar need, or knows of a smarter way to go about this type of thing.

import re

def parse_rules(rules):    
    rule_dict = dict()
    dicts = [rule_dict]
    spaces = re.compile(' *')
    for i, line in enumerate(rules.splitlines()):
        i += 11  # Used to match error line number to my file
        if line.strip() == '': continue
        indent = len(spaces.match(line).group())
        if indent % 4 != 0:
            raise RuntimeError(f'Broken indent on line {i}')
        indent = indent // 4 + 1
        if indent > len(dicts):
            raise RuntimeError(f'Extra indentation on line {i}')
        dicts = dicts[:indent]
        line = line.strip()
        if line.endswith(':'):
            key = line[:-1].strip()
            new_dict = dict()
            dicts[-1][key] = new_dict
            dicts.append(new_dict)
        else:
            try:
                key, content = line.split(':')
                dicts[-1][key.strip()] = content.strip()
            except Exception as error:
                raise RuntimeError(f'Cannot parse line {i}', error)
    return rule_dict

I only need the contents as strings, but for other use cases you might throw in an eval around the content to support numbers, strings, lists and whatnot.