Skip to content

Commit

Permalink
Improve code documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcs committed Jun 17, 2017
1 parent 8622168 commit 55c503f
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Transformation function
.......................

The transformation function receive two parameters, a string ``representation`` with the raw value, and a
``wanted_type`` with the value of the annotation (usually, but not mandatorily a ``type``).
``wanted_type`` with the value of the annotation (usually, but not necessarily a ``type``).

Nested Configuration
--------------------
Expand Down
80 changes: 66 additions & 14 deletions ecological/autoconfig.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
from typing import (AnyStr, ByteString, Callable, Dict, FrozenSet, GenericMeta, List, Optional, Set, Tuple, Any,
TypeVar)
from typing import (AnyStr, ByteString, Callable, Dict, FrozenSet, GenericMeta, List, Optional, Set,
Tuple, Any, TypeVar, Union)

import os

import ast

import collections


_NOT_IMPORTED = object()


try: # Types added in Python 3.6.1
from typing import Counter, Deque
except ImportError:
Expand All @@ -30,16 +28,15 @@
Tuple: tuple
}


WantedType = TypeVar('WantedType')


def cast(representation: str, wanted_type: type):
"""
Casts the string ``representation`` to the ``wanted type``.
This function also supports some ``typing``types by mapping them to 'real' types.
This function also supports some ``typing`` types by mapping them to 'real' types.
Some types need to be parsed with ast.
Some types, like ``bool`` and ``list``, need to be parsed with ast.
"""
# If it's a typing meta replace it with the real type
wanted_type = TYPING_TO_REGULAR_TYPE.get(wanted_type, wanted_type)
Expand All @@ -59,6 +56,7 @@ class Variable:
"""
Class to handle specific properties
"""

def __init__(self, variable_name: str, default=_NO_DEFAULT, *,
transform: Callable[[str, type], Any] = cast):
"""
Expand All @@ -70,10 +68,14 @@ def __init__(self, variable_name: str, default=_NO_DEFAULT, *,
self.default = default
self.transform = transform

def get(self, wanted_type: WantedType) -> WantedType:
def get(self, wanted_type: WantedType) -> Union[WantedType, Any]:
"""
Gets ``self.variable_name`` from the environment and tries to cast it to ``wanted_type``.
If ``self.default`` is ``_NO_DEFAULT`` and the env variable is not set this will raise an ``AttributeError``.
If ``self.default`` is ``_NO_DEFAULT`` and the env variable is not set this will raise an
``AttributeError``, if the ``self.default`` is set to something else, its value will be
returned.
If casting fails, this function will raise a ``ValueError``.
:param wanted_type: type to return
Expand All @@ -96,9 +98,32 @@ def get(self, wanted_type: WantedType) -> WantedType:


class ConfigMeta(type):
def __new__(cls, class_name, super_classes, attribute_dict: Dict[str, Any],
"""
Metaclass that does the "magic" behind ``AutoConfig``.
"""

# noinspection PyInitNewSignature
def __new__(mcs, class_name, super_classes, attribute_dict: Dict[str, Any],
prefix: Optional[str] = None):
# TODO document this
"""
The new class' ``attribute_dict`` includes attributes with a default value and some special
keys like ``__annotations__`` which includes annotations of all attributes, including the
ones that don't have a value.
To simplify the class building process ``ConfigMeta`` injects the annotated attributes that
don't have a value in the ``attribute_dict`` with a ``_NO_DEFAULT`` sentinel object.
After this ``ConfigMeta`` goes through all the public attributes of the class, and does one
of three things:
- If the attribute value is an instance of ``ConfigMeta`` it is kept as is to allow nested
configuration.
- If the attribute value is of type ``Variable``, ``ConfigMeta`` will class its ``get``
method with the attribute's annotation type as the only parameter
- Otherwise, ``ConfigMeta`` will create a ``Variable`` instance, with
"{prefix}_{attribute_name}" as the environment variable name and the attribute value
(the default value or ``_NO_DEFAULT``) and do the same process as the previous point.
"""
annotations: Dict[str, type] = attribute_dict.get('__annotations__', {})

# Add attributes without defaults to the the attribute dict
Expand All @@ -123,13 +148,40 @@ def __new__(cls, class_name, super_classes, attribute_dict: Dict[str, Any],
env_variable_name = attribute_name.upper()
attribute = Variable(env_variable_name, default_value)

attribute_type = annotations.get(attribute_name, str) # by default attributes are strings
attribute_type = annotations.get(attribute_name,
str) # by default attributes are strings
value = attribute.get(attribute_type)
attribute_dict[attribute_name] = value

return type.__new__(cls, class_name, super_classes, attribute_dict)
# noinspection PyTypeChecker
return type.__new__(mcs, class_name, super_classes, attribute_dict)


class AutoConfig(metaclass=ConfigMeta):
# TODO document this
"""
When ``AutoConfig`` sub classes are created ``Ecological`` will automatically set it's
attributes based on the environment variables.
For example if ``DEBUG`` is set to ``"True"`` and ``PORT`` is set to ``"8080"`` and your
configuration class looks like::
class Configuration(ecological.AutoConfig):
port: int
debug: bool
``Configuration.port`` will be ``8080`` and ``Configuration.debug`` will be ``True``, with the
correct types.
Caveats and Known Limitations
=============================
- ``Ecological`` doesn't support (public) methods in ``AutoConfig`` classes.
Further Information
===================
Further information is available in the ``README.rst``.
"""
# TODO Document errors, typing support, prefix
pass
7 changes: 5 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

VERSION_MAJOR = 1
VERSION_MINOR = 2
VERSION = f'{VERSION_MAJOR}.{VERSION_MINOR}'
REVISION = 1
VERSION = f'{VERSION_MAJOR}.{VERSION_MINOR}.{REVISION}'

python_version_major, python_version_minor = (int(version) for version in platform.python_version_tuple()[:-1])
python_version_major, python_version_minor = (int(version)
for version
in platform.python_version_tuple()[:-1])

if (python_version_major, python_version_minor) < (3, 6):
print("Ecological doesn't support Python <= 3.6")
Expand Down

0 comments on commit 55c503f

Please sign in to comment.