Skip to content

Commit

Permalink
Merge pull request #5 from erickwilder/add-root-path-limit
Browse files Browse the repository at this point in the history
Add root path limit
  • Loading branch information
bertonha committed Aug 4, 2015
2 parents f827ef3 + 47a151a commit 46efc14
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ python:
- 3.4
- pypy
install:
- pip install coveralls
- pip install coveralls testfixtures
script:
nosetests --with-coverage --cover-package=prettyconf
after_success:
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
0.4.0
=====

- Add ``root_path`` to stop looking indefinitely for configuration files until the OS root path
- Add advanced usage docs
- Include a simple (but working) tox configuration for py27 + py34 to the project


0.3.3
=====

Expand Down
76 changes: 76 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,82 @@ Useful third-parties casts
* `django-cache-url`_ - Parses URLs like ``memcached://server:port/prefix``
into Django ``CACHES`` configuration format.


Advanced usage
~~~~~~~~~~~~~~
You can always create your own ``Configuration`` instance and change it's default behaviour.

.. code-block:: python
from prettyconf.configuration import Configuration
config = Configuration()
Set the starting path
+++++++++++++++++++++
By default the library will use the directory of the file where ``config()`` was called
as the starting directory to look for configuration files. Consider the following file
structure:

.. code-block:: shell
project/
settings.ini
app/
settings.py
When calling ``config()`` from ``project/app/settings.py`` the library will start looking
for configuration files at ``project/app``

You can change that behaviour, by setting a different ``starting_path`` when instantiating
your ``Configuration``:

.. code-block:: python
# Code example in project/app/settings.py
import os
from prettyconf.configuration import Configuration
project_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
config = Configuration(project_path)
The example above will start looking for files at ``project/`` instead of ``project/app``.

Set a different root path
+++++++++++++++++++++++++
By default, the library will try to look for configuration files until it finds valid
configuration files **or** it reaches ``Configuration.root_path``.

Consider the following file structure:

.. code-block:: shell
/projects/
any_settings.ini
project/
app/
settings.py
The default root path is set to the user home directory (e.g. ``$HOME`` or
``/home/yourusername``). You can change this behaviour by setting any parent directory
of the ``starting_path`` as the ``root_path`` when instantiating ``Configuration``:

.. code-block:: python
# Code example in project/app/settings.py
import os
from prettyconf.configuration import Configuration
app_path = os.path.dirname(__file__)
project_path = os.path.realpath(os.path.join(app_path), '..'))
config = Configuration(root_path=project_path)
The example above will start looking for files at ``project/app/`` and will stop looking
for configuration files at ``project/``, actually never looking at ``any_settings.ini``
and no configuration being loaded at all.

.. _dj-database-url: https://github.com/kennethreitz/dj-database-url
.. _django-cache-url: https://github.com/ghickman/django-cache-url

25 changes: 21 additions & 4 deletions prettyconf/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,29 @@
class ConfigurationDiscovery(object):
default_filetypes = (EnvFileConfigurationLoader, IniFileConfigurationLoader)

def __init__(self, starting_path, filetypes=None):
def __init__(self, starting_path, filetypes=None, root_path=None):
"""
Setup the configuration file discovery.
:param starting_path: The path to begin looking for configuration files
:param filetypes: tuple with configuration loaders. Defaults to
``(EnvFileConfigurationLoader, IniFileConfigurationLoader)``
:param root_path: Configuration lookup will stop at the given path. Defaults to
the current user directory
"""
self.starting_path = os.path.realpath(os.path.abspath(starting_path))
if filetypes is None:
filetypes = self.default_filetypes
if root_path is None:
root_path = os.path.expanduser('~')

self.root_path = os.path.realpath(root_path)
self.filetypes = filetypes
self._config_files = None

if self.root_path not in self.starting_path:
raise InvalidPath('Invalid root path given')

def _scan_path(self, path):
config_files = []

Expand All @@ -44,7 +60,7 @@ def _discover(self):

