Repository: getnikola/nikola

Issue:
Posts without metadata make nikola fail with cryptic error.
When building a Nikola site with the `ipynb` compiler enabled, any notebooks not created or imported using `nikola new_post` will raise a `ValueError` from because of the missing metadata, e.g.:

`ValueError: Invalid date '' in file posts/notes-on-meshgrid-ordering.ipynb`

This is caused by the return of an empty dictionary in [`read_metadata()`](https://github.com/getnikola/nikola/blob/master/nikola/plugins/compile/ipynb.py#L138).

I understand that this can be worked around with `nikola new_post -i` and creation of a separate metadata file, but the workflow I'd personally prefer when using Nikola is to author my notebooks in a way that is entirely Nikola-agnostic. Drawing inspiration from [what's done for page metadata](https://github.com/getnikola/nikola/blob/master/nikola/post.py#L194-L195), I kludged together a solution in `ipynb.py` that works for my use case.

After speaking with @Kwpolska a little in Freenode #python, it appears this is partially a design decision, but I think it's pretty inelegant to just propagate the `ValueError` to the user.

For my workflow, my ideal user story would be:
> I place an IPython notebook in `posts/` and run `nikola build`  
> No Nikola metadata is found  
> The build script prompts me to enter the same info `nikola new_post` would ask for  
> After completing this process, the build script continues as normal

I'm happy to maintain my own version of the ipynb compiler for my personal use, but I figured I'd find out if this is a feature that would add value to the project.  If not, then at the very least it seems appropriate to point the user to the `new_post` command when this error is raised.


Discussion:
Build scripts are really not meant to be interactive. A better error message is indeed a good idea.

Also, this is not really ipynb specific, I am changing the title to reflect that.

Resolving patch:
diff --git a/nikola/__main__.py b/nikola/__main__.py
--- a/nikola/__main__.py
+++ b/nikola/__main__.py
@@ -275,13 +275,17 @@ def load_tasks(self, cmd, opt_values, pos_args):
             }
         DOIT_CONFIG['default_tasks'] = ['render_site', 'post_render']
         DOIT_CONFIG.update(self.nikola._doit_config)
-        tasks = generate_tasks(
-            'render_site',
-            self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.'))
-        latetasks = generate_tasks(
-            'post_render',
-            self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.'))
-        signal('initialized').send(self.nikola)
+        try:
+            tasks = generate_tasks(
+                'render_site',
+                self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.'))
+            latetasks = generate_tasks(
+                'post_render',
+                self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.'))
+            signal('initialized').send(self.nikola)
+        except Exception:
+            LOGGER.error('Error loading tasks')
+            sys.exit(3)
         return tasks + latetasks, DOIT_CONFIG
 
 
@@ -363,7 +367,12 @@ def run(self, cmd_args):
                 LOGGER.error("This command needs to run inside an "
                              "existing Nikola site.")
                 return 3
-        return super(DoitNikola, self).run(cmd_args)
+        try:
+            return super(DoitNikola, self).run(cmd_args)
+        except Exception:
+            if self.nikola.debug:
+                raise
+            return 1
 
     @staticmethod
     def print_version():
diff --git a/nikola/nikola.py b/nikola/nikola.py
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -2125,7 +2125,11 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
         self.pages = []
 
         for p in sorted(self.plugin_manager.getPluginsOfCategory('PostScanner'), key=operator.attrgetter('name')):
-            timeline = p.plugin_object.scan()
+            try:
+                timeline = p.plugin_object.scan()
+            except Exception:
+                utils.LOGGER.error('Error reading timeline')
+                raise
             # FIXME: can there be conflicts here?
             self.timeline.extend(timeline)
 
diff --git a/nikola/post.py b/nikola/post.py
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -198,7 +198,12 @@ def __init__(
         try:
             self.date = to_datetime(self.meta[self.default_lang]['date'], tzinfo)
         except ValueError:
-            raise ValueError("Invalid date '{0}' in file {1}".format(self.meta[self.default_lang]['date'], source_path))
+            if not self.meta[self.default_lang]['date']:
+                msg = 'Missing date in file {}'.format(source_path)
+            else:
+                msg = "Invalid date '{0}' in file {1}".format(self.meta[self.default_lang]['date'], source_path)
+            LOGGER.error(msg)
+            raise ValueError(msg)
 
         if 'updated' not in default_metadata:
             default_metadata['updated'] = default_metadata.get('date', None)
@@ -976,7 +981,9 @@ def get_metadata_from_file(source_path, config=None, lang=None):
             meta_data = [x.strip() for x in meta_file.readlines()]
         return _get_metadata_from_file(meta_data)
     except (UnicodeDecodeError, UnicodeEncodeError):
-        raise ValueError('Error reading {0}: Nikola only supports UTF-8 files'.format(source_path))
+        msg = 'Error reading {0}: Nikola only supports UTF-8 files'.format(source_path)
+        LOGGER.error(msg)
+        raise ValueError(msg)
     except Exception:  # The file may not exist, for multilingual sites
         return {}
 


Test patch: