Repository: getpelican/pelican

Issue:
Rename PLUGIN_PATH to PLUGIN_PATHS
PR #1276 addressed #981 by changing the `PLUGIN_PATH` setting value from a string to a list. As @smartass101 pointed out, perhaps we should change the name of this setting to `PLUGIN_PATHS` for the sake of consistency and clarity.

The previous `PLUGIN_PATH` setting should still be honored, with a deprecation warning, so that existing Pelican users don't have any disruption when upgrading to the next release.



Discussion:

Resolving patch:
diff --git a/pelican/__init__.py b/pelican/__init__.py
--- a/pelican/__init__.py
+++ b/pelican/__init__.py
@@ -63,9 +63,9 @@ def init_path(self):
 
     def init_plugins(self):
         self.plugins = []
-        logger.debug('Temporarily adding PLUGIN_PATH to system path')
+        logger.debug('Temporarily adding PLUGIN_PATHS to system path')
         _sys_path = sys.path[:]
-        for pluginpath in self.settings['PLUGIN_PATH']:
+        for pluginpath in self.settings['PLUGIN_PATHS']:
             sys.path.insert(0, pluginpath)
         for plugin in self.settings['PLUGINS']:
             # if it's a string, then import it
diff --git a/pelican/generators.py b/pelican/generators.py
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -110,29 +110,30 @@ def _include_path(self, path, extensions=None):
             return True
         return False
 
-    def get_files(self, path, exclude=[], extensions=None):
+    def get_files(self, paths, exclude=[], extensions=None):
         """Return a list of files to use, based on rules
 
-        :param path: the path to search (relative to self.path)
+        :param paths: the list pf paths to search (relative to self.path)
         :param exclude: the list of path to exclude
         :param extensions: the list of allowed extensions (if False, all
             extensions are allowed)
         """
         files = []
-        root = os.path.join(self.path, path)
-
-        if os.path.isdir(root):
-            for dirpath, dirs, temp_files in os.walk(root, followlinks=True):
-                for e in exclude:
-                    if e in dirs:
-                        dirs.remove(e)
-                reldir = os.path.relpath(dirpath, self.path)
-                for f in temp_files:
-                    fp = os.path.join(reldir, f)
-                    if self._include_path(fp, extensions):
-                        files.append(fp)
-        elif os.path.exists(root) and self._include_path(path, extensions):
-            files.append(path)  # can't walk non-directories
+        for path in paths:
+            root = os.path.join(self.path, path)
+
+            if os.path.isdir(root):
+                for dirpath, dirs, temp_files in os.walk(root, followlinks=True):
+                    for e in exclude:
+                        if e in dirs:
+                            dirs.remove(e)
+                    reldir = os.path.relpath(dirpath, self.path)
+                    for f in temp_files:
+                        fp = os.path.join(reldir, f)
+                        if self._include_path(fp, extensions):
+                            files.append(fp)
+            elif os.path.exists(root) and self._include_path(path, extensions):
+                files.append(path)  # can't walk non-directories
         return files
 
     def add_source_path(self, content):
@@ -462,7 +463,7 @@ def generate_context(self):
         all_articles = []
         all_drafts = []
         for f in self.get_files(
-                self.settings['ARTICLE_DIR'],
+                self.settings['ARTICLE_PATHS'],
                 exclude=self.settings['ARTICLE_EXCLUDES']):
             article = self.get_cached_data(f, None)
             if article is None:
@@ -586,7 +587,7 @@ def generate_context(self):
         all_pages = []
         hidden_pages = []
         for f in self.get_files(
-                self.settings['PAGE_DIR'],
+                self.settings['PAGE_PATHS'],
                 exclude=self.settings['PAGE_EXCLUDES']):
             page = self.get_cached_data(f, None)
             if page is None:
