Skip to content


Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 7ca09724c4
Fetching contributors…

Cannot retrieve contributors at this time

352 lines (275 sloc) 12.795 kb
#!/usr/bin/env python
# -*- coding: utf-8 -*-
.. currentmodule:: gpypi.config
Configuration module
Implements :class:`Config` and :class:`ConfigManager` to be used as
"configuration holders" and validators.
import os
import shutil
import logging
from ConfigParser import SafeConfigParser
from portage.output import colorize
from gpypi.utils import asbool
from gpypi.exc import *
log = logging.getLogger(__name__)
HERE = os.path.dirname(os.path.abspath(__file__))
class Config(dict):
"""Holds config values retrieved from various sources. To load
configuration from a source use one of :meth:`from_*` methods.
Class also defines specification for supported options in :attr:`allowed_options`.
Values are retrieved with help of :meth:`Config.validator` method.
>>> Config.from_pypi({'homepage': 'foobar'})
<Config {'homepage': 'foobar'}>
:attr:`allowed_options` format::
'name': ('Question ..', obj_type, default_value)
allowed_options = {
# 'config_name': ("doc", "type", "default_value"),
'up_pn': ('Upstream package name', str, ""),
'up_pv': ('Upstream package version', str, ""),
'pn': ('Specify PN to use when naming ebuild', str, ""),
'pv': ('Specify PV to use when naming ebuild', str, ""),
# TODO: move my_* stuff into config, make [] as default and make sure it handles lists from ini
'my_pv': ('Specify MY_PV used in ebuild', str, ""),
'my_pn': ('Specify MY_PN used in ebuild', str, ""),
'my_p': ('Specify MY_P used in ebuild', str, ""),
'uri': ('Specify SRC_URI of the package', str, ""),
'index_url': ('Base URL for PyPi', str, ""),
'overlay': ('Specify overlay to use by name (stored in $OVERLAY/profiles/repo_name)', str, "local"),
'overwrite': ('Overwrite existing ebuild', bool, False),
'no_deps': ("Don't create ebuilds for any needed dependencies", bool, False),
'category': ("Specify portage category to use when creating ebuild", str, ""),
'format': ("Format when printing to stdout (use pygments identifier)", str, "none"),
'command': ("Name of command that was invoked on CLI", str, ""),
'nocolors': ("Disable colorful output", bool, False),
'background': ("Background of terminal when using formatting", str, 'dark'),
#'pretend': ("Print ebuild to stdout, don't write ebuild file, don't download SRC_URI", bool, False),
'license': ("Portage license for the ebuild", str, ""),
'keywords': ("PyPI keywords", str, ""),
# TODO: homepage will be a list
'homepage': ("Homepage of the package", str, ""),
'description': ("Short description of the package", str, ""),
'long_description': ("Long description of the package", str, ""),
'gentoo_keywords': ("Portage keywords for ebuild masking", str, "~x86"),
# metadata
'metadata_disable': ("Disable metadata generation", bool, False),
'metadata_use_echangelog_user': ("Use ECHANGELOG_USER", bool, False),
'metadata_herd': ("Herd for ebuild metadata", str, ""),
'metadata_maintainer_description': ("Maintainer descriptions for ebuild metadata (comma separated)", str, ""),
'metadata_maintainer_email': ("Maintainer emails for ebuild metadata (comma separated)", str, ""),
'metadata_maintainer_name': ("Maintainer names for ebuild metadata (comma separated)", str, ""),
# echangelog
'echangelog_disable': ("Disable echangelog", bool, False),
'echangelog_message': ("Echangelog commit message", str, "Initial ebuild generated by g-pypi"),
# repoman
'repoman_commands': ("List of repoman commands to issue on each ebuild (separated by space)", str, "manifest"),
def __repr__(self):
return "<Config %s>" % dict.__repr__(self)
## from_config
def from_pypi(cls, metadata):
"""Load config from :term:`PyPi`
:param metadata: Metadata retrieved from :term:`PyPi` query
:type metadata: dict
:returns: :class:`Config` instance
return cls(metadata)
def from_ini(cls, path_to_ini, section='config'):
"""Load config from .ini
:param path_to_ini: Retrieve dictionary from `path_to_ini` file, from `section`
:type path_to_ini: file path
:param section: Name of the section to be used
:type section: string
:returns: :class:`Config` instance
config = SafeConfigParser()
d = map(lambda i: (i[0], cls.validate(i[0], i[1])), config.items(section))
return cls(d)
def from_setup_py(cls, keywords):
"""Load config from
:param keywords: option passed to :func:`setup` function in **setup.y** file.
:type keywords: dict
:returns: :class:`Config` instance
return cls(keywords)
def from_argparse(cls, options):
"""Load config from argparse options.
:param options: Arguments retrieved from `parser.parse_args()`
:type options: `argparse.Namespace` instance
:returns: :class:`Config` instance
return cls(filter(lambda i: i[1] is not None, options.__dict__.iteritems()))
## validate types
def validate(cls, name, value):
"""Validates and parses config value. Will dispatch calls to
subvalidators based on type of the config option.
:param name: key from :attr:`Config.allowed_options`
:type name: string
:param value: Value to be validated and parsed
:type value: everything
validator = cls.allowed_options[name][1]
if isinstance(validator, type):
f = getattr(cls, 'validate_%s' % validator.__name__)
f = getattr(cls, 'validate_%s' % validator)
return f(value)
def validate_bool(cls, value):
"""Subvalidator which handles string values into bool
:raises: :exc:`GPyPiValidationError` if not a bool
return asbool(value)
except ValueError:
raise GPyPiValidationError("Not a boolean (write y/n): %r" % value)
def validate_str(cls, value, encoding='utf-8'):
"""Subvalidator for string. Also converts to unicode
:raises: :exc:`GPyPiValidationError` if not a string
if isinstance(value, basestring):
if isinstance(value, str):
value = unicode(value, encoding)
return value
raise GPyPiValidationError("Not a string: %r" % value)
class ConfigManager(object):
"""Holds multiple :class:`Config` instances and retrieves
values from them.
:param use: Order of configuration taken in account
:type use: list of strings
:param questionnaire_options: What options will not use default if not
given, but rather invoke interactive :class:`Questionnaire`
:type questionnaire_options: list of strings
:param questionnaire_class: class to be used for questionnaire,
defaults to :class:`Questionnaire`
:type questionnaire_class: class
:raises: :exc:`gpypi.exc.GPyPiConfigurationError` when:
* no config is set
* when option is retrieved that does not exist in :attr:`Config.allowed_options`
* `use` does not have unique elements
:attr:`INI_TEMPLATE_PATH` -- Absolute path to .ini template file
>>> mgr = ConfigManager(['pypi', 'setup_py'])
>>> mgr.configs['pypi'] = (Config.from_pypi({}))
>>> mgr.configs['setup_py'] = (Config.from_setup_py({'overlay': 'foobar'}))
>>> print mgr.overlay
INI_TEMPLATE_PATH = os.path.join(HERE, 'templates', 'gpypi.ini')
def __init__(self, use, questionnaire_options=None, questionnaire_class=None):
for config in use:
if use.count(config) != 1:
raise GPyPiConfigurationError("ConfigManager could not be setup"
", config order has non-unique member: %s" % config)
self.use = ['questionnaire'] + use
self.questionnaire_options = questionnaire_options or []
self.q = (questionnaire_class or Questionnaire)(self)
self.configs = {'questionnaire': {}}
def __repr__(self):
return "<ConfigManager configs(%s) use(%s)>" % (self.configs.keys(), self.use)
def __getattr__(self, name):
if len(self.configs) == 1:
raise GPyPiConfigurationError("At least one config file must be used.")
if name not in Config.allowed_options:
raise GPyPiConfigurationError("No such option in Config.allowed_options: %s" % name)
for config_name in self.use:
value = self.configs.get(config_name, {}).get(name, None)
log.debug("Got %r from %s", value, config_name)
if value is not None:
return value
return self.default_or_question(name)
def default_or_question(self, name):
"""When no value is retrieved from :attr:`ConfigManager.configs`,
:class:`Questionnaire` is used for interactive request if ``name``
is in :attr:`ConfigManager.questionnaire_options`. Otherwise, default
is used.
:param name: Option name to be processed
:param type: string
:returns: config value
if name in self.questionnaire_options:
self.configs['questionnaire'][name] = self.q.ask(name)
return self.configs['questionnaire'][name]
return Config.allowed_options[name][2]
def load_from_ini(cls, path_to_ini, section="config_manager"):
"""Load :class:`ConfigManager` from ini file. Also populates
:param path_to_ini: Filesystem path to ini file
:type path_to_ini: string
:param section: ini section to be used for :class:`ConfigManager` configuration
:type section: string
if not os.path.exists(path_to_ini):
shutil.copy(cls.INI_TEMPLATE_PATH, path_to_ini)'Config was generated at %s', path_to_ini)
config = SafeConfigParser()
# add sections if not available
for sec in [section, 'config']:
if sec not in config.sections():
config.write(open(path_to_ini, 'w'))
config_mgr = dict(config.items(section))
use = config_mgr.get('use', '').split()
q_options = config_mgr.get('questionnaire_options', '').split()
mgr = cls(use, q_options)
mgr.configs['ini'] = Config.from_ini(path_to_ini)
return mgr
class Questionnaire(object):
"""Class that handles interactive shell questions when
no :class:`Config` instance provides the value for requested
def __init__(self, options):
self.options = options
def ask(self, name, input_f=raw_input):
"""Ask for a config value.
:param name: Name of config option to be retrieved
:type name: string
:param input_f: function to do the asking
:type: input_f: function
:returns: Config value or if given option not valid, ask again.
if self.options.nocolors:
msg = "%s [%r]: "
msg = colorize("GOOD", " * ") + "%s" + colorize("BRACKET", " [")\
+ "%r" + colorize("BRACKET", ']') + ": "
option = Config.allowed_options[name]
# ask the question or use default
answer = input_f(msg % (option[0].capitalize(), option[2])) or option[2]
return Config.validate(name, answer)
except GPyPiValidationError, e:
log.error("Error: %s" % e)
return self.ask(name, input_f)
def print_help(self):
"""Print short description that interactive mode is turned on.
Will print only once.
""""You are using interactive mode for configuration.")"Answer questions with configuration value or press enter")"to use default value printed in brackets.\n")
Jump to Line
Something went wrong with that request. Please try again.