0001#!/usr/bin/env python
0002import optparse
0003import fnmatch
0004import re
0005import os
0006import sys
0007import textwrap
0008import warnings
0009try:
0010    from paste import pyconfig
0011    from paste import CONFIG
0012except ImportError, e:
0013    pyconfig = None
0014    CONFIG = {}
0015import time
0016
0017import sqlobject
0018from sqlobject import col
0019from sqlobject.util import moduleloader
0020from sqlobject.declarative import DeclarativeMeta
0021
0022# It's not very unsafe to use tempnam like we are doing:
0023warnings.filterwarnings(
0024    'ignore', 'tempnam is a potential security risk.*',
0025    RuntimeWarning, '.*command', 28)
0026
0027def nowarning_tempnam(*args, **kw):
0028    return os.tempnam(*args, **kw)
0029
0030class SQLObjectVersionTable(sqlobject.SQLObject):
0031    """
0032    This table is used to store information about the database and
0033    its version (used with record and update commands).
0034    """
0035    class sqlmeta:
0036        table = 'sqlobject_db_version'
0037    version = col.StringCol()
0038    updated = col.DateTimeCol(default=col.DateTimeCol.now)
0039
0040def db_differences(soClass, conn):
0041    """
0042    Returns the differences between a class and the table in a
0043    connection.  Returns [] if no differences are found.  This
0044    function does the best it can; it can miss many differences.
0045    """
0046    # @@: Repeats a lot from CommandStatus.command, but it's hard
0047    # to actually factor out the display logic.  Or I'm too lazy
0048    # to do so.
0049    diffs = []
0050    if not conn.tableExists(soClass.sqlmeta.table):
0051        if soClass.sqlmeta.columns:
0052            diffs.append('Does not exist in database')
0053    else:
0054        try:
0055            columns = conn.columnsFromSchema(soClass.sqlmeta.table,
0056                                             soClass)
0057        except AttributeError:
0058            # Database does not support reading columns
0059            pass
0060        else:
0061            existing = {}
0062            for col in columns:
0063                col = col.withClass(soClass)
0064                existing[col.dbName] = col
0065            missing = {}
0066            for col in soClass.sqlmeta.columnList:
0067                if existing.has_key(col.dbName):
0068                    del existing[col.dbName]
0069                else:
0070                    missing[col.dbName] = col
0071            for col in existing.values():
0072                diffs.append('Database has extra column: %s'
0073                             % col.dbName)
0074            for col in missing.values():
0075                diffs.append('Database missing column: %s' % col.dbName)
0076    return diffs
0077
0078class CommandRunner(object):
0079
0080    def __init__(self):
0081        self.commands = {}
0082        self.command_aliases = {}
0083
0084    def run(self, argv):
0085        invoked_as = argv[0]
0086        args = argv[1:]
0087        for i in range(len(args)):
0088            if not args[i].startswith('-'):
0089                # this must be a command
0090                command = args[i].lower()
0091                del args[i]
0092                break
0093        else:
0094            # no command found
0095            self.invalid('No COMMAND given (try "%s help")'
0096                         % os.path.basename(invoked_as))
0097        real_command = self.command_aliases.get(command, command)
0098        if real_command not in self.commands.keys():
0099            self.invalid('COMMAND %s unknown' % command)
0100        runner = self.commands[real_command](
0101            invoked_as, command, args, self)
0102        runner.run()
0103
0104    def register(self, command):
0105        name = command.name
0106        self.commands[name] = command
0107        for alias in command.aliases:
0108            self.command_aliases[alias] = name
0109
0110    def invalid(self, msg, code=2):
0111        print msg
0112        sys.exit(code)
0113
0114the_runner = CommandRunner()
0115register = the_runner.register
0116
0117def standard_parser(connection=True, simulate=True,
0118                    interactive=False, find_modules=True):
0119    parser = optparse.OptionParser()
0120    parser.add_option('-v', '--verbose',
0121                      help='Be verbose (multiple times for more verbosity)',
0122                      action='count',
0123                      dest='verbose',
0124                      default=0)
0125    if simulate:
0126        parser.add_option('-n', '--simulate',
0127                          help="Don't actually do anything (implies -v)",
0128                          action='store_true',
0129                          dest='simulate')
0130    if connection:
0131        parser.add_option('-c', '--connection',
0132                          help="The database connection URI",
0133                          metavar='URI',
0134                          dest='connection_uri')
0135    parser.add_option('-f', '--config-file',
0136                      help="The Paste config file that contains the database URI (in the database key)",
0137                      metavar="FILE",
0138                      dest="config_file")
0139    if find_modules:
0140        parser.add_option('-m', '--module',
0141                          help="Module in which to find SQLObject classes",
0142                          action='append',
0143                          metavar='MODULE',
0144                          dest='modules',
0145                          default=[])
0146        parser.add_option('-p', '--package',
0147                          help="Package to search for SQLObject classes",
0148                          action="append",
0149                          metavar="PACKAGE",
0150                          dest="packages",
0151                          default=[])
0152        parser.add_option('--class',
0153                          help="Select only named classes (wildcards allowed)",
0154                          action="append",
0155                          metavar="NAME",
0156                          dest="class_matchers",
0157                          default=[])
0158    if interactive:
0159        parser.add_option('-i', '--interactive',
0160                          help="Ask before doing anything (use twice to be more careful)",
0161                          action="count",
0162                          dest="interactive",
0163                          default=0)
0164    parser.add_option('--egg',
0165                      help="Select modules from the given Egg, using sqlobject.txt",
0166                      action="append",
0167                      metavar="EGG_SPEC",
0168                      dest="eggs",
0169                      default=[])
0170    return parser
0171
0172class Command(object):
0173
0174    __metaclass__ = DeclarativeMeta
0175
0176    min_args = 0
0177    min_args_error = 'You must provide at least %(min_args)s arguments'
0178    max_args = 0
0179    max_args_error = 'You must provide no more than %(max_args)s arguments'
0180    aliases = ()
0181    required_args = []
0182    description = None
0183
0184    help = ''
0185
0186    def __classinit__(cls, new_args):
0187        if cls.__bases__ == (object,):
0188            # This abstract base class
0189            return
0190        register(cls)
0191
0192    def __init__(self, invoked_as, command_name, args, runner):
0193        self.invoked_as = invoked_as
0194        self.command_name = command_name
0195        self.raw_args = args
0196        self.runner = runner
0197
0198    def run(self):
0199        self.parser.usage = "%%prog [options]\n%s" % self.summary
0200        if self.help:
0201            help = textwrap.fill(
0202                self.help, int(os.environ.get('COLUMNS', 80))-4)
0203            self.parser.usage += '\n' + help
0204        self.parser.prog = '%s %s' % (
0205            os.path.basename(self.invoked_as),
0206            self.command_name)
0207        if self.description:
0208            self.parser.description = description
0209        self.options, self.args = self.parser.parse_args(self.raw_args)
0210        if (getattr(self.options, 'simulate', False)
0211            and not self.options.verbose):
0212            self.options.verbose = 1
0213        if self.min_args is not None and len(self.args) < self.min_args:
0214            self.runner.invalid(
0215                self.min_args_error % {'min_args': self.min_args,
0216                                       'actual_args': len(self.args)})
0217        if self.max_args is not None and len(self.args) > self.max_args:
0218            self.runner.invalid(
0219                self.max_args_error % {'max_args': self.max_args,
0220                                       'actual_args': len(self.args)})
0221        for var_name, option_name in self.required_args:
0222            if not getattr(self.options, var_name, None):
0223                self.runner.invalid(
0224                    'You must provide the option %s' % option_name)
0225        conf = self.config()
0226        if conf and conf.get('sys_path'):
0227            update_sys_path(conf['sys_path'], self.options.verbose)
0228        if conf and conf.get('database'):
0229            conn = sqlobject.connectionForURI(conf['database'])
0230            sqlobject.sqlhub.processConnection = conn
0231        for egg_spec in getattr(self.options, 'eggs', []):
0232            self.load_options_from_egg(egg_spec)
0233        self.command()
0234
0235    def classes(self, require_connection=True,
0236                require_some=False):
0237        all = []
0238        conf = self.config()
0239        for module_name in self.options.modules:
0240            all.extend(self.classes_from_module(
0241                moduleloader.load_module(module_name)))
0242        for package_name in self.options.packages:
0243            all.extend(self.classes_from_package(package_name))
0244        for egg_spec in self.options.eggs:
0245            all.extend(self.classes_from_egg(egg_spec))
0246        if self.options.class_matchers:
0247            filtered = []
0248            for soClass in all:
0249                name = soClass.__name__
0250                for matcher in self.options.class_matchers:
0251                    if fnmatch.fnmatch(name, matcher):
0252                        filtered.append(soClass)
0253                        break
0254            all = filtered
0255        conn = self.connection()
0256        if conn:
0257            for soClass in all:
0258                soClass._connection = conn
0259        else:
0260            missing = []
0261            for soClass in all:
0262                try:
0263                    if not soClass._connection:
0264                        missing.append(soClass)
0265                except AttributeError:
0266                    missing.append(soClass)
0267            if missing and require_connection:
0268                self.runner.invalid(
0269                    'These classes do not have connections set:\n  * %s\n'
0270                    'You must indicate --connection=URI'
0271                    % '\n  * '.join([soClass.__name__
0272                                     for soClass in missing]))
0273        if require_some and not all:
0274            print 'No classes found!'
0275            if self.options.modules:
0276                print 'Looked in modules: %s' % ', '.join(self.options.modules)
0277            else:
0278                print 'No modules specified'
0279            if self.options.packages:
0280                print 'Looked in packages: %s' % ', '.join(self.options.packages)
0281            else:
0282                print 'No packages specified'
0283            if self.options.class_matchers:
0284                print 'Matching class pattern: %s' % self.options.class_matches
0285            if self.options.eggs:
0286                print 'Looked in eggs: %s' % ', '.join(self.options.eggs)
0287            else:
0288                print 'No eggs specified'
0289            sys.exit(1)
0290        return all
0291
0292    def classes_from_module(self, module):
0293        all = []
0294        if hasattr(module, 'soClasses'):
0295            for name_or_class in module.soClasses:
0296                if isinstance(name_or_class, str):
0297                    name_or_class = getattr(module, name_or_class)
0298                all.append(name_or_class)
0299        else:
0300            for name in dir(module):
0301                value = getattr(module, name)
0302                if (isinstance(value, type)
0303                    and issubclass(value, sqlobject.SQLObject)
0304                    and value.__module__ == module.__name__):
0305                    all.append(value)
0306        return all
0307
0308    def connection(self):
0309        config = self.config()
0310        if config is not None:
0311            assert config.get('database'), (
0312                "No database variable found in config file %s"
0313                % self.options.config_file)
0314            return sqlobject.connectionForURI(config['database'])
0315        elif getattr(self.options, 'connection_uri', None):
0316            return sqlobject.connectionForURI(self.options.connection_uri)
0317        else:
0318            return None
0319
0320    def config(self):
0321        if not getattr(self.options, 'config_file', None):
0322            return None
0323        if pyconfig and self.options.config_fn.endswith('.conf'):
0324            config = pyconfig.Config(with_default=True)
0325            config.load(self.options.config_file)
0326            CONFIG.push_process_config(config)
0327            return config
0328        else:
0329            return self.ini_config(self.options.config_file)
0330
0331    def ini_config(self, conf_fn):
0332        conf_section = 'main'
0333        if '#' in conf_fn:
0334            conf_fn, conf_section = conf_fn.split('#', 1)
0335
0336        from ConfigParser import ConfigParser
0337        p = ConfigParser()
0338        # Case-sensitive:
0339        p.optionxform = str
0340        if not os.path.exists(conf_fn):
0341            # Stupid RawConfigParser doesn't give an error for
0342            # non-existant files:
0343            raise OSError(
0344                "Config file %s does not exist" % self.options.config_file)
0345        p.read([conf_fn])
0346        p._defaults.setdefault(
0347            'here', os.path.dirname(os.path.abspath(conf_fn)))
0348
0349        possible_sections = []
0350        for section in p.sections():
0351            name = section.strip().lower()
0352            if (conf_section == name or
0353                (conf_section == name.split(':')[-1]
0354                 and name.split(':')[0] in ('app', 'application'))):
0355                possible_sections.append(section)
0356
0357        if not possible_sections:
0358            raise OSError(
0359                "Config file %s does not have a section [%s] or [*:%s]"
0360                % (conf_fn, conf_section, conf_section))
0361        if len(possible_sections) > 1:
0362            raise OSError(
0363                "Config file %s has multiple sections matching %s: %s"
0364                % (conf_fn, conf_section, ', '.join(possible_sections)))
0365
0366        config = {}
0367        for op in p.options(possible_sections[0]):
0368            config[op] = p.get(possible_sections[0], op)
0369        return config
0370
0371    def classes_from_package(self, package_name):
0372        all = []
0373        package = moduleloader.load_module(package_name)
0374        package_dir = os.path.dirname(package.__file__)
0375
0376        def find_classes_in_file(arg, dir_name, filenames):
0377            if dir_name.startswith('.svn'):
0378                return
0379            filenames = filter(lambda fname: fname.endswith('.py') and fname != '__init__.py',
0380                               filenames)
0381            for fname in filenames:
0382                module_name = os.path.join(dir_name, fname)
0383                module_name = module_name[module_name.find(package_name):]
0384                module_name = module_name.replace(os.path.sep,'.')[:-3]
0385                try:
0386                    module = moduleloader.load_module(module_name)
0387                except ImportError, err:
0388                    if self.options.verbose:
0389                        print 'Could not import module "%s". Error was : "%s"' % (module_name, err)
0390                    continue
0391                except Exception, exc:
0392                    if self.options.verbose:
0393                        print 'Unknown exception while processing module "%s" : "%s"' % (module_name, exc)
0394                    continue
0395                classes = self.classes_from_module(module)
0396                all.extend(classes)
0397
0398        os.path.walk(package_dir, find_classes_in_file, None)
0399        return all
0400
0401    def classes_from_egg(self, egg_spec):
0402        modules = []
0403        dist, conf = self.config_from_egg(egg_spec, warn_no_sqlobject=True)
0404        for mod in conf.get('db_module', '').split(','):
0405            mod = mod.strip()
0406            if not mod:
0407                continue
0408            if self.options.verbose:
0409                print 'Looking in module %s' % mod
0410            modules.extend(self.classes_from_module(
0411                moduleloader.load_module(mod)))
0412        return modules
0413
0414    def load_options_from_egg(self, egg_spec):
0415        dist, conf = self.config_from_egg(egg_spec)
0416        if (