-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
import io | ||
import os | ||
import sys | ||
from functools import partial | ||
|
||
from distutils.errors import DistutilsOptionError | ||
from setuptools.py26compat import import_module | ||
from setuptools.extern.six import string_types | ||
|
||
|
||
class ConfigHandler(object): | ||
"""Handles metadata supplied in configuration files.""" | ||
|
||
section_prefix = None | ||
|
||
def __init__(self, target_obj, options): | ||
sections = {} | ||
|
||
section_prefix = self.section_prefix | ||
for section_name, section_options in options.items(): | ||
if not section_name.startswith(section_prefix): | ||
continue | ||
|
||
section_name = section_name.replace(section_prefix, '').strip(':') | ||
sections[section_name] = section_options | ||
|
||
self.target_obj = target_obj | ||
self.sections = sections | ||
|
||
@property | ||
def parsers(self): | ||
"""Metadata item name to parser function mapping.""" | ||
raise NotImplementedError( | ||
'%s must provide .parsers property' % self.__class__.__name__) | ||
|
||
def __setitem__(self, option_name, value): | ||
unknown = tuple() | ||
target_obj = self.target_obj | ||
|
||
current_value = getattr(target_obj, option_name, unknown) | ||
|
||
if current_value is unknown: | ||
raise KeyError(option_name) | ||
|
||
if current_value: | ||
# Already inhabited. Skipping. | ||
return | ||
|
||
parser = self.parsers.get(option_name) | ||
if parser: | ||
value = parser(value) | ||
|
||
setter = getattr(target_obj, 'set_%s' % option_name, None) | ||
if setter is None: | ||
setattr(target_obj, option_name, value) | ||
else: | ||
setter(value) | ||
|
||
@classmethod | ||
def _parse_list(cls, value, separator=','): | ||
"""Represents value as a list. | ||
Value is split either by comma or by lines. | ||
:param value: | ||
:param separator: List items separator character. | ||
:rtype: list | ||
""" | ||
if isinstance(value, list): # _parse_complex case | ||
return value | ||
|
||
if '\n' in value: | ||
value = value.splitlines() | ||
else: | ||
value = value.split(separator) | ||
|
||
return [chunk.strip() for chunk in value] | ||
|
||
@classmethod | ||
def _parse_dict(cls, value): | ||
"""Represents value as a dict. | ||
:param value: | ||
:rtype: dict | ||
""" | ||
separator = '=' | ||
result = {} | ||
for line in cls._parse_list(value): | ||
key, sep, val = line.partition(separator) | ||
if sep != separator: | ||
raise DistutilsOptionError( | ||
'Unable to parse option value to dict: %s' % value) | ||
result[key.strip()] = val.strip() | ||
|
||
return result | ||
|
||
@classmethod | ||
def _parse_bool(cls, value): | ||
"""Represents value as boolean. | ||
:param value: | ||
:rtype: bool | ||
""" | ||
value = value.lower() | ||
return value in ('1', 'true', 'yes') | ||
|
||
@classmethod | ||
def _parse_file(cls, value): | ||
"""Represents value as a string, allowing including text | ||
from nearest files using include(). | ||
Examples: | ||
include: LICENSE | ||
include: src/file.txt | ||
:param str value: | ||
:rtype: str | ||
""" | ||
if not isinstance(value, string_types): | ||
return value | ||
|
||
include_directive = 'file:' | ||
if not value.startswith(include_directive): | ||
return value | ||
|
||
filepath = value.replace(include_directive, '').strip() | ||
|
||
if os.path.isfile(filepath): | ||
with io.open(filepath, encoding='utf-8') as f: | ||
value = f.read() | ||
|
||
return value | ||
|
||
@classmethod | ||
def _get_parser_compound(cls, *parse_methods): | ||
"""Returns parser function to represents value as a list. | ||
Parses a value applying given methods one after another. | ||
:param parse_methods: | ||
:rtype: callable | ||
""" | ||
def parse(value): | ||
parsed = value | ||
|
||
for method in parse_methods: | ||
parsed = method(parsed) | ||
|
||
return parsed | ||
|
||
return parse | ||
|
||
@classmethod | ||
def _parse_section_to_dict(cls, section_options, values_parser=None): | ||
"""Parses section options into a dictionary. | ||
Optionally applies a given parser to values. | ||
:param dict section_options: | ||
:param callable values_parser: | ||
:rtype: dict | ||
""" | ||
value = {} | ||
values_parser = values_parser or (lambda val: val) | ||
for key, (_, val) in section_options.items(): | ||
value[key] = values_parser(val) | ||
return value | ||
|
||
def parse_section(self, section_options): | ||
"""Parses configuration file section. | ||
:param dict section_options: | ||
""" | ||
for (name, (_, value)) in section_options.items(): | ||
try: | ||
self[name] = value | ||
except KeyError: | ||
raise DistutilsOptionError( | ||
'Unknown distribution option: %s' % name) | ||
|
||
def parse(self): | ||
"""Parses configuration file items from one | ||
or more related sections. | ||
""" | ||
for section_name, section_options in self.sections.items(): | ||
|
||
method_postfix = '' | ||
if section_name: # [section:option] variant | ||
method_postfix = '_%s' % section_name | ||
|
||
section_parser_method = getattr( | ||
self, 'parse_section%s' % method_postfix, None) | ||
|
||
if section_parser_method is None: | ||
raise DistutilsOptionError( | ||
'Unsupported distribution option section: [%s:%s]' % ( | ||
self.section_prefix, section_name)) | ||
|
||
section_parser_method(section_options) | ||
|
||
|
||
class ConfigMetadataHandler(ConfigHandler): | ||
|
||
section_prefix = 'metadata' | ||
|
||
@property | ||
def parsers(self): | ||
"""Metadata item name to parser function mapping.""" | ||
parse_list = self._parse_list | ||
parse_file = self._parse_file | ||
|
||
return { | ||
'platforms': parse_list, | ||
'keywords': parse_list, | ||
'provides': parse_list, | ||
'requires': parse_list, | ||
'obsoletes': parse_list, | ||
'classifiers': self._get_parser_compound(parse_file, parse_list), | ||
'license': parse_file, | ||
'description': parse_file, | ||
'long_description': parse_file, | ||
'version': self._parse_version, | ||
} | ||
|
||
def parse_section_classifiers(self, section_options): | ||
"""Parses configuration file section. | ||
:param dict section_options: | ||
""" | ||
classifiers = [] | ||
for begin, (_, rest) in section_options.items(): | ||
classifiers.append('%s :%s' % (begin.title(), rest)) | ||
|
||
self['classifiers'] = classifiers | ||
|
||
def _parse_version(self, value): | ||
"""Parses `version` option value. | ||
:param value: | ||
:rtype: str | ||
""" | ||
attr_directive = 'attr:' | ||
if not value.startswith(attr_directive): | ||
return value | ||
|
||
attrs_path = value.replace(attr_directive, '').strip().split('.') | ||
attr_name = attrs_path.pop() | ||
|
||
module_name = '.'.join(attrs_path) | ||
module_name = module_name or '__init__' | ||
|
||
sys.path.insert(0, os.getcwd()) | ||
try: | ||
module = import_module(module_name) | ||
version = getattr(module, attr_name) | ||
|
||
if callable(version): | ||
version = version() | ||
|
||
if not isinstance(version, string_types): | ||
if hasattr(version, '__iter__'): | ||
version = '.'.join(map(str, version)) | ||
else: | ||
version = '%s' % version | ||
|
||
finally: | ||
sys.path = sys.path[1:] | ||
|
||
return version | ||
|
||
|
||
class ConfigOptionsHandler(ConfigHandler): | ||
|
||
section_prefix = 'options' | ||
|
||
@property | ||
def parsers(self): | ||
"""Metadata item name to parser function mapping.""" | ||
parse_list = self._parse_list | ||
parse_list_semicolon = partial(self._parse_list, separator=';') | ||
parse_bool = self._parse_bool | ||
parse_dict = self._parse_dict | ||
|
||
return { | ||
'zip_safe': parse_bool, | ||
'use_2to3': parse_bool, | ||
'include_package_data': parse_bool, | ||
'package_dir': parse_dict, | ||
'use_2to3_fixers': parse_list, | ||
'use_2to3_exclude_fixers': parse_list, | ||
'convert_2to3_doctests': parse_list, | ||
'scripts': parse_list, | ||
'eager_resources': parse_list, | ||
'dependency_links': parse_list, | ||
'namespace_packages': parse_list, | ||
'install_requires': parse_list_semicolon, | ||
'setup_requires': parse_list_semicolon, | ||
'tests_require': parse_list_semicolon, | ||
'packages': self._parse_packages, | ||
'entry_points': self._parse_file, | ||
} | ||
|
||
def _parse_packages(self, value): | ||
"""Parses `packages` option value. | ||
:param value: | ||
:rtype: list | ||
""" | ||
find_directive = 'find:' | ||
|
||
if not value.startswith(find_directive): | ||
return self._parse_list(value) | ||
|
||
from setuptools import find_packages | ||
return find_packages() | ||
|
||
def parse_section_dependency_links(self, section_options): | ||
"""Parses `dependency_links` configuration file section. | ||
:param dict section_options: | ||
""" | ||
parsed = self._parse_section_to_dict(section_options) | ||
self['dependency_links'] = list(parsed.values()) | ||
|
||
def parse_section_entry_points(self, section_options): | ||
"""Parses `entry_points` configuration file section. | ||
:param dict section_options: | ||
""" | ||
parsed = self._parse_section_to_dict(section_options, self._parse_list) | ||
self['entry_points'] = parsed | ||
|
||
def _parse_package_data(self, section_options): | ||
parsed = self._parse_section_to_dict(section_options, self._parse_list) | ||
|
||
root = parsed.get('*') | ||
if root: | ||
parsed[''] = root | ||
del parsed['*'] | ||
|
||
return parsed | ||
|
||
def parse_section_package_data(self, section_options): | ||
"""Parses `package_data` configuration file section. | ||
:param dict section_options: | ||
""" | ||
self['package_data'] = self._parse_package_data(section_options) | ||
|
||
def parse_section_exclude_package_data(self, section_options): | ||
"""Parses `exclude_package_data` configuration file section. | ||
:param dict section_options: | ||
""" | ||
self['exclude_package_data'] = self._parse_package_data( | ||
section_options) | ||
|
||
def parse_section_extras_require(self, section_options): | ||
"""Parses `extras_require` configuration file section. | ||
:param dict section_options: | ||
""" | ||
parse_list = partial(self._parse_list, separator=';') | ||
self['extras_require'] = self._parse_section_to_dict( | ||
section_options, parse_list) |