Skip to content

Commit

Permalink
Merge pull request #362 from ankostis/subcmd_callable
Browse files Browse the repository at this point in the history
Support callable subcommands
  • Loading branch information
minrk authored Jan 25, 2017
2 parents 25b16ca + bc746ac commit 61f6d42
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 9 deletions.
24 changes: 21 additions & 3 deletions docs/source/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -379,11 +379,29 @@ after :command:`git`, and are called with the form :command:`command subcommand
$ ipython qtconsole --profile myprofile
.. currentmodule:: traitlets.config

Subcommands are specified as a dictionary on :class:`~traitlets.config.Application`
instances, mapping subcommand names to two-tuples containing:
instances, mapping *subcommand names* to two-tuples containing these:

1. A subclass of :class:`Application` to handle the subcommand.
This can be specified as:
- simply as a class, where its :meth:`SingletonConfigurable.instance()`
will be invoked (straight-forward, but loads subclasses on import time);
- as a string which can be imported to produce the above class;
- as a factory function accepting a single argument like that::

app_factory(parent_app: Application) -> Application

.. note::
The return value of the facory above is an *instance*, not a class,
son the :meth:`SingletonConfigurable.instance()` is not invoked
in this case.

In all cases, the instanciated app is stored in :attr:`Application.subapp`
and its :meth:`Application.initialize()` is invoked.

1. The application class for the subcommand, or a string which can be imported
to give this.
2. A short description of the subcommand for use in help output.

To see a list of the available aliases, flags, and subcommands for a configurable
Expand Down
20 changes: 14 additions & 6 deletions traitlets/config/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,16 +468,24 @@ def print_version(self):
@catch_config_error
def initialize_subcommand(self, subc, argv=None):
"""Initialize a subcommand with argv."""
subapp,help = self.subcommands.get(subc)
subapp, _ = self.subcommands.get(subc)

if isinstance(subapp, six.string_types):
subapp = import_item(subapp)

# clear existing instances
self.__class__.clear_instance()
# instantiate
self.subapp = subapp.instance(parent=self)
# and initialize subapp
## Cannot issubclass() on a non-type (SOhttp://stackoverflow.com/questions/8692430)
if isinstance(subapp, type) and issubclass(subapp, Application):
# Clear existing instances before...
self.__class__.clear_instance()
# instantiating subapp...
self.subapp = subapp.instance(parent=self)
elif callable(subapp):
# or ask factory to create it...
self.subapp = subapp(self)
else:
raise AssertionError('Invalid mappings for subcommand %s!' % subc)

# ... and finally initialize subapp.
self.subapp.initialize(argv)

def flatten_flags(self):
Expand Down
53 changes: 53 additions & 0 deletions traitlets/config/tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,59 @@ def test_raise_on_bad_config(self):
with self.assertRaises(SyntaxError):
app.load_config_file(name, path=[td])

def test_subcommands_instanciation(self):
"""Try all ways to specify how to create sub-apps."""
app = Root.instance()
app.parse_command_line(['sub1'])

self.assertIsInstance(app.subapp, Sub1)
## Check parent hierarchy.
self.assertIs(app.subapp.parent, app)

Root.clear_instance()
Sub1.clear_instance() # Otherwise, replaced spuriously and hierarchy check fails.
app = Root.instance()

app.parse_command_line(['sub1', 'sub2'])
self.assertIsInstance(app.subapp, Sub1)
self.assertIsInstance(app.subapp.subapp, Sub2)
## Check parent hierarchy.
self.assertIs(app.subapp.parent, app)
self.assertIs(app.subapp.subapp.parent, app.subapp)

Root.clear_instance()
Sub1.clear_instance() # Otherwise, replaced spuriously and hierarchy check fails.
app = Root.instance()

app.parse_command_line(['sub1', 'sub3'])
self.assertIsInstance(app.subapp, Sub1)
self.assertIsInstance(app.subapp.subapp, Sub3)
self.assertTrue(app.subapp.subapp.flag) # Set by factory.
## Check parent hierarchy.
# self.assertIs(app.subapp.parent, app)
# self.assertIs(app.subapp.subapp.parent, app.subapp) # Set by factory.


class Root(Application):
subcommands = {
'sub1': ('traitlets.config.tests.test_application.Sub1', 'import string'),
}


class Sub3(Application):
flag = Bool(False)


class Sub2(Application):
pass


class Sub1(Application):
subcommands = {
'sub2': (Sub2, 'Application class'),
'sub3': (lambda root: Sub3(parent=root, flag=True), 'factory'),
}


class DeprecatedApp(Application):
override_called = False
Expand Down

0 comments on commit 61f6d42

Please sign in to comment.