Skip to content

Commit

Permalink
Added singleton module and save_config method to config module.
Browse files Browse the repository at this point in the history
  • Loading branch information
douglasdaly committed Jul 12, 2019
1 parent 52a117f commit 9e0d9d6
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 30 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ package, each component is self-contained in its respective file/folder
allowing for easy vendorization. Components are not dependent on one
another and rely solely on the standard library. This makes
vendorization of a component as simple as copying just the file/folder
for the component(s) that you need.
for the component(s) that you need (same goes for the unit tests).


## About
Expand All @@ -41,6 +41,7 @@ convenient and easy-to-use manner.
### Features

- ``config``: for global configuration/settings management.
- ``singleton``: for singleton classes.


## License
Expand Down
1 change: 1 addition & 0 deletions docs/api/frequent.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ frequent
.. toctree::

frequent.config
frequent.singleton
7 changes: 7 additions & 0 deletions docs/api/frequent.singleton.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
singleton
=========

.. automodule:: frequent.singleton
:members:
:undoc-members:
:show-inheritance:
145 changes: 119 additions & 26 deletions src/frequent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,44 @@
#
"""
Configuration module for global configuration settings.
These functions can be used to manage a global configuration state for
your applications. The :obj:`Configuration` object manages this global
state and includes the ability to `save` and `load` from a JSON file.
This class can be modified to serialize to any format you want by
overloading the `loads` and `dumps` methods (which will automatically
pass through to the `save` and `load` calls).
Examples
--------
Set or get a setting:
>>> set_config('files.path', '/home/doug/frequent-files/')
>>> get_config('files.path')
'/home/doug/frequent-files/'
>>> get_config('files')
{'path': '/home/doug/frequent-files/'}
Save to file:
>>> save_config('/home/doug/config.json')
Load from file:
>>> load_config('/home/doug/config.json')
>>> get_config('files.path')
'/home/doug/frequent-files/'
Set temporary settings:
>>> with temp_config(files={'path': '/home/doug/tmp'}):
... print(get_config('files.path'))
'/home/doug/tmp'
>>> get_config('files.path')
'/home/doug/frequent-files/'
"""
from collections.abc import MutableMapping
from contextlib import contextmanager
Expand All @@ -15,22 +53,24 @@
from typing import Callable
from typing import Dict
from typing import Iterator as T_Iterator
from typing import Optional
from typing import Tuple
from typing import Type

__all__ = [
'Configuration',
'get_config',
'load_config',
'save_config',
'set_config',
'temp_config',
]


_global_config = None
_GLOBAL_CONFIG: Optional['Configuration'] = None


def _make_sentinel(name='_MISSING'):
def _make_sentinel(name: str = '_MISSING') -> 'Sentinel':
"""Creates a new sentinel object, code adapted from boltons:
https://github.com/mahmoud/boltons/
"""
Expand All @@ -55,6 +95,45 @@ def __nonzero__(self):
class Configuration(MutableMapping):
"""
Configuration storage object.
This object is basically a :obj:`dict` with some additional bells
and whistles, including:
- The ability to access/modify items like attributes.
- Serialize to/from strings via `dumps` and `loads`.
- `save` and `load` to/from files.
- Easily convert standard :obj:`dict` objects with `to_dict` and
`from_dict`.
Examples
--------
This object works like a :obj:`dict`, where settings can be
retrieved and set using:
>>> config = Configuration()
>>> config['answer'] = 42
>>> config['answer']
42
Additionally, you can nest settings using the `.` as a seperator,
for instance:
>>> config['nested.setting'] = 'value'
>>> config['nested']
{'setting': 'value'}
>>> config['nested.setting']
'value'
>>> config['nested']['setting']
'value'
Furthermore, you can work with settings as if they were attributes:
>>> config.nested.setting
'value'
>>> config.dirs.temp = '/home/doug/tmp'
>>> config['dirs.temp']
'/home/doug/tmp'
"""
__key_seperator__ = '.'

Expand Down Expand Up @@ -161,7 +240,6 @@ def dumps(self, compact: bool = True, **kwargs) -> str:
if not compact:
json_kws['indent'] = 2
json_kws.update(kwargs)

return json.dumps(self.to_dict(), **json_kws)

@classmethod
Expand Down Expand Up @@ -217,7 +295,7 @@ def load(cls, path: str, **kwargs) -> 'Configuration':
return cls.loads(''.join(text), **kwargs)

@classmethod
def from_dict(cls, data: dict) -> 'Configuration':
def from_dict(cls, data: Dict[str, Any]) -> 'Configuration':
"""Creates a configuration object from the given :obj:`dict`.
Parameters
Expand All @@ -238,7 +316,7 @@ def from_dict(cls, data: dict) -> 'Configuration':
rv[k] = v
return rv

def to_dict(self) -> Dict:
def to_dict(self) -> Dict[str, Any]:
"""Converts this configuration object to a standard :obj:`dict`.
Returns
Expand Down Expand Up @@ -266,7 +344,7 @@ def _key_helper(cls, key: str) -> Tuple[str, str]:


