Skip to content

HTTPS clone URL

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.
Example::
>>> 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, "http://pypi.python.org/pypi"),
'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
@classmethod
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)
@classmethod
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()
config.read(path_to_ini)
d = map(lambda i: (i[0], cls.validate(i[0], i[1])), config.items(section))
return cls(d)
@classmethod
def from_setup_py(cls, keywords):
"""Load config from setup.py
:param keywords: option passed to :func:`setup` function in **setup.y** file.
:type keywords: dict
:returns: :class:`Config` instance
"""
return cls(keywords)
@classmethod
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
@classmethod
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__)
else:
f = getattr(cls, 'validate_%s' % validator)
return f(value)
@classmethod
def validate_bool(cls, value):
"""Subvalidator which handles string values into bool
:raises: :exc:`GPyPiValidationError` if not a bool
"""
try:
return asbool(value)
except ValueError:
raise GPyPiValidationError("Not a boolean (write y/n): %r" % value)
@classmethod
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
else:
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
Example::
>>> mgr = ConfigManager(['pypi', 'setup_py'])
>>> mgr.configs['pypi'] = (Config.from_pypi({}))
>>> mgr.configs['setup_py'] = (Config.from_setup_py({'overlay': 'foobar'}))
>>> print mgr.overlay
foobar
"""
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
else:
continue
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]
else:
return Config.allowed_options[name][2]
@classmethod
def load_from_ini(cls, path_to_ini, section="config_manager"):
"""Load :class:`ConfigManager` from ini file. Also populates
``Config.configs[ini]``.
: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
"""
CHANGED = False
if not os.path.exists(path_to_ini):
shutil.copy(cls.INI_TEMPLATE_PATH, path_to_ini)
log.info('Config was generated at %s', path_to_ini)
config = SafeConfigParser()
config.read(path_to_ini)
# add sections if not available
for sec in [section, 'config']:
if sec not in config.sections():
config.add_section(sec)
CHANGED = True
if CHANGED:
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
option.
"""
IS_FIRST_QUESTION = True
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.IS_FIRST_QUESTION:
self.print_help()
if self.options.nocolors:
msg = "%s [%r]: "
else:
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]
try:
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.
"""
log.info("You are using interactive mode for configuration.")
log.info("Answer questions with configuration value or press enter")
log.info("to use default value printed in brackets.\n")
self.IS_FIRST_QUESTION = False
Jump to Line
Something went wrong with that request. Please try again.