Repository: Polyconseil/getconf

Issue:
Load particular files from a directory
When using ``ansible`` to deploy a new configuration, ``ansible`` will create backups of the files it plans to update.
Here, this would be ``/path/to/file.ini.20150218``, which *will* be loaded by ``getconf``.

I'd like ``getconf`` to only load ``*.ini`` files.

(original french message below)
> Lorsqu'ansible déploie une configuration il cré systématiquement un backup des fichiers à modifier. Dans le cas de la configuration, il cré un fichier `SINAME.ini.DATE` ce qui peut poser problème si on charge tous les fichiers d'un répertoire.
>
> Il ne faudrait donc charger que les fichiers `*.ini`

Discussion:

Patch:
diff --git a/ChangeLog b/ChangeLog
index 4ef00ca..55b40c1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,8 @@ ChangeLog
 *New:*
 
     * Add getfloat() method
+    * Allow globs in `config_files`
+    * <PROJECT>_CONFIG env var will now have the same behaviour than `config_files` items
 
 
 1.2.1 (2014-10-24)
diff --git a/README.rst b/README.rst
index 61d8638..5601b1f 100644
--- a/README.rst
+++ b/README.rst
@@ -94,7 +94,7 @@ Features
 --------
 
 **Env-based configuration files**
-    An extra configuration file can be provided through ``MYPROJ_CONFIG``;
+    An extra configuration file/directory/glob can be provided through ``MYPROJ_CONFIG``;
     it takes precedence over other files
 
 **Default options**
diff --git a/docs/reference.rst b/docs/reference.rst
index 6bf9e47..dd5d19b 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -16,7 +16,9 @@ The ``ConfigGetter`` class
                           will be loaded.
     :param list config_files: List of ini-style configuration files to use.
                               Each item may either be the path to a simple file, or to a directory
-                              (if the path ends with a '/'). Each directory path will be replaced by the list of
+                              (if the path ends with a '/') or a glob pattern (which will select all the files
+                              matching the pattern according to the rules used by the shell).
+                              Each directory path will be replaced by the list of
                               its directly contained files, in alphabetical order, excluding those whose name
                               starts with a '.'.
                               Provided configuration files are read in the order their name was provided,
diff --git a/getconf/base.py b/getconf/base.py
index 1d61b67..8c7908e 100644
--- a/getconf/base.py
+++ b/getconf/base.py
@@ -67,27 +67,31 @@ class ConfigGetter(object):
         self.namespace = namespace
         self.defaults = defaults or {}
 
-        final_config_files = []
-        for path in config_files:
+        self.search_files = []
+        extra_config_file = os.environ.get(self._env_key('config'), None)
+
+        for path in list(config_files) + [extra_config_file]:
+            if path is None:
+                continue
             # Handle '~/.foobar.conf'
             path = os.path.abspath(os.path.expanduser(path))
             if os.path.isdir(path):
-                directory_files = glob.glob(os.path.join(path, '*'))
+                path = os.path.join(path, '*')
+
+            self.search_files.append(path)
+
+        final_config_files = []
+        for path in self.search_files:
+            directory_files = glob.glob(path)
+            if directory_files:
                 # Reverse order: final_config_files is parsed from left to right,
                 # so 99_foo naturally takes precedence over 10_base
                 final_config_files.extend(sorted(directory_files))
-            else:
-                final_config_files.append(path)
-
-        extra_config_file = os.environ.get(self._env_key('config'), '')
-        if extra_config_file:
-            final_config_files.append(extra_config_file)
 
-        self.search_files = final_config_files
         # ConfigParser's precedence rules say "later files take precedence over previous ones".
         # Since our final_config_files are sorted from least important to most important,
         # that's exactly what we need.
-        self.found_files = self.parser.read(self.search_files)
+        self.found_files = self.parser.read(final_config_files)
 
         logger.info(
             "Successfully loaded configuration from files %r (searching in %r)",


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index c1e9ba6..17673ce 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -106,7 +106,7 @@ class ConfigGetterTestCase(unittest.TestCase):
     def test_from_directory(self):
         """Test fetching from a directory."""
         getter = getconf.ConfigGetter('TESTNS', config_files=[self.example_directory])
-        self.assertEqual([self.example_path, self.example2_path], getter.search_files)
+        self.assertEqual([os.path.join(self.example_directory, '*')], getter.search_files)
         self.assertEqual([self.example_path, self.example2_path], getter.found_files)
         with Environ(TESTNS_FOO='blah'):
             # A non-file-defined value
@@ -123,7 +123,7 @@ class ConfigGetterTestCase(unittest.TestCase):
     def test_from_directory_and_files(self):
         """Test fetching from both directories and files"""
         getter = getconf.ConfigGetter('TESTNS', [self.example_directory, self.example_path])
-        self.assertEqual([self.example_path, self.example2_path, self.example_path], getter.search_files)
+        self.assertEqual([os.path.join(self.example_directory, '*'), self.example_path], getter.search_files)
         self.assertEqual([self.example_path, self.example2_path, self.example_path], getter.found_files)
         with Environ(TESTNS_FOO='blah'):
             # A non-file-defined value
@@ -137,6 +137,36 @@ class ConfigGetterTestCase(unittest.TestCase):
             # A section.key defined in the base file, not overridden
             self.assertEqual('21', getter.get('section1.otherfoo'))
 
+    def test_from_globs(self):
+        """Test fetching from globs"""
+        getter = getconf.ConfigGetter('TESTNS', [os.path.join(self.example_directory, '*2.ini')])
+        self.assertEqual([os.path.join(self.example_directory, '*2.ini')], getter.search_files)
+        self.assertEqual([self.example2_path], getter.found_files)
+        with Environ(TESTNS_FOO='blah'):
+            # A non-file-defined value
+            self.assertEqual('blah', getter.get('foo', 'foo'))
+            # A sectionless file-defined key
+            self.assertEqual('', getter.get('bar'))
+            # A section.key file-defined in all three => example wins
+            self.assertEqual('24', getter.get('section1.foo'))
+            # A section.key defined in the second file
+            self.assertEqual('13', getter.get('section2.bar'))
+            # A section.key defined in the base file, not overridden
+            self.assertEqual('', getter.get('section1.otherfoo'))
+
+    def test_environ_defined_globs(self):
+        """Test reading from an environment-defined config globs"""
+        with Environ(TESTNS_CONFIG=os.path.join(self.example_directory, '*2.ini'), TESTNS_FOO='blah'):
+            getter = getconf.ConfigGetter('TESTNS', [])
+            self.assertEqual([os.path.join(self.example_directory, '*2.ini')], getter.search_files)
+            self.assertEqual([self.example2_path], getter.found_files)
+            # A non-file-defined value
+            self.assertEqual('blah', getter.get('foo', 'foo'))
+            # A sectionless file-defined key
+            self.assertEqual('', getter.get('bar'))
+            # A section.key file-defined
+            self.assertEqual('24', getter.get('section1.foo'))
+
     def test_environ_defined_file(self):
         """Test reading from an environment-defined config file."""
         with Environ(TESTNS_CONFIG=self.example_path, TESTNS_FOO='blah'):