def load_config(
path: str = None, config_cls: Type[Configuration] = Configuration
path: str = None, config_cls: Type[Configuration] = Configuration
) -> None:
"""Loads the global configuration from the given file path.
Expand All @@ -280,25 +358,39 @@ def load_config(
standard :obj:`Configuration` class).
"""
global _global_config
global _GLOBAL_CONFIG

if path:
_global_config = config_cls.load(path)
_GLOBAL_CONFIG = config_cls.load(path)
else:
_global_config = config_cls()
_GLOBAL_CONFIG = config_cls()
return


def _ensure_config(f: Callable) -> Callable:
@wraps(f)
def wrapper(*args, **kwargs):
global _global_config
if _global_config is None:
global _GLOBAL_CONFIG
if _GLOBAL_CONFIG is None:
load_config()
return f(*args, **kwargs)
return wrapper


@_ensure_config
def save_config(path: str) -> None:
"""Saves the current global configuration to the given file path.
Parameters
----------
path : str
The file path to save the configuration to.
"""
global _GLOBAL_CONFIG
return _GLOBAL_CONFIG.save(path)


@_ensure_config
def get_config(name: str = None, default: Any = _MISSING) -> Any:
"""Gets the global configuration.
Expand All @@ -319,14 +411,14 @@ def get_config(name: str = None, default: Any = _MISSING) -> Any:
requested.
"""
global _global_config
global _GLOBAL_CONFIG

if not name:
return _global_config.copy()
return _GLOBAL_CONFIG.copy()

if default == _MISSING:
return _global_config[name]
return _global_config.get(name, default)
return _GLOBAL_CONFIG[name]
return _GLOBAL_CONFIG.get(name, default)


@_ensure_config
Expand All @@ -341,20 +433,22 @@ def set_config(name: str, value: Any) -> None:
The value to set for the given `name`.
"""
global _global_config
_global_config[name] = value
global _GLOBAL_CONFIG
_GLOBAL_CONFIG[name] = value
return


def clear_config() -> None:
"""Clears the currently-set configuration."""
global _global_config
_global_config.clear()
_global_config = None
"""Clears the currently-set global configuration."""
global _GLOBAL_CONFIG
if _GLOBAL_CONFIG is not None:
_GLOBAL_CONFIG.clear()
_GLOBAL_CONFIG = None
return


@contextmanager
@_ensure_config
def temp_config(**settings) -> Configuration:
"""Gets a context with a temporary configuration.
Expand All @@ -375,15 +469,14 @@ def temp_config(**settings) -> Configuration:
The temporary configuration object.
"""
global _global_config
global _GLOBAL_CONFIG

curr_config = _global_config.copy()
curr_config = _GLOBAL_CONFIG.copy()
try:
for k, v in settings.items():
set_config(k, v)
yield _global_config.copy()
yield _GLOBAL_CONFIG.copy()
finally:
_global_config = curr_config

_GLOBAL_CONFIG = curr_config
return

58 changes: 58 additions & 0 deletions src/frequent/singleton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
#
# This module is part of the frequent project:
# https://github.com/douglasdaly/frequent-py
#
"""
Singleton utility metaclass.
"""
from abc import ABC
from typing import Any
from typing import Mapping
from weakref import WeakValueDictionary


class Singleton(type):
"""
Metaclass for singleton objects.
Example
-------
This class is easy to use:
.. code-block:: python
class MyClass(SomeBaseClass, metaclass=Singleton):
def __init__(self, x: int) -> None:
self.x = x
return
Note the behavior of subsequent calls:
>>> my_instance = MyClass(42)
>>> my_instance.x
42
>>> another_instance = MyClass(43)
>>> another_instance.x
42
Note that values set in subsequent calls to `__init__` will have no
effect on the attribute. To change the attribute do so on any of
the instances:
>>> another_instance.x = 43
>>> my_instance.x
43
>>> my_instance.x = 42
>>> another_instance.x
42
"""
__instances = WeakValueDictionary()

def __call__(cls: type, *args, **kwargs) -> None:
if cls not in cls.__instances:
instance = super().__call__(*args, **kwargs)
cls.__instances[cls] = instance
return cls.__instances[cls]
4 changes: 2 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Unit tests for the config module.
Unit tests for the `config` module.
"""
import os
from tempfile import mkstemp
Expand All @@ -10,7 +10,7 @@

class TestConfiguration(object):
"""
Tests for the Configuration class
Tests for the :obj:`Configuration` class.
"""

def test_usage(self):
Expand Down

0 comments on commit 9e0d9d6

Please sign in to comment.