PageRenderTime 56ms CodeModel.GetById 15ms app.highlight 35ms RepoModel.GetById 0ms app.codeStats 1ms

/nose/config.py

https://bitbucket.org/jpellerin/nose/
Python | 638 lines | 521 code | 64 blank | 53 comment | 77 complexity | 6edb344923487e916d5f3cacf84a35cd MD5 | raw file
  1import logging
  2import optparse
  3import os
  4import re
  5import sys
  6import ConfigParser
  7from optparse import OptionParser
  8from nose.util import absdir, tolist
  9from nose.plugins.manager import NoPlugins
 10from warnings import warn
 11
 12log = logging.getLogger(__name__)
 13
 14# not allowed in config files
 15option_blacklist = ['help', 'verbose']
 16
 17config_files = [
 18    # Linux users will prefer this
 19    "~/.noserc",
 20    # Windows users will prefer this
 21    "~/nose.cfg"
 22    ]
 23
 24# plaforms on which the exe check defaults to off
 25# Windows and IronPython
 26exe_allowed_platforms = ('win32', 'cli')
 27
 28
 29class NoSuchOptionError(Exception):
 30    def __init__(self, name):
 31        Exception.__init__(self, name)
 32        self.name = name
 33
 34
 35class ConfigError(Exception):
 36    pass
 37
 38
 39class ConfiguredDefaultsOptionParser(object):
 40    """
 41    Handler for options from commandline and config files.
 42    """
 43    def __init__(self, parser, config_section, error=None, file_error=None):
 44        self._parser = parser
 45        self._config_section = config_section
 46        if error is None:
 47            error = self._parser.error
 48        self._error = error
 49        if file_error is None:
 50            file_error = lambda msg, **kw: error(msg)
 51        self._file_error = file_error
 52
 53    def _configTuples(self, cfg, filename):
 54        config = []
 55        if self._config_section in cfg.sections():
 56            for name, value in cfg.items(self._config_section):
 57                config.append((name, value, filename))
 58        return config
 59
 60    def _readFromFilenames(self, filenames):
 61        config = []
 62        for filename in filenames:
 63            cfg = ConfigParser.RawConfigParser()
 64            try:
 65                cfg.read(filename)
 66            except ConfigParser.Error, exc:
 67                raise ConfigError("Error reading config file %r: %s" %
 68                                  (filename, str(exc)))
 69            config.extend(self._configTuples(cfg, filename))
 70        return config
 71
 72    def _readFromFileObject(self, fh):
 73        cfg = ConfigParser.RawConfigParser()
 74        try:
 75            filename = fh.name
 76        except AttributeError:
 77            filename = '<???>'
 78        try:
 79            cfg.readfp(fh)
 80        except ConfigParser.Error, exc:
 81            raise ConfigError("Error reading config file %r: %s" %
 82                              (filename, str(exc)))
 83        return self._configTuples(cfg, filename)
 84
 85    def _readConfiguration(self, config_files):
 86        try:
 87            config_files.readline
 88        except AttributeError:
 89            filename_or_filenames = config_files
 90            if isinstance(filename_or_filenames, basestring):
 91                filenames = [filename_or_filenames]
 92            else:
 93                filenames = filename_or_filenames
 94            config = self._readFromFilenames(filenames)
 95        else:
 96            fh = config_files
 97            config = self._readFromFileObject(fh)
 98        return config
 99
100    def _processConfigValue(self, name, value, values, parser):
101        opt_str = '--' + name
102        option = parser.get_option(opt_str)
103        if option is None:
104            raise NoSuchOptionError(name)
105        else:
106            option.process(opt_str, value, values, parser)
107
108    def _applyConfigurationToValues(self, parser, config, values):
109        for name, value, filename in config:
110            if name in option_blacklist:
111                continue
112            try:
113                self._processConfigValue(name, value, values, parser)
114            except NoSuchOptionError, exc:
115                self._file_error(
116                    "Error reading config file %r: "
117                    "no such option %r" % (filename, exc.name),
118                    name=name, filename=filename)
119            except optparse.OptionValueError, exc:
120                msg = str(exc).replace('--' + name, repr(name), 1)
121                self._file_error("Error reading config file %r: "
122                                 "%s" % (filename, msg),
123                                 name=name, filename=filename)
124
125    def parseArgsAndConfigFiles(self, args, config_files):
126        values = self._parser.get_default_values()
127        try:
128            config = self._readConfiguration(config_files)
129        except ConfigError, exc:
130            self._error(str(exc))
131        else:
132            self._applyConfigurationToValues(self._parser, config, values)
133        return self._parser.parse_args(args, values)
134
135
136class Config(object):
137    """nose configuration.
138
139    Instances of Config are used throughout nose to configure
140    behavior, including plugin lists. Here are the default values for
141    all config keys::
142
143      self.env = env = kw.pop('env', {})
144      self.args = ()
145      self.testMatch = re.compile(r'(?:^|[\\b_\\.%s-])[Tt]est' % os.sep)
146      self.addPaths = not env.get('NOSE_NOPATH', False)
147      self.configSection = 'nosetests'
148      self.debug = env.get('NOSE_DEBUG')
149      self.debugLog = env.get('NOSE_DEBUG_LOG')
150      self.exclude = None
151      self.getTestCaseNamesCompat = False
152      self.includeExe = env.get('NOSE_INCLUDE_EXE',
153                                sys.platform in exe_allowed_platforms)
154      self.ignoreFiles = (re.compile(r'^\.'),
155                          re.compile(r'^_'),
156                          re.compile(r'^setup\.py$')
157                          )
158      self.include = None
159      self.loggingConfig = None
160      self.logStream = sys.stderr
161      self.options = NoOptions()
162      self.parser = None
163      self.plugins = NoPlugins()
164      self.srcDirs = ('lib', 'src')
165      self.runOnInit = True
166      self.stopOnError = env.get('NOSE_STOP', False)
167      self.stream = sys.stderr
168      self.testNames = ()
169      self.verbosity = int(env.get('NOSE_VERBOSE', 1))
170      self.where = ()
171      self.py3where = ()
172      self.workingDir = None   
173    """
174
175    def __init__(self, **kw):
176        self.env = env = kw.pop('env', {})
177        self.args = ()
178        self.testMatchPat = env.get('NOSE_TESTMATCH',
179                                    r'(?:^|[\b_\.%s-])[Tt]est' % os.sep)
180        self.testMatch = re.compile(self.testMatchPat)
181        self.addPaths = not env.get('NOSE_NOPATH', False)
182        self.configSection = 'nosetests'
183        self.debug = env.get('NOSE_DEBUG')
184        self.debugLog = env.get('NOSE_DEBUG_LOG')
185        self.exclude = None
186        self.getTestCaseNamesCompat = False
187        self.includeExe = env.get('NOSE_INCLUDE_EXE',
188                                  sys.platform in exe_allowed_platforms)
189        self.ignoreFilesDefaultStrings = [r'^\.',
190                                          r'^_',
191                                          r'^setup\.py$',
192                                          ]
193        self.ignoreFiles = map(re.compile, self.ignoreFilesDefaultStrings)
194        self.include = None
195        self.loggingConfig = None
196        self.logStream = sys.stderr
197        self.options = NoOptions()
198        self.parser = None
199        self.plugins = NoPlugins()
200        self.srcDirs = ('lib', 'src')
201        self.runOnInit = True
202        self.stopOnError = env.get('NOSE_STOP', False)
203        self.stream = sys.stderr
204        self.testNames = []
205        self.verbosity = int(env.get('NOSE_VERBOSE', 1))
206        self.where = ()
207        self.py3where = ()
208        self.workingDir = os.getcwd()
209        self.traverseNamespace = False
210        self.firstPackageWins = False
211        self.parserClass = OptionParser
212        self.worker = False
213        
214        self._default = self.__dict__.copy()
215        self.update(kw)
216        self._orig = self.__dict__.copy()
217
218    def __getstate__(self):
219        state = self.__dict__.copy()
220        del state['stream']
221        del state['_orig']
222        del state['_default']
223        del state['env']
224        del state['logStream']
225        # FIXME remove plugins, have only plugin manager class
226        state['plugins'] = self.plugins.__class__
227        return state
228
229    def __setstate__(self, state):
230        plugincls = state.pop('plugins')
231        self.update(state)
232        self.worker = True
233        # FIXME won't work for static plugin lists
234        self.plugins = plugincls()
235        self.plugins.loadPlugins()
236        # needed so .can_configure gets set appropriately
237        dummy_parser = self.parserClass()
238        self.plugins.addOptions(dummy_parser, {})
239        self.plugins.configure(self.options, self)
240    
241    def __repr__(self):
242        d = self.__dict__.copy()
243        # don't expose env, could include sensitive info
244        d['env'] = {}
245        keys = [ k for k in d.keys()
246                 if not k.startswith('_') ]
247        keys.sort()
248        return "Config(%s)" % ', '.join([ '%s=%r' % (k, d[k])
249                                          for k in keys ])
250    __str__ = __repr__
251
252    def _parseArgs(self, argv, cfg_files):
253        def warn_sometimes(msg, name=None, filename=None):
254            if (hasattr(self.plugins, 'excludedOption') and
255                self.plugins.excludedOption(name)):
256                msg = ("Option %r in config file %r ignored: "
257                       "excluded by runtime environment" %
258                       (name, filename))
259                warn(msg, RuntimeWarning)
260            else:
261                raise ConfigError(msg)
262        parser = ConfiguredDefaultsOptionParser(
263            self.getParser(), self.configSection, file_error=warn_sometimes)
264        return parser.parseArgsAndConfigFiles(argv[1:], cfg_files)
265
266    def configure(self, argv=None, doc=None):
267        """Configure the nose running environment. Execute configure before
268        collecting tests with nose.TestCollector to enable output capture and
269        other features.
270        """
271        env = self.env
272        if argv is None:
273            argv = sys.argv
274
275        cfg_files = getattr(self, 'files', [])
276        options, args = self._parseArgs(argv, cfg_files)
277        # If -c --config has been specified on command line,
278        # load those config files and reparse
279        if getattr(options, 'files', []):
280            options, args = self._parseArgs(argv, options.files)
281
282        self.options = options
283        if args:
284            self.testNames = args
285        if options.testNames is not None:
286            self.testNames.extend(tolist(options.testNames))
287
288        if options.py3where is not None:
289            if sys.version_info >= (3,):
290                options.where = options.py3where
291
292        # `where` is an append action, so it can't have a default value 
293        # in the parser, or that default will always be in the list
294        if not options.where:
295            options.where = env.get('NOSE_WHERE', None)
296
297        # include and exclude also
298        if not options.ignoreFiles:
299            options.ignoreFiles = env.get('NOSE_IGNORE_FILES', [])
300        if not options.include:
301            options.include = env.get('NOSE_INCLUDE', [])
302        if not options.exclude:
303            options.exclude = env.get('NOSE_EXCLUDE', [])
304
305        self.addPaths = options.addPaths
306        self.stopOnError = options.stopOnError
307        self.verbosity = options.verbosity
308        self.includeExe = options.includeExe
309        self.traverseNamespace = options.traverseNamespace
310        self.debug = options.debug
311        self.debugLog = options.debugLog
312        self.loggingConfig = options.loggingConfig
313        self.firstPackageWins = options.firstPackageWins
314        self.configureLogging()
315
316        if options.where is not None:
317            self.configureWhere(options.where)
318        
319        if options.testMatch:
320            self.testMatch = re.compile(options.testMatch)
321        
322        if options.ignoreFiles:
323            self.ignoreFiles = map(re.compile, tolist(options.ignoreFiles))
324            log.info("Ignoring files matching %s", options.ignoreFiles)
325        else:
326            log.info("Ignoring files matching %s", self.ignoreFilesDefaultStrings)
327        
328        if options.include:
329            self.include = map(re.compile, tolist(options.include))
330            log.info("Including tests matching %s", options.include)
331
332        if options.exclude:
333            self.exclude = map(re.compile, tolist(options.exclude))
334            log.info("Excluding tests matching %s", options.exclude)
335
336        # When listing plugins we don't want to run them
337        if not options.showPlugins:
338            self.plugins.configure(options, self)
339            self.plugins.begin()
340
341    def configureLogging(self):
342        """Configure logging for nose, or optionally other packages. Any logger
343        name may be set with the debug option, and that logger will be set to
344        debug level and be assigned the same handler as the nose loggers, unless
345        it already has a handler.
346        """
347        if self.loggingConfig:
348            from logging.config import fileConfig
349            fileConfig(self.loggingConfig)
350            return
351        
352        format = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
353        if self.debugLog:
354            handler = logging.FileHandler(self.debugLog)
355        else:
356            handler = logging.StreamHandler(self.logStream)
357        handler.setFormatter(format)
358
359        logger = logging.getLogger('nose')
360        logger.propagate = 0
361
362        # only add our default handler if there isn't already one there
363        # this avoids annoying duplicate log messages.
364        if handler not in logger.handlers:
365            logger.addHandler(handler)
366
367        # default level    
368        lvl = logging.WARNING
369        if self.verbosity >= 5:
370            lvl = 0
371        elif self.verbosity >= 4:
372            lvl = logging.DEBUG
373        elif self.verbosity >= 3:
374            lvl = logging.INFO
375        logger.setLevel(lvl)
376
377        # individual overrides
378        if self.debug:
379            # no blanks
380            debug_loggers = [ name for name in self.debug.split(',')
381                              if name ]
382            for logger_name in debug_loggers:
383                l = logging.getLogger(logger_name)
384                l.setLevel(logging.DEBUG)
385                if not l.handlers and not logger_name.startswith('nose'):
386                    l.addHandler(handler)
387
388    def configureWhere(self, where):
389        """Configure the working directory or directories for the test run.
390        """
391        from nose.importer import add_path
392        self.workingDir = None
393        where = tolist(where)
394        warned = False
395        for path in where:
396            if not self.workingDir:
397                abs_path = absdir(path)
398                if abs_path is None:
399                    raise ValueError("Working directory %s not found, or "
400                                     "not a directory" % path)
401                log.info("Set working dir to %s", abs_path)
402                self.workingDir = abs_path
403                if self.addPaths and \
404                       os.path.exists(os.path.join(abs_path, '__init__.py')):
405                    log.info("Working directory %s is a package; "
406                             "adding to sys.path" % abs_path)
407                    add_path(abs_path)
408                continue
409            if not warned:
410                warn("Use of multiple -w arguments is deprecated and "
411                     "support may be removed in a future release. You can "
412                     "get the same behavior by passing directories without "
413                     "the -w argument on the command line, or by using the "
414                     "--tests argument in a configuration file.",
415                     DeprecationWarning)
416            self.testNames.append(path)
417
418    def default(self):
419        """Reset all config values to defaults.
420        """
421        self.__dict__.update(self._default)
422
423    def getParser(self, doc=None):
424        """Get the command line option parser.
425        """
426        if self.parser:
427            return self.parser
428        env = self.env
429        parser = self.parserClass(doc)
430        parser.add_option(
431            "-V","--version", action="store_true",
432            dest="version", default=False,
433            help="Output nose version and exit")
434        parser.add_option(
435            "-p", "--plugins", action="store_true",
436            dest="showPlugins", default=False,
437            help="Output list of available plugins and exit. Combine with "
438            "higher verbosity for greater detail")
439        parser.add_option(
440            "-v", "--verbose",
441            action="count", dest="verbosity",
442            default=self.verbosity,
443            help="Be more verbose. [NOSE_VERBOSE]")
444        parser.add_option(
445            "--verbosity", action="store", dest="verbosity",
446            metavar='VERBOSITY',
447            type="int", help="Set verbosity; --verbosity=2 is "
448            "the same as -v")
449        parser.add_option(
450            "-q", "--quiet", action="store_const", const=0, dest="verbosity",
451            help="Be less verbose")
452        parser.add_option(
453            "-c", "--config", action="append", dest="files",
454            metavar="FILES",
455            help="Load configuration from config file(s). May be specified "
456            "multiple times; in that case, all config files will be "
457            "loaded and combined")
458        parser.add_option(
459            "-w", "--where", action="append", dest="where",
460            metavar="WHERE",
461            help="Look for tests in this directory. "
462            "May be specified multiple times. The first directory passed "
463            "will be used as the working directory, in place of the current "
464            "working directory, which is the default. Others will be added "
465            "to the list of tests to execute. [NOSE_WHERE]"
466            )
467        parser.add_option(
468            "--py3where", action="append", dest="py3where",
469            metavar="PY3WHERE",
470            help="Look for tests in this directory under Python 3.x. "
471            "Functions the same as 'where', but only applies if running under "
472            "Python 3.x or above.  Note that, if present under 3.x, this "
473            "option completely replaces any directories specified with "
474            "'where', so the 'where' option becomes ineffective. "
475            "[NOSE_PY3WHERE]"
476            )
477        parser.add_option(
478            "-m", "--match", "--testmatch", action="store",
479            dest="testMatch", metavar="REGEX",
480            help="Files, directories, function names, and class names "
481            "that match this regular expression are considered tests.  "
482            "Default: %s [NOSE_TESTMATCH]" % self.testMatchPat,
483            default=self.testMatchPat)
484        parser.add_option(
485            "--tests", action="store", dest="testNames", default=None,
486            metavar='NAMES',
487            help="Run these tests (comma-separated list). This argument is "
488            "useful mainly from configuration files; on the command line, "
489            "just pass the tests to run as additional arguments with no "
490            "switch.")
491        parser.add_option(
492            "-l", "--debug", action="store",
493            dest="debug", default=self.debug,
494            help="Activate debug logging for one or more systems. "
495            "Available debug loggers: nose, nose.importer, "
496            "nose.inspector, nose.plugins, nose.result and "
497            "nose.selector. Separate multiple names with a comma.")
498        parser.add_option(
499            "--debug-log", dest="debugLog", action="store",
500            default=self.debugLog, metavar="FILE",
501            help="Log debug messages to this file "
502            "(default: sys.stderr)")
503        parser.add_option(
504            "--logging-config", "--log-config",
505            dest="loggingConfig", action="store",
506            default=self.loggingConfig, metavar="FILE",
507            help="Load logging config from this file -- bypasses all other"
508            " logging config settings.")
509        parser.add_option(
510            "-I", "--ignore-files", action="append", dest="ignoreFiles",
511            metavar="REGEX",
512            help="Completely ignore any file that matches this regular "
513            "expression. Takes precedence over any other settings or "
514            "plugins. "
515            "Specifying this option will replace the default setting. "
516            "Specify this option multiple times "
517            "to add more regular expressions [NOSE_IGNORE_FILES]")
518        parser.add_option(
519            "-e", "--exclude", action="append", dest="exclude",
520            metavar="REGEX",
521            help="Don't run tests that match regular "
522            "expression [NOSE_EXCLUDE]")
523        parser.add_option(
524            "-i", "--include", action="append", dest="include",
525            metavar="REGEX",
526            help="This regular expression will be applied to files, "
527            "directories, function names, and class names for a chance "
528            "to include additional tests that do not match TESTMATCH.  "
529            "Specify this option multiple times "
530            "to add more regular expressions [NOSE_INCLUDE]")
531        parser.add_option(
532            "-x", "--stop", action="store_true", dest="stopOnError",
533            default=self.stopOnError,
534            help="Stop running tests after the first error or failure")
535        parser.add_option(
536            "-P", "--no-path-adjustment", action="store_false",
537            dest="addPaths",
538            default=self.addPaths,
539            help="Don't make any changes to sys.path when "
540            "loading tests [NOSE_NOPATH]")
541        parser.add_option(
542            "--exe", action="store_true", dest="includeExe",
543            default=self.includeExe,
544            help="Look for tests in python modules that are "
545            "executable. Normal behavior is to exclude executable "
546            "modules, since they may not be import-safe "
547            "[NOSE_INCLUDE_EXE]")
548        parser.add_option(
549            "--noexe", action="store_false", dest="includeExe",
550            help="DO NOT look for tests in python modules that are "
551            "executable. (The default on the windows platform is to "
552            "do so.)")
553        parser.add_option(
554            "--traverse-namespace", action="store_true",
555            default=self.traverseNamespace, dest="traverseNamespace",
556            help="Traverse through all path entries of a namespace package")
557        parser.add_option(
558            "--first-package-wins", "--first-pkg-wins", "--1st-pkg-wins",
559            action="store_true", default=False, dest="firstPackageWins",
560            help="nose's importer will normally evict a package from sys."
561            "modules if it sees a package with the same name in a different "
562            "location. Set this option to disable that behavior.")
563
564        self.plugins.loadPlugins()
565        self.pluginOpts(parser)
566
567        self.parser = parser
568        return parser
569
570    def help(self, doc=None):
571        """Return the generated help message
572        """
573        return self.getParser(doc).format_help()
574
575    def pluginOpts(self, parser):
576        self.plugins.addOptions(parser, self.env)
577
578    def reset(self):
579        self.__dict__.update(self._orig)
580
581    def todict(self):
582        return self.__dict__.copy()
583        
584    def update(self, d):
585        self.__dict__.update(d)
586
587
588class NoOptions(object):
589    """Options container that returns None for all options.
590    """
591    def __getstate__(self):
592        return {}
593    
594    def __setstate__(self, state):
595        pass
596
597    def __getnewargs__(self):
598        return ()
599    
600    def __getattr__(self, attr):
601        return None
602
603    def __nonzero__(self):
604        return False
605
606
607def user_config_files():
608    """Return path to any existing user config files
609    """
610    return filter(os.path.exists,
611                  map(os.path.expanduser, config_files))
612
613
614def all_config_files():
615    """Return path to any existing user config files, plus any setup.cfg
616    in the current working directory.
617    """
618    user = user_config_files()
619    if os.path.exists('setup.cfg'):
620        return user + ['setup.cfg']
621    return user
622
623
624# used when parsing config files
625def flag(val):
626    """Does the value look like an on/off flag?"""
627    if val == 1:
628        return True
629    elif val == 0:
630        return False
631    val = str(val)
632    if len(val) > 5:
633        return False
634    return val.upper() in ('1', '0', 'F', 'T', 'TRUE', 'FALSE', 'ON', 'OFF')
635
636
637def _bool(val):
638    return str(val).upper() in ('1', 'T', 'TRUE', 'ON')