Repository: getnikola/nikola

Issue:
Create an object that would make executing commands in the console easier
As per off-topic discussion in #824.

Something like this:

``` pycon
Nikola v6.1.1 -- Python Console (conf = configuration, SITE = site engine, ACT = actions)
>>> ACT.new_post(title='Sample Worthless Post')
Creating New Post
-----------------

Title: Sample Worthless Post
[2013-10-14T07:42:46Z] NOTICE: new_post: Your post's text is at: posts/sample-worthless-post.rst
>>>
```



Discussion:

Resolving patch:
diff --git a/nikola/__main__.py b/nikola/__main__.py
--- a/nikola/__main__.py
+++ b/nikola/__main__.py
@@ -230,6 +230,7 @@ class DoitNikola(DoitMain):
 
     def __init__(self, nikola, quiet=False):
         self.nikola = nikola
+        nikola.doit = self
         self.task_loader = self.TASK_LOADER(nikola, quiet)
 
     def get_commands(self):
diff --git a/nikola/nikola.py b/nikola/nikola.py
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -199,9 +199,6 @@ def __init__(self, **config):
         self.invariant = config.pop('__invariant__', False)
         self.configured = bool(config)
 
-        config['__colorful__'] = self.colorful
-        config['__invariant__'] = self.invariant
-
         self.template_hooks = {
             'extra_head': utils.TemplateHookRegistry('extra_head', self),
             'body_end': utils.TemplateHookRegistry('body_end', self),
@@ -334,6 +331,16 @@ def __init__(self, **config):
 
         self.config.update(config)
 
+        # __builtins__ contains useless cruft
+        if '__builtins__' in self.config:
+            try:
+                del self.config['__builtins__']
+            except KeyError:
+                del self.config[b'__builtins__']
+
+        self.config['__colorful__'] = self.colorful
+        self.config['__invariant__'] = self.invariant
+
         # Make sure we have sane NAVIGATION_LINKS.
         if not self.config['NAVIGATION_LINKS']:
             self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()}
