Repository: getnikola/nikola

Issue:
Get rid of sys.exit() calls
We should return error codes from functions and use `sys.exit()` preferably only in `__main__.py`.



Discussion:
Why not raise (a specific kind of) exceptions?

What about all the `sys.exit()` calls which are left, like in `nikola.Nikola.__init__()` or in the task plugins?

These are hard to get rid of without some restructuring.


Resolving patch:
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
--- a/nikola/plugins/task/galleries.py
+++ b/nikola/plugins/task/galleries.py
@@ -33,7 +33,6 @@
 import json
 import mimetypes
 import os
-import sys
 try:
     from urlparse import urljoin
 except ImportError:
@@ -94,8 +93,8 @@ def set_site(self, site):
         for source, dest in self.kw['gallery_folders'].items():
             if source in appearing_paths or dest in appearing_paths:
                 problem = source if source in appearing_paths else dest
-                utils.LOGGER.error("The gallery input or output folder '{0}' appears in more than one entry in GALLERY_FOLDERS, exiting.".format(problem))
-                sys.exit(1)
+                utils.LOGGER.error("The gallery input or output folder '{0}' appears in more than one entry in GALLERY_FOLDERS, ignoring.".format(problem))
+                continue
             appearing_paths.add(source)
             appearing_paths.add(dest)
 
@@ -116,10 +115,11 @@ def _find_gallery_path(self, name):
             if len(candidates) == 1:
                 return candidates[0]
             self.logger.error("Gallery name '{0}' is not unique! Possible output paths: {1}".format(name, candidates))
+            raise RuntimeError("Gallery name '{0}' is not unique! Possible output paths: {1}".format(name, candidates))
         else:
             self.logger.error("Unknown gallery '{0}'!".format(name))
             self.logger.info("Known galleries: " + str(list(self.proper_gallery_links.keys())))
-        sys.exit(1)
+            raise RuntimeError("Unknown gallery '{0}'!".format(name))
 
     def gallery_path(self, name, lang):
         """Link to an image gallery's path.
diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py
--- a/nikola/plugins/task/listings.py
+++ b/nikola/plugins/task/listings.py
@@ -29,7 +29,6 @@
 from __future__ import unicode_literals, print_function
 
 from collections import defaultdict
-import sys
 import os
 import lxml.html
 
@@ -73,7 +72,7 @@ def set_site(self, site):
             if source in appearing_paths or dest in appearing_paths:
                 problem = source if source in appearing_paths else dest
                 utils.LOGGER.error("The listings input or output folder '{0}' appears in more than one entry in LISTINGS_FOLDERS, exiting.".format(problem))
-                sys.exit(1)
+                continue
             appearing_paths.add(source)
             appearing_paths.add(dest)
 
@@ -275,14 +274,14 @@ def listing_path(self, namep, lang):
                 # ambiguities.
                 if len(self.improper_input_file_mapping[name]) > 1:
                     utils.LOGGER.error("Using non-unique listing name '{0}', which maps to more than one listing name ({1})!".format(name, str(self.improper_input_file_mapping[name])))
-                    sys.exit(1)
+                    return ["ERROR"]
                 if len(self.site.config['LISTINGS_FOLDERS']) > 1:
                     utils.LOGGER.notice("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.")
                 name = list(self.improper_input_file_mapping[name])[0]
                 break
         else:
             utils.LOGGER.error("Unknown listing name {0}!".format(namep))
-            sys.exit(1)
+            return ["ERROR"]
         if not name.endswith(os.sep + self.site.config["INDEX_FILE"]):
             name += '.html'
         path_parts = name.split(os.sep)
diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py
--- a/nikola/plugins/task/tags.py
+++ b/nikola/plugins/task/tags.py
@@ -29,7 +29,6 @@
 from __future__ import unicode_literals
 import json
 import os
-import sys
 import natsort
 try:
     from urlparse import urljoin
@@ -107,7 +106,6 @@ def gen_tasks(self):
                 if len(intersect) > 0:
                     for slug in intersect:
                         utils.LOGGER.error("Category '{0}' and tag '{1}' both have the same slug '{2}' for language {3}!".format('/'.join(categories[slug]), tags[slug], slug, lang))
-                    sys.exit(1)
 
             # Test for category slug clashes
             categories = {}
@@ -116,13 +114,13 @@ def gen_tasks(self):
                 for part in slug:
                     if len(part) == 0:
                         utils.LOGGER.error("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
-                        sys.exit(1)
+                        raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
                 if slug in categories:
                     other_category = categories[slug]
                     utils.LOGGER.error('You have categories that are too similar: {0} and {1} (language {2})'.format(category, other_category, lang))
                     utils.LOGGER.error('Category {0} is used in: {1}'.format(category, ', '.join([p.source_path for p in self.site.posts_per_category[category]])))
                     utils.LOGGER.error('Category {0} is used in: {1}'.format(other_category, ', '.join([p.source_path for p in self.site.posts_per_category[other_category]])))
-                    sys.exit(1)
+                    raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
                 categories[slug] = category
 
         tag_list = list(self.site.posts_per_tag.items())


Test patch: