config.py 25 KB

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