Commits

Jason R. Coombs committed 8e5a725

Fixed support for 'null' entry points (for initializing the config but not running the application).

Comments (0)

Files changed (2)

 Eggmonster Change Notes
 =======================
 
+5.3
+---
+
+* Fixed support for "null" entry points (for initializing the config
+  but not running the application) while retaining backward compatibility
+  for namespace packages. See `eggmonster.runner.EggmonsterApp` docs
+  for details.
+
+5.2
+---
+
+* Added FakeMonster, a trimmed down launcher for eggmonster apps.
+
 5.1
 ---
 

eggmonster/runner.py

 
     cfg = _load_config(config_path)
 
+    # parse the app string
+    app = EggmonsterApp.from_string(app)
+
     # Pull the appropriate environment from the configuration.
-    pkgname, __sep, entry_point = app.rpartition('.')
     try:
-        entry_point, env, pkginfo, options = cfg.apps[app]
+        entry_point, env, pkginfo, options = cfg.apps[str(app)]
     except KeyError:
         # No configuration mentioned? OK, well we can still run the
         # application as long as it is a valid entry point (we'll
         # verify it further down.
         try:
-            env, pkginfo = cfg.packages[pkgname]
+            env, pkginfo = cfg.packages[app.package]
+            entry_point = app.name
         except KeyError:
-            raise SystemExit('ERROR: No such package: "%s"' % pkgname)
+            raise SystemExit('ERROR: No such package: "%s"' % app.package)
 
     # We only need the original entry point for pulling out a configuration,
     # we can refer to the spawned application directly now.
         from eggmonster.client import _em_emi
         env = _em_emi.fill_config_placeholders(env, num)
     eggmonster.update_locals(env)
-    pkg = load_dependencies(pkginfo or pkgname)
+    pkg = load_dependencies(pkginfo or app.package)
 
     if entry_point is None:
         # caller supplied pkgname, but not appname. In this case, we just
         # Try to use iPython for the prompt if possible.
         runner = (start_ipython_shell if 'IPython' in globals() else
             start_python_shell)
-        run_app_func = runner(the_env, pkgname)
+        run_app_func = runner(the_env, app.package)
 
         if run_app_func:
             print 'Exiting console, executing application.'
         else:
             app_func()
 
-class FakeMonster(object):
-    def __init__(self):
-        self.start(*self.parse_args())
+class EggmonsterApp(object):
+    """
+    Describe an eggmonster application based on its package and
+    eggmonster.applictions entry point (app).
+    """
+    def __init__(self, package, name=None):
+        self.package = package
+        self.name = name
 
     @classmethod
+    def from_string(cls, spec):
+        """
+        Traditionally, on EM app was represented as a single string, using a
+        dot for a delimiter, but this technique has necessary ambiguity, as
+        package names can have dots (yg.irc) as can an app (foo.runner).
+        Also, app can be not specified (null).
+
+        >>> EggmonsterApp.from_string('foo')
+        ('foo', None)
+        >>> EggmonsterApp.from_string('foo.bar')
+        ('foo', 'bar')
+        >>> EggmonsterApp.from_string('foo.baz.bar')
+        ('foo.baz', 'bar')
+
+        For compatibility, assume one or more dots present means an app is
+        specified. To force specifying a namespace package when no app is
+        desired, use a trailing period.
+
+        >>> EggmonsterApp.from_string('foo.')
+        ('foo', None)
+        >>> EggmonsterApp.from_string('foo.bar.')
+        ('foo.bar', None)
+        """
+        if not '.' in spec:
+            return cls(spec)
+        package_name, _, app_name = spec.rpartition('.')
+        app_name = app_name or None
+        return cls(package_name, app_name)
+
+    def __str__(self):
+        return str(unicode(self))
+
+    def __unicode__(self):
+        """
+        You can get the original string using str/unicode.
+
+        >>> str(EggmonsterApp.from_string('foo.bar.'))
+        'foo.bar.'
+        >>> str(EggmonsterApp.from_string('foo'))
+        'foo'
+        """
+        if '.' in self.package:
+            return '.'.join((self.package, self.name or ''))
+        return '.'.join(filter(None, [self.package, self.name]))
+
+    def __repr__(self):
+        return repr((self.package, self.name))
+
+class FakeMonster(object):
+    @classmethod
     def run(cls):
-        cls()
+        cls().start(*cls.parse_args())
 
-    def parse_args(self):
+    @staticmethod
+    def parse_args():
+        resolve_env = lambda key: os.environ[key]
         parser = argparse.ArgumentParser()
         parser.add_argument('app')
         parser.add_argument('--config-path', default='settings.yaml')
         parser.add_argument('--config-path-env', dest='config_path',
-            type=self.resolve_env)
+            type=resolve_env)
         args = parser.parse_args()
-        return args.app, args.config_path
-
-    def resolve_env(self, key):
-        return os.environ[key]
+        em_app = EggmonsterApp.from_string(args.app)
+        return em_app, args.config_path
 
     def start(self, app, config_path):
         """
         eggmonster._set_managed_env()
         with open(config_path) as in_file:
             eggmonster.update_locals(yaml.load(in_file))
-        package_name, _, app_name = app.partition('.')
-        pkg = load_dependencies(package_name)
-        app_func = pkg.load_entry_point('eggmonster.applications', app_name)
-        args = (app_name, []) if can_take_app_args(app_func) else ()
+        pkg = load_dependencies(app.package)
+        if not app.name:
+            # no app name was specified, so just return.
+            return
+        app_func = pkg.load_entry_point('eggmonster.applications', app.name)
+        args = (app.name, []) if can_take_app_args(app_func) else ()
         return app_func(*args)