Skip to content

Commit

Permalink
Merge pull request #27 from jmcs/issue-15
Browse files Browse the repository at this point in the history
Support source on Variable, allow more global, class level options
  • Loading branch information
marcinzaremba authored Jun 18, 2019
2 parents 2a3e6de + 0f11055 commit 2f4eca1
Show file tree
Hide file tree
Showing 7 changed files with 502 additions and 288 deletions.
188 changes: 116 additions & 72 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.. image:: https://travis-ci.org/jmcs/ecological.svg?branch=master
:target: https://travis-ci.org/jmcs/ecological
:target: https://travis-ci.org/jmcs/ecological

.. image:: https://api.codacy.com/project/badge/Grade/1ff45d0e1a5a40b8ad0569e3edb0539d
:alt: Codacy Badge
:target: https://www.codacy.com/app/jmcs/ecological?utm_source=github.com&utm_medium=referral&utm_content=jmcs/ecological&utm_campaign=badger
.. image:: https://api.codacy.com/project/badge/Coverage/1ff45d0e1a5a40b8ad0569e3edb0539d

.. image:: https://api.codacy.com/project/badge/Coverage/1ff45d0e1a5a40b8ad0569e3edb0539d
:target: https://www.codacy.com/app/jmcs/ecological?utm_source=github.com&utm_medium=referral&utm_content=jmcs/ecological&utm_campaign=Badge_Coverage

==========
Expand Down Expand Up @@ -33,53 +33,11 @@ And then set the environment variables ``PORT``, ``DEBUG`` and ``LOG_LEVEL``. ``
class properties from the environment variables with the same (but upper cased) name.

By default the values are set at the class definition type and assigned to the class itself (i.e. the class doesn't need to be
instantiated). If needed this behavior can be changed (see the next section).

Autoloading
=============
It is possible to defer/disable autoloading (setting) of variable values by specifying the ``autoload`` option on class definition.

On class creation (default)
---------------------------
When no option is provided values are loaded immediately on class creation and assigned to class attributes:

.. code-block:: python
class Configuration(ecological.Config):
port: int
# Values already read and set at this point.
# assert Configuration.port == <value-of-PORT-env-var>
Never
------
When this option is chosen, no autoloading happens. In order to set variable values, the ``Config.load`` method needs to be called explicitly:

.. code-block:: python
class Configuration(ecological.Config, autoload=ecological.Autoload.NEVER):
port: int
# Values not set at this point.
# Accessing Configuration.port would throw AttributeError.
Configuration.load()
# Values read and set at this point.
# assert Configuration.port == <value-of-PORT-env-var>
On object instance initialization
----------------------------------
If it is preferred to load and store attribute values on the object instance instead of the class itself, the ``Autoload.OBJECT`` strategy can be used:

.. code-block:: python
class Configuration(ecological.Config, autoload=ecological.Autoload.OBJECT):
port: int
# Values not set at this point.
config = Configuration()
# Values read and set at this point on ``config``.
# assert config.port == <value-of-PORT-env-var>
# Accessing ``Configuration.port`` would throw AttributeError.
instantiated). If needed this behavior can be changed (see the Autoloading section).

Tutorial
--------
The `tutorial <tutorial.ipynb>`_ can be used to get to know with the library's basic features interactively.

Typing Support
==============
Expand Down Expand Up @@ -108,27 +66,8 @@ You can also decide to prefix your application configuration, for example, to av
In this case the ``home`` property will be fetched from the ``MYAPP_HOME`` environment property.

Fine-grained control
--------------------
You can control how the configuration properties are set by providing a ``ecological.Variable`` instance as the default
value.

``ecological.Variable`` receives the following parameters:

- ``variable_name`` (mandatory) - exact name of the environment variable that will be used.
- ``default`` (optional) - default value for the property if it isn't set.
- ``transform`` (optional) - function that converts the string in the environment to the value and type you
expect in your application. The default ``transform`` function will try to cast the string to the annotation
type of the property.

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 necessarily a ``type``).

Nested Configuration
--------------------
=====================
``Ecological.Config`` also supports nested configurations, for example:

.. code-block:: python
Expand All @@ -142,10 +81,115 @@ Nested Configuration
This way you can group related configuration properties hierarchically.

Tutorial
Advanced
========
The `tutorial <tutorial.ipynb>`_ includes real examples of all the available
features.

Fine-grained Control
---------------------
You can control some behavior of how the configuration properties are set.

It can be achieved by providing a ``ecological.Variable`` instance as the default
value for an attribute or by specifying global options on the class level:

