Mastering Python Second Edition Release Code

This commit is contained in:
Rick van Hattem
2022-05-05 18:25:55 +02:00
commit 3223a43fe3
454 changed files with 20230 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
Chapter 8, Metaclasses
##############################################################################
| Making Classes (not instances) Smarter goes deeper into the creation of classes and how class behavior can be completely modified.
@@ -0,0 +1,27 @@
>>> class Spam(object):
... eggs = 'my eggs'
>>> Spam = type('Spam', (object,), dict(eggs='my eggs'))
------------------------------------------------------------------------------
>>> class Spam(object):
... eggs = 'my eggs'
>>> spam = Spam()
>>> spam.eggs
'my eggs'
>>> type(spam)
<class '...Spam'>
>>> type(Spam)
<class 'type'>
>>> Spam = type('Spam', (object,), dict(eggs='my eggs'))
>>> spam = Spam()
>>> spam.eggs
'my eggs'
>>> type(spam)
<class '...Spam'>
>>> type(Spam)
<class 'type'>
@@ -0,0 +1,40 @@
# The metaclass definition, note the inheritance of type instead
of object
>>> class MetaSandwich(type):
...
... # Notice how the __new__ method has the same arguments
... # as the type function we used earlier?
... def __new__(metaclass, name, bases, namespace):
... name = 'SandwichCreatedByMeta'
... bases = (int,) + bases
... namespace['lettuce'] = 1
... return type.__new__(metaclass, name, bases, namespace)
# First, the regular Sandwich:
>>> class Sandwich(object):
... pass
>>> Sandwich.__name__
'Sandwich'
>>> issubclass(Sandwich, int)
False
>>> Sandwich.lettuce
Traceback (most recent call last):
...
AttributeError: type object 'Sandwich' has no attribute 'lettuce'
# Now the meta-Sandwich
>>> class Sandwich(object, metaclass=MetaSandwich):
... pass
>>> Sandwich.__name__
'SandwichCreatedByMeta'
>>> issubclass(Sandwich, int)
True
>>> Sandwich.lettuce
1
@@ -0,0 +1,43 @@
>>> class AddClassAttributeMeta(type):
... def __init__(metaclass, name, bases, namespace, **kwargs):
... # The kwargs should not be passed on to the
... # type.__init__
... type.__init__(metaclass, name, bases, namespace)
...
... def __new__(metaclass, name, bases, namespace, **kwargs):
... for k, v in kwargs.items():
... # setdefault so we don't overwrite attributes
... namespace.setdefault(k, v)
...
... return type.__new__(metaclass, name, bases, namespace)
>>> class WithArgument(metaclass=AddClassAttributeMeta, a=1234):
... pass
>>> WithArgument.a
1234
>>> with_argument = WithArgument()
>>> with_argument.a
1234
------------------------------------------------------------------
>>> class AddClassAttribute:
... def __init_subclass__(cls, **kwargs):
... super().__init_subclass__()
...
... for k, v in kwargs.items():
... setattr(cls, k, v)
>>> class WithAttribute(metaclass=AddClassAttributeMeta, a=1234):
... pass
>>> WithAttribute.a
1234
>>> with_attribute = WithAttribute()
>>> with_attribute.a
1234
@@ -0,0 +1,33 @@
>>> class Meta(type):
...
... @property
... def some_property(cls):
... return 'property of %r' % cls
...
... def some_method(self):
... return 'method of %r' % self
>>> class SomeClass(metaclass=Meta):
... pass
# Accessing through the class definition
>>> SomeClass.some_property
"property of <class '...SomeClass'>"
>>> SomeClass.some_method
<bound method Meta.some_method of <class '__main__.SomeClass'>>
>>> SomeClass.some_method()
"method of <class '__main__.SomeClass'>"
# Accessing through an instance
>>> some_class = SomeClass()
>>> some_class.some_property
Traceback (most recent call last):
...
AttributeError: 'SomeClass' object has no attribute 'some_property'
>>> some_class.some_method
Traceback (most recent call last):
...
AttributeError: 'SomeClass' object has no attribute 'some_method'
@@ -0,0 +1,133 @@
>>> import abc
>>> class AbstractClass(metaclass=abc.ABCMeta):
...
... @abc.abstractmethod
... def some_method(self):
... raise NotImplemented()
>>> class ConcreteClass(AbstractClass):
... pass
>>> ConcreteClass()
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class ConcreteClass ...
>>> class ImplementedConcreteClass(ConcreteClass):
... def some_method():
... pass
>>> instance = ImplementedConcreteClass()
------------------------------------------------------------------------------
>>> import abc
>>> class AbstractClass(object, metaclass=abc.ABCMeta):
... @property
... @abc.abstractmethod
... def some_property(self):
... raise NotImplemented()
...
... @classmethod
... @abc.abstractmethod
... def some_classmethod(cls):
... raise NotImplemented()
...
... @staticmethod
... @abc.abstractmethod
... def some_staticmethod():
... raise NotImplemented()
...
... @abc.abstractmethod
... def some_method():
... raise NotImplemented()
------------------------------------------------------------------------------
>>> class AbstractMeta(type):
... def __new__(metaclass, name, bases, namespace):
... cls = super().__new__(metaclass, name, bases,
... namespace)
... cls.__abstractmethods__ = frozenset(('something',))
... return cls
>>> class ConcreteClass(metaclass=AbstractMeta):
... pass
>>> ConcreteClass()
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class ConcreteClass ...
------------------------------------------------------------------------------
>>> import functools
>>> class AbstractMeta(type):
... def __new__(metaclass, name, bases, namespace):
... # Create the class instance
... cls = super().__new__(metaclass, name, bases,
... namespace)
...
... # Collect all local methods marked as abstract
... abstracts = set()
... for k, v in namespace.items():
... if getattr(v, '__abstract__', False):
... abstracts.add(k)
...
... # Look for abstract methods in the base classes and
... # add them to the list of abstracts
... for base in bases:
... for k in getattr(base, '__abstracts__', ()):
... v = getattr(cls, k, None)
... if getattr(v, '__abstract__', False):
... abstracts.add(k)
...
... # store the abstracts in a frozenset so they cannot be
... # modified
... cls.__abstracts__ = frozenset(abstracts)
...
... # Decorate the __new__ function to check if all
... # abstract functions were implemented
... original_new = cls.__new__
... @functools.wraps(original_new)
... def new(self, *args, **kwargs):
... for k in self.__abstracts__:
... v = getattr(self, k)
... if getattr(v, '__abstract__', False):
... raise RuntimeError(
... '%r is not implemented' % k)
...
... return original_new(self, *args, **kwargs)
...
... cls.__new__ = new
... return cls
# Create a decorator that sets the `__abstract__` attribute
>>> def abstractmethod(function):
... function.__abstract__ = True
... return function
>>> class ConcreteClass(metaclass=AbstractMeta):
... @abstractmethod
... def some_method(self):
... pass
# Instantiating the function, we can see that it functions as the
regular ABCMeta does
>>> ConcreteClass()
Traceback (most recent call last):
...
RuntimeError: 'some_method' is not implemented
@@ -0,0 +1,59 @@
>>> import abc
>>> class CustomList(abc.ABC):
... '''This class implements a list-like interface'''
>>> class CustomInheritingList(list, abc.ABC):
... '''This class implements a list-like interface'''
>>> issubclass(list, CustomList)
False
>>> issubclass(list, CustomInheritingList)
False
>>> CustomList.register(list)
<class 'list'>
# We can't make it go both ways however
>>> CustomInheritingList.register(list)
Traceback (most recent call last):
...
RuntimeError: Refusing to create an inheritance cycle
>>> issubclass(list, CustomList)
True
>>> issubclass(list, CustomInheritingList)
False
# We need to inherit list to make it work the other way around
>>> issubclass(CustomList, list)
False
>>> isinstance(CustomList(), list)
False
>>> issubclass(CustomInheritingList, list)
True
>>> isinstance(CustomInheritingList(), list)
True
------------------------------------------------------------------------------
>>> import abc
>>> class UniversalClass(abc.ABC):
... @classmethod
... def __subclasshook__(cls, subclass):
... return True
>>> issubclass(list, UniversalClass)
True
>>> issubclass(bool, UniversalClass)
True
>>> isinstance(True, UniversalClass)
True
>>> issubclass(UniversalClass, bool)
False
@@ -0,0 +1,39 @@
>>> import abc
>>> class Plugins(abc.ABCMeta):
... plugins = dict()
...
... def __new__(metaclass, name, bases, namespace):
... cls = abc.ABCMeta.__new__(metaclass, name, bases,
... namespace)
... if isinstance(cls.name, str):
... metaclass.plugins[cls.name] = cls
... return cls
...
... @classmethod
... def get(cls, name):
... return cls.plugins[name]
>>> class PluginBase(metaclass=Plugins):
... @property
... @abc.abstractmethod
... def name(self):
... raise NotImplemented()
>>> class PluginA(PluginBase):
... name = 'a'
>>> class PluginB(PluginBase):
... name = 'b'
>>> Plugins.get('a')
<class '...PluginA'>
>>> Plugins.plugins
{'a': <class '...PluginA'>,
'b': <class '...PluginB'>}
@@ -0,0 +1,3 @@
Loading plugins from plugins.a
<class 'plugins.a.A'>
<class 'plugins.a.A'>
@@ -0,0 +1,4 @@
import plugins
print(plugins.PluginsOnDemand.get('a'))
print(plugins.PluginsOnDemand.get('a'))
@@ -0,0 +1,5 @@
Loading plugins from plugins.a
Loading plugins from plugins.b
After load
<class 'plugins.a.A'>
<class 'plugins.a.A'>
@@ -0,0 +1,10 @@
import plugins
plugins.PluginsThroughConfiguration.load(
'a',
'b',
)
print('After load')
print(plugins.PluginsThroughConfiguration.get('a'))
print(plugins.PluginsThroughConfiguration.get('a'))
@@ -0,0 +1,6 @@
Loading plugins from plugins.a
Loading plugins from plugins.b
After load
{'a': <class 'plugins.a.A'>,
'b': <class 'plugins.b.B'>,
'plugin': <class 'plugins.base.Plugin'>}
@@ -0,0 +1,7 @@
import pprint
import plugins
plugins.PluginsThroughFilesystem.autoload()
print('After load')
pprint.pprint(plugins.PluginsThroughFilesystem.plugins)
+71
View File
@@ -0,0 +1,71 @@
import inspect
class Dataclass(type):
def _get_signature(namespace):
# Get the annotations from the class
annotations = namespace.get('__annotations__', dict())
# Signatures are immutable so we need to build the
# parameter list before creating the signature
parameters = []
for name, annotation in annotations.items():
# Create Parameter shortcut for readability
Parameter = inspect.Parameter
# Create the parameter with the correct type
# annotation and default. You could also choose to
# make the arguments keyword/positional only here
parameters.append(Parameter(
name=name,
kind=Parameter.POSITIONAL_OR_KEYWORD,
default=namespace.get(name, Parameter.empty),
annotation=annotation,
))
return inspect.Signature(parameters)
def _create_init(namespace, signature):
# If init exists we don't need to do anything
if '__init__' in namespace:
return
# Create the __init__ method and use the signature to
# process the arguments
def __init__(self, *args, **kwargs):
bound = signature.bind(*args, **kwargs)
bound.apply_defaults()
for key, value in bound.arguments.items():
# Convert to the annotation to enforce types
parameter = signature.parameters[key]
# Set the casted value
setattr(self, key, parameter.annotation(value))
# Override the signature for __init__ so help() works
__init__.__signature__ = signature
namespace['__init__'] = __init__
def _create_repr(namespace, signature):
def __repr__(self):
arguments = []
for key, value in vars(self).items():
arguments.append(f'{key}={value!r}')
arguments = ', '.join(arguments)
return f'{self.__class__.__name__}({arguments})'
namespace['__repr__'] = __repr__
def __new__(metaclass, name, bases, namespace):
signature = metaclass._get_signature(namespace)
metaclass._create_init(namespace, signature)
metaclass._create_repr(namespace, signature)
cls = super().__new__(metaclass, name, bases, namespace)
return cls
+28
View File
@@ -0,0 +1,28 @@
>>> from T_10_dataclasses import Dataclass
>>> class Sandwich(metaclass=Dataclass):
... spam: int
... eggs: int = 3
>>> Sandwich(1, 2)
Sandwich(spam=1, eggs=2)
>>> sandwich = Sandwich(4)
>>> sandwich
Sandwich(spam=4, eggs=3)
>>> sandwich.eggs
3
>>> help(Sandwich.__init__)
Help on function __init__ in ...
<BLANKLINE>
__init__(spam: int, eggs: int = 3)
<BLANKLINE>
>>> Sandwich('a')
Traceback (most recent call last):
...
ValueError: invalid literal for int() with base 10: 'a'
>>> Sandwich('1234', 56.78)
Sandwich(spam=1234, eggs=56)
@@ -0,0 +1,71 @@
>>> import functools
>>> def decorator(name):
... def _decorator(cls):
... @functools.wraps(cls)
... def __decorator(*args, **kwargs):
... print('decorator(%s)' % name)
... return cls(*args, **kwargs)
... return __decorator
... return _decorator
>>> class SpamMeta(type):
...
... @decorator('SpamMeta.__init__')
... def __init__(self, name, bases, namespace, **kwargs):
... print('SpamMeta.__init__()')
... return type.__init__(self, name, bases, namespace)
...
... @staticmethod
... @decorator('SpamMeta.__new__')
... def __new__(cls, name, bases, namespace, **kwargs):
... print('SpamMeta.__new__()')
... return type.__new__(cls, name, bases, namespace)
...
... @classmethod
... @decorator('SpamMeta.__prepare__')
... def __prepare__(cls, names, bases, **kwargs):
... print('SpamMeta.__prepare__()')
... namespace = dict(spam=5)
... return namespace
>>> @decorator('Spam')
... class Spam(metaclass=SpamMeta):
...
... @decorator('Spam.__init__')
... def __init__(self, eggs=10):
... print('Spam.__init__()')
... self.eggs = eggs
decorator(SpamMeta.__prepare__)
SpamMeta.__prepare__()
decorator(SpamMeta.__new__)
SpamMeta.__new__()
decorator(SpamMeta.__init__)
SpamMeta.__init__()
# Testing with the class object
>>> spam = Spam
>>> spam.spam
5
>>> spam.eggs
Traceback (most recent call last):
...
File "<doctest T_11_order_of_operations.rst[6]>", line 1, in ...
AttributeError: 'function' object has no attribute 'eggs'
# Testing with a class instance
>>> spam = Spam()
decorator(Spam)
decorator(Spam.__init__)
Spam.__init__()
>>> spam.spam
5
>>> spam.eggs
10
@@ -0,0 +1,86 @@
>>> import itertools
>>> class Field(object):
... counter = itertools.count()
...
... def __init__(self, name=None):
... self.name = name
... self.index = next(Field.counter)
...
... def __repr__(self):
... return '<%s[%d] %s>' % (
... self.__class__.__name__,
... self.index,
... self.name,
... )
>>> class FieldsMeta(type):
... def __new__(metaclass, name, bases, namespace):
... cls = type.__new__(metaclass, name, bases, namespace)
... fields = []
... for k, v in namespace.items():
... if isinstance(v, Field):
... fields.append(v)
... v.name = v.name or k
...
... cls.fields = sorted(fields, key=lambda f: f.index)
... return cls
>>> class Fields(metaclass=FieldsMeta):
... spam = Field()
... eggs = Field()
>>> Fields.fields
[<Field[0] spam>, <Field[1] eggs>]
>>> fields = Fields()
>>> fields.eggs.index
1
>>> fields.spam.index
0
>>> fields.fields
[<Field[0] spam>, <Field[1] eggs>]
------------------------------------------------------------------------------
>>> import collections
>>> class Field(object):
... def __init__(self, name=None):
... self.name = name
...
... def __repr__(self):
... return '<%s %s>' % (
... self.__class__.__name__,
... self.name,
... )
>>> class FieldsMeta(type):
... @classmethod
... def __prepare__(metaclass, name, bases):
... return collections.OrderedDict()
...
... def __new__(metaclass, name, bases, namespace):
... cls = type.__new__(metaclass, name, bases, namespace)
... cls.fields = []
... for k, v in namespace.items():
... if isinstance(v, Field):
... cls.fields.append(v)
... v.name = v.name or k
...
... return cls
>>> class Fields(metaclass=FieldsMeta):
... spam = Field()
... eggs = Field()
>>> Fields.fields
[<Field spam>, <Field eggs>]
>>> fields = Fields()
>>> fields.fields
[<Field spam>, <Field eggs>]
View File
+7
View File
@@ -0,0 +1,7 @@
import sys
import pathlib
# Little hack to add the current directory to sys.path so we can
# find the imports
path = pathlib.Path(__file__).parent
sys.path.append(str(path.resolve()))
+10
View File
@@ -0,0 +1,10 @@
from .base import Plugin
from .base import Plugins
from .base import PluginsOnDemand
from .base import PluginsThroughConfiguration
from .base import PluginsThroughFilesystem
__all__ = [
'Plugin', 'Plugins', 'PluginsOnDemand',
'PluginsThroughConfiguration', 'PluginsThroughFilesystem']
+6
View File
@@ -0,0 +1,6 @@
from . import base
class A(base.Plugin):
pass
+6
View File
@@ -0,0 +1,6 @@
from . import base
class B(base.Plugin):
pass
+64
View File
@@ -0,0 +1,64 @@
import re
import abc
import pathlib
import importlib
CURRENT_FILE = pathlib.Path(__file__)
PLUGINS_DIR = CURRENT_FILE.parent
MODULE_NAME_RE = re.compile('[a-z][a-z0-9_]*', re.IGNORECASE)
class Plugins(abc.ABCMeta):
plugins = dict()
def __new__(metaclass, name, bases, namespace):
cls = abc.ABCMeta.__new__(
metaclass, name, bases, namespace)
metaclass.plugins[name.lower()] = cls
return cls
@classmethod
def get(cls, name):
return cls.plugins[name]
class Plugin(metaclass=Plugins):
pass
class PluginsOnDemand(Plugins):
@classmethod
def get(cls, name):
if name not in cls.plugins:
print('Loading plugins from plugins.%s' % name)
importlib.import_module('plugins.%s' % name)
return cls.plugins[name]
class PluginsThroughConfiguration(PluginsOnDemand):
@classmethod
def load(cls, *plugin_names):
for plugin_name in plugin_names:
cls.get(plugin_name)
class PluginsThroughFilesystem(PluginsThroughConfiguration):
@classmethod
def autoload(cls):
for filename in PLUGINS_DIR.glob('*.py'):
# Skip __init__.py and other non-plugin files
if not MODULE_NAME_RE.match(filename.stem):
continue
cls.get(filename.stem)
# Skip this file
if filename == CURRENT_FILE:
continue
# Load the plugin
cls.get(filename.stem)