├── REQUIREMENTS
├── test
└── example
│ ├── __init__.py
│ ├── echoapp
│ ├── __init__.py
│ ├── models.py
│ └── commands.py
│ ├── manage.py
│ ├── urls.py
│ └── settings.py
├── MANIFEST.in
├── src
└── djboss
│ ├── __init__.py
│ ├── parser.py
│ ├── cli.py
│ └── commands.py
├── .gitignore
├── UNLICENSE
├── setup.py
├── README.md
└── distribute_setup.py
/REQUIREMENTS:
--------------------------------------------------------------------------------
1 | argparse>=1.0.1
--------------------------------------------------------------------------------
/test/example/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/example/echoapp/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/example/echoapp/models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include REQUIREMENTS
2 | include distribute_setup.py
3 |
--------------------------------------------------------------------------------
/src/djboss/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | __version__ = '0.6.3'
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pyc
3 | *.pyo
4 | .DS_Store
5 | MANIFEST
6 | build
7 | dist
8 | doc/.html
9 | doc/.tmp
10 |
--------------------------------------------------------------------------------
/test/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | try:
4 | import settings # Assumed to be in the same directory.
5 | except ImportError:
6 | import sys
7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
8 | sys.exit(1)
9 |
10 | if __name__ == "__main__":
11 | execute_manager(settings)
12 |
--------------------------------------------------------------------------------
/test/example/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 |
3 | # Uncomment the next two lines to enable the admin:
4 | # from django.contrib import admin
5 | # admin.autodiscover()
6 |
7 | urlpatterns = patterns('',
8 | # Example:
9 | # (r'^example/', include('example.foo.urls')),
10 |
11 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
12 | # to INSTALLED_APPS to enable admin documentation:
13 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
14 |
15 | # Uncomment the next line to enable the admin:
16 | # (r'^admin/', include(admin.site.urls)),
17 | )
18 |
--------------------------------------------------------------------------------
/src/djboss/parser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import argparse
4 |
5 | import djboss
6 |
7 |
8 | PARSER = argparse.ArgumentParser(
9 | prog = 'djboss',
10 | description = "Run django-boss management commands.",
11 | epilog = """
12 | To discover sub-commands, djboss first finds and imports your Django
13 | settings. The DJANGO_SETTINGS_MODULE environment variable takes precedence,
14 | but if unspecified, djboss will look for a `settings` module in the current
15 | directory.
16 |
17 | Commands should be defined in a `commands` submodule of each app. djboss
18 | will search each of your INSTALLED_APPS for management commands.""",
19 | )
20 |
21 |
22 | PARSER.add_argument('--version', action='version', version=djboss.__version__)
23 |
24 |
25 | PARSER.add_argument('-l', '--log-level', metavar='LEVEL',
26 | default='WARN', choices='DEBUG INFO WARN ERROR'.split(),
27 | help="Choose a log level from DEBUG, INFO, WARN or ERROR "
28 | "(default: %(default)s)")
29 |
30 | SUBPARSERS = PARSER.add_subparsers(dest='command', title='commands', metavar='COMMAND')
31 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import re
6 |
7 | from distribute_setup import use_setuptools; use_setuptools()
8 | from setuptools import setup, find_packages
9 |
10 |
11 | rel_file = lambda *args: os.path.join(os.path.dirname(os.path.abspath(__file__)), *args)
12 |
13 | def read_from(filename):
14 | fp = open(filename)
15 | try:
16 | return fp.read()
17 | finally:
18 | fp.close()
19 |
20 | def get_version():
21 | data = read_from(rel_file('src', 'djboss', '__init__.py'))
22 | return re.search(r"__version__ = '([^']+)'", data).group(1)
23 |
24 | def get_requirements():
25 | data = read_from(rel_file('REQUIREMENTS'))
26 | lines = map(lambda s: s.strip(), data.splitlines())
27 | return filter(None, lines)
28 |
29 |
30 | setup(
31 | name = 'django-boss',
32 | version = get_version(),
33 | author = "Zachary Voase",
34 | author_email = "zacharyvoase@me.com",
35 | url = 'http://github.com/zacharyvoase/django-boss',
36 | description = "Django management commands, revisited.",
37 | packages = find_packages(where='src'),
38 | package_dir = {'': 'src'},
39 | entry_points = {'console_scripts': ['djboss = djboss.cli:main']},
40 | install_requires = get_requirements(),
41 | )
42 |
--------------------------------------------------------------------------------
/test/example/echoapp/commands.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from djboss.commands import *
4 | import sys
5 |
6 |
7 | @command
8 | @argument('-n', '--no-newline', action='store_true',
9 | help="Don't print a newline afterwards.")
10 | @argument('words', nargs='*')
11 | def echo(args):
12 | """Echo the arguments back to the console."""
13 |
14 | string = ' '.join(args.words)
15 | if args.no_newline:
16 | sys.stdout.write(string)
17 | else:
18 | print string
19 |
20 |
21 | @command
22 | def hello(args):
23 | """Print a cliche to the console."""
24 |
25 | print "Hello, World!"
26 |
27 |
28 | @command
29 | @argument('app', type=APP_LABEL)
30 | def app_path(args):
31 | """Print a path to the specified app."""
32 |
33 | import os.path as p
34 |
35 | path, base = p.split(p.splitext(args.app.__file__)[0])
36 | if base == '__init__':
37 | print p.join(path, '')
38 | else:
39 | if p.splitext(args.app.__file__[-4:])[1] in ('.pyc', '.pyo'):
40 | print args.app.__file__[:-1]
41 | else:
42 | print args.app.__file__
43 |
44 |
45 | @command
46 | @argument('model', type=MODEL_LABEL)
47 | def model_fields(args):
48 | """Print all the fields on a specified model."""
49 |
50 | justify = 1
51 | table = []
52 | for field in args.model._meta.fields:
53 | justify = max(justify, len(field.name))
54 | table.append((field.name, field.db_type()))
55 |
56 | for name, db_type in table:
57 | print (name + ':').ljust(justify + 1) + '\t' + db_type
58 |
59 |
--------------------------------------------------------------------------------
/test/example/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for example project.
2 |
3 | DEBUG = True
4 | TEMPLATE_DEBUG = DEBUG
5 |
6 | ADMINS = (
7 | ('Zachary Voase', 'zacharyvoase@me.com'),
8 | )
9 |
10 | MANAGERS = ADMINS
11 |
12 | DATABASE_ENGINE = 'sqlite3'
13 | DATABASE_NAME = 'dev.db'
14 | DATABASE_USER = ''
15 | DATABASE_PASSWORD = ''
16 | DATABASE_HOST = ''
17 | DATABASE_PORT = ''
18 |
19 | # Local time zone for this installation. Choices can be found here:
20 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
21 | # although not all choices may be available on all operating systems.
22 | # If running in a Windows environment this must be set to the same as your
23 | # system time zone.
24 | TIME_ZONE = 'Europe/London'
25 |
26 | # Language code for this installation. All choices can be found here:
27 | # http://www.i18nguy.com/unicode/language-identifiers.html
28 | LANGUAGE_CODE = 'en-gb'
29 |
30 | SITE_ID = 1
31 |
32 | # If you set this to False, Django will make some optimizations so as not
33 | # to load the internationalization machinery.
34 | USE_I18N = True
35 |
36 | # Absolute path to the directory that holds media.
37 | # Example: "/home/media/media.lawrence.com/"
38 | MEDIA_ROOT = ''
39 |
40 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
41 | # trailing slash if there is a path component (optional in other cases).
42 | # Examples: "http://media.lawrence.com", "http://example.com/media/"
43 | MEDIA_URL = ''
44 |
45 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
46 | # trailing slash.
47 | # Examples: "http://foo.com/media/", "/media/".
48 | ADMIN_MEDIA_PREFIX = '/media/'
49 |
50 | # Make this unique, and don't share it with anybody.
51 | SECRET_KEY = '8@+k3lm3=s+ml6_*(cnpbg1w=6k9xpk5f=irs+&j4_6i=62fy^'
52 |
53 | # List of callables that know how to import templates from various sources.
54 | TEMPLATE_LOADERS = (
55 | 'django.template.loaders.filesystem.load_template_source',
56 | 'django.template.loaders.app_directories.load_template_source',
57 | # 'django.template.loaders.eggs.load_template_source',
58 | )
59 |
60 | MIDDLEWARE_CLASSES = (
61 | 'django.middleware.common.CommonMiddleware',
62 | 'django.contrib.sessions.middleware.SessionMiddleware',
63 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
64 | )
65 |
66 | ROOT_URLCONF = 'example.urls'
67 |
68 | TEMPLATE_DIRS = (
69 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
70 | # Always use forward slashes, even on Windows.
71 | # Don't forget to use absolute paths, not relative paths.
72 | )
73 |
74 | INSTALLED_APPS = (
75 | 'django.contrib.auth',
76 | 'django.contrib.contenttypes',
77 | 'django.contrib.sessions',
78 | 'django.contrib.sites',
79 | 'echoapp',
80 | )
81 |
--------------------------------------------------------------------------------
/src/djboss/cli.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import logging
4 | import os
5 | import sys
6 | import textwrap
7 |
8 | from django.utils.importlib import import_module
9 |
10 | from djboss.commands import Command
11 |
12 |
13 | class SettingsImportError(ImportError):
14 | pass
15 |
16 |
17 | def get_settings():
18 | sys.path.append(os.getcwd())
19 | if 'DJANGO_SETTINGS_MODULE' in os.environ:
20 | try:
21 | return import_module(os.environ['DJANGO_SETTINGS_MODULE'])
22 | except ImportError, exc:
23 | raise SettingsImportError(textwrap.dedent("""\
24 | There was an error importing the module specified by the
25 | DJANGO_SETTINGS_MODULE environment variable. Make sure that it
26 | refers to a valid and importable Python module."""), exc)
27 |
28 | try:
29 | import settings
30 | except ImportError, exc:
31 | raise SettingsImportError(textwrap.dedent("""\
32 | Couldn't import a settings module. Make sure that a `settings.py`
33 | file exists in the current directory, and that it can be imported,
34 | or that the DJANGO_SETTINGS_MODULE environment variable points
35 | to a valid and importable Python module."""), exc)
36 | return settings
37 |
38 |
39 | def find_commands(app):
40 | """Return a dict of `command_name: command_obj` for the given app."""
41 |
42 | commands = {}
43 | app_module = import_module(app) # Fail loudly if an app doesn't exist.
44 | try:
45 | commands_module = import_module(app + '.commands')
46 | except ImportError:
47 | pass
48 | else:
49 | for command in vars(commands_module).itervalues():
50 | if isinstance(command, Command):
51 | commands[command.name] = command
52 | return commands
53 |
54 |
55 | def find_all_commands(apps):
56 | """Return a dict of `command_name: command_obj` for all the given apps."""
57 |
58 | commands = {}
59 | commands.update(find_commands('djboss'))
60 | for app in apps:
61 | commands.update(find_commands(app))
62 | return commands
63 |
64 |
65 | def main():
66 | try:
67 | settings = get_settings()
68 | except SettingsImportError, exc:
69 | print >> sys.stderr, exc.args[0]
70 | print >> sys.stderr
71 | print >> sys.stderr, "The original exception was:"
72 | print >> sys.stderr, '\t' + str(exc.args[1])
73 | sys.exit(1)
74 |
75 | from django.core import management as mgmt
76 | mgmt.setup_environ(settings)
77 |
78 | commands = find_all_commands(settings.INSTALLED_APPS)
79 |
80 | from djboss.parser import PARSER
81 |
82 | PARSER.set_defaults(settings=settings)
83 | if settings.DEBUG:
84 | PARSER.set_defaults(log_level='DEBUG')
85 | else:
86 | PARSER.set_defaults(log_level='WARN')
87 |
88 | args = PARSER.parse_args()
89 | logging.root.setLevel(getattr(logging, args.log_level))
90 |
91 | # Call the command.
92 | commands[args.command](args)
93 |
94 |
95 | if __name__ == '__main__':
96 | main()
97 |
--------------------------------------------------------------------------------
/src/djboss/commands.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import functools
4 | import re
5 | import sys
6 |
7 | from djboss.parser import SUBPARSERS
8 |
9 |
10 | __all__ = ['Command', 'command', 'argument', 'APP_LABEL', 'MODEL_LABEL']
11 |
12 |
13 | class Command(object):
14 |
15 | """Wrapper to manage creation and population of sub-parsers on functions."""
16 |
17 | def __init__(self, function, **kwargs):
18 | self.function = function
19 | self.parser = self._make_parser(**kwargs)
20 | self._init_arguments()
21 |
22 | add_argument = property(lambda self: self.parser.add_argument)
23 |
24 | def __call__(self, args):
25 | return self.function(args)
26 |
27 | def name(self):
28 | """The name of this command."""
29 |
30 | if hasattr(self.function, 'djboss_name'):
31 | return self.function.djboss_name
32 | else:
33 | return self.function.__name__.replace('_', '-')
34 | name = property(name)
35 |
36 | def help(self):
37 | if hasattr(self.function, 'djboss_help'):
38 | return self.function.djboss_help
39 | elif getattr(self.function, '__doc__', None):
40 | # Just the first line of the docstring.
41 | return self.function.__doc__.splitlines()[0]
42 | help = property(help)
43 |
44 | def description(self):
45 | if hasattr(self.function, 'djboss_description'):
46 | return self.function.djboss_description
47 | elif getattr(self.function, '__doc__', None):
48 | return self.function.__doc__
49 | description = property(description)
50 |
51 | def _make_parser(self, **kwargs):
52 | """Create and register a subparser for this command."""
53 |
54 | kwargs.setdefault('help', self.help)
55 | kwargs.setdefault('description', self.description)
56 | return SUBPARSERS.add_parser(self.name, **kwargs)
57 |
58 | def _init_arguments(self):
59 | """Initialize the subparser with arguments stored on the function."""
60 |
61 | if hasattr(self.function, 'djboss_arguments'):
62 | while self.function.djboss_arguments:
63 | args, kwargs = self.function.djboss_arguments.pop()
64 | self.add_argument(*args, **kwargs)
65 |
66 |
67 | def APP_LABEL(label=None, **kwargs):
68 |
69 | """
70 | argparse type to resolve arguments to Django apps.
71 |
72 | Example Usage:
73 |
74 | * `@argument('app', type=APP_LABEL)`
75 | * `@argument('app', type=APP_LABEL(empty=False))`
76 | * `APP_LABEL('auth')` => ``
77 | """
78 |
79 | from django.db import models
80 | from django.conf import settings
81 | from django.utils.importlib import import_module
82 |
83 | if label is None:
84 | return functools.partial(APP_LABEL, **kwargs)
85 |
86 | # `get_app('auth')` will return the `django.contrib.auth.models` module.
87 | models_module = models.get_app(label, emptyOK=kwargs.get('empty', True))
88 | if models_module is None:
89 | for installed_app in settings.INSTALLED_APPS:
90 | # 'app' should resolve to 'path.to.app'.
91 | if installed_app.split('.')[-1] == label:
92 | return import_module(installed_app)
93 | else:
94 | # 'path.to.app.models' => 'path.to.app'
95 | return import_module(models_module.__name__.rsplit('.', 1)[0])
96 |
97 |
98 | def MODEL_LABEL(label):
99 |
100 | """
101 | argparse type to resolve arguments to Django models.
102 |
103 | Example Usage:
104 |
105 | * `@argument('app.model', type=MODEL_LABEL)
106 | * `MODEL_LABEL('auth.user')` => ``
107 | """
108 |
109 | from django.db import models
110 |
111 | match = re.match(r'^([\w_]+)\.([\w_]+)$', label)
112 | if not match:
113 | raise TypeError
114 |
115 | model = models.get_model(*match.groups())
116 | if not model:
117 | raise ValueError
118 | return model
119 |
120 |
121 | def command(*args, **kwargs):
122 | """Decorator to declare that a function is a command."""
123 |
124 | def decorator(function):
125 | return Command(function, **kwargs)
126 |
127 | if args:
128 | return decorator(*args)
129 | return decorator
130 |
131 |
132 | def argument(*args, **kwargs):
133 | """Decorator to add an argument to a command."""
134 |
135 | def decorator(function):
136 | if isinstance(function, Command):
137 | func = function.function
138 | else:
139 | func = function
140 |
141 | if not hasattr(func, 'djboss_arguments'):
142 | func.djboss_arguments = []
143 | func.djboss_arguments.append((args, kwargs))
144 |
145 | return function
146 | return decorator
147 |
148 |
149 | def manage(args):
150 | """Run native Django management commands under djboss."""
151 |
152 | from django.core import management as mgmt
153 |
154 | OldOptionParser = mgmt.LaxOptionParser
155 | class LaxOptionParser(mgmt.LaxOptionParser):
156 | def __init__(self, *args, **kwargs):
157 | kwargs['prog'] = 'djboss manage'
158 | OldOptionParser.__init__(self, *args, **kwargs)
159 | mgmt.LaxOptionParser = LaxOptionParser
160 |
161 | utility = mgmt.ManagementUtility(['djboss manage'] + args.args)
162 | utility.prog_name = 'djboss manage'
163 | utility.execute()
164 |
165 | # `prefix_chars='\x00'` will stop argparse from interpreting the management
166 | # sub-command options as options on this command. Unless, of course, those
167 | # arguments begin with a null byte.
168 | manage = Command(manage, add_help=False, prefix_chars='\x00')
169 | manage.add_argument('args', nargs='*')
170 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `django-boss`
2 |
3 | `django-boss` is an implementation of the ideas outlined in [my blog post][0] on
4 | Django management commands. With `django-boss`, you can specify commands in
5 | individual apps and then run them using the `djboss` command-line interface.
6 |
7 | [0]: http://blog.zacharyvoase.com/2009/12/09/django-boss/
8 |
9 | ## News
10 |
11 | #### 0.6.1
12 |
13 | If `settings.DEBUG` is `True`, the default logging level will be set to `DEBUG`.
14 | Otherwise it will be `WARN`.
15 |
16 | ### 0.6
17 |
18 | `django-boss` is now free and unencumbered software released into the public
19 | domain.
20 |
21 | ### 0.5
22 |
23 | You can now use the `MODEL_LABEL` argparse type to define arguments which take
24 | model specifiers. The arguments object will contain the model class. For
25 | example:
26 |
27 | from djboss.commands import *
28 |
29 | @command
30 | @argument('model', type=MODEL_LABEL)
31 | def print_model(args):
32 | """Print the class object for a specified model."""
33 | print args.model
34 |
35 | Could be run like this:
36 |
37 | $ djboss print-model auth.user
38 |
39 |
40 | Model specifiers take the form `app_label.model_name`, where `model_name` is
41 | case-insensitive.
42 |
43 | ### 0.4
44 |
45 | You can now use the `APP_LABEL` argparse type to define an argument which takes
46 | the name of an installed Django app; the attribute on the arguments object will
47 | hold the appropriate module object. For example, this:
48 |
49 | from djboss.commands import *
50 |
51 | @command
52 | @argument('app', type=APP_LABEL)
53 | def print_app(args):
54 | """Print the module object for a specified app."""
55 | print args.app
56 |
57 | Would run like this:
58 |
59 | $ djboss print-app auth
60 |
61 |
62 | ### 0.2
63 |
64 | `django-boss` now comes with a `manage` command that lets you run *native*
65 | Django management commands under `djboss`. For example:
66 |
67 | $ djboss manage --help
68 | $ djboss manage syncdb
69 | $ djboss manage runserver --help
70 |
71 | So you can (if you want) delete the `./manage.py` file in your project and use
72 | `djboss` exclusively!
73 |
74 | ## Installing `django-boss`
75 |
76 | At the moment, installation is done via `easy_install django-boss` or
77 | `pip install django-boss`. The only prerequisites are
78 | [argparse](http://argparse.googlecode.com), whose installation is handled by
79 | setuptools, and [Django](http://djangoproject.com/), which you should have
80 | installed by now anyway.
81 |
82 | ## Writing Commands
83 |
84 | Commands are defined as instances of `djboss.commands.Command`, present in a
85 | `commands` submodule inside an installed app. For example, take the following
86 | app layout:
87 |
88 | echoapp/
89 | |-- __init__.py
90 | |-- commands.py
91 | `-- models.py
92 |
93 | The `commands.py` file is a submodule that can be imported as
94 | `echoapp.commands`.
95 |
96 | ### With Decorators
97 |
98 | The following is a complete example of a valid `commands.py` file:
99 |
100 | from djboss.commands import *
101 |
102 | @command
103 | def hello(args):
104 | """Print a cliche to the console."""
105 |
106 | print "Hello, World!"
107 |
108 | This example uses the `@command` decorator to declare that the function is a
109 | `django-boss` command. You can add arguments to commands too; just use the
110 | `@argument` decorator (make sure they come *after* the `@command`):
111 |
112 | @command
113 | @argument('-n', '--no-newline', action='store_true',
114 | help="Don't append a trailing newline.")
115 | def hello(args):
116 | """Print a cliche to the console."""
117 |
118 | if args.no_newline:
119 | import sys
120 | sys.stdout.write("Hello, World!")
121 | else:
122 | print "Hello, World!"
123 |
124 | The `@argument` decorator accepts whatever
125 | `argparse.ArgumentParser.add_argument()` does; consult the [argparse docs][1]
126 | for more information.
127 |
128 | [1]: http://argparse.googlecode.com/svn/tags/r101/doc/add_argument.html
129 |
130 | You can also annotate commands by giving keyword arguments to `@command`:
131 |
132 | @command(name="something", description="Does something.")
133 | def do_something(args):
134 | """Do something."""
135 |
136 | print "something has been done."
137 |
138 | In this case, the command will be called `"something"` instead of the
139 | auto-generated `"do-something"`, and its description will differ from its
140 | docstring. For more information on what can be passed in here, consult the
141 | [argparse.ArgumentParser docs][2].
142 |
143 | [2]: http://argparse.googlecode.com/svn/tags/r101/doc/ArgumentParser.html
144 |
145 | ### Without Decorators
146 |
147 | The API is very similar without decorators. The `Command` class is used to wrap
148 | functions, and you can give keyword arguments when invoking it as with
149 | `@command`:
150 |
151 | def echo(args):
152 | ...
153 | echo = Command(echo, name='...', description='...')
154 |
155 | Adding arguments uses the `Command.add_argument()` method, which is just a
156 | reference to the generated sub-parser’s `add_argument()` method:
157 |
158 | def echo(args):
159 | ...
160 | echo = Command(echo, name='...', description='...')
161 | echo.add_argument('-n', '--no-newline', ...)
162 | echo.add_argument('words', nargs='*')
163 |
164 | ## Running Commands
165 |
166 | Commands are executed via the `djboss` command-line interface. For this to run
167 | correctly, you need one of two things:
168 |
169 | * A `DJANGO_SETTINGS_MODULE` environment variable which refers to a valid,
170 | importable Python module.
171 | * A valid, importable `settings` module in the current working directory.
172 |
173 | Once one of those is covered, you can run it:
174 |
175 | $ djboss --help
176 | usage: djboss [-h] [-v] [-l LEVEL] COMMAND ...
177 |
178 | Run django-boss management commands.
179 |
180 | optional arguments:
181 | -h, --help show this help message and exit
182 | -v, --version show program's version number and exit
183 | -l LEVEL, --log-level LEVEL
184 | Choose a log level from DEBUG, INFO, WARN (default)
185 | or ERROR.
186 |
187 | commands:
188 | COMMAND
189 | echo Echo the arguments back to the console.
190 | hello Print a cliche to the console.
191 |
192 | To discover sub-commands, djboss first finds and imports your Django settings.
193 | The DJANGO_SETTINGS_MODULE environment variable takes precedence, but if
194 | unspecified, djboss will look for a `settings` module in the current
195 | directory. Commands should be defined in a `commands` submodule of each app.
196 | djboss will search each of your INSTALLED_APPS for management commands.
197 |
198 | Each subcommand gets a `--help` option too:
199 |
200 | $ djboss echo --help
201 | usage: djboss echo [-h] [-n] [words [words ...]]
202 |
203 | Echo the arguments back to the console.
204 |
205 | positional arguments:
206 | words
207 |
208 | optional arguments:
209 | -h, --help show this help message and exit
210 | -n, --no-newline Don't print a newline afterwards.
211 |
212 | And then you can run it:
213 |
214 | $ djboss echo some words here
215 | some words here
216 |
217 | More of the same:
218 |
219 | $ djboss hello --help
220 | usage: djboss hello [-h]
221 |
222 | Print a cliche to the console.
223 |
224 | optional arguments:
225 | -h, --help show this help message and exit
226 |
227 | And finally:
228 |
229 | $ djboss hello
230 | Hello, World!
231 |
232 | ## (Un)license
233 |
234 | This is free and unencumbered software released into the public domain.
235 |
236 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
237 | software, either in source code form or as a compiled binary, for any purpose,
238 | commercial or non-commercial, and by any means.
239 |
240 | In jurisdictions that recognize copyright laws, the author or authors of this
241 | software dedicate any and all copyright interest in the software to the public
242 | domain. We make this dedication for the benefit of the public at large and to
243 | the detriment of our heirs and successors. We intend this dedication to be an
244 | overt act of relinquishment in perpetuity of all present and future rights to
245 | this software under copyright law.
246 |
247 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
248 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
249 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE
250 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
251 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
252 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
253 |
254 | For more information, please refer to
255 |
--------------------------------------------------------------------------------
/distribute_setup.py:
--------------------------------------------------------------------------------
1 | #!python
2 | """Bootstrap distribute installation
3 |
4 | If you want to use setuptools in your package's setup.py, just include this
5 | file in the same directory with it, and add this to the top of your setup.py::
6 |
7 | from distribute_setup import use_setuptools
8 | use_setuptools()
9 |
10 | If you want to require a specific version of setuptools, set a download
11 | mirror, or use an alternate download directory, you can do so by supplying
12 | the appropriate options to ``use_setuptools()``.
13 |
14 | This file can also be run as a script to install or upgrade setuptools.
15 | """
16 | import os
17 | import sys
18 | import time
19 | import fnmatch
20 | import tempfile
21 | import tarfile
22 | from distutils import log
23 |
24 | try:
25 | from site import USER_SITE
26 | except ImportError:
27 | USER_SITE = None
28 |
29 | try:
30 | import subprocess
31 |
32 | def _python_cmd(*args):
33 | args = (sys.executable,) + args
34 | return subprocess.call(args) == 0
35 |
36 | except ImportError:
37 | # will be used for python 2.3
38 | def _python_cmd(*args):
39 | args = (sys.executable,) + args
40 | # quoting arguments if windows
41 | if sys.platform == 'win32':
42 | def quote(arg):
43 | if ' ' in arg:
44 | return '"%s"' % arg
45 | return arg
46 | args = [quote(arg) for arg in args]
47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
48 |
49 | DEFAULT_VERSION = "0.6.10"
50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
51 | SETUPTOOLS_FAKED_VERSION = "0.6c11"
52 |
53 | SETUPTOOLS_PKG_INFO = """\
54 | Metadata-Version: 1.0
55 | Name: setuptools
56 | Version: %s
57 | Summary: xxxx
58 | Home-page: xxx
59 | Author: xxx
60 | Author-email: xxx
61 | License: xxx
62 | Description: xxx
63 | """ % SETUPTOOLS_FAKED_VERSION
64 |
65 |
66 | def _install(tarball):
67 | # extracting the tarball
68 | tmpdir = tempfile.mkdtemp()
69 | log.warn('Extracting in %s', tmpdir)
70 | old_wd = os.getcwd()
71 | try:
72 | os.chdir(tmpdir)
73 | tar = tarfile.open(tarball)
74 | _extractall(tar)
75 | tar.close()
76 |
77 | # going in the directory
78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
79 | os.chdir(subdir)
80 | log.warn('Now working in %s', subdir)
81 |
82 | # installing
83 | log.warn('Installing Distribute')
84 | if not _python_cmd('setup.py', 'install'):
85 | log.warn('Something went wrong during the installation.')
86 | log.warn('See the error message above.')
87 | finally:
88 | os.chdir(old_wd)
89 |
90 |
91 | def _build_egg(egg, tarball, to_dir):
92 | # extracting the tarball
93 | tmpdir = tempfile.mkdtemp()
94 | log.warn('Extracting in %s', tmpdir)
95 | old_wd = os.getcwd()
96 | try:
97 | os.chdir(tmpdir)
98 | tar = tarfile.open(tarball)
99 | _extractall(tar)
100 | tar.close()
101 |
102 | # going in the directory
103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
104 | os.chdir(subdir)
105 | log.warn('Now working in %s', subdir)
106 |
107 | # building an egg
108 | log.warn('Building a Distribute egg in %s', to_dir)
109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
110 |
111 | finally:
112 | os.chdir(old_wd)
113 | # returning the result
114 | log.warn(egg)
115 | if not os.path.exists(egg):
116 | raise IOError('Could not build the egg.')
117 |
118 |
119 | def _do_download(version, download_base, to_dir, download_delay):
120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
121 | % (version, sys.version_info[0], sys.version_info[1]))
122 | if not os.path.exists(egg):
123 | tarball = download_setuptools(version, download_base,
124 | to_dir, download_delay)
125 | _build_egg(egg, tarball, to_dir)
126 | sys.path.insert(0, egg)
127 | import setuptools
128 | setuptools.bootstrap_install_from = egg
129 |
130 |
131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
132 | to_dir=os.curdir, download_delay=15, no_fake=True):
133 | # making sure we use the absolute path
134 | to_dir = os.path.abspath(to_dir)
135 | was_imported = 'pkg_resources' in sys.modules or \
136 | 'setuptools' in sys.modules
137 | try:
138 | try:
139 | import pkg_resources
140 | if not hasattr(pkg_resources, '_distribute'):
141 | if not no_fake:
142 | _fake_setuptools()
143 | raise ImportError
144 | except ImportError:
145 | return _do_download(version, download_base, to_dir, download_delay)
146 | try:
147 | pkg_resources.require("distribute>="+version)
148 | return
149 | except pkg_resources.VersionConflict:
150 | e = sys.exc_info()[1]
151 | if was_imported:
152 | sys.stderr.write(
153 | "The required version of distribute (>=%s) is not available,\n"
154 | "and can't be installed while this script is running. Please\n"
155 | "install a more recent version first, using\n"
156 | "'easy_install -U distribute'."
157 | "\n\n(Currently using %r)\n" % (version, e.args[0]))
158 | sys.exit(2)
159 | else:
160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok
161 | return _do_download(version, download_base, to_dir,
162 | download_delay)
163 | except pkg_resources.DistributionNotFound:
164 | return _do_download(version, download_base, to_dir,
165 | download_delay)
166 | finally:
167 | if not no_fake:
168 | _create_fake_setuptools_pkg_info(to_dir)
169 |
170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
171 | to_dir=os.curdir, delay=15):
172 | """Download distribute from a specified location and return its filename
173 |
174 | `version` should be a valid distribute version number that is available
175 | as an egg for download under the `download_base` URL (which should end
176 | with a '/'). `to_dir` is the directory where the egg will be downloaded.
177 | `delay` is the number of seconds to pause before an actual download
178 | attempt.
179 | """
180 | # making sure we use the absolute path
181 | to_dir = os.path.abspath(to_dir)
182 | try:
183 | from urllib.request import urlopen
184 | except ImportError:
185 | from urllib2 import urlopen
186 | tgz_name = "distribute-%s.tar.gz" % version
187 | url = download_base + tgz_name
188 | saveto = os.path.join(to_dir, tgz_name)
189 | src = dst = None
190 | if not os.path.exists(saveto): # Avoid repeated downloads
191 | try:
192 | log.warn("Downloading %s", url)
193 | src = urlopen(url)
194 | # Read/write all in one block, so we don't create a corrupt file
195 | # if the download is interrupted.
196 | data = src.read()
197 | dst = open(saveto, "wb")
198 | dst.write(data)
199 | finally:
200 | if src:
201 | src.close()
202 | if dst:
203 | dst.close()
204 | return os.path.realpath(saveto)
205 |
206 |
207 | def _patch_file(path, content):
208 | """Will backup the file then patch it"""
209 | existing_content = open(path).read()
210 | if existing_content == content:
211 | # already patched
212 | log.warn('Already patched.')
213 | return False
214 | log.warn('Patching...')
215 | _rename_path(path)
216 | f = open(path, 'w')
217 | try:
218 | f.write(content)
219 | finally:
220 | f.close()
221 | return True
222 |
223 |
224 | def _same_content(path, content):
225 | return open(path).read() == content
226 |
227 | def _no_sandbox(function):
228 | def __no_sandbox(*args, **kw):
229 | try:
230 | from setuptools.sandbox import DirectorySandbox
231 | def violation(*args):
232 | pass
233 | DirectorySandbox._old = DirectorySandbox._violation
234 | DirectorySandbox._violation = violation
235 | patched = True
236 | except ImportError:
237 | patched = False
238 |
239 | try:
240 | return function(*args, **kw)
241 | finally:
242 | if patched:
243 | DirectorySandbox._violation = DirectorySandbox._old
244 | del DirectorySandbox._old
245 |
246 | return __no_sandbox
247 |
248 | @_no_sandbox
249 | def _rename_path(path):
250 | new_name = path + '.OLD.%s' % time.time()
251 | log.warn('Renaming %s into %s', path, new_name)
252 | os.rename(path, new_name)
253 | return new_name
254 |
255 | def _remove_flat_installation(placeholder):
256 | if not os.path.isdir(placeholder):
257 | log.warn('Unkown installation at %s', placeholder)
258 | return False
259 | found = False
260 | for file in os.listdir(placeholder):
261 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
262 | found = True
263 | break
264 | if not found:
265 | log.warn('Could not locate setuptools*.egg-info')
266 | return
267 |
268 | log.warn('Removing elements out of the way...')
269 | pkg_info = os.path.join(placeholder, file)
270 | if os.path.isdir(pkg_info):
271 | patched = _patch_egg_dir(pkg_info)
272 | else:
273 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
274 |
275 | if not patched:
276 | log.warn('%s already patched.', pkg_info)
277 | return False
278 | # now let's move the files out of the way
279 | for element in ('setuptools', 'pkg_resources.py', 'site.py'):
280 | element = os.path.join(placeholder, element)
281 | if os.path.exists(element):
282 | _rename_path(element)
283 | else:
284 | log.warn('Could not find the %s element of the '
285 | 'Setuptools distribution', element)
286 | return True
287 |
288 |
289 | def _after_install(dist):
290 | log.warn('After install bootstrap.')
291 | placeholder = dist.get_command_obj('install').install_purelib
292 | _create_fake_setuptools_pkg_info(placeholder)
293 |
294 | @_no_sandbox
295 | def _create_fake_setuptools_pkg_info(placeholder):
296 | if not placeholder or not os.path.exists(placeholder):
297 | log.warn('Could not find the install location')
298 | return
299 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
300 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \
301 | (SETUPTOOLS_FAKED_VERSION, pyver)
302 | pkg_info = os.path.join(placeholder, setuptools_file)
303 | if os.path.exists(pkg_info):
304 | log.warn('%s already exists', pkg_info)
305 | return
306 |
307 | log.warn('Creating %s', pkg_info)
308 | f = open(pkg_info, 'w')
309 | try:
310 | f.write(SETUPTOOLS_PKG_INFO)
311 | finally:
312 | f.close()
313 |
314 | pth_file = os.path.join(placeholder, 'setuptools.pth')
315 | log.warn('Creating %s', pth_file)
316 | f = open(pth_file, 'w')
317 | try:
318 | f.write(os.path.join(os.curdir, setuptools_file))
319 | finally:
320 | f.close()
321 |
322 | def _patch_egg_dir(path):
323 | # let's check if it's already patched
324 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
325 | if os.path.exists(pkg_info):
326 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
327 | log.warn('%s already patched.', pkg_info)
328 | return False
329 | _rename_path(path)
330 | os.mkdir(path)
331 | os.mkdir(os.path.join(path, 'EGG-INFO'))
332 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
333 | f = open(pkg_info, 'w')
334 | try:
335 | f.write(SETUPTOOLS_PKG_INFO)
336 | finally:
337 | f.close()
338 | return True
339 |
340 |
341 | def _before_install():
342 | log.warn('Before install bootstrap.')
343 | _fake_setuptools()
344 |
345 |
346 | def _under_prefix(location):
347 | if 'install' not in sys.argv:
348 | return True
349 | args = sys.argv[sys.argv.index('install')+1:]
350 | for index, arg in enumerate(args):
351 | for option in ('--root', '--prefix'):
352 | if arg.startswith('%s=' % option):
353 | top_dir = arg.split('root=')[-1]
354 | return location.startswith(top_dir)
355 | elif arg == option:
356 | if len(args) > index:
357 | top_dir = args[index+1]
358 | return location.startswith(top_dir)
359 | elif option == '--user' and USER_SITE is not None:
360 | return location.startswith(USER_SITE)
361 | return True
362 |
363 |
364 | def _fake_setuptools():
365 | log.warn('Scanning installed packages')
366 | try:
367 | import pkg_resources
368 | except ImportError:
369 | # we're cool
370 | log.warn('Setuptools or Distribute does not seem to be installed.')
371 | return
372 | ws = pkg_resources.working_set
373 | try:
374 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
375 | replacement=False))
376 | except TypeError:
377 | # old distribute API
378 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
379 |
380 | if setuptools_dist is None:
381 | log.warn('No setuptools distribution found')
382 | return
383 | # detecting if it was already faked
384 | setuptools_location = setuptools_dist.location
385 | log.warn('Setuptools installation detected at %s', setuptools_location)
386 |
387 | # if --root or --preix was provided, and if
388 | # setuptools is not located in them, we don't patch it
389 | if not _under_prefix(setuptools_location):
390 | log.warn('Not patching, --root or --prefix is installing Distribute'
391 | ' in another location')
392 | return
393 |
394 | # let's see if its an egg
395 | if not setuptools_location.endswith('.egg'):
396 | log.warn('Non-egg installation')
397 | res = _remove_flat_installation(setuptools_location)
398 | if not res:
399 | return
400 | else:
401 | log.warn('Egg installation')
402 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
403 | if (os.path.exists(pkg_info) and
404 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
405 | log.warn('Already patched.')
406 | return
407 | log.warn('Patching...')
408 | # let's create a fake egg replacing setuptools one
409 | res = _patch_egg_dir(setuptools_location)
410 | if not res:
411 | return
412 | log.warn('Patched done.')
413 | _relaunch()
414 |
415 |
416 | def _relaunch():
417 | log.warn('Relaunching...')
418 | # we have to relaunch the process
419 | args = [sys.executable] + sys.argv
420 | sys.exit(subprocess.call(args))
421 |
422 |
423 | def _extractall(self, path=".", members=None):
424 | """Extract all members from the archive to the current working
425 | directory and set owner, modification time and permissions on
426 | directories afterwards. `path' specifies a different directory
427 | to extract to. `members' is optional and must be a subset of the
428 | list returned by getmembers().
429 | """
430 | import copy
431 | import operator
432 | from tarfile import ExtractError
433 | directories = []
434 |
435 | if members is None:
436 | members = self
437 |
438 | for tarinfo in members:
439 | if tarinfo.isdir():
440 | # Extract directories with a safe mode.
441 | directories.append(tarinfo)
442 | tarinfo = copy.copy(tarinfo)
443 | tarinfo.mode = 448 # decimal for oct 0700
444 | self.extract(tarinfo, path)
445 |
446 | # Reverse sort directories.
447 | if sys.version_info < (2, 4):
448 | def sorter(dir1, dir2):
449 | return cmp(dir1.name, dir2.name)
450 | directories.sort(sorter)
451 | directories.reverse()
452 | else:
453 | directories.sort(key=operator.attrgetter('name'), reverse=True)
454 |
455 | # Set correct owner, mtime and filemode on directories.
456 | for tarinfo in directories:
457 | dirpath = os.path.join(path, tarinfo.name)
458 | try:
459 | self.chown(tarinfo, dirpath)
460 | self.utime(tarinfo, dirpath)
461 | self.chmod(tarinfo, dirpath)
462 | except ExtractError:
463 | e = sys.exc_info()[1]
464 | if self.errorlevel > 1:
465 | raise
466 | else:
467 | self._dbg(1, "tarfile: %s" % e)
468 |
469 |
470 | def main(argv, version=DEFAULT_VERSION):
471 | """Install or upgrade setuptools and EasyInstall"""
472 | tarball = download_setuptools()
473 | _install(tarball)
474 |
475 |
476 | if __name__ == '__main__':
477 | main(sys.argv[1:])
478 |
--------------------------------------------------------------------------------