.. code-block:: python
my_source = {"KEY1": "VALUE1"}
class Configuration(ecological.Config, transform=lambda v, wt: v, wanted_type=int, ...):
my_var1: WantedType = ecological.Variable(transform=lambda v, wt: wt(v), source=my_source, ...)
my_var2: str
# ...
All possible options and their meaning can be found in the table below:

+-------------------+---------------+-----------------+-------------------------------------------------+-------------------------------------------------------------------+
| Option | Class level | Variable level | Default | Description |
+===================+===============+=================+=================================================+===================================================================+
| ``prefix`` | yes | no | ``None`` | A prefix that is uppercased and prepended when a variable name |
| | | | | is derived from an attribute name. |
+-------------------+---------------+-----------------+-------------------------------------------------+-------------------------------------------------------------------+
| ``variable_name`` | yes | yes | Derived from attribute name and prefixed | When specified on the variable level it states |
| | | | with ``prefix`` if specified; uppercased. | the exact name of the source variable that will be used. |
| | | | | |
| | | | | When specified on the class level it is treated as a function |
| | | | | that returns a variable name from the attribute name with |
| | | | | the following signature: |
| | | | | |
| | | | | ``def func(attribute_name: str, prefix: Optional[str] = None)`` |
+-------------------+---------------+-----------------+-------------------------------------------------+-------------------------------------------------------------------+
| ``default`` | no | yes | (no default) | Default value for the property if it isn't set. |
+-------------------+---------------+-----------------+-------------------------------------------------+-------------------------------------------------------------------+
| ``transform`` | yes | yes | A source value is casted to the ``wanted_type`` | A function that converts a value from the ``source`` to the value |
| | | | In case of non-scalar types (+ scalar ``bool``) | and ``wanted_type`` you expect with the following signature: |
| | | | the value is Python-parsed first. | |
| | | | | ``def func(source_value: str, wanted_type: Union[Type, str])`` |
+-------------------+---------------+-----------------+-------------------------------------------------+-------------------------------------------------------------------+
| ``source`` | yes | yes | ``os.environ`` | Dictionary that the value will be loaded from. |
+-------------------+---------------+-----------------+-------------------------------------------------+-------------------------------------------------------------------+
| ``wanted_type`` | yes | yes | ``str`` | Desired Python type of the attribute's value. |
| | | | | |
| | | | | On the variable level it is specified via a type annotation on |
| | | | | the attribute: ``my_var_1: my_wanted_type``. |
| | | | | |
| | | | | However it can be also specified on the class level, then it acts |
| | | | | as a default when the annotation is not provided: |
| | | | | |
| | | | | ``class MyConfig(ecological.Config, wanted_type=int, ...)`` |
+-------------------+---------------+-----------------+-------------------------------------------------+-------------------------------------------------------------------+

The following rules apply when options are resolved:

- when options are specified on both levels (variable and class),
the variable ones take precedence over class ones,
- when some options are missing on the variable level, their default values
are taken from the class level,
- it is not necessary to assign an ``ecological.Variable`` instance to
change the behavior; it can still be changed on the class level (globally).

Autoloading
------------
It is possible to defer/disable autoloading (setting) of variable values by specifying the ``autoload`` option on class definition.

On class creation (default)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
When no option is provided values are loaded immediately on class creation and assigned to class attributes:

.. code-block:: python
class Configuration(ecological.Config):
port: int
# Values already read and set at this point.
# assert Configuration.port == <value-of-PORT-env-var>
Never
~~~~~
When this option is chosen, no autoloading happens. In order to set variable values, the ``Config.load`` method needs to be called explicitly:

.. code-block:: python
class Configuration(ecological.Config, autoload=ecological.Autoload.NEVER):
port: int
# Values not set at this point.
# Accessing Configuration.port would throw AttributeError.
Configuration.load()
# Values read and set at this point.
# assert Configuration.port == <value-of-PORT-env-var>
On object instance initialization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If it is preferred to load and store attribute values on the object instance instead of the class itself, the ``Autoload.OBJECT`` strategy can be used:

.. code-block:: python
class Configuration(ecological.Config, autoload=ecological.Autoload.OBJECT):
port: int
# Values not set at this point.
config = Configuration()
# Values read and set at this point on ``config``.
# assert config.port == <value-of-PORT-env-var>
# Accessing ``Configuration.port`` would throw AttributeError.
Caveats and Known Limitations
=============================
Expand Down
Loading

0 comments on commit 2f4eca1

Please sign in to comment.