Skip to content

Instantly share code, notes, and snippets.

@krzentner
Last active October 27, 2018 17:11
Show Gist options
  • Save krzentner/1f8da2a270cac415cc96098dcc4d0986 to your computer and use it in GitHub Desktop.
Save krzentner/1f8da2a270cac415cc96098dcc4d0986 to your computer and use it in GitHub Desktop.
Decorator to avoid writing out field names twice.
#!/usr/bin/env python3
import copy
import types
def configure_with(config_type):
'''
Attach a configuration type to the provided type.
This will allow calling instances of the configuration type to construct
instances of the provided type, by copying fields from the configuration
object.
This function doesn't impose any constraints on the two types, but generally
the configuration type should be picklable.
'''
def configure_with_decorator(configured_type):
# Check that we haven't already attached this configuration.
if hasattr(config_type, '_configured_type'):
raise TypeError(f'{config_type.__name__} cannot configure '
f'{configured_type.__name__}, since it is already a configuration '
f'for ⁠⁠{config_type._configured_type.__name__}.')
# Check that the configuration type doesn't define __call__.
# Looking up __call__ will first attempt to get the class's field. If
# the class defines __call__, this will return a function type, since
# we're looking up through the class (and not an instance).
# If the class doesn't define __call__, looking up __call__ will look up
# the __call__ method defined on the `type` type itself.
class_has_call = isinstance(config_type.__call__, types.FunctionType)
if class_has_call and config_type.__call__ is not Config.__call__:
raise TypeError(f'{config_type.__name__} cannot be a configuration,'
' since it already defines __call__.')
config_type._configured_type = configured_type
def instantiate(config):
instance = configured_type.__new__(configured_type)
instance.__dict__ = dict((k, copy.deepcopy(v))
for k, v in config.__dict__.items())
instance.__init__()
return instance
# Finish attaching to the configuration type.
config_type.__call__ = instantiate
return configured_type
return configure_with_decorator
class Config:
def __call__(self):
# This should only happen if configure_with wasn't used to attach the
# specific Config type to a class.
raise TypeError(f"{type(self).__name__} doesn't configure any type.")
# Simple simulation of deploying a config.
def test_deploy(config):
import pickle
print()
print('config', config)
s = pickle.dumps(config)
# print('pickled config', s)
loaded_config = pickle.loads(s)
# print('loaded config', s)
instance = loaded_config()
print('instance', instance)
print()
## Example without using config:
class MyEnv:
def __init__(self, name):
self._name = name
# We can't pickle a lambda, so we use `functools.partial` for this example.
import functools # pylint: disable=C0413
test_deploy(functools.partial(MyEnv, 'MyEnv'))
## Example with recommended practices.
class ExampleEnvConfig(Config):
def __init__(self, width, height=None):
if height is None:
height = width
self.width = width
self.height = height
# If the decorator is forgotten, the following error occurs:
# TypeError: ExampleEnvConfig doesn't configure any type
@configure_with(ExampleEnvConfig)
class ExampleEnv(ExampleEnvConfig):
# To get better tab completion / pylint output, inherit from
# ExampleEnvConfig, even though we don't need to.
def __init__(self):
# If we inherit, we still don't need to call the config __init__(), so
# tell pylint.
# pylint: disable=W0231
print(f"creating ExampleEnv with {self.width}, {self.height}")
self._world = {}
test_deploy(ExampleEnvConfig(width=8))
## Example with minimal practices.
class MinimalConfig:
def __init__(self, width, height=None):
if height is None:
height = width
self.width = width
self.height = height
# If the decorator is forgotten, the following error occurs:
# TypeError: 'ExampleEnvConfig' object is not callable
@configure_with(MinimalConfig)
class MinimalEnv:
# If we don't inherit, nothing breaks, but pylint and friends can no longer
# determine our fields.
# We probably want the following:
# pylint: disable=no-member
def __init__(self):
print(f"creating MinimalEnv with {self.width}, {self.height}")
self._world = {}
test_deploy(MinimalConfig(width=8))
## Example with showing results of attaching configuration to two types.
class RepeatedConfig:
def __init__(self, width, height=None):
if height is None:
height = width
self.width = width
self.height = height
@configure_with(RepeatedConfig)
class FirstEnv:
# pylint: disable=no-member
def __init__(self):
print(f"creating FirstEnv with {self.width}, {self.height}")
self._world = {}
try:
@configure_with(RepeatedConfig)
class SecondEnv(RepeatedConfig):
pass
except TypeError as e:
print()
print('Error message resulting from repeated configure_with call:')
# RepeatedConfig cannot configure SecondEnv, since it is already a
# configuration for ⁠⁠FirstEnv.
print(e)
print()
## Example with a config that already defined __call__.
class ErroneousConfig:
def __init__(self, width, height=None):
if height is None:
height = width
self.width = width
self.height = height
def __call__(self):
print('oh no!')
try:
@configure_with(ErroneousConfig)
class ErroneousEnv:
pass
except TypeError as e:
print()
print('Error message resulting from config defining __call__:')
# ErroneousConfig cannot be a configuration, since it already defines
# __call__.
print(e)
print()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment