Mercurial > p > roundup > code
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) |