self._config_files += self._scan_path(path)

if self._config_files or path == os.path.sep:
if self._config_files or path == self.root_path or path == os.path.sep:
break

path = os.path.dirname(path)
Expand All @@ -63,11 +79,12 @@ class Configuration(object):
list = List()
option = Option

def __init__(self, configs=None, starting_path=None):
def __init__(self, configs=None, starting_path=None, root_path=None):
if configs is None:
configs = [EnvVarConfigurationLoader()]
self.configurations = configs
self.starting_path = starting_path
self.root_path = root_path

if starting_path:
self._init_configs()
Expand All @@ -81,7 +98,7 @@ def _caller_path():
return path

def _init_configs(self):
discovery = ConfigurationDiscovery(self.starting_path)
discovery = ConfigurationDiscovery(self.starting_path, root_path=self.root_path)
self.configurations.extend(discovery.config_files)

def __call__(self, item, cast=lambda v: v, **kwargs):
Expand Down
50 changes: 49 additions & 1 deletion tests/test_filediscover.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
# coding: utf-8


import os

from testfixtures import TempDirectory

from .base import BaseTestCase
from prettyconf.configuration import ConfigurationDiscovery
from prettyconf.exceptions import InvalidPath


class ConfigFilesDiscoveryTestCase(BaseTestCase):

def setUp(self):
super(ConfigFilesDiscoveryTestCase, self).setUp()
self.tmpdirs = []

def tearDown(self):
super(ConfigFilesDiscoveryTestCase, self).tearDown()
for tmpdir in self.tmpdirs:
tmpdir.cleanup_all()

def test_config_file_parsing(self):
self._create_file(self.test_files_path + "/../.env")
Expand Down Expand Up @@ -53,3 +61,43 @@ def test_should_look_for_parent_directory_when_it_finds_invalid_configurations(s
filenames = [cfg.filename for cfg in discovery.config_files]
self.assertIn(os.path.abspath(self.test_files_path + '/../../.env'), filenames)
self.assertIn(os.path.abspath(self.test_files_path + '/../../settings.ini'), filenames)

def test_default_root_path_should_default_to_user_directory(self):
discovery = ConfigurationDiscovery(os.path.dirname(self.test_files_path))
assert discovery.root_path == os.path.expanduser('~')

def test_root_path_should_be_parent_of_starting_path(self):
with self.assertRaises(InvalidPath):
ConfigurationDiscovery('/foo', root_path='/foo/bar/baz/')

def test_use_configuration_from_root_path_when_no_other_was_found(self):
root_dir = TempDirectory()
self.tmpdirs.append(root_dir)

start_path = root_dir.makedir('some/directories/to/start/looking/for/settings')
test_file = os.path.realpath(os.path.join(root_dir.path, 'settings.ini'))
with open(test_file, 'a') as file_:
file_.write('[settings]')
self.files.append(test_file) # Required to removed it at tearDown

discovery = ConfigurationDiscovery(start_path, root_path=root_dir.path)
filenames = [cfg.filename for cfg in discovery.config_files]
self.assertEqual([test_file], filenames)

def test_lookup_should_stop_at_root_path(self):
test_dir = TempDirectory()
self.tmpdirs.append(test_dir) # Cleanup dir at tearDown

start_path = test_dir.makedir('some/dirs/without/config')

# create a file in the test_dir
test_file = os.path.realpath(os.path.join(test_dir.path, 'settings.ini'))
with open(test_file, 'a') as file_:
file_.write('[settings]')
self.files.append(test_file) # Required to removed it at tearDown

root_dir = os.path.join(test_dir.path, 'some', 'dirs') # No settings here

discovery = ConfigurationDiscovery(start_path, root_path=root_dir)
self.assertEqual(discovery.config_files, [])

8 changes: 8 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[tox]
envlist = py27, py34

[testenv]
commands = python setup.py test
deps =
nose
testfixtures

0 comments on commit 46efc14

Please sign in to comment.