aboutsummaryrefslogtreecommitdiffstats
path: root/setuptools/config.py
diff options
context:
space:
mode:
Diffstat (limited to 'setuptools/config.py')
-rw-r--r--setuptools/config.py164
1 files changed, 133 insertions, 31 deletions
diff --git a/setuptools/config.py b/setuptools/config.py
index 9a62e2ec..b6626043 100644
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -2,12 +2,20 @@ from __future__ import absolute_import, unicode_literals
import io
import os
import sys
+
+import warnings
+import functools
from collections import defaultdict
from functools import partial
+from functools import wraps
+from importlib import import_module
from distutils.errors import DistutilsOptionError, DistutilsFileError
-from setuptools.py26compat import import_module
-from setuptools.extern.six import string_types
+from setuptools.extern.packaging.version import LegacyVersion, parse
+from setuptools.extern.six import string_types, PY3
+
+
+__metaclass__ = type
def read_configuration(
@@ -57,6 +65,18 @@ def read_configuration(
return configuration_to_dict(handlers)
+def _get_option(target_obj, key):
+ """
+ Given a target object and option key, get that option from
+ the target object, either through a get_{key} method or
+ from an attribute directly.
+ """
+ getter_name = 'get_{key}'.format(**locals())
+ by_attribute = functools.partial(getattr, target_obj, key)
+ getter = getattr(target_obj, getter_name, by_attribute)
+ return getter()
+
+
def configuration_to_dict(handlers):
"""Returns configuration data gathered by given handlers as a dict.
@@ -68,20 +88,9 @@ def configuration_to_dict(handlers):
config_dict = defaultdict(dict)
for handler in handlers:
-
- obj_alias = handler.section_prefix
- target_obj = handler.target_obj
-
for option in handler.set_options:
- getter = getattr(target_obj, 'get_%s' % option, None)
-
- if getter is None:
- value = getattr(target_obj, option)
-
- else:
- value = getter()
-
- config_dict[obj_alias][option] = value
+ value = _get_option(handler.target_obj, option)
+ config_dict[handler.section_prefix][option] = value
return config_dict
@@ -101,18 +110,19 @@ def parse_configuration(
If False exceptions are propagated as expected.
:rtype: list
"""
- meta = ConfigMetadataHandler(
- distribution.metadata, command_options, ignore_option_errors)
- meta.parse()
-
options = ConfigOptionsHandler(
distribution, command_options, ignore_option_errors)
options.parse()
- return [meta, options]
+ meta = ConfigMetadataHandler(
+ distribution.metadata, command_options, ignore_option_errors,
+ distribution.package_dir)
+ meta.parse()
+
+ return meta, options
-class ConfigHandler(object):
+class ConfigHandler:
"""Handles metadata supplied in configuration files."""
section_prefix = None
@@ -237,6 +247,26 @@ class ConfigHandler(object):
return value in ('1', 'true', 'yes')
@classmethod
+ def _exclude_files_parser(cls, key):
+ """Returns a parser function to make sure field inputs
+ are not files.
+
+ Parses a value after getting the key so error messages are
+ more informative.
+
+ :param key:
+ :rtype: callable
+ """
+ def parser(value):
+ exclude_directive = 'file:'
+ if value.startswith(exclude_directive):
+ raise ValueError(
+ 'Only strings are accepted for the {0} field, '
+ 'files are not accepted'.format(key))
+ return value
+ return parser
+
+ @classmethod
def _parse_file(cls, value):
"""Represents value as a string, allowing including text
from nearest files using `file:` directive.
@@ -245,7 +275,6 @@ class ConfigHandler(object):
directory with setup.py.
Examples:
- file: LICENSE
file: README.rst, CHANGELOG.md, src/file.txt
:param str value:
@@ -280,7 +309,7 @@ class ConfigHandler(object):
return f.read()
@classmethod
- def _parse_attr(cls, value):
+ def _parse_attr(cls, value, package_dir=None):
"""Represents value as a module attribute.
Examples:
@@ -300,7 +329,21 @@ class ConfigHandler(object):
module_name = '.'.join(attrs_path)
module_name = module_name or '__init__'
- sys.path.insert(0, os.getcwd())
+ parent_path = os.getcwd()
+ if package_dir:
+ if attrs_path[0] in package_dir:
+ # A custom path was specified for the module we want to import
+ custom_path = package_dir[attrs_path[0]]
+ parts = custom_path.rsplit('/', 1)
+ if len(parts) > 1:
+ parent_path = os.path.join(os.getcwd(), parts[0])
+ module_name = parts[1]
+ else:
+ module_name = custom_path
+ elif '' in package_dir:
+ # A custom parent directory was specified for all root modules
+ parent_path = os.path.join(os.getcwd(), package_dir[''])
+ sys.path.insert(0, parent_path)
try:
module = import_module(module_name)
value = getattr(module, attr_name)
@@ -370,7 +413,7 @@ class ConfigHandler(object):
section_parser_method = getattr(
self,
- # Dots in section names are tranlsated into dunderscores.
+ # Dots in section names are translated into dunderscores.
('parse_section%s' % method_postfix).replace('.', '__'),
None)
@@ -381,6 +424,20 @@ class ConfigHandler(object):
section_parser_method(section_options)
+ def _deprecated_config_handler(self, func, msg, warning_class):
+ """ this function will wrap around parameters that are deprecated
+
+ :param msg: deprecation message
+ :param warning_class: class of warning exception to be raised
+ :param func: function to be wrapped around
+ """
+ @wraps(func)
+ def config_handler(*args, **kwargs):
+ warnings.warn(msg, warning_class)
+ return func(*args, **kwargs)
+
+ return config_handler
+
class ConfigMetadataHandler(ConfigHandler):
@@ -399,23 +456,36 @@ class ConfigMetadataHandler(ConfigHandler):
"""
+ def __init__(self, target_obj, options, ignore_option_errors=False,
+ package_dir=None):
+ super(ConfigMetadataHandler, self).__init__(target_obj, options,
+ ignore_option_errors)
+ self.package_dir = package_dir
+
@property
def parsers(self):
"""Metadata item name to parser function mapping."""
parse_list = self._parse_list
parse_file = self._parse_file
+ parse_dict = self._parse_dict
+ exclude_files_parser = self._exclude_files_parser
return {
'platforms': parse_list,
'keywords': parse_list,
'provides': parse_list,
- 'requires': parse_list,
+ 'requires': self._deprecated_config_handler(
+ parse_list,
+ "The requires parameter is deprecated, please use "
+ "install_requires for runtime dependencies.",
+ DeprecationWarning),
'obsoletes': parse_list,
'classifiers': self._get_parser_compound(parse_file, parse_list),
- 'license': parse_file,
+ 'license': exclude_files_parser('license'),
'description': parse_file,
'long_description': parse_file,
'version': self._parse_version,
+ 'project_urls': parse_dict,
}
def _parse_version(self, value):
@@ -425,7 +495,22 @@ class ConfigMetadataHandler(ConfigHandler):
:rtype: str
"""
- version = self._parse_attr(value)
+ version = self._parse_file(value)
+
+ if version != value:
+ version = version.strip()
+ # Be strict about versions loaded from file because it's easy to
+ # accidentally include newlines and other unintended content
+ if isinstance(parse(version), LegacyVersion):
+ tmpl = (
+ 'Version loaded from {value} does not '
+ 'comply with PEP 440: {version}'
+ )
+ raise DistutilsOptionError(tmpl.format(**locals()))
+
+ return version
+
+ version = self._parse_attr(value, self.package_dir)
if callable(version):
version = version()
@@ -477,16 +562,25 @@ class ConfigOptionsHandler(ConfigHandler):
:param value:
:rtype: list
"""
- find_directive = 'find:'
+ find_directives = ['find:', 'find_namespace:']
+ trimmed_value = value.strip()
- if not value.startswith(find_directive):
+ if trimmed_value not in find_directives:
return self._parse_list(value)
+ findns = trimmed_value == find_directives[1]
+ if findns and not PY3:
+ raise DistutilsOptionError(
+ 'find_namespace: directive is unsupported on Python < 3.3')
+
# Read function arguments from a dedicated section.
find_kwargs = self.parse_section_packages__find(
self.sections.get('packages.find', {}))
- from setuptools import find_packages
+ if findns:
+ from setuptools import find_namespace_packages as find_packages
+ else:
+ from setuptools import find_packages
return find_packages(**find_kwargs)
@@ -552,3 +646,11 @@ class ConfigOptionsHandler(ConfigHandler):
parse_list = partial(self._parse_list, separator=';')
self['extras_require'] = self._parse_section_to_dict(
section_options, parse_list)
+
+ def parse_section_data_files(self, section_options):
+ """Parses `data_files` configuration file section.
+
+ :param dict section_options:
+ """
+ parsed = self._parse_section_to_dict(section_options, self._parse_list)
+ self['data_files'] = [(k, v) for k, v in parsed.items()]