Mastering Python Second Edition Release Code
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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>]
|
||||
|
||||
@@ -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()))
|
||||
@@ -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']
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import base
|
||||
|
||||
|
||||
class A(base.Plugin):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import base
|
||||
|
||||
|
||||
class B(base.Plugin):
|
||||
pass
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user