"""
The `MetaBunch` metaclass is a simplified version of the
last example in the _How a Metaclass Creates a Class_ section
of _Chapter 4: Object Oriented Python_ from
[_Python in a Nutshell, 3rd edition_](https://learning.oreilly.com/library/view/python-in-a/9781491913833)
by Alex Martelli, Anna Ravenscroft, and Steve Holden.

Here are a few tests. ``bunch_test.py`` has a few more.

# tag::BUNCH_POINT_DEMO_1[]
    >>> class Point(Bunch):
    ...     x = 0.0
    ...     y = 0.0
    ...     color = 'gray'
    ...
    >>> Point(x=1.2, y=3, color='green')
    Point(x=1.2, y=3, color='green')
    >>> p = Point()
    >>> p.x, p.y, p.color
    (0.0, 0.0, 'gray')
    >>> p
    Point()

# end::BUNCH_POINT_DEMO_1[]

# tag::BUNCH_POINT_DEMO_2[]

    >>> Point(x=1, y=2, z=3)
    Traceback (most recent call last):
      ...
    AttributeError: No slots left for: 'z'
    >>> p = Point(x=21)
    >>> p.y = 42
    >>> p
    Point(x=21, y=42)
    >>> p.flavor = 'banana'
    Traceback (most recent call last):
      ...
    AttributeError: 'Point' object has no attribute 'flavor' and no __dict__ for setting new attributes

# end::BUNCH_POINT_DEMO_2[]
"""

# tag::METABUNCH[]
class MetaBunch(type):  # <1>
    def __new__(meta_cls, cls_name, bases, cls_dict):  # <2>

        defaults = {}  # <3>

        def __init__(self, **kwargs):  # <4>
            for name, default in defaults.items():  # <5>
                setattr(self, name, kwargs.pop(name, default))
            if kwargs:  # <6>
                extra = ', '.join(kwargs)
                raise AttributeError(f'No slots left for: {extra!r}')

        def __repr__(self):  # <7>
            rep = ', '.join(f'{name}={value!r}'
                            for name, default in defaults.items()
                            if (value := getattr(self, name)) != default)
            return f'{cls_name}({rep})'

        new_dict = dict(__slots__=[],
                        __init__=__init__,
                        __repr__=__repr__)  # <8>
        

        for name, value in cls_dict.items():  # <9>
            if name.startswith('__') and name.endswith('__'):  # <10>
                if name in new_dict:
                    msg = f"Can't set {name!r} in {cls_name!r}"
                    raise AttributeError(msg)
                new_dict[name] = value
            else:  # <11>
                new_dict['__slots__'].append(name)
                defaults[name] = value
        return super().__new__(meta_cls, cls_name, bases, new_dict)  # <12>


class Bunch(metaclass=MetaBunch):  # <13>
    pass
# end::METABUNCH[]
