renumbering chapters >= 19

This commit is contained in:
Luciano Ramalho
2021-09-10 12:34:39 -03:00
parent cbd13885fc
commit 4ae4096c4c
154 changed files with 7 additions and 1134 deletions

View File

@@ -0,0 +1,20 @@
# Examples from Python in a Nutshell, 3rd edition
The metaclass `MetaBunch` example in `original/bunch.py` is an exact copy 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.
The version in `pre3.6/bunch.py` is slightly simplified by taking advantage
of Python 3 `super()` and removing comments and docstrings,
to make it easier to compare to the `from3.6` version.
The version in `from3.6/bunch.py` is further simplified by taking advantage
of the order-preserving `dict` that appeared in Python 3.6,
as well as other simplifications,
such as leveraging closures in `__init__` and `__repr__`
to avoid adding a `__defaults__` mapping to the class.
The external behavior of all three versions is the same, and
the test files `bunch_test.py` are identical in the three directories.

View File

@@ -0,0 +1,77 @@
"""
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: 'Point' object has no attribute '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'
# 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>
setattr(self, *kwargs.popitem())
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:
raise AttributeError(f"Can't set {name!r} in {cls_name!r}")
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[]

View File

@@ -0,0 +1,59 @@
import pytest
from bunch import Bunch
class Point(Bunch):
""" A point has x and y coordinates, defaulting to 0.0,
and a color, defaulting to 'gray'—and nothing more,
except what Python and the metaclass conspire to add,
such as __init__ and __repr__
"""
x = 0.0
y = 0.0
color = 'gray'
def test_init_defaults():
p = Point()
assert repr(p) == 'Point()'
def test_init():
p = Point(x=1.2, y=3.4, color='red')
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
def test_init_wrong_argument():
with pytest.raises(AttributeError) as exc:
p = Point(x=1.2, y=3.4, flavor='coffee')
assert "no attribute 'flavor'" in str(exc.value)
def test_slots():
p = Point()
with pytest.raises(AttributeError) as exc:
p.z = 5.6
assert "no attribute 'z'" in str(exc.value)
def test_dunder_permitted():
class Cat(Bunch):
name = ''
weight = 0
def __str__(self):
return f'{self.name} ({self.weight} kg)'
cheshire = Cat(name='Cheshire')
assert str(cheshire) == 'Cheshire (0 kg)'
def test_dunder_forbidden():
with pytest.raises(AttributeError) as exc:
class Cat(Bunch):
name = ''
weight = 0
def __init__(self):
pass
assert "Can't set '__init__' in 'Cat'" in str(exc.value)

View File

@@ -0,0 +1,85 @@
import collections
import warnings
class MetaBunch(type):
"""
Metaclass for new and improved "Bunch": implicitly defines
__slots__, __init__ and __repr__ from variables bound in
class scope.
A class statement for an instance of MetaBunch (i.e., for a
class whose metaclass is MetaBunch) must define only
class-scope data attributes (and possibly special methods, but
NOT __init__ and __repr__). MetaBunch removes the data
attributes from class scope, snuggles them instead as items in
a class-scope dict named __dflts__, and puts in the class a
__slots__ with those attributes' names, an __init__ that takes
as optional named arguments each of them (using the values in
__dflts__ as defaults for missing ones), and a __repr__ that
shows the repr of each attribute that differs from its default
value (the output of __repr__ can be passed to __eval__ to make
an equal instance, as per usual convention in the matter, if
each non-default-valued attribute respects the convention too).
In v3, the order of data attributes remains the same as in the
class body; in v2, there is no such guarantee.
"""
def __prepare__(name, *bases, **kwargs):
# precious in v3—harmless although useless in v2
return collections.OrderedDict()
def __new__(mcl, classname, bases, classdict):
""" Everything needs to be done in __new__, since
type.__new__ is where __slots__ are taken into account.
"""
# define as local functions the __init__ and __repr__ that
# we'll use in the new class
def __init__(self, **kw):
""" Simplistic __init__: first set all attributes to
default values, then override those explicitly
passed in kw.
"""
for k in self.__dflts__:
setattr(self, k, self.__dflts__[k])
for k in kw:
setattr(self, k, kw[k])
def __repr__(self):
""" Clever __repr__: show only attributes that differ
from default values, for compactness.
"""
rep = ['{}={!r}'.format(k, getattr(self, k))
for k in self.__dflts__
if getattr(self, k) != self.__dflts__[k]
]
return '{}({})'.format(classname, ', '.join(rep))
# build the newdict that we'll use as class-dict for the
# new class
newdict = { '__slots__':[],
'__dflts__':collections.OrderedDict(),
'__init__':__init__, '__repr__':__repr__, }
for k in classdict:
if k.startswith('__') and k.endswith('__'):
# dunder methods: copy to newdict, or warn
# about conflicts
if k in newdict:
warnings.warn(
"Can't set attr {!r} in bunch-class {!r}".
format(k, classname))
else:
newdict[k] = classdict[k]
else:
# class variables, store name in __slots__, and
# name and value as an item in __dflts__
newdict['__slots__'].append(k)
newdict['__dflts__'][k] = classdict[k]
# finally delegate the rest of the work to type.__new__
return super(MetaBunch, mcl).__new__(
mcl, classname, bases, newdict)
class Bunch(metaclass=MetaBunch):
""" For convenience: inheriting from Bunch can be used to get
the new metaclass (same as defining metaclass= yourself).
In v2, remove the (metaclass=MetaBunch) above and add
instead __metaclass__=MetaBunch as the class body.
"""
pass

View File

@@ -0,0 +1,38 @@
import pytest
from bunch import Bunch
class Point(Bunch):
""" A point has x and y coordinates, defaulting to 0.0,
and a color, defaulting to 'gray'—and nothing more,
except what Python and the metaclass conspire to add,
such as __init__ and __repr__
"""
x = 0.0
y = 0.0
color = 'gray'
def test_init_defaults():
p = Point()
assert repr(p) == 'Point()'
def test_init():
p = Point(x=1.2, y=3.4, color='red')
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
def test_init_wrong_argument():
with pytest.raises(AttributeError) as exc:
p = Point(x=1.2, y=3.4, flavor='coffee')
assert "no attribute 'flavor'" in str(exc.value)
def test_slots():
p = Point()
with pytest.raises(AttributeError) as exc:
p.z = 5.6
assert "no attribute 'z'" in str(exc.value)

View File

@@ -0,0 +1,70 @@
import warnings
class metaMetaBunch(type):
"""
metaclass for new and improved "Bunch": implicitly defines
__slots__, __init__ and __repr__ from variables bound in class scope.
An instance of metaMetaBunch (a class whose metaclass is metaMetaBunch)
defines only class-scope variables (and possibly special methods, but
NOT __init__ and __repr__!). metaMetaBunch removes those variables from
class scope, snuggles them instead as items in a class-scope dict named
__dflts__, and puts in the class a __slots__ listing those variables'
names, an __init__ that takes as optional keyword arguments each of
them (using the values in __dflts__ as defaults for missing ones), and
a __repr__ that shows the repr of each attribute that differs from its
default value (the output of __repr__ can be passed to __eval__ to make
an equal instance, as per the usual convention in the matter).
"""
def __new__(cls, classname, bases, classdict):
""" Everything needs to be done in __new__, since type.__new__ is
where __slots__ are taken into account.
"""
# define as local functions the __init__ and __repr__ that we'll
# use in the new class
def __init__(self, **kw):
""" Simplistic __init__: first set all attributes to default
values, then override those explicitly passed in kw.
"""
for k in self.__dflts__: setattr(self, k, self.__dflts__[k])
for k in kw: setattr(self, k, kw[k])
def __repr__(self):
""" Clever __repr__: show only attributes that differ from the
respective default values, for compactness.
"""
rep = [ '%s=%r' % (k, getattr(self, k)) for k in self.__dflts__
if getattr(self, k) != self.__dflts__[k]
]
return '%s(%s)' % (classname, ', '.join(rep))
# build the newdict that we'll use as class-dict for the new class
newdict = { '__slots__':[], '__dflts__':{},
'__init__':__init__, '__repr__':__repr__, }
for k in classdict:
if k.startswith('__'):
# special methods &c: copy to newdict, warn about conflicts
if k in newdict:
warnings.warn("Can't set attr %r in bunch-class %r" % (
k, classname))
else:
newdict[k] = classdict[k]
else:
# class variables, store name in __slots__ and name and
# value as an item in __dflts__
newdict['__slots__'].append(k)
newdict['__dflts__'][k] = classdict[k]
# finally delegate the rest of the work to type.__new__
return type.__new__(cls, classname, bases, newdict)
class MetaBunch(metaclass=metaMetaBunch):
""" For convenience: inheriting from MetaBunch can be used to get
the new metaclass (same as defining __metaclass__ yourself).
"""
__metaclass__ = metaMetaBunch

View File

@@ -0,0 +1,38 @@
import pytest
from bunch import MetaBunch
class Point(MetaBunch):
""" A point has x and y coordinates, defaulting to 0.0,
and a color, defaulting to 'gray'—and nothing more,
except what Python and the metaclass conspire to add,
such as __init__ and __repr__
"""
x = 0.0
y = 0.0
color = 'gray'
def test_init_defaults():
p = Point()
assert repr(p) == 'Point()'
def test_init():
p = Point(x=1.2, y=3.4, color='red')
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
def test_init_wrong_argument():
with pytest.raises(AttributeError) as exc:
p = Point(x=1.2, y=3.4, flavor='coffee')
assert "no attribute 'flavor'" in str(exc.value)
def test_slots():
p = Point()
with pytest.raises(AttributeError) as exc:
p.z = 5.6
assert "no attribute 'z'" in str(exc.value)

View File

@@ -0,0 +1,41 @@
import collections
import warnings
class MetaBunch(type):
def __prepare__(name, *bases, **kwargs):
return collections.OrderedDict()
def __new__(meta_cls, cls_name, bases, cls_dict):
def __init__(self, **kw):
for k in self.__defaults__:
setattr(self, k, self.__defaults__[k])
for k in kw:
setattr(self, k, kw[k])
def __repr__(self):
rep = ['{}={!r}'.format(k, getattr(self, k))
for k in self.__defaults__
if getattr(self, k) != self.__defaults__[k]
]
return '{}({})'.format(cls_name, ', '.join(rep))
new_dict = { '__slots__':[],
'__defaults__':collections.OrderedDict(),
'__init__':__init__, '__repr__':__repr__, }
for k in cls_dict:
if k.startswith('__') and k.endswith('__'):
if k in new_dict:
warnings.warn(
"Can't set attr {!r} in bunch-class {!r}".
format(k, cls_name))
else:
new_dict[k] = cls_dict[k]
else:
new_dict['__slots__'].append(k)
new_dict['__defaults__'][k] = cls_dict[k]
return super().__new__(meta_cls, cls_name, bases, new_dict)
class Bunch(metaclass=MetaBunch):
pass

View File

@@ -0,0 +1,38 @@
import pytest
from bunch import Bunch
class Point(Bunch):
""" A point has x and y coordinates, defaulting to 0.0,
and a color, defaulting to 'gray'—and nothing more,
except what Python and the metaclass conspire to add,
such as __init__ and __repr__
"""
x = 0.0
y = 0.0
color = 'gray'
def test_init_defaults():
p = Point()
assert repr(p) == 'Point()'
def test_init():
p = Point(x=1.2, y=3.4, color='red')
assert repr(p) == "Point(x=1.2, y=3.4, color='red')"
def test_init_wrong_argument():
with pytest.raises(AttributeError) as exc:
p = Point(x=1.2, y=3.4, flavor='coffee')
assert "no attribute 'flavor'" in str(exc.value)
def test_slots():
p = Point()
with pytest.raises(AttributeError) as exc:
p.z = 5.6
assert "no attribute 'z'" in str(exc.value)