comparison roundup/configuration.py @ 6638:e1588ae185dc issue2550923_computed_property

merge from default branch. Fix travis.ci so CI builds don't error out
author John Rouillard <rouilj@ieee.org>
date Thu, 21 Apr 2022 16:54:17 -0400
parents 0d99ae7c8de6
children 408fd477761f
comparison
equal deleted inserted replaced
6508:85db90cc1705 6638:e1588ae185dc
7 # Roundup if used with Python 2 because it generates unicode objects 7 # Roundup if used with Python 2 because it generates unicode objects
8 # where not expected by the Python code. Thus, a version check is 8 # where not expected by the Python code. Thus, a version check is
9 # used here instead of try/except. 9 # used here instead of try/except.
10 import sys 10 import sys
11 import getopt 11 import getopt
12 import logging, logging.config 12 import errno
13 import logging
14 import logging.config
13 import os 15 import os
14 import re 16 import re
15 import time 17 import time
16 import smtplib 18 import smtplib
17 19
29 else: 31 else:
30 import ConfigParser as configparser # Python 2 32 import ConfigParser as configparser # Python 2
31 33
32 from roundup.exceptions import RoundupException 34 from roundup.exceptions import RoundupException
33 35
34 # XXX i don't think this module needs string translation, does it?
35
36 ### Exceptions 36 ### Exceptions
37 37
38 38
39 class ConfigurationError(RoundupException): 39 class ConfigurationError(RoundupException):
40 pass 40 pass
41
42
43 class ParsingOptionError(ConfigurationError):
44 def __str__(self):
45 return self.args[0]
41 46
42 47
43 class NoConfigError(ConfigurationError): 48 class NoConfigError(ConfigurationError):
44 49
45 """Raised when configuration loading fails 50 """Raised when configuration loading fails
259 } 264 }
260 return _rv 265 return _rv
261 266
262 def load_ini(self, config): 267 def load_ini(self, config):
263 """Load value from ConfigParser object""" 268 """Load value from ConfigParser object"""
264 if config.has_option(self.section, self.setting): 269 try:
265 self.set(config.get(self.section, self.setting)) 270 if config.has_option(self.section, self.setting):
271 self.set(config.get(self.section, self.setting))
272 except configparser.InterpolationSyntaxError as e:
273 raise ParsingOptionError(
274 _("Error in %(filepath)s with section [%(section)s] at "
275 "option %(option)s: %(message)s") % {
276 "filepath": self.config.filepath,
277 "section": e.section,
278 "option": e.option,
279 "message": str(e)})
266 280
267 281
268 class BooleanOption(Option): 282 class BooleanOption(Option):
269 283
270 """Boolean option: yes or no""" 284 """Boolean option: yes or no"""
406 _val = value.lower() 420 _val = value.lower()
407 if _val in self.allowed: 421 if _val in self.allowed:
408 return _val 422 return _val
409 raise OptionValueError(self, value, self.class_description) 423 raise OptionValueError(self, value, self.class_description)
410 424
425
411 class IndexerOption(Option): 426 class IndexerOption(Option):
412 """Valid options for indexer""" 427 """Valid options for indexer"""
413 428
414 allowed = ['', 'xapian', 'whoosh', 'native'] 429 allowed = ['', 'xapian', 'whoosh', 'native', 'native-fts']
415 class_description = "Allowed values: %s" % ', '.join("'%s'" % a 430 class_description = "Allowed values: %s" % ', '.join("'%s'" % a
416 for a in allowed) 431 for a in allowed)
417 432
433 # FIXME this is the result of running:
434 # SELECT cfgname FROM pg_ts_config;
435 # on a postgresql 14.1 server.
436 # So the best we can do is hardcode this.
437 valid_langs = [ "simple",
438 "custom1",
439 "custom2",
440 "custom3",
441 "custom4",
442 "custom5",
443 "arabic",
444 "armenian",
445 "basque",
446 "catalan",
447 "danish",
448 "dutch",
449 "english",
450 "finnish",
451 "french",
452 "german",
453 "greek",
454 "hindi",
455 "hungarian",
456 "indonesian",
457 "irish",
458 "italian",
459 "lithuanian",
460 "nepali",
461 "norwegian",
462 "portuguese",
463 "romanian",
464 "russian",
465 "serbian",
466 "spanish",
467 "swedish",
468 "tamil",
469 "turkish",
470 "yiddish" ]
471
418 def str2value(self, value): 472 def str2value(self, value):
419 _val = value.lower() 473 _val = value.lower()
420 if _val in self.allowed: 474 if _val in self.allowed:
421 return _val 475 return _val
422 raise OptionValueError(self, value, self.class_description) 476 raise OptionValueError(self, value, self.class_description)
477
478 def validate(self, options):
479
480 if self._value in ("", "xapian"):
481 try:
482 import xapian
483 except ImportError:
484 # indexer is probably '' and xapian isn't present
485 # so just return at end of method
486 pass
487 else:
488 try:
489 lang = options["INDEXER_LANGUAGE"]._value
490 xapian.Stem(lang)
491 except xapian.InvalidArgumentError:
492 import textwrap
493 lang_avail = b2s(xapian.Stem.get_available_languages())
494 languages = textwrap.fill(_("Valid languages: ") +
495 lang_avail, 75,
496 subsequent_indent=" ")
497 raise OptionValueError(options["INDEXER_LANGUAGE"],
498 lang, languages)
499
500 if self._value == "native-fts":
501 lang = options["INDEXER_LANGUAGE"]._value
502 if lang not in self.valid_langs:
503 import textwrap
504 languages = textwrap.fill(_("Expected languages: ") +
505 " ".join(self.valid_langs), 75,
506 subsequent_indent=" ")
507 raise OptionValueError(options["INDEXER_LANGUAGE"],
508 lang, languages)
423 509
424 class MailAddressOption(Option): 510 class MailAddressOption(Option):
425 511
426 """Email address 512 """Email address
427 513
550 raise OptionValueError(self, value, "Value must not be empty.") 636 raise OptionValueError(self, value, "Value must not be empty.")
551 else: 637 else:
552 return value 638 return value
553 639
554 640
641 class SecretOption(Option):
642 """A string not beginning with file:// or a file starting with file://
643
644 Paths may be either absolute or relative to the HOME.
645 Value for option is the first line in the file.
646 It is mean to store secret information in the config file but
647 allow the config file to be stored in version control without
648 storing the secret there.
649
650 """
651
652 class_description = \
653 "A string that starts with 'file://' is interpreted as a file path \n" \
654 "relative to the tracker home. Using 'file:///' defines an absolute \n" \
655 "path. The first line of the file will be used as the value. Any \n" \
656 "string that does not start with 'file://' is used as is. It \n" \
657 "removes any whitespace at the end of the line, so a newline can \n" \
658 "be put in the file.\n"
659
660 def get(self):
661 _val = Option.get(self)
662 if isinstance(_val, str) and _val.startswith('file://'):
663 filepath = _val[7:]
664 if filepath and not os.path.isabs(filepath):
665 filepath = os.path.join(self.config["HOME"], filepath.strip())
666 try:
667 with open(filepath) as f:
668 _val = f.readline().rstrip()
669 # except FileNotFoundError: py2/py3
670 # compatible version
671 except EnvironmentError as e:
672 if e.errno != errno.ENOENT:
673 raise
674 else:
675 raise OptionValueError(self, _val,
676 "Unable to read value for %s. Error opening "
677 "%s: %s." % (self.name, e.filename, e.args[1]))
678 return self.str2value(_val)
679
680 def validate(self, options):
681 if self.name == 'MAIL_PASSWORD':
682 if options['MAIL_USERNAME']._value:
683 # MAIL_PASSWORD is an exception. It is mandatory only
684 # if MAIL_USERNAME is set. So check only if username
685 # is set.
686 try:
687 self.get()
688 except OptionUnsetError:
689 # provide error message with link to MAIL_USERNAME
690 raise OptionValueError(options["MAIL_PASSWORD"],
691 "not defined",
692 "Mail username is set, so this must be defined.")
693 else:
694 self.get()
695
696
555 class WebUrlOption(Option): 697 class WebUrlOption(Option):
556 """URL MUST start with http/https scheme and end with '/'""" 698 """URL MUST start with http/https scheme and end with '/'"""
557 699
558 def str2value(self, value): 700 def str2value(self, value):
559 if not value: 701 if not value:
610 752
611 # .get() and class_description are from FilePathOption, 753 # .get() and class_description are from FilePathOption,
612 get = FilePathOption.get 754 get = FilePathOption.get
613 class_description = FilePathOption.class_description 755 class_description = FilePathOption.class_description
614 # everything else taken from NullableOption (inheritance order) 756 # everything else taken from NullableOption (inheritance order)
757
758
759 class SecretMandatoryOption(MandatoryOption, SecretOption):
760 # use get from SecretOption and rest from MandatoryOption
761 get = SecretOption.get
762 class_description = SecretOption.class_description
763
764
765 class SecretNullableOption(NullableOption, SecretOption):
766 # use get from SecretOption and rest from NullableOption
767 get = SecretOption.get
768 class_description = SecretOption.class_description
615 769
616 770
617 class TimezoneOption(Option): 771 class TimezoneOption(Option):
618 772
619 class_description = \ 773 class_description = \
634 try: 788 try:
635 roundup.date.get_timezone(value) 789 roundup.date.get_timezone(value)
636 except KeyError: 790 except KeyError:
637 raise OptionValueError(self, value, 791 raise OptionValueError(self, value,
638 "Timezone name or numeric hour offset required") 792 "Timezone name or numeric hour offset required")
793 return value
794
795
796 class HttpVersionOption(Option):
797 """Used by roundup-server to verify http version is set to valid
798 string."""
799
800 def str2value(self, value):
801 if value not in ["HTTP/1.0", "HTTP/1.1"]:
802 raise OptionValueError(self, value,
803 "Valid vaues for -V or --http_version are: HTTP/1.0, HTTP/1.1")
639 return value 804 return value
640 805
641 806
642 class RegExpOption(Option): 807 class RegExpOption(Option):
643 808
756 "Offer registration confirmation by email or only through the web?"), 921 "Offer registration confirmation by email or only through the web?"),
757 (IndexerOption, "indexer", "", 922 (IndexerOption, "indexer", "",
758 "Force Roundup to use a particular text indexer.\n" 923 "Force Roundup to use a particular text indexer.\n"
759 "If no indexer is supplied, the first available indexer\n" 924 "If no indexer is supplied, the first available indexer\n"
760 "will be used in the following order:\n" 925 "will be used in the following order:\n"
761 "Possible values: xapian, whoosh, native (internal)."), 926 "Possible values: xapian, whoosh, native (internal), "
927 "native-fts.\nNote 'native-fts' will only be used if set."),
762 (Option, "indexer_language", "english", 928 (Option, "indexer_language", "english",
763 "Used to determine what language should be used by the\n" 929 "Used to determine what language should be used by the\n"
764 "indexer above. Currently only affects Xapian indexer. It\n" 930 "indexer above. Applies to Xapian and PostgreSQL native-fts\n"
765 "sets the language for the stemmer.\n" 931 "indexer. It sets the language for the stemmer, and PostgreSQL\n"
932 "native-fts stopwords and other dictionaries.\n"
766 "Possible values: must be a valid language for the indexer,\n" 933 "Possible values: must be a valid language for the indexer,\n"
767 "see indexer documentation for details."), 934 "see indexer documentation for details."),
768 (WordListOption, "indexer_stopwords", "", 935 (WordListOption, "indexer_stopwords", "",
769 "Additional stop-words for the full-text indexer specific to\n" 936 "Additional stop-words for the full-text indexer specific to\n"
770 "your tracker. See the indexer source for the default list of\n" 937 "your tracker. See the indexer source for the default list of\n"
771 "stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)"), 938 "stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...). This is\n"
939 "not used by the native-fts indexer."),
772 (OctalNumberOption, "umask", "0o002", 940 (OctalNumberOption, "umask", "0o002",
773 "Defines the file creation mode mask."), 941 "Defines the file creation mode mask."),
774 (IntegerNumberGeqZeroOption, 'csv_field_size', '131072', 942 (IntegerNumberGeqZeroOption, 'csv_field_size', '131072',
775 "Maximum size of a csv-field during import. Roundups export\n" 943 "Maximum size of a csv-field during import. Roundups export\n"
776 "format is a csv (comma separated values) variant. The csv\n" 944 "format is a csv (comma separated values) variant. The csv\n"
1036 "tracker admin."), 1204 "tracker admin."),
1037 (BooleanOption, "migrate_passwords", "yes", 1205 (BooleanOption, "migrate_passwords", "yes",
1038 "Setting this option makes Roundup migrate passwords with\n" 1206 "Setting this option makes Roundup migrate passwords with\n"
1039 "an insecure password-scheme to a more secure scheme\n" 1207 "an insecure password-scheme to a more secure scheme\n"
1040 "when the user logs in via the web-interface."), 1208 "when the user logs in via the web-interface."),
1041 (MandatoryOption, "secret_key", create_token(), 1209 (SecretMandatoryOption, "secret_key", create_token(),
1042 "A per tracker secret used in etag calculations for\n" 1210 "A per tracker secret used in etag calculations for\n"
1043 "an object. It must not be empty.\n" 1211 "an object. It must not be empty.\n"
1044 "It prevents reverse engineering hidden data in an object\n" 1212 "It prevents reverse engineering hidden data in an object\n"
1045 "by calculating the etag for a sample object. Then modifying\n" 1213 "by calculating the etag for a sample object. Then modifying\n"
1046 "hidden properties until the sample object's etag matches\n" 1214 "hidden properties until the sample object's etag matches\n"
1048 "Changing this changes the etag and invalidates updates by\n" 1216 "Changing this changes the etag and invalidates updates by\n"
1049 "clients. It must be persistent across application restarts.\n" 1217 "clients. It must be persistent across application restarts.\n"
1050 "(Note the default value changes every time\n" 1218 "(Note the default value changes every time\n"
1051 " roundup-admin updateconfig\n" 1219 " roundup-admin updateconfig\n"
1052 "is run, so it must be explicitly set to a non-empty string.\n"), 1220 "is run, so it must be explicitly set to a non-empty string.\n"),
1053 (MandatoryOption, "jwt_secret", "disabled", 1221 (SecretNullableOption, "jwt_secret", "disabled",
1054 "This is used to generate/validate json web tokens (jwt).\n" 1222 "This is used to generate/validate json web tokens (jwt).\n"
1055 "Even if you don't use jwts it must not be empty.\n" 1223 "Even if you don't use jwts it must not be empty.\n"
1056 "If less than 256 bits (32 characters) in length it will\n" 1224 "If less than 256 bits (32 characters) in length it will\n"
1057 "disable use of jwt. Changing this invalidates all jwts\n" 1225 "disable use of jwt. Changing this invalidates all jwts\n"
1058 "issued by the roundup instance requiring *all* users to\n" 1226 "issued by the roundup instance requiring *all* users to\n"
1074 "for MySQL default port number is 3306.\n" 1242 "for MySQL default port number is 3306.\n"
1075 "Leave this option empty to use backend default"), 1243 "Leave this option empty to use backend default"),
1076 (NullableOption, 'user', 'roundup', 1244 (NullableOption, 'user', 'roundup',
1077 "Database user name that Roundup should use.", 1245 "Database user name that Roundup should use.",
1078 ['MYSQL_DBUSER']), 1246 ['MYSQL_DBUSER']),
1079 (NullableOption, 'password', 'roundup', 1247 (SecretNullableOption, 'password', 'roundup',
1080 "Database user password.", 1248 "Database user password.",
1081 ['MYSQL_DBPASSWORD']), 1249 ['MYSQL_DBPASSWORD']),
1082 (NullableOption, 'read_default_file', '~/.my.cnf', 1250 (NullableOption, 'read_default_file', '~/.my.cnf',
1083 "Name of the MySQL defaults file.\n" 1251 "Name of the MySQL defaults file.\n"
1084 "Only used in MySQL connections."), 1252 "Only used in MySQL connections."),
1155 "SMTP mail host that roundup will use to send mail", 1323 "SMTP mail host that roundup will use to send mail",
1156 ["MAILHOST"],), 1324 ["MAILHOST"],),
1157 (Option, "username", "", "SMTP login name.\n" 1325 (Option, "username", "", "SMTP login name.\n"
1158 "Set this if your mail host requires authenticated access.\n" 1326 "Set this if your mail host requires authenticated access.\n"
1159 "If username is not empty, password (below) MUST be set!"), 1327 "If username is not empty, password (below) MUST be set!"),
1160 (Option, "password", NODEFAULT, "SMTP login password.\n" 1328 (SecretMandatoryOption, "password", NODEFAULT, "SMTP login password.\n"
1161 "Set this if your mail host requires authenticated access."), 1329 "Set this if your mail host requires authenticated access."),
1162 (IntegerNumberGeqZeroOption, "port", smtplib.SMTP_PORT, 1330 (IntegerNumberGeqZeroOption, "port", smtplib.SMTP_PORT,
1163 "Default port to send SMTP on.\n" 1331 "Default port to send SMTP on.\n"
1164 "Set this if your mail server runs on a different port."), 1332 "Set this if your mail server runs on a different port."),
1165 (NullableOption, "local_hostname", '', 1333 (NullableOption, "local_hostname", '',
1398 # mapping from option names and aliases to Option instances 1566 # mapping from option names and aliases to Option instances
1399 options = None 1567 options = None
1400 # actual name of the config file. set on load. 1568 # actual name of the config file. set on load.
1401 filepath = os.path.join(HOME, INI_FILE) 1569 filepath = os.path.join(HOME, INI_FILE)
1402 1570
1403 def __init__(self, config_path=None, layout=None, settings={}): 1571 # List of option names that need additional validation after
1572 # all options are loaded.
1573 option_validators = []
1574
1575 def __init__(self, config_path=None, layout=None, settings=None):
1404 """Initialize confing instance 1576 """Initialize confing instance
1405 1577
1406 Parameters: 1578 Parameters:
1407 config_path: 1579 config_path:
1408 optional directory or file name of the config file. 1580 optional directory or file name of the config file.
1415 settings: 1587 settings:
1416 optional setting overrides (dictionary). 1588 optional setting overrides (dictionary).
1417 The overrides are applied after loading config file. 1589 The overrides are applied after loading config file.
1418 1590
1419 """ 1591 """
1592 if settings is None:
1593 settings = {}
1420 # initialize option containers: 1594 # initialize option containers:
1421 self.sections = [] 1595 self.sections = []
1422 self.section_descriptions = {} 1596 self.section_descriptions = {}
1423 self.section_options = {} 1597 self.section_options = {}
1424 self.options = {} 1598 self.options = {}
1468 # (section, name) key is used for writing .ini file 1642 # (section, name) key is used for writing .ini file
1469 self.options[(_section, _name)] = option 1643 self.options[(_section, _name)] = option
1470 # make the option known under all of its A.K.A.s 1644 # make the option known under all of its A.K.A.s
1471 for _name in option.aliases: 1645 for _name in option.aliases:
1472 self.options[_name] = option 1646 self.options[_name] = option
1647
1648 if hasattr(option, 'validate'):
1649 self.option_validators.append(option.name)
1473 1650
1474 def update_option(self, name, klass, 1651 def update_option(self, name, klass,
1475 default=NODEFAULT, description=None): 1652 default=NODEFAULT, description=None):
1476 """Override behaviour of early created option. 1653 """Override behaviour of early created option.
1477 1654
1813 1990
1814 # user configs 1991 # user configs
1815 ext = None 1992 ext = None
1816 detectors = None 1993 detectors = None
1817 1994
1818 def __init__(self, home_dir=None, settings={}): 1995 def __init__(self, home_dir=None, settings=None):
1996 if settings is None:
1997 settings = {}
1819 Config.__init__(self, home_dir, layout=SETTINGS, settings=settings) 1998 Config.__init__(self, home_dir, layout=SETTINGS, settings=settings)
1820 # load the config if home_dir given 1999 # load the config if home_dir given
1821 if home_dir is None: 2000 if home_dir is None:
1822 self.init_logging() 2001 self.init_logging()
1823 2002
1880 2059
1881 Used to validate settings when options are dependent 2060 Used to validate settings when options are dependent
1882 on each other. E.G. indexer_language can only be 2061 on each other. E.G. indexer_language can only be
1883 validated if xapian indexer is used. 2062 validated if xapian indexer is used.
1884 """ 2063 """
1885 if options['INDEXER']._value in ("", "xapian"): 2064
1886 try: 2065 for option in self.option_validators:
1887 import xapian 2066 # validate() should throw an exception if there is an issue.
1888 except ImportError: 2067 options[option].validate(options)
1889 # indexer is probably '' and xapian isn't present
1890 # so just return at end of method
1891 pass
1892 else:
1893 try:
1894 lang = options["INDEXER_LANGUAGE"]._value
1895 xapian.Stem(lang)
1896 except xapian.InvalidArgumentError:
1897 import textwrap
1898 lang_avail = b2s(xapian.Stem.get_available_languages())
1899 languages = textwrap.fill(_("Valid languages: ") +
1900 lang_avail, 75,
1901 subsequent_indent=" ")
1902 raise OptionValueError( options["INDEXER_LANGUAGE"],
1903 lang, languages)
1904 2068
1905 def load(self, home_dir): 2069 def load(self, home_dir):
1906 """Load configuration from path designated by home_dir argument""" 2070 """Load configuration from path designated by home_dir argument"""
1907 if os.path.isfile(os.path.join(home_dir, self.INI_FILE)): 2071 if os.path.isfile(os.path.join(home_dir, self.INI_FILE)):
1908 self.load_ini(home_dir) 2072 self.load_ini(home_dir)

Roundup Issue Tracker: http://roundup-tracker.org/