@@ -1289,6 +1296,9 @@ def generic_post_list_renderer(self, lang, posts, output_name,
 
         return utils.apply_filters(task, filters)
 
+    def __repr__(self):
+        return '<Nikola Site: {0}>'.format(self.config['BLOG_TITLE']())
+
 
 def sanitized_locales(locale_fallback, locale_default, locales, translations):
     """Sanitizes all locales availble into a nikola session
diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py
--- a/nikola/plugins/command/console.py
+++ b/nikola/plugins/command/console.py
@@ -28,6 +28,8 @@
 
 import os
 
+from doit.cmdparse import CmdParse
+
 from nikola import __version__
 from nikola.plugin_categories import Command
 from nikola.utils import get_logger, STDERR_HANDLER, req_missing
@@ -41,9 +43,9 @@ class CommandConsole(Command):
     shells = ['ipython', 'bpython', 'plain']
     doc_purpose = "start an interactive Python console with access to your site"
     doc_description = """\
-The site engine is accessible as `SITE`, and the config as `conf`.
-If no option (-b, -i, -p), it tries -i, then -b, then -p."""
-    header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration, SITE = site engine)"
+The site engine is accessible as `site`, the config file as `conf`, and commands are available as `commands`.
+If there is no console to use specified (as -b, -i, -p) it tries IPython, then falls back to bpython, and finally falls back to the plain Python console."""
+    header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration file, site = site engine, commands = nikola commands)"
     cmd_options = [
         {
             'name': 'bpython',
@@ -73,75 +75,61 @@ class CommandConsole(Command):
 
     def ipython(self, willful=True):
         """IPython shell."""
-        from nikola import Nikola
         try:
-            import conf
-        except ImportError:
-            LOGGER.error("No configuration found, cannot run the console.")
+            import IPython
+        except ImportError as e:
+            if willful:
+                req_missing(['IPython'], 'use the IPython console')
+            raise e  # That’s how _execute knows whether to try something else.
         else:
-            try:
-                import IPython
-            except ImportError as e:
-                if willful:
-                    req_missing(['IPython'], 'use the IPython console')
-                raise e  # That’s how _execute knows whether to try something else.
-            else:
-                SITE = Nikola(**conf.__dict__)
-                SITE.scan_posts()
-                IPython.embed(header=self.header.format('IPython'))
+            site = self.context['site']  # NOQA
+            conf = self.context['conf']  # NOQA
+            commands = self.context['commands']  # NOQA
+            IPython.embed(header=self.header.format('IPython'))
 
     def bpython(self, willful=True):
         """bpython shell."""
-        from nikola import Nikola
         try:
-            import conf
-        except ImportError:
-            LOGGER.error("No configuration found, cannot run the console.")
+            import bpython
+        except ImportError as e:
+            if willful:
+                req_missing(['bpython'], 'use the bpython console')
+            raise e  # That’s how _execute knows whether to try something else.
         else:
-            try:
-                import bpython
-            except ImportError as e:
-                if willful:
-                    req_missing(['bpython'], 'use the bpython console')
-                raise e  # That’s how _execute knows whether to try something else.
-            else:
-                SITE = Nikola(**conf.__dict__)
-                SITE.scan_posts()
-                gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
-                bpython.embed(banner=self.header.format('bpython'), locals_=gl)
+            bpython.embed(banner=self.header.format('bpython'), locals_=self.context)
 
     def plain(self, willful=True):
         """Plain Python shell."""
-        from nikola import Nikola
+        import code
         try:
-            import conf
-            SITE = Nikola(**conf.__dict__)
-            SITE.scan_posts()
-            gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
+            import readline
         except ImportError:
-            LOGGER.error("No configuration found, cannot run the console.")
+            pass
         else:
-            import code
+            import rlcompleter
+            readline.set_completer(rlcompleter.Completer(self.context).complete)
+            readline.parse_and_bind("tab:complete")
+
+        pythonrc = os.environ.get("PYTHONSTARTUP")
+        if pythonrc and os.path.isfile(pythonrc):
             try:
-                import readline
-            except ImportError:
+                execfile(pythonrc)  # NOQA
+            except NameError:
                 pass
-            else:
-                import rlcompleter
-                readline.set_completer(rlcompleter.Completer(gl).complete)
-                readline.parse_and_bind("tab:complete")
 
-            pythonrc = os.environ.get("PYTHONSTARTUP")
-            if pythonrc and os.path.isfile(pythonrc):
-                try:
-                    execfile(pythonrc)  # NOQA
-                except NameError:
-                    pass
-
-            code.interact(local=gl, banner=self.header.format('Python'))
+        code.interact(local=self.context, banner=self.header.format('Python'))
 
     def _execute(self, options, args):
         """Start the console."""
+        self.site.scan_posts()
+        # Create nice object with all commands:
+        commands = Commands(self.site.doit)
+
+        self.context = {
+            'conf': self.site.config,
+            'site': self.site,
+            'commands': commands,
+        }
         if options['bpython']:
             self.bpython(True)
         elif options['ipython']:
@@ -155,3 +143,79 @@ def _execute(self, options, args):
                 except ImportError:
                     pass
             raise ImportError
+
+
+class CommandWrapper(object):
+    """Converts commands into functions."""
+
+    def __init__(self, cmd, commands_object):
+        self.cmd = cmd
+        self.commands_object = commands_object
+
+    def __call__(self, *args, **kwargs):
+        if args or (not args and not kwargs):
+            self.commands_object._run([self.cmd] + list(args))
+        else:
+            # Here's where the keyword magic would have to go
+            self.commands_object._run_with_kw(self.cmd, *args, **kwargs)
+
+
+class Commands(object):
+
+    """Nikola Commands.
+CommandWrapper
+    Sample usage:
+    >>> commands.check('-l')                     # doctest: +SKIP
+
+    Or, if you know the internal argument names:
+    >>> commands.check(list=True)                # doctest: +SKIP
+    """
+
+    def __init__(self, main):
+        """Takes a main instance, works as wrapper for commands."""
+        self._cmdnames = []
+        for k, v in main.get_commands().items():
+            self.cmdnames.append(k)
+            if k in ['run', 'init']:
+                continue
+            nc = type(
+                bytes(k),
+                (CommandWrapper,),
+                {
+                    '__doc__': options2docstring(k, main.sub_cmds[k].options)
+                })
+            setattr(self, k, nc(k, self))
+        self.main = main
+
+    def _run(self, cmd_args):
+        self.main.run(cmd_args)
+
+    def _run_with_kw(self, cmd, *a, **kw):
+        cmd = self.main.sub_cmds[cmd]
+        options, _ = CmdParse(cmd.options).parse([])
+        options.update(kw)
+        if isinstance(cmd, Command):
+            cmd.execute(options=options, args=a)
+        else:  # Doit command
+            cmd.execute(options, a)
+
+    def __repr__(self):
+        """Return useful and verbose help."""
+
+        return """\
+<Nikola Commands>
+
+    Sample usage:
+    >>> commands.check('-l')
+
+    Or, if you know the internal argument names:
+    >>> commands.check(list=True)
+
+Available commands: {0}.""".format(', '.join(self._cmdnames))
+
+
+def options2docstring(name, options):
+    result = ['Function wrapper for command %s' % name, 'arguments:']
+    for opt in options:
+        result.append('{0} type {1} default {2}'.format(opt.name, opt.type.__name__, opt.default))
+    return '\n'.join(result)


Test patch: