├── 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 | --------------------------------------------------------------------------------