3ad193d7dba9dd7fc48374e0120544dbbe842058
1 # Roundup Issue Tracker configuration support
3 # $Id: configuration.py,v 1.51 2008-09-01 02:30:06 richard Exp $
5 __docformat__
= "restructuredtext"
10 import logging
, logging
.config
19 # XXX i don't think this module needs string translation, does it?
23 class ConfigurationError(Exception):
26 class NoConfigError(ConfigurationError
):
28 """Raised when configuration loading fails
30 Constructor parameters: path to the directory that was used as HOME
35 return "No valid configuration files found in directory %s" \
38 class InvalidOptionError(ConfigurationError
, KeyError, AttributeError):
40 """Attempted access to non-existing configuration option
42 Configuration options may be accessed as configuration object
43 attributes or items. So this exception instances also are
44 instances of KeyError (invalid item access) and AttributeError
45 (invalid attribute access).
47 Constructor parameter: option name
52 return "Unsupported configuration option: %s" % self
.args
[0]
54 class OptionValueError(ConfigurationError
, ValueError):
56 """Raised upon attempt to assign an invalid value to config option
58 Constructor parameters: Option instance, offending value
59 and optional info string.
65 _rv
= "Invalid value for %(option)s: %(value)r" % {
66 "option": _args
[0].name
, "value": _args
[1]}
68 _rv
+= "\n".join(("",) + _args
[2:])
71 class OptionUnsetError(ConfigurationError
):
73 """Raised when no Option value is available - neither set, nor default
75 Constructor parameters: Option instance.
80 return "%s is not set and has no default" % self
.args
[0].name
82 class UnsetDefaultValue
:
84 """Special object meaning that default value for Option is not specified"""
89 NODEFAULT
= UnsetDefaultValue()
95 """Single configuration option.
97 Options have following attributes:
100 reference to the containing Config object
102 name of the section in the tracker .ini file
104 option name in the tracker .ini file
108 option description. Makes a comment in the tracker .ini file
110 "canonical name" of the configuration option.
111 For items in the 'main' section this is uppercased
112 'setting' name. For other sections, the name is
113 composed of the section name and the setting name,
114 joined with underscore.
116 list of "also known as" names. Used to access the settings
117 by old names used in previous Roundup versions.
118 "Canonical name" is also included.
120 The name and aliases are forced to be uppercase.
121 The setting name is forced to lowercase.
125 class_description
= None
127 def __init__(self
, config
, section
, setting
,
128 default
=NODEFAULT
, description
=None, aliases
=None
131 self
.section
= section
132 self
.setting
= setting
.lower()
133 self
.default
= default
134 self
.description
= description
135 self
.name
= setting
.upper()
136 if section
!= "main":
137 self
.name
= "_".join((section
.upper(), self
.name
))
139 self
.aliases
= [alias
.upper() for alias
in list(aliases
)]
142 self
.aliases
.insert(0, self
.name
)
143 # convert default to internal representation
144 if default
is NODEFAULT
:
147 _value
= self
.str2value(default
)
148 # value is private. use get() and set() to access
149 self
._value
= self
._default_value
= _value
151 def str2value(self
, value
):
152 """Return 'value' argument converted to internal representation"""
155 def _value2str(self
, value
):
156 """Return 'value' argument converted to external representation
158 This is actual conversion method called only when value
159 is not NODEFAULT. Heirs with different conversion rules
160 override this method, not the public .value2str().
165 def value2str(self
, value
=NODEFAULT
, current
=0):
166 """Return 'value' argument converted to external representation
168 If 'current' is True, use current option value.
173 if value
is NODEFAULT
:
176 return self
._value2str(value
)
179 """Return current option value"""
180 if self
._value
is NODEFAULT
:
181 raise OptionUnsetError(self
)
184 def set(self
, value
):
185 """Update the value"""
186 self
._value
= self
.str2value(value
)
189 """Reset the value to default"""
190 self
._value
= self
._default_value
193 """Return True if current value is the default one"""
194 return self
._value
== self
._default_value
197 """Return True if the value is available (either set or default)"""
198 return self
._value
!= NODEFAULT
201 return self
.value2str(self
._value
)
205 _format
= "<%(class)s %(name)s (default): %(value)s>"
207 _format
= "<%(class)s %(name)s (default: %(default)s): %(value)s>"
209 "class": self
.__class__
.__name__
,
211 "default": self
.value2str(self
._default_value
),
212 "value": self
.value2str(self
._value
),
216 """Return .ini file fragment for this option"""
218 for _description
in (self
.description
, self
.class_description
):
220 _desc_lines
.extend(_description
.split("\n"))
221 # comment out the setting line if there is no value
226 _rv
= "# %(description)s\n# Default: %(default)s\n" \
227 "%(is_set)s%(name)s = %(value)s\n" % {
228 "description": "\n# ".join(_desc_lines
),
229 "default": self
.value2str(self
._default_value
),
230 "name": self
.setting
,
231 "value": self
.value2str(self
._value
),
236 def load_ini(self
, config
):
237 """Load value from ConfigParser object"""
238 if config
.has_option(self
.section
, self
.setting
):
239 self
.set(config
.get(self
.section
, self
.setting
))
241 def load_pyconfig(self
, config
):
242 """Load value from old-style config (python module)"""
243 for _name
in self
.aliases
:
244 if hasattr(config
, _name
):
245 self
.set(getattr(config
, _name
))
248 class BooleanOption(Option
):
250 """Boolean option: yes or no"""
252 class_description
= "Allowed values: yes, no"
254 def _value2str(self
, value
):
260 def str2value(self
, value
):
261 if type(value
) == type(""):
263 if _val
in ("yes", "true", "on", "1"):
265 elif _val
in ("no", "false", "off", "0"):
268 raise OptionValueError(self
, value
, self
.class_description
)
270 _val
= value
and 1 or 0
273 class WordListOption(Option
):
275 """List of strings"""
277 class_description
= "Allowed values: comma-separated list of words"
279 def _value2str(self
, value
):
280 return ','.join(value
)
282 def str2value(self
, value
):
283 return value
.split(',')
285 class RunDetectorOption(Option
):
287 """When a detector is run: always, never or for new items only"""
289 class_description
= "Allowed values: yes, no, new"
291 def str2value(self
, value
):
293 if _val
in ("yes", "no", "new"):
296 raise OptionValueError(self
, value
, self
.class_description
)
298 class MailAddressOption(Option
):
302 Email addresses may be either fully qualified or local.
303 In the latter case MAIL_DOMAIN is automatically added.
308 _val
= Option
.get(self
)
310 _val
= "@".join((_val
, self
.config
["MAIL_DOMAIN"]))
313 class FilePathOption(Option
):
315 """File or directory path name
317 Paths may be either absolute or relative to the HOME.
321 class_description
= "The path may be either absolute or relative\n" \
322 "to the directory containig this config file."
325 _val
= Option
.get(self
)
326 if _val
and not os
.path
.isabs(_val
):
327 _val
= os
.path
.join(self
.config
["HOME"], _val
)
330 class FloatNumberOption(Option
):
332 """Floating point numbers"""
334 def str2value(self
, value
):
338 raise OptionValueError(self
, value
,
339 "Floating point number required")
341 def _value2str(self
, value
):
343 # strip fraction part from integer numbers
344 if _val
.endswith(".0"):
348 class IntegerNumberOption(Option
):
350 """Integer numbers"""
352 def str2value(self
, value
):
356 raise OptionValueError(self
, value
, "Integer number required")
358 class OctalNumberOption(Option
):
360 """Octal Integer numbers"""
362 def str2value(self
, value
):
366 raise OptionValueError(self
, value
, "Octal Integer number required")
368 def _value2str(self
, value
):
371 class NullableOption(Option
):
373 """Option that is set to None if its string value is one of NULL strings
375 Default nullable strings list contains empty string only.
376 There is constructor parameter allowing to specify different nullables.
378 Conversion to external representation returns the first of the NULL
379 strings list when the value is None.
385 def __init__(self
, config
, section
, setting
,
386 default
=NODEFAULT
, description
=None, aliases
=None,
387 null_strings
=NULL_STRINGS
389 self
.null_strings
= list(null_strings
)
390 Option
.__init__(self
, config
, section
, setting
, default
,
391 description
, aliases
)
393 def str2value(self
, value
):
394 if value
in self
.null_strings
:
399 def _value2str(self
, value
):
401 return self
.null_strings
[0]
405 class NullableFilePathOption(NullableOption
, FilePathOption
):
407 # .get() and class_description are from FilePathOption,
408 get
= FilePathOption
.get
409 class_description
= FilePathOption
.class_description
410 # everything else taken from NullableOption (inheritance order)
412 class TimezoneOption(Option
):
414 class_description
= \
415 "If pytz module is installed, value may be any valid\n" \
416 "timezone specification (e.g. EET or Europe/Warsaw).\n" \
417 "If pytz is not installed, value must be integer number\n" \
418 "giving local timezone offset from UTC in hours."
420 def str2value(self
, value
):
422 roundup
.date
.get_timezone(value
)
424 raise OptionValueError(self
, value
,
425 "Timezone name or numeric hour offset required")
428 class RegExpOption(Option
):
430 """Regular Expression option (value is Regular Expression Object)"""
432 class_description
= "Value is Python Regular Expression (UTF8-encoded)."
434 RE_TYPE
= type(re
.compile(""))
436 def __init__(self
, config
, section
, setting
,
437 default
=NODEFAULT
, description
=None, aliases
=None,
441 Option
.__init__(self
, config
, section
, setting
, default
,
442 description
, aliases
)
444 def _value2str(self
, value
):
445 assert isinstance(value
, self
.RE_TYPE
)
448 def str2value(self
, value
):
449 if not isinstance(value
, unicode):
451 # if it is 7-bit ascii, use it as string,
452 # otherwise convert to unicode.
454 value
.decode("ascii")
456 value
= value
.decode("utf-8")
457 return re
.compile(value
, self
.flags
)
459 ### Main configuration layout.
460 # Config is described as a sequence of sections,
461 # where each section name is followed by a sequence
462 # of Option definitions. Each Option definition
463 # is a sequence containing class name and constructor
464 # parameters, starting from the setting name:
465 # setting, default, [description, [aliases]]
466 # Note: aliases should only exist in historical options for backwards
467 # compatibility - new options should *not* have aliases!
470 (FilePathOption
, "database", "db", "Database directory path."),
471 (FilePathOption
, "templates", "html",
472 "Path to the HTML templates directory."),
473 (NullableFilePathOption
, "static_files", "",
474 "Path to directory holding additional static files\n"
475 "available via Web UI. This directory may contain\n"
476 "sitewide images, CSS stylesheets etc. and is searched\n"
477 "for these files prior to the TEMPLATES directory\n"
478 "specified above. If this option is not set, all static\n"
479 "files are taken from the TEMPLATES directory"),
480 (MailAddressOption
, "admin_email", "roundup-admin",
481 "Email address that roundup will complain to if it runs\n"
483 "If no domain is specified then the config item\n"
484 "mail -> domain is added."),
485 (MailAddressOption
, "dispatcher_email", "roundup-admin",
486 "The 'dispatcher' is a role that can get notified\n"
487 "of new items to the database.\n"
488 "It is used by the ERROR_MESSAGES_TO config setting.\n"
489 "If no domain is specified then the config item\n"
490 "mail -> domain is added."),
491 (Option
, "email_from_tag", "",
492 "Additional text to include in the \"name\" part\n"
493 "of the From: address used in nosy messages.\n"
494 "If the sending user is \"Foo Bar\", the From: line\n"
495 "is usually: \"Foo Bar\" <issue_tracker@tracker.example>\n"
496 "the EMAIL_FROM_TAG goes inside the \"Foo Bar\" quotes like so:\n"
497 "\"Foo Bar EMAIL_FROM_TAG\" <issue_tracker@tracker.example>"),
498 (Option
, "new_web_user_roles", "User",
499 "Roles that a user gets when they register"
500 " with Web User Interface.\n"
501 "This is a comma-separated string of role names"
502 " (e.g. 'Admin,User')."),
503 (Option
, "new_email_user_roles", "User",
504 "Roles that a user gets when they register"
505 " with Email Gateway.\n"
506 "This is a comma-separated string of role names"
507 " (e.g. 'Admin,User')."),
508 (Option
, "error_messages_to", "user",
509 # XXX This description needs better wording,
510 # with explicit allowed values list.
511 "Send error message emails to the dispatcher, user, or both?\n"
512 "The dispatcher is configured using the DISPATCHER_EMAIL"
514 (Option
, "html_version", "html4",
515 "HTML version to generate. The templates are html4 by default.\n"
516 "If you wish to make them xhtml, then you'll need to change this\n"
517 "var to 'xhtml' too so all auto-generated HTML is compliant.\n"
518 "Allowed values: html4, xhtml"),
519 (TimezoneOption
, "timezone", "UTC", "Default timezone offset,"
520 " applied when user's timezone is not set.",
521 ["DEFAULT_TIMEZONE"]),
522 (BooleanOption
, "instant_registration", "no",
523 "Register new users instantly, or require confirmation via\n"
525 (BooleanOption
, "email_registration_confirmation", "yes",
526 "Offer registration confirmation by email or only through the web?"),
527 (WordListOption
, "indexer_stopwords", "",
528 "Additional stop-words for the full-text indexer specific to\n"
529 "your tracker. See the indexer source for the default list of\n"
530 "stop-words (eg. A,AND,ARE,AS,AT,BE,BUT,BY, ...)"),
531 (OctalNumberOption
, "umask", "02",
532 "Defines the file creation mode mask."),
533 (IntegerNumberOption
, 'csv_field_size', '131072',
534 "Maximum size of a csv-field during import. Roundups export\n"
535 "format is a csv (comma separated values) variant. The csv\n"
536 "reader has a limit on the size of individual fields\n"
537 "starting with python 2.5. Set this to a higher value if you\n"
538 "get the error 'Error: field larger than field limit' during\n"
542 (Option
, "name", "Roundup issue tracker",
543 "A descriptive name for your roundup instance."),
544 (Option
, "web", NODEFAULT
,
545 "The web address that the tracker is viewable at.\n"
546 "This will be included in information"
547 " sent to users of the tracker.\n"
548 "The URL MUST include the cgi-bin part or anything else\n"
549 "that is required to get to the home page of the tracker.\n"
550 "You MUST include a trailing '/' in the URL."),
551 (MailAddressOption
, "email", "issue_tracker",
552 "Email address that mail to roundup should go to.\n"
553 "If no domain is specified then mail_domain is added."),
554 (NullableOption
, "language", "",
555 "Default locale name for this tracker.\n"
556 "If this option is not set, the language is determined\n"
557 "by OS environment variable LANGUAGE, LC_ALL, LC_MESSAGES,\n"
558 "or LANG, in that order of preference."),
561 (BooleanOption
, "allow_html_file", "no",
562 "Setting this option enables Roundup to serve uploaded HTML\n"
563 "file content *as HTML*. This is a potential security risk\n"
564 "and is therefore disabled by default. Set to 'yes' if you\n"
565 "trust *all* users uploading content to your tracker."),
566 (BooleanOption
, 'http_auth', "yes",
567 "Whether to use HTTP Basic Authentication, if present.\n"
568 "Roundup will use either the REMOTE_USER or HTTP_AUTHORIZATION\n"
569 "variables supplied by your web server (in that order).\n"
570 "Set this option to 'no' if you do not wish to use HTTP Basic\n"
571 "Authentication in your web interface."),
572 (BooleanOption
, 'use_browser_language', "yes",
573 "Whether to use HTTP Accept-Language, if present.\n"
574 "Browsers send a language-region preference list.\n"
575 "It's usually set in the client's browser or in their\n"
576 "Operating System.\n"
577 "Set this option to 'no' if you want to ignore it."),
578 (BooleanOption
, "debug", "no",
579 "Setting this option makes Roundup display error tracebacks\n"
580 "in the user's browser rather than emailing them to the\n"
584 (Option
, 'name', 'roundup',
585 "Name of the database to use.",
587 (NullableOption
, 'host', 'localhost',
588 "Database server host.",
590 (NullableOption
, 'port', '',
591 "TCP port number of the database server.\n"
592 "Postgresql usually resides on port 5432 (if any),\n"
593 "for MySQL default port number is 3306.\n"
594 "Leave this option empty to use backend default"),
595 (NullableOption
, 'user', 'roundup',
596 "Database user name that Roundup should use.",
598 (NullableOption
, 'password', 'roundup',
599 "Database user password.",
600 ['MYSQL_DBPASSWORD']),
601 (NullableOption
, 'read_default_file', '~/.my.cnf',
602 "Name of the MySQL defaults file.\n"
603 "Only used in MySQL connections."),
604 (NullableOption
, 'read_default_group', 'roundup',
605 "Name of the group to use in the MySQL defaults file (.my.cnf).\n"
606 "Only used in MySQL connections."),
607 (IntegerNumberOption
, 'cache_size', '100',
608 "Size of the node cache (in elements)"),
609 ), "Settings in this section are used"
610 " by RDBMS backends only"
613 (FilePathOption
, "config", "",
614 "Path to configuration file for standard Python logging module.\n"
615 "If this option is set, logging configuration is loaded\n"
616 "from specified file; options 'filename' and 'level'\n"
617 "in this section are ignored."),
618 (FilePathOption
, "filename", "",
619 "Log file name for minimal logging facility built into Roundup.\n"
620 "If no file name specified, log messages are written on stderr.\n"
621 "If above 'config' option is set, this option has no effect."),
622 (Option
, "level", "ERROR",
623 "Minimal severity level of messages written to log file.\n"
624 "If above 'config' option is set, this option has no effect.\n"
625 "Allowed values: DEBUG, INFO, WARNING, ERROR"),
628 (Option
, "domain", NODEFAULT
,
629 "The email domain that admin_email, issue_tracker and\n"
630 "dispatcher_email belong to.\n"
631 "This domain is added to those config items if they don't\n"
632 "explicitly include a domain.\n"
633 "Do not include the '@' symbol."),
634 (Option
, "host", NODEFAULT
,
635 "SMTP mail host that roundup will use to send mail",
637 (Option
, "username", "", "SMTP login name.\n"
638 "Set this if your mail host requires authenticated access.\n"
639 "If username is not empty, password (below) MUST be set!"),
640 (Option
, "password", NODEFAULT
, "SMTP login password.\n"
641 "Set this if your mail host requires authenticated access."),
642 (IntegerNumberOption
, "port", smtplib
.SMTP_PORT
,
643 "Default port to send SMTP on.\n"
644 "Set this if your mail server runs on a different port."),
645 (NullableOption
, "local_hostname", '',
646 "The local hostname to use during SMTP transmission.\n"
647 "Set this if your mail server requires something specific."),
648 (BooleanOption
, "tls", "no",
649 "If your SMTP mail host provides or requires TLS\n"
650 "(Transport Layer Security) then set this option to 'yes'."),
651 (NullableFilePathOption
, "tls_keyfile", "",
652 "If TLS is used, you may set this option to the name\n"
653 "of a PEM formatted file that contains your private key."),
654 (NullableFilePathOption
, "tls_certfile", "",
655 "If TLS is used, you may set this option to the name\n"
656 "of a PEM formatted certificate chain file."),
657 (Option
, "charset", "utf-8",
658 "Character set to encode email headers with.\n"
659 "We use utf-8 by default, as it's the most flexible.\n"
660 "Some mail readers (eg. Eudora) can't cope with that,\n"
661 "so you might need to specify a more limited character set\n"
664 (FilePathOption
, "debug", "",
665 "Setting this option makes Roundup to write all outgoing email\n"
666 "messages to this file *instead* of sending them.\n"
667 "This option has the same effect as environment variable"
668 " SENDMAILDEBUG.\nEnvironment variable takes precedence."),
669 (BooleanOption
, "add_authorinfo", "yes",
670 "Add a line with author information at top of all messages\n"
672 (BooleanOption
, "add_authoremail", "yes",
673 "Add the mail address of the author to the author information at\n"
674 "the top of all messages.\n"
675 "If this is false but add_authorinfo is true, only the name\n"
676 "of the actor is added which protects the mail address of the\n"
677 "actor from being exposed at mail archives, etc."),
678 ), "Outgoing email options.\nUsed for nozy messages and approval requests"),
680 (BooleanOption
, "keep_quoted_text", "yes",
681 "Keep email citations when accepting messages.\n"
682 "Setting this to \"no\" strips out \"quoted\" text"
683 " from the message.\n"
684 "Signatures are also stripped.",
685 ["EMAIL_KEEP_QUOTED_TEXT"]),
686 (BooleanOption
, "leave_body_unchanged", "no",
687 "Preserve the email body as is - that is,\n"
688 "keep the citations _and_ signatures.",
689 ["EMAIL_LEAVE_BODY_UNCHANGED"]),
690 (Option
, "default_class", "issue",
691 "Default class to use in the mailgw\n"
692 "if one isn't supplied in email subjects.\n"
693 "To disable, leave the value blank.",
694 ["MAIL_DEFAULT_CLASS"]),
695 (NullableOption
, "language", "",
696 "Default locale name for the tracker mail gateway.\n"
697 "If this option is not set, mail gateway will use\n"
698 "the language of the tracker instance."),
699 (Option
, "subject_prefix_parsing", "strict",
700 "Controls the parsing of the [prefix] on subject\n"
701 "lines in incoming emails. \"strict\" will return an\n"
702 "error to the sender if the [prefix] is not recognised.\n"
703 "\"loose\" will attempt to parse the [prefix] but just\n"
704 "pass it through as part of the issue title if not\n"
705 "recognised. \"none\" will always pass any [prefix]\n"
706 "through as part of the issue title."),
707 (Option
, "subject_suffix_parsing", "strict",
708 "Controls the parsing of the [suffix] on subject\n"
709 "lines in incoming emails. \"strict\" will return an\n"
710 "error to the sender if the [suffix] is not recognised.\n"
711 "\"loose\" will attempt to parse the [suffix] but just\n"
712 "pass it through as part of the issue title if not\n"
713 "recognised. \"none\" will always pass any [suffix]\n"
714 "through as part of the issue title."),
715 (Option
, "subject_suffix_delimiters", "[]",
716 "Defines the brackets used for delimiting the prefix and \n"
717 'suffix in a subject line. The presence of "suffix" in\n'
718 "the config option name is a historical artifact and may\n"
720 (Option
, "subject_content_match", "always",
721 "Controls matching of the incoming email subject line\n"
722 "against issue titles in the case where there is no\n"
723 "designator [prefix]. \"never\" turns off matching.\n"
724 "\"creation + interval\" or \"activity + interval\"\n"
725 "will match an issue for the interval after the issue's\n"
726 "creation or last activity. The interval is a standard\n"
727 "Roundup interval."),
728 (RegExpOption
, "refwd_re", "(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+",
729 "Regular expression matching a single reply or forward\n"
730 "prefix prepended by the mailer. This is explicitly\n"
731 "stripped from the subject during parsing."),
732 (RegExpOption
, "origmsg_re",
733 "^[>|\s]*-----\s?Original Message\s?-----$",
734 "Regular expression matching start of an original message\n"
735 "if quoted the in body."),
736 (RegExpOption
, "sign_re", "^[>|\s]*-- ?$",
737 "Regular expression matching the start of a signature\n"
738 "in the message body."),
739 (RegExpOption
, "eol_re", r
"[\r\n]+",
740 "Regular expression matching end of line."),
741 (RegExpOption
, "blankline_re", r
"[\r\n]+\s*[\r\n]+",
742 "Regular expression matching a blank line."),
743 (BooleanOption
, "ignore_alternatives", "no",
744 "When parsing incoming mails, roundup uses the first\n"
745 "text/plain part it finds. If this part is inside a\n"
746 "multipart/alternative, and this option is set, all other\n"
747 "parts of the multipart/alternative are ignored. The default\n"
748 "is to keep all parts and attach them to the issue."),
749 ), "Roundup Mail Gateway options"),
751 (BooleanOption
, "enable", "no",
752 "Enable PGP processing. Requires pyme."),
753 (NullableOption
, "roles", "",
754 "If specified, a comma-separated list of roles to perform\n"
755 "PGP processing on. If not specified, it happens for all\n"
757 (NullableOption
, "homedir", "",
758 "Location of PGP directory. Defaults to $HOME/.gnupg if\n"
760 ), "OpenPGP mail processing options"),
762 (RunDetectorOption
, "messages_to_author", "no",
763 "Send nosy messages to the author of the message.",
764 ["MESSAGES_TO_AUTHOR"]),
765 (Option
, "signature_position", "bottom",
766 "Where to place the email signature.\n"
767 "Allowed values: top, bottom, none",
768 ["EMAIL_SIGNATURE_POSITION"]),
769 (RunDetectorOption
, "add_author", "new",
770 "Does the author of a message get placed on the nosy list\n"
771 "automatically? If 'new' is used, then the author will\n"
772 "only be added when a message creates a new issue.\n"
773 "If 'yes', then the author will be added on followups too.\n"
774 "If 'no', they're never added to the nosy.\n",
775 ["ADD_AUTHOR_TO_NOSY"]),
776 (RunDetectorOption
, "add_recipients", "new",
777 "Do the recipients (To:, Cc:) of a message get placed on the\n"
778 "nosy list? If 'new' is used, then the recipients will\n"
779 "only be added when a message creates a new issue.\n"
780 "If 'yes', then the recipients will be added on followups too.\n"
781 "If 'no', they're never added to the nosy.\n",
782 ["ADD_RECIPIENTS_TO_NOSY"]),
783 (Option
, "email_sending", "single",
784 "Controls the email sending from the nosy reactor. If\n"
785 "\"multiple\" then a separate email is sent to each\n"
786 "recipient. If \"single\" then a single email is sent with\n"
787 "each recipient as a CC address."),
788 (IntegerNumberOption
, "max_attachment_size", sys
.maxint
,
789 "Attachments larger than the given number of bytes\n"
790 "won't be attached to nosy mails. They will be replaced by\n"
791 "a link to the tracker's download page for the file.")
792 ), "Nosy messages sending"),
795 ### Configuration classes
799 """Base class for configuration objects.
801 Configuration options may be accessed as attributes or items
802 of instances of this class. All option names are uppercased.
807 INI_FILE
= "config.ini"
809 # Object attributes that should not be taken as common configuration
810 # options in __setattr__ (most of them are initialized in constructor):
811 # builtin pseudo-option - package home directory
813 # names of .ini file sections, in order
816 section_descriptions
= None
817 # lists of option names for each section, in order
818 section_options
= None
819 # mapping from option names and aliases to Option instances
821 # actual name of the config file. set on load.
822 filepath
= os
.path
.join(HOME
, INI_FILE
)
824 def __init__(self
, config_path
=None, layout
=None, settings
={}):
825 """Initialize confing instance
829 optional directory or file name of the config file.
830 If passed, load the config after processing layout (if any).
831 If config_path is a directory name, use default base name
834 optional configuration layout, a sequence of
835 section definitions suitable for .add_section()
837 optional setting overrides (dictionary).
838 The overrides are applied after loading config file.
841 # initialize option containers:
843 self
.section_descriptions
= {}
844 self
.section_options
= {}
846 # add options from the layout structure
848 for section
in layout
:
849 self
.add_section(*section
)
850 if config_path
is not None:
851 self
.load(config_path
)
852 for (name
, value
) in settings
.items():
853 self
[name
.upper()] = value
855 def add_section(self
, section
, options
, description
=None):
856 """Define new config section
859 section - name of the config.ini section
860 options - a sequence of Option definitions.
861 Each Option definition is a sequence
862 containing class object and constructor
863 parameters, starting from the setting name:
864 setting, default, [description, [aliases]]
865 description - optional section comment
867 Note: aliases should only exist in historical options
868 for backwards compatibility - new options should
872 if description
or not self
.section_descriptions
.has_key(section
):
873 self
.section_descriptions
[section
] = description
874 for option_def
in options
:
875 klass
= option_def
[0]
876 args
= option_def
[1:]
877 option
= klass(self
, section
, *args
)
878 self
.add_option(option
)
880 def add_option(self
, option
):
881 """Adopt a new Option object"""
882 _section
= option
.section
883 _name
= option
.setting
884 if _section
not in self
.sections
:
885 self
.sections
.append(_section
)
886 _options
= self
._get_section_options(_section
)
887 if _name
not in _options
:
888 _options
.append(_name
)
889 # (section, name) key is used for writing .ini file
890 self
.options
[(_section
, _name
)] = option
891 # make the option known under all of its A.K.A.s
892 for _name
in option
.aliases
:
893 self
.options
[_name
] = option
895 def update_option(self
, name
, klass
,
896 default
=NODEFAULT
, description
=None
898 """Override behaviour of early created option.
904 one of the Option classes
906 optional default value for the option
908 optional new description for the option
910 Conversion from current option value to new class value
911 is done via string representation.
913 This method may be used to attach some brains
914 to options autocreated by UserConfig.
917 # fetch current option
918 option
= self
._get_option(name
)
919 # compute constructor parameters
920 if default
is NODEFAULT
:
921 default
= option
.default
922 if description
is None:
923 description
= option
.description
924 value
= option
.value2str(current
=1)
925 # resurrect the option
926 option
= klass(self
, option
.section
, option
.setting
,
927 default
=default
, description
=description
)
930 # incorporate new option
932 self
.add_option(option
)
935 """Set all options to their default values"""
936 for _option
in self
.items():
939 # Meant for commandline tools.
940 # Allows automatic creation of configuration files like this:
941 # roundup-server -p 8017 -u roundup --save-config
942 def getopt(self
, args
, short_options
="", long_options
=(),
943 config_load_options
=("C", "config"), **options
945 """Apply options specified in command line arguments.
949 command line to parse (sys.argv[1:])
951 optional string of letters for command line options
952 that are not config options
954 optional list of names for long options
955 that are not config options
957 two-element sequence (letter, long_option) defining
958 the options for config file. If unset, don't load
959 config file; otherwise config file is read prior
960 to applying other options. Short option letter
961 must not have a colon and long_option name must
962 not have an equal sign or '--' prefix.
964 mapping from option names to command line option specs.
965 e.g. server_port="p:", server_user="u:"
966 Names are forced to lower case for commandline parsing
967 (long options) and to upper case to find config options.
968 Command line options accepting no value are assumed
969 to be binary and receive value 'yes'.
971 Return value: same as for python standard getopt(), except that
972 processed options are removed from returned option list.
975 # take a copy of long_options
976 long_options
= list(long_options
)
980 for (name
, letter
) in options
.items():
981 cfg_name
= name
.upper()
982 short_opt
= "-" + letter
[0]
983 name
= name
.lower().replace("_", "-")
984 cfg_names
.update({short_opt
: cfg_name
, "--" + name
: cfg_name
})
986 short_options
+= letter
987 if letter
[-1] == ":":
988 long_options
.append(name
+ "=")
990 booleans
.append(short_opt
)
991 long_options
.append(name
)
993 if config_load_options
:
994 short_options
+= config_load_options
[0] + ":"
995 long_options
.append(config_load_options
[1] + "=")
996 # compute names that will be searched in getopt return value
997 config_load_options
= (
998 "-" + config_load_options
[0],
999 "--" + config_load_options
[1],
1001 # parse command line arguments
1002 optlist
, args
= getopt
.getopt(args
, short_options
, long_options
)
1003 # load config file if requested
1004 if config_load_options
:
1005 for option
in optlist
:
1006 if option
[0] in config_load_options
:
1007 self
.load_ini(option
[1])
1008 optlist
.remove(option
)
1012 for (opt
, arg
) in optlist
:
1013 if (opt
in booleans
): # and not arg
1016 name
= cfg_names
[opt
]
1018 extra_options
.append((opt
, arg
))
1021 return (extra_options
, args
)
1023 # option and section locators (used in option access methods)
1025 def _get_option(self
, name
):
1027 return self
.options
[name
]
1029 raise InvalidOptionError(name
)
1031 def _get_section_options(self
, name
):
1032 return self
.section_options
.setdefault(name
, [])
1034 def _get_unset_options(self
):
1035 """Return options that need manual adjustments
1037 Return value is a dictionary where keys are section
1038 names and values are lists of option names as they
1039 appear in the config file.
1043 for option
in self
.items():
1044 if not option
.isset():
1045 need_set
.setdefault(option
.section
, []).append(option
.setting
)
1048 def _adjust_options(self
, config
):
1049 """Load ad-hoc option definitions from ConfigParser instance."""
1052 def _get_name(self
):
1053 """Return the service name for config file heading"""
1058 def load_ini(self
, config_path
, defaults
=None):
1059 """Set options from config.ini file in given home_dir
1063 directory or file name of the config file.
1064 If config_path is a directory name, use default
1065 base name of the config file
1067 optional dictionary of defaults for ConfigParser
1069 Note: if home_dir does not contain config.ini file,
1070 no error is raised. Config will be reset to defaults.
1073 if os
.path
.isdir(config_path
):
1074 home_dir
= config_path
1075 config_path
= os
.path
.join(config_path
, self
.INI_FILE
)
1077 home_dir
= os
.path
.dirname(config_path
)
1079 config_defaults
= {"HOME": home_dir
}
1081 config_defaults
.update(defaults
)
1082 config
= ConfigParser
.ConfigParser(config_defaults
)
1083 config
.read([config_path
])
1084 # .ini file loaded ok.
1085 self
.HOME
= home_dir
1086 self
.filepath
= config_path
1087 self
._adjust_options(config
)
1088 # set the options, starting from HOME
1090 for option
in self
.items():
1091 option
.load_ini(config
)
1093 def load(self
, home_dir
):
1094 """Load configuration settings from home_dir"""
1095 self
.load_ini(home_dir
)
1097 def save(self
, ini_file
=None):
1098 """Write current configuration to .ini file
1100 'ini_file' argument, if passed, must be valid full path
1101 to the file to write. If omitted, default file in current
1104 If the file to write already exists, it is saved with '.bak'
1108 if ini_file
is None:
1109 ini_file
= self
.filepath
1110 _tmp_file
= os
.path
.splitext(ini_file
)[0]
1111 _bak_file
= _tmp_file
+ ".bak"
1112 _tmp_file
= _tmp_file
+ ".tmp"
1113 _fp
= file(_tmp_file
, "wt")
1114 _fp
.write("# %s configuration file\n" % self
._get_name())
1115 _fp
.write("# Autogenerated at %s\n" % time
.asctime())
1116 need_set
= self
._get_unset_options()
1118 _fp
.write("\n# WARNING! Following options need adjustments:\n")
1119 for section
, options
in need_set
.items():
1120 _fp
.write("# [%s]: %s\n" % (section
, ", ".join(options
)))
1121 for section
in self
.sections
:
1122 comment
= self
.section_descriptions
.get(section
, None)
1124 _fp
.write("\n# ".join([""] + comment
.split("\n")) +"\n")
1126 # no section comment - just leave a blank line between sections
1128 _fp
.write("[%s]\n" % section
)
1129 for option
in self
._get_section_options(section
):
1130 _fp
.write("\n" + self
.options
[(section
, option
)].format())
1132 if os
.access(ini_file
, os
.F_OK
):
1133 if os
.access(_bak_file
, os
.F_OK
):
1134 os
.remove(_bak_file
)
1135 os
.rename(ini_file
, _bak_file
)
1136 os
.rename(_tmp_file
, ini_file
)
1138 # container emulation
1141 return len(self
.items())
1143 def __getitem__(self
, name
):
1147 return self
._get_option(name
).get()
1149 def __setitem__(self
, name
, value
):
1153 self
._get_option(name
).set(value
)
1155 def __delitem__(self
, name
):
1156 _option
= self
._get_option(name
)
1157 _section
= _option
.section
1158 _name
= _option
.setting
1159 self
._get_section_options(_section
).remove(_name
)
1160 del self
.options
[(_section
, _name
)]
1161 for _alias
in _option
.aliases
:
1162 del self
.options
[_alias
]
1165 """Return the list of Option objects, in .ini file order
1167 Note that HOME is not included in this list
1168 because it is builtin pseudo-option, not a real Option
1169 object loaded from or saved to .ini file.
1172 return [self
.options
[(_section
, _name
)]
1173 for _section
in self
.sections
1174 for _name
in self
._get_section_options(_section
)
1178 """Return the list of "canonical" names of the options
1180 Unlike .items(), this list also includes HOME
1183 return ["HOME"] + [_option
.name
for _option
in self
.items()]
1185 # .values() is not implemented because i am not sure what should be
1186 # the values returned from this method: Option instances or config values?
1188 # attribute emulation
1190 def __setattr__(self
, name
, value
):
1191 if self
.__dict__
.has_key(name
) or hasattr(self
.__class__
, name
):
1192 self
.__dict__
[name
] = value
1194 self
._get_option(name
).set(value
)
1196 # Note: __getattr__ is not symmetric to __setattr__:
1197 # self.__dict__ lookup is done before calling this method
1198 def __getattr__(self
, name
):
1201 class UserConfig(Config
):
1203 """Configuration for user extensions.
1205 Instances of this class have no predefined configuration layout.
1206 Options are created on the fly for each setting present in the
1211 def _adjust_options(self
, config
):
1212 # config defaults appear in all sections.
1213 # we'll need to filter them out.
1214 defaults
= config
.defaults().keys()
1215 # see what options are already defined and add missing ones
1216 preset
= [(option
.section
, option
.setting
) for option
in self
.items()]
1217 for section
in config
.sections():
1218 for name
in config
.options(section
):
1219 if ((section
, name
) not in preset
) \
1220 and (name
not in defaults
):
1221 self
.add_option(Option(self
, section
, name
))
1223 class CoreConfig(Config
):
1225 """Roundup instance configuration.
1227 Core config has a predefined layout (see the SETTINGS structure),
1228 supports loading of old-style pythonic configurations and holds
1229 three additional attributes:
1231 instance logging engine, from standard python logging module
1232 or minimalistic logger implemented in Roundup
1234 user-defined configuration for detectors
1236 user-defined configuration for extensions
1240 # module name for old style configuration
1246 def __init__(self
, home_dir
=None, settings
={}):
1247 Config
.__init__(self
, home_dir
, layout
=SETTINGS
, settings
=settings
)
1248 # load the config if home_dir given
1249 if home_dir
is None:
1254 new
.sections
= list(self
.sections
)
1255 new
.section_descriptions
= dict(self
.section_descriptions
)
1256 new
.section_options
= dict(self
.section_options
)
1257 new
.options
= dict(self
.options
)
1260 def _get_unset_options(self
):
1261 need_set
= Config
._get_unset_options(self
)
1262 # remove MAIL_PASSWORD if MAIL_USER is empty
1263 if "password" in need_set
.get("mail", []):
1264 if not self
["MAIL_USERNAME"]:
1265 settings
= need_set
["mail"]
1266 settings
.remove("password")
1268 del need_set
["mail"]
1271 def _get_name(self
):
1272 return self
["TRACKER_NAME"]
1279 self
.detectors
.reset()
1282 def init_logging(self
):
1283 _file
= self
["LOGGING_CONFIG"]
1284 if _file
and os
.path
.isfile(_file
):
1285 logging
.config
.fileConfig(_file
)
1288 _file
= self
["LOGGING_FILENAME"]
1289 # set file & level on the root logger
1290 logger
= logging
.getLogger()
1292 hdlr
= logging
.FileHandler(_file
)
1294 hdlr
= logging
.StreamHandler(sys
.stdout
)
1295 formatter
= logging
.Formatter(
1296 '%(asctime)s %(levelname)s %(message)s')
1297 hdlr
.setFormatter(formatter
)
1298 # no logging API to remove all existing handlers!?!
1299 for h
in logger
.handlers
:
1301 logger
.removeHandler(hdlr
)
1302 logger
.handlers
= [hdlr
]
1303 logger
.setLevel(logging
._levelNames
[self
["LOGGING_LEVEL"] or "ERROR"])
1305 def load(self
, home_dir
):
1306 """Load configuration from path designated by home_dir argument"""
1307 if os
.path
.isfile(os
.path
.join(home_dir
, self
.INI_FILE
)):
1308 self
.load_ini(home_dir
)
1310 self
.load_pyconfig(home_dir
)
1312 self
.ext
= UserConfig(os
.path
.join(home_dir
, "extensions"))
1313 self
.detectors
= UserConfig(os
.path
.join(home_dir
, "detectors"))
1315 def load_ini(self
, home_dir
, defaults
=None):
1316 """Set options from config.ini file in given home_dir directory"""
1317 config_defaults
= {"TRACKER_HOME": home_dir
}
1319 config_defaults
.update(defaults
)
1320 Config
.load_ini(self
, home_dir
, config_defaults
)
1322 def load_pyconfig(self
, home_dir
):
1323 """Set options from config.py file in given home_dir directory"""
1324 # try to locate and import the module
1328 _module
= imp
.find_module(self
.PYCONFIG
, [home_dir
])
1329 _mod_fp
= _module
[0]
1330 _config
= imp
.load_module(self
.PYCONFIG
, *_module
)
1332 raise NoConfigError(home_dir
)
1334 if _mod_fp
is not None:
1336 # module loaded ok. set the options, starting from HOME
1338 self
.HOME
= home_dir
1339 for _option
in self
.items():
1340 _option
.load_pyconfig(_config
)
1341 # backward compatibility:
1342 # SMTP login parameters were specified as a tuple in old style configs
1343 # convert them to new plain string options
1344 _mailuser
= getattr(_config
, "MAILUSER", ())
1345 if len(_mailuser
) > 0:
1346 self
.MAIL_USERNAME
= _mailuser
[0]
1347 if len(_mailuser
) > 1:
1348 self
.MAIL_PASSWORD
= _mailuser
[1]
1350 # in this config, HOME is also known as TRACKER_HOME
1351 def __getitem__(self
, name
):
1352 if name
== "TRACKER_HOME":
1355 return Config
.__getitem__(self
, name
)
1357 def __setitem__(self
, name
, value
):
1358 if name
== "TRACKER_HOME":
1361 self
._get_option(name
).set(value
)
1363 def __setattr__(self
, name
, value
):
1364 if name
== "TRACKER_HOME":
1365 self
.__dict__
["HOME"] = value
1367 Config
.__setattr__(self
, name
, value
)
1369 # vim: set et sts=4 sw=4 :