├── .gitignore ├── pyproject.toml ├── confucius.py └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | confucius.egg-info/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "confucius" 3 | version = "1.0.3" 4 | description = "An easy way to provide environ backed config in your projects." 5 | authors = ["Curtis Maloney "] 6 | license = "MIT" 7 | homepage = "https://github.com/funkybob/confucius" 8 | readme = "README.rst" 9 | classifiers = [ 10 | 'License :: OSI Approved :: MIT License', 11 | 'Programming Language :: Python', 12 | 'Programming Language :: Python :: 3', 13 | 'Programming Language :: Python :: 3.6', 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.6" 18 | 19 | [tool.poetry.dev-dependencies] 20 | 21 | [build-system] 22 | requires = ["poetry>=0.12"] 23 | build-backend = "poetry.masonry.api" 24 | -------------------------------------------------------------------------------- /confucius.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | from typing import get_type_hints 3 | 4 | 5 | class MetaConfig(type): 6 | __types__ = { 7 | bool: lambda v: str(v).lower() in {"yes", "y", "t", "true", "1", "on"}, 8 | } 9 | 10 | def __new__(cls, name, bases, namespace, **kwargs): 11 | namespace["__slots__"] = () 12 | types = {} 13 | attr_types = {} 14 | # Walk the parents and collate: 15 | # - all the __types__ dicts. 16 | # - all the attribute types 17 | for parent in reversed(bases): 18 | types.update(getattr(parent, "__types__", {})) 19 | attr_types.update({k: v for k, v in get_type_hints(parent).items() if k.isupper()}) 20 | types.update(namespace.get("__types__", {})) 21 | namespace["__types__"] = types 22 | 23 | new_cls = type.__new__(cls, name, bases, namespace, **kwargs) 24 | 25 | # Validate we don't re-type anything 26 | for k, v in get_type_hints(new_cls).items(): 27 | if not k.isupper() or k not in attr_types: 28 | continue 29 | assert v == attr_types[k], f"Type of locally declared {k} ({v}) does not match parent ({attr_types[k]})" 30 | 31 | return new_cls 32 | 33 | def __call__(cls): 34 | raise TypeError(f"Can not create instance of singleton config {cls.__name__}") 35 | 36 | def as_dict(cls): 37 | return {key: getattr(cls, key) for key in dir(cls) if key.isupper()} 38 | 39 | def __getattribute__(cls, key): 40 | if not key.isupper(): 41 | return object.__getattribute__(cls, key) 42 | 43 | raw = super().__getattribute__(key) 44 | 45 | if callable(raw): 46 | raw = raw(cls) 47 | 48 | value = getenv(key, raw) 49 | 50 | _type = get_type_hints(cls).get(key, None) 51 | _type = cls.__types__.get(_type, _type) 52 | 53 | if _type is not None: 54 | value = _type(value) 55 | 56 | return value 57 | 58 | def module_getattr_factory(cls): 59 | """ 60 | Factory function to build a module-level __getattr__ for tools (like 61 | Django) which need a whole-module settings. 62 | 63 | __getattr__ = config.module_getattr_factory() 64 | """ 65 | 66 | def __getattr__(name): 67 | return getattr(cls, name) 68 | 69 | return __getattr__ 70 | 71 | 72 | class BaseConfig(object, metaclass=MetaConfig): 73 | """Base Config class""" 74 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Confucius 2 | --------- 3 | 4 | .. rubric:: A simpler, clearer approach to configuration. 5 | 6 | 7 | Quick Start 8 | =========== 9 | 10 | .. code-block:: python 11 | :name: config.py 12 | 13 | from confucius import BaseConfig 14 | 15 | class Config(BaseConfig) 16 | HOST = '127.0.0.1' 17 | PORT : int = 8000 18 | 19 | DEBUG : bool = False 20 | 21 | .. code-block:: python 22 | :name: app.py 23 | 24 | from myapp import Server 25 | from config import Config 26 | 27 | 28 | server = Server(Config.HOST, Config.PORT) 29 | 30 | 31 | .. code-block:: sh 32 | 33 | $ python app.py 34 | - Starting server: 127.0.0.1:8000 35 | 36 | $ PORT=80 python app.py 37 | - Starting server: 127.0.0.1:80 38 | 39 | $ DEBUG=y python app.py 40 | - Starting debug server: 127.0.0.1:80 41 | 42 | 43 | Types 44 | ===== 45 | 46 | Any ``ANGRY_SNAKE_CASE`` attributes of a ``BaseConfig`` sub-class will be 47 | intercepted by the meta-class, and checked for in the environment using 48 | ``os.getenv``. 49 | 50 | Their type will be determined by their annotation in the class, or fall back to 51 | ``str``. 52 | 53 | Methods will automatically behave like a ``property``, with access to ``self``. 54 | 55 | Handling of type casting can be overridden [as it is for bool] by adding it to 56 | the ``__types__`` dict: 57 | 58 | .. code-block:: python 59 | 60 | class Config(BaseConfig): 61 | __types__ = { 62 | json: lambda v: json.loads(v) if isinstance(v, str) else v, 63 | } 64 | 65 | LOGGING : json = {'version': 1 ...} 66 | 67 | All types defined on parent ``Config`` classes will be merged with this dict. 68 | 69 | Inheritance 70 | =========== 71 | 72 | Classes, as usual, inherit from their parents. If the type of an attribute is 73 | changed, it will raise an ``AssertionError``. 74 | 75 | Methods 76 | ======= 77 | 78 | Method in all-caps will be invoked, and can access ``self`` as usual: 79 | 80 | .. code-block:: python 81 | 82 | class Config(BaseConfig): 83 | DB_ENGINE = 'postgresql' 84 | DB_HOST = 'localhost' 85 | DB_PORT : int = 5432 86 | DB_USER = 'test_user' 87 | DB_PASS = 'secret' 88 | DB_NAME = 'test-db' 89 | 90 | def CONNECTION_STRING(self): 91 | return f'{self.DB_ENGINE}://{self.DB_USER}:{self.DB_PASS}@{self.DB_HOST}/{self.DB_NAME}' 92 | 93 | 94 | Using in Django 95 | --------------- 96 | 97 | In your ``settings.py``, put your settings class (or classes), then use the 98 | following code to select one to use: 99 | 100 | .. code-block:: python 101 | 102 | import os 103 | MODE = os.getenv('DJANGO_MODE', 'Local') 104 | globals().update(globals()[f'{ MODE.title() }Settings'].as_dict()) 105 | 106 | 107 | With Python 3.7 108 | =============== 109 | 110 | In Python 3.7, a new feature was added which allowed you to define 111 | ``__getattr__`` for a module (See `PEP 562 112 | `_). 113 | 114 | The ``BaseConfig`` metaclass provides a ``module_getattr_factory`` factory 115 | method to provide a ``__getattr__`` that will look up the ``Config`` object. 116 | 117 | 118 | .. code-block:: python 119 | 120 | from confucius import BaseConfig 121 | 122 | class Config(BaseConfig): 123 | DB_HOST = 'localhost' 124 | DB_PORT : int = 5432 125 | 126 | __getattr__ = Config.module_getattr_factory() 127 | 128 | 129 | After importing this module, attempts to access attributes will resolve 130 | normally and, if they're not found, call ``__getattr__``, just like on an 131 | object. 132 | --------------------------------------------------------------------------------