@@ -660,20 +661,17 @@ def _copy_paths(self, paths, source, destination, output_path,
 
     def generate_context(self):
         self.staticfiles = []
-
-        # walk static paths
-        for static_path in self.settings['STATIC_PATHS']:
-            for f in self.get_files(
-                    static_path, extensions=False):
-                static = self.readers.read_file(
-                    base_path=self.path, path=f, content_class=Static,
-                    fmt='static', context=self.context,
-                    preread_signal=signals.static_generator_preread,
-                    preread_sender=self,
-                    context_signal=signals.static_generator_context,
-                    context_sender=self)
-                self.staticfiles.append(static)
-                self.add_source_path(static)
+        for f in self.get_files(self.settings['STATIC_PATHS'],
+                                extensions=False):
+            static = self.readers.read_file(
+                base_path=self.path, path=f, content_class=Static,
+                fmt='static', context=self.context,
+                preread_signal=signals.static_generator_preread,
+                preread_sender=self,
+                context_signal=signals.static_generator_context,
+                context_sender=self)
+            self.staticfiles.append(static)
+            self.add_source_path(static)
         self._update_context(('staticfiles',))
         signals.static_generator_finalized.send(self)
 
diff --git a/pelican/settings.py b/pelican/settings.py
--- a/pelican/settings.py
+++ b/pelican/settings.py
@@ -29,10 +29,10 @@
                              'themes', 'notmyidea')
 DEFAULT_CONFIG = {
     'PATH': os.curdir,
-    'ARTICLE_DIR': '',
-    'ARTICLE_EXCLUDES': ('pages',),
-    'PAGE_DIR': 'pages',
-    'PAGE_EXCLUDES': (),
+    'ARTICLE_PATHS': [''],
+    'ARTICLE_EXCLUDES': [],
+    'PAGE_PATHS': ['pages'],
+    'PAGE_EXCLUDES': [],
     'THEME': DEFAULT_THEME,
     'OUTPUT_PATH': 'output',
     'READERS': {},
@@ -114,7 +114,7 @@
     'ARTICLE_PERMALINK_STRUCTURE': '',
     'TYPOGRIFY': False,
     'SUMMARY_MAX_LENGTH': 50,
-    'PLUGIN_PATH': [],
+    'PLUGIN_PATHS': [],
     'PLUGINS': [],
     'PYGMENTS_RST_OPTIONS': {},
     'TEMPLATE_PAGES': {},
@@ -147,13 +147,17 @@ def read_settings(path=None, override=None):
                 if p not in ('THEME') or os.path.exists(absp):
                     local_settings[p] = absp
 
-        if isinstance(local_settings['PLUGIN_PATH'], six.string_types):
-            logger.warning("Defining %s setting as string has been deprecated (should be a list)" % 'PLUGIN_PATH')
-            local_settings['PLUGIN_PATH'] = [local_settings['PLUGIN_PATH']]
-        else:
-            if 'PLUGIN_PATH' in local_settings and local_settings['PLUGIN_PATH'] is not None:
-                local_settings['PLUGIN_PATH'] = [os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(path), pluginpath)))
-                                    if not isabs(pluginpath) else pluginpath for pluginpath in local_settings['PLUGIN_PATH']]
+        if 'PLUGIN_PATH' in local_settings:
+            logger.warning('PLUGIN_PATH setting has been replaced by '
+                           'PLUGIN_PATHS, moving it to the new setting name.')
+            local_settings['PLUGIN_PATHS'] = local_settings['PLUGIN_PATH']
+            del local_settings['PLUGIN_PATH']
+        if isinstance(local_settings['PLUGIN_PATHS'], six.string_types):
+            logger.warning("Defining %s setting as string has been deprecated (should be a list)" % 'PLUGIN_PATHS')
+            local_settings['PLUGIN_PATHS'] = [local_settings['PLUGIN_PATHS']]
+        elif local_settings['PLUGIN_PATHS'] is not None:
+                local_settings['PLUGIN_PATHS'] = [os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(path), pluginpath)))
+                                    if not isabs(pluginpath) else pluginpath for pluginpath in local_settings['PLUGIN_PATHS']]
     else:
         local_settings = copy.deepcopy(DEFAULT_CONFIG)
 
@@ -311,6 +315,16 @@ def configure_settings(settings):
         key=lambda r: r[0],
     )
 
+    # move {ARTICLE,PAGE}_DIR -> {ARTICLE,PAGE}_PATHS
+    for key in ['ARTICLE', 'PAGE']:
+        old_key = key + '_DIR'
+        new_key = key + '_PATHS'
+        if old_key in settings:
+            logger.warning('Deprecated setting {}, moving it to {} list'.format(
+                old_key, new_key))
+            settings[new_key] = [settings[old_key]]   # also make a list
+            del settings[old_key]
+
     # Save people from accidentally setting a string rather than a list
     path_keys = (
         'ARTICLE_EXCLUDES',
@@ -324,13 +338,27 @@ def configure_settings(settings):
         'PLUGINS',
         'STATIC_PATHS',
         'THEME_STATIC_PATHS',
+        'ARTICLE_PATHS',
+        'PAGE_PATHS',
     )
     for PATH_KEY in filter(lambda k: k in settings, path_keys):
-            if isinstance(settings[PATH_KEY], six.string_types):
-                logger.warning("Detected misconfiguration with %s setting "
-                               "(must be a list), falling back to the default"
-                               % PATH_KEY)
-                settings[PATH_KEY] = DEFAULT_CONFIG[PATH_KEY]
+        if isinstance(settings[PATH_KEY], six.string_types):
+            logger.warning("Detected misconfiguration with %s setting "
+                           "(must be a list), falling back to the default"
+                           % PATH_KEY)
+            settings[PATH_KEY] = DEFAULT_CONFIG[PATH_KEY]
+
+    # Add {PAGE,ARTICLE}_PATHS to {ARTICLE,PAGE}_EXCLUDES
+    mutually_exclusive = ('ARTICLE', 'PAGE')
+    for type_1, type_2 in [mutually_exclusive, mutually_exclusive[::-1]]:
+        try:
+            includes = settings[type_1 + '_PATHS']
+            excludes = settings[type_2 + '_EXCLUDES']
+            for path in includes:
+                if path not in excludes:
+                    excludes.append(path)
+        except KeyError:
+            continue            # setting not specified, nothing to do
 
     for old, new, doc in [
             ('LESS_GENERATOR', 'the Webassets plugin', None),


Test patch:
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -372,8 +372,8 @@ def distill_pages(self, pages):
 
     def test_generate_context(self):
         settings = get_settings(filenames={})
-        settings['PAGE_DIR'] = 'TestPages'  # relative to CUR_DIR
         settings['CACHE_PATH'] = self.temp_cache
+        settings['PAGE_PATHS'] = ['TestPages']  # relative to CUR_DIR
         settings['DEFAULT_DATE'] = (1970, 1, 1)
 
         generator = PagesGenerator(
diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py
--- a/pelican/tests/test_settings.py
+++ b/pelican/tests/test_settings.py
@@ -43,7 +43,10 @@ def test_read_empty_settings(self):
         # Providing no file should return the default values.
         settings = read_settings(None)
         expected = copy.deepcopy(DEFAULT_CONFIG)
-        expected['FEED_DOMAIN'] = ''  # Added by configure settings
+        # Added by configure settings
+        expected['FEED_DOMAIN'] = ''
+        expected['ARTICLE_EXCLUDES'] = ['pages']
+        expected['PAGE_EXCLUDES'] = ['']
         self.maxDiff = None
         self.assertDictEqual(settings, expected)