Last active
October 27, 2018 17:11
-
-
Save krzentner/1f8da2a270cac415cc96098dcc4d0986 to your computer and use it in GitHub Desktop.
Decorator to avoid writing out field names twice.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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