Django

Code

root/django/trunk/django/core/management/base.py

Revision 9110, 15.8 kB (checked in by russellm, 2 months ago)

Promoted --verbosity to be a top level option for all management commands. Also added -v as a consistent short form of --verbosity. This is mostly to save Malcolm's poor arthritic fingers.

  • Property svn:eol-style set to native
Line 
1 """
2 Base classes for writing management commands (named commands which can
3 be executed through ``django-admin.py`` or ``manage.py``).
4
5 """
6
7 import os
8 import sys
9 from optparse import make_option, OptionParser
10
11 import django
12 from django.core.exceptions import ImproperlyConfigured
13 from django.core.management.color import color_style
14
15 try:
16     set
17 except NameError:
18     from sets import Set as set     # For Python 2.3
19
20 class CommandError(Exception):
21     """
22     Exception class indicating a problem while executing a management
23     command.
24
25     If this exception is raised during the execution of a management
26     command, it will be caught and turned into a nicely-printed error
27     message to the appropriate output stream (i.e., stderr); as a
28     result, raising this exception (with a sensible description of the
29     error) is the preferred way to indicate that something has gone
30     wrong in the execution of a command.
31     
32     """
33     pass
34
35 def handle_default_options(options):
36     """
37     Include any default options that all commands should accept here
38     so that ManagementUtility can handle them before searching for
39     user commands.
40     
41     """
42     if options.settings:
43         os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
44     if options.pythonpath:
45         sys.path.insert(0, options.pythonpath)
46
47 class BaseCommand(object):
48     """
49     The base class from which all management commands ultimately
50     derive.
51
52     Use this class if you want access to all of the mechanisms which
53     parse the command-line arguments and work out what code to call in
54     response; if you don't need to change any of that behavior,
55     consider using one of the subclasses defined in this file.
56
57     If you are interested in overriding/customizing various aspects of
58     the command-parsing and -execution behavior, the normal flow works
59     as follows:
60
61     1. ``django-admin.py`` or ``manage.py`` loads the command class
62        and calls its ``run_from_argv()`` method.
63
64     2. The ``run_from_argv()`` method calls ``create_parser()`` to get
65        an ``OptionParser`` for the arguments, parses them, performs
66        any environment changes requested by options like
67        ``pythonpath``, and then calls the ``execute()`` method,
68        passing the parsed arguments.
69
70     3. The ``execute()`` method attempts to carry out the command by
71        calling the ``handle()`` method with the parsed arguments; any
72        output produced by ``handle()`` will be printed to standard
73        output and, if the command is intended to produce a block of
74        SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``.
75
76     4. If ``handle()`` raised a ``ComandError``, ``execute()`` will
77        instead print an error message to ``stderr``.
78
79     Thus, the ``handle()`` method is typically the starting point for
80     subclasses; many built-in commands and command types either place
81     all of their logic in ``handle()``, or perform some additional
82     parsing work in ``handle()`` and then delegate from it to more
83     specialized methods as needed.
84
85     Several attributes affect behavior at various steps along the way:
86     
87     ``args``
88         A string listing the arguments accepted by the command,
89         suitable for use in help messages; e.g., a command which takes
90         a list of application names might set this to '<appname
91         appname ...>'.
92
93     ``can_import_settings``
94         A boolean indicating whether the command needs to be able to
95         import Django settings; if ``True``, ``execute()`` will verify
96         that this is possible before proceeding. Default value is
97         ``True``.
98
99     ``help``
100         A short description of the command, which will be printed in
101         help messages.
102
103     ``option_list``
104         This is the list of ``optparse`` options which will be fed
105         into the command's ``OptionParser`` for parsing arguments.
106
107     ``output_transaction``
108         A boolean indicating whether the command outputs SQL
109         statements; if ``True``, the output will automatically be
110         wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is
111         ``False``.
112
113     ``requires_model_validation``
114         A boolean; if ``True``, validation of installed models will be
115         performed prior to executing the command. Default value is
116         ``True``. To validate an individual application's models
117         rather than all applications' models, call
118         ``self.validate(app)`` from ``handle()``, where ``app`` is the
119         application's Python module.
120     
121     """
122     # Metadata about this command.
123     option_list = (
124         make_option('-v', '--verbosity', action='store', dest='verbosity', default='1',
125             type='choice', choices=['0', '1', '2'],
126             help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
127         make_option('--settings',
128             help='The Python path to a settings module, e.g. "myproject.settings.main". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.'),
129         make_option('--pythonpath',
130             help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'),
131         make_option('--traceback', action='store_true',
132             help='Print traceback on exception'),
133     )
134     help = ''
135     args = ''
136
137     # Configuration shortcuts that alter various logic.
138     can_import_settings = True
139     requires_model_validation = True
140     output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
141
142     def __init__(self):
143         self.style = color_style()
144
145     def get_version(self):
146         """
147         Return the Django version, which should be correct for all
148         built-in Django commands. User-supplied commands should
149         override this method.
150         
151         """
152         return django.get_version()
153
154     def usage(self, subcommand):
155         """
156         Return a brief description of how to use this command, by
157         default from the attribute ``self.help``.
158         
159         """
160         usage = '%%prog %s [options] %s' % (subcommand, self.args)
161         if self.help:
162             return '%s\n\n%s' % (usage, self.help)
163         else:
164             return usage
165
166     def create_parser(self, prog_name, subcommand):
167         """
168         Create and return the ``OptionParser`` which will be used to
169         parse the arguments to this command.
170         
171         """
172         return OptionParser(prog=prog_name,
173                             usage=self.usage(subcommand),
174                             version=self.get_version(),
175                             option_list=self.option_list)
176
177     def print_help(self, prog_name, subcommand):
178         """
179         Print the help message for this command, derived from
180         ``self.usage()``.
181         
182         """
183         parser = self.create_parser(prog_name, subcommand)
184         parser.print_help()
185
186     def run_from_argv(self, argv):
187         """
188         Set up any environment changes requested (e.g., Python path
189         and Django settings), then run this command.
190         
191         """
192         parser = self.create_parser(argv[0], argv[1])
193         options, args = parser.parse_args(argv[2:])
194         handle_default_options(options)
195         self.execute(*args, **options.__dict__)
196
197     def execute(self, *args, **options):
198         """
199         Try to execute this command, performing model validation if
200         needed (as controlled by the attribute
201         ``self.requires_model_validation``). If the command raises a
202         ``CommandError``, intercept it and print it sensibly to
203         stderr.
204         
205         """
206         # Switch to English, because django-admin.py creates database content
207         # like permissions, and those shouldn't contain any translations.
208         # But only do this if we can assume we have a working settings file,
209         # because django.utils.translation requires settings.
210         if self.can_import_settings:
211             try:
212                 from django.utils import translation
213                 translation.activate('en-us')
214             except ImportError, e:
215                 # If settings should be available, but aren't,
216                 # raise the error and quit.
217                 sys.stderr.write(self.style.ERROR(str('Error: %s\n' % e)))
218                 sys.exit(1)
219         try:
220             if self.requires_model_validation:
221                 self.validate()
222             output = self.handle(*args, **options)
223             if output:
224                 if self.output_transaction:
225                     # This needs to be imported here, because it relies on settings.
226                     from django.db import connection
227                     if connection.ops.start_transaction_sql():
228                         print self.style.SQL_KEYWORD(connection.ops.start_transaction_sql())
229                 print output
230                 if self.output_transaction:
231                     print self.style.SQL_KEYWORD("COMMIT;")
232         except CommandError, e:
233             sys.stderr.write(self.style.ERROR(str('Error: %s\n' % e)))
234             sys.exit(1)
235
236     def validate(self, app=None, display_num_errors=False):
237         """
238         Validates the given app, raising CommandError for any errors.
239         
240         If app is None, then this will validate all installed apps.
241         
242         """
243         from django.core.management.validation import get_validation_errors
244         try:
245             from cStringIO import StringIO
246         except ImportError:
247             from StringIO import StringIO
248         s = StringIO()
249         num_errors = get_validation_errors(s, app)
250         if num_errors:
251             s.seek(0)
252             error_text = s.read()
253             raise CommandError("One or more models did not validate:\n%s" % error_text)
254         if display_num_errors:
255             print "%s error%s found" % (num_errors, num_errors != 1 and 's' or '')
256
257     def handle(self, *args, **options):
258         """
259         The actual logic of the command. Subclasses must implement
260         this method.
261         
262         """
263         raise NotImplementedError()
264
265 class AppCommand(BaseCommand):
266     """
267     A management command which takes one or more installed application
268     names as arguments, and does something with each of them.
269
270     Rather than implementing ``handle()``, subclasses must implement
271     ``handle_app()``, which will be called once for each application.
272     
273     """
274     args = '<appname appname ...>'
275
276     def handle(self, *app_labels, **options):
277         from django.db import models
278         if not app_labels:
279             raise CommandError('Enter at least one appname.')
280         try:
281             app_list = [models.get_app(app_label) for app_label in app_labels]
282         except (ImproperlyConfigured, ImportError), e:
283             raise CommandError("%s. Are you sure your INSTALLED_APPS setting is correct?" % e)
284         output = []
285         for app in app_list:
286             app_output = self.handle_app(app, **options)
287             if app_output:
288                 output.append(app_output)
289         return '\n'.join(output)
290
291     def handle_app(self, app, **options):
292         """
293         Perform the command's actions for ``app``, which will be the
294         Python module corresponding to an application name given on
295         the command line.
296         
297         """
298         raise NotImplementedError()
299
300 class LabelCommand(BaseCommand):
301     """
302     A management command which takes one or more arbitrary arguments
303     (labels) on the command line, and does something with each of
304     them.
305
306     Rather than implementing ``handle()``, subclasses must implement
307     ``handle_label()``, which will be called once for each label.
308
309     If the arguments should be names of installed applications, use
310     ``AppCommand`` instead.
311     
312     """
313     args = '<label label ...>'
314     label = 'label'
315
316     def handle(self, *labels, **options):
317         if not labels:
318             raise CommandError('Enter at least one %s.' % self.label)
319
320         output = []
321         for label in labels:
322             label_output = self.handle_label(label, **options)
323             if label_output:
324                 output.append(label_output)
325         return '\n'.join(output)
326
327     def handle_label(self, label, **options):
328         """
329         Perform the command's actions for ``label``, which will be the
330         string as given on the command line.
331         
332         """
333         raise NotImplementedError()
334
335 class NoArgsCommand(BaseCommand):
336     """
337     A command which takes no arguments on the command line.
338
339     Rather than implementing ``handle()``, subclasses must implement
340     ``handle_noargs()``; ``handle()`` itself is overridden to ensure
341     no arguments are passed to the command.
342
343     Attempting to pass arguments will raise ``CommandError``.
344     
345     """
346     args = ''
347
348     def handle(self, *args, **options):
349         if args:
350             raise CommandError("Command doesn't accept any arguments")
351         return self.handle_noargs(**options)
352
353     def handle_noargs(self, **options):
354         """
355         Perform this command's actions.
356         
357         """
358         raise NotImplementedError()
359
360 def copy_helper(style, app_or_project, name, directory, other_name=''):
361     """
362     Copies either a Django application layout template or a Django project
363     layout template into the specified directory.
364
365     """
366     # style -- A color style object (see django.core.management.color).
367     # app_or_project -- The string 'app' or 'project'.
368     # name -- The name of the application or project.
369     # directory -- The directory to which the layout template should be copied.
370     # other_name -- When copying an application layout, this should be the name
371     #               of the project.
372     import re
373     import shutil
374     other = {'project': 'app', 'app': 'project'}[app_or_project]
375     if not re.search(r'^[_a-zA-Z]\w*$', name): # If it's not a valid directory name.
376         # Provide a smart error message, depending on the error.
377         if not re.search(r'^[_a-zA-Z]', name):
378             message = 'make sure the name begins with a letter or underscore'
379         else:
380             message = 'use only numbers, letters and underscores'
381         raise CommandError("%r is not a valid %s name. Please %s." % (name, app_or_project, message))
382     top_dir = os.path.join(directory, name)
383     try:
384         os.mkdir(top_dir)
385     except OSError, e:
386         raise CommandError(e)
387
388     # Determine where the app or project templates are. Use
389     # django.__path__[0] because we don't know into which directory
390     # django has been installed.
391     template_dir = os.path.join(django.__path__[0], 'conf', '%s_template' % app_or_project)
392
393     for d, subdirs, files in os.walk(template_dir):
394         relative_dir = d[len(template_dir)+1:].replace('%s_name' % app_or_project, name)
395         if relative_dir:
396             os.mkdir(os.path.join(top_dir, relative_dir))
397         for i, subdir in enumerate(subdirs):
398             if subdir.startswith('.'):
399                 del subdirs[i]
400         for f in files:
401             if f.endswith('.pyc'):
402                 continue
403             path_old = os.path.join(d, f)
404             path_new = os.path.join(top_dir, relative_dir, f.replace('%s_name' % app_or_project, name))
405             fp_old = open(path_old, 'r')
406             fp_new = open(path_new, 'w')
407             fp_new.write(fp_old.read().replace('{{ %s_name }}' % app_or_project, name).replace('{{ %s_name }}' % other, other_name))
408             fp_old.close()
409             fp_new.close()
410             try:
411                 shutil.copymode(path_old, path_new)
412                 _make_writeable(path_new)
413             except OSError:
414                 sys.stderr.write(style.NOTICE("Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" % path_new))
415
416 def _make_writeable(filename):
417     """
418     Make sure that the file is writeable. Useful if our source is
419     read-only.
420     
421     """
422     import stat
423     if sys.platform.startswith('java'):
424         # On Jython there is no os.access()
425         return
426     if not os.access(filename, os.W_OK):
427         st = os.stat(filename)
428         new_permissions = stat.S_IMODE(st.st_mode) | stat.S_IWUSR
429         os.chmod(filename, new_permissions)
Note: See TracBrowser for help on using the browser.