| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749 | import astimport ioimport osimport sysimport warningsimport functoolsimport importlibfrom collections import defaultdictfrom functools import partialfrom functools import wrapsfrom glob import iglobimport contextlibfrom distutils.errors import DistutilsOptionError, DistutilsFileErrorfrom setuptools.extern.packaging.version import LegacyVersion, parsefrom setuptools.extern.packaging.specifiers import SpecifierSetclass StaticModule:    """    Attempt to load the module by the name    """    def __init__(self, name):        spec = importlib.util.find_spec(name)        with open(spec.origin) as strm:            src = strm.read()        module = ast.parse(src)        vars(self).update(locals())        del self.self    def __getattr__(self, attr):        try:            return next(                ast.literal_eval(statement.value)                for statement in self.module.body                if isinstance(statement, ast.Assign)                for target in statement.targets                if isinstance(target, ast.Name) and target.id == attr            )        except Exception as e:            raise AttributeError(                "{self.name} has no attribute {attr}".format(**locals())            ) from e@contextlib.contextmanagerdef patch_path(path):    """    Add path to front of sys.path for the duration of the context.    """    try:        sys.path.insert(0, path)        yield    finally:        sys.path.remove(path)def read_configuration(filepath, find_others=False, ignore_option_errors=False):    """Read given configuration file and returns options from it as a dict.    :param str|unicode filepath: Path to configuration file        to get options from.    :param bool find_others: Whether to search for other configuration files        which could be on in various places.    :param bool ignore_option_errors: Whether to silently ignore        options, values of which could not be resolved (e.g. due to exceptions        in directives such as file:, attr:, etc.).        If False exceptions are propagated as expected.    :rtype: dict    """    from setuptools.dist import Distribution, _Distribution    filepath = os.path.abspath(filepath)    if not os.path.isfile(filepath):        raise DistutilsFileError('Configuration file %s does not exist.' % filepath)    current_directory = os.getcwd()    os.chdir(os.path.dirname(filepath))    try:        dist = Distribution()        filenames = dist.find_config_files() if find_others else []        if filepath not in filenames:            filenames.append(filepath)        _Distribution.parse_config_files(dist, filenames=filenames)        handlers = parse_configuration(            dist, dist.command_options, ignore_option_errors=ignore_option_errors        )    finally:        os.chdir(current_directory)    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.    :param list[ConfigHandler] handlers: Handlers list,        usually from parse_configuration()    :rtype: dict    """    config_dict = defaultdict(dict)    for handler in handlers:        for option in handler.set_options:            value = _get_option(handler.target_obj, option)            config_dict[handler.section_prefix][option] = value    return config_dictdef parse_configuration(distribution, command_options, ignore_option_errors=False):    """Performs additional parsing of configuration options    for a distribution.    Returns a list of used option handlers.    :param Distribution distribution:    :param dict command_options:    :param bool ignore_option_errors: Whether to silently ignore        options, values of which could not be resolved (e.g. due to exceptions        in directives such as file:, attr:, etc.).        If False exceptions are propagated as expected.    :rtype: list    """    options = ConfigOptionsHandler(distribution, command_options, ignore_option_errors)    options.parse()    meta = ConfigMetadataHandler(        distribution.metadata,        command_options,        ignore_option_errors,        distribution.package_dir,    )    meta.parse()    return meta, optionsclass ConfigHandler:    """Handles metadata supplied in configuration files."""    section_prefix = None    """Prefix for config sections handled by this handler.    Must be provided by class heirs.    """    aliases = {}    """Options aliases.    For compatibility with various packages. E.g.: d2to1 and pbr.    Note: `-` in keys is replaced with `_` by config parser.    """    def __init__(self, target_obj, options, ignore_option_errors=False):        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.ignore_option_errors = ignore_option_errors        self.target_obj = target_obj        self.sections = sections        self.set_options = []    @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        # Translate alias into real name.        option_name = self.aliases.get(option_name, option_name)        current_value = getattr(target_obj, option_name, unknown)        if current_value is unknown:            raise KeyError(option_name)        if current_value:            # Already inhabited. Skipping.            return        skip_option = False        parser = self.parsers.get(option_name)        if parser:            try:                value = parser(value)            except Exception:                skip_option = True                if not self.ignore_option_errors:                    raise        if skip_option:            return        setter = getattr(target_obj, 'set_%s' % option_name, None)        if setter is None:            setattr(target_obj, option_name, value)        else:            setter(value)        self.set_options.append(option_name)    @classmethod    def _parse_list(cls, value, separator=','):        """Represents value as a list.        Value is split either by separator (defaults to comma) or by lines.        :param value:        :param separator: List items separator character.        :rtype: list        """        if isinstance(value, list):  # _get_parser_compound case            return value        if '\n' in value:            value = value.splitlines()        else:            value = value.split(separator)        return [chunk.strip() for chunk in value if chunk.strip()]    @classmethod    def _parse_list_glob(cls, value, separator=','):        """Equivalent to _parse_list() but expands any glob patterns using glob().        However, unlike with glob() calls, the results remain relative paths.        :param value:        :param separator: List items separator character.        :rtype: list        """        glob_characters = ('*', '?', '[', ']', '{', '}')        values = cls._parse_list(value, separator=separator)        expanded_values = []        for value in values:            # Has globby characters?            if any(char in value for char in glob_characters):                # then expand the glob pattern while keeping paths *relative*:                expanded_values.extend(sorted(                    os.path.relpath(path, os.getcwd())                    for path in iglob(os.path.abspath(value))))            else:                # take the value as-is:                expanded_values.append(value)        return expanded_values    @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 _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.        Directive is sandboxed and won't reach anything outside        directory with setup.py.        Examples:            file: README.rst, CHANGELOG.md, src/file.txt        :param str value:        :rtype: str        """        include_directive = 'file:'        if not isinstance(value, str):            return value        if not value.startswith(include_directive):            return value        spec = value[len(include_directive) :]        filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))        return '\n'.join(            cls._read_file(path)            for path in filepaths            if (cls._assert_local(path) or True) and os.path.isfile(path)        )    @staticmethod    def _assert_local(filepath):        if not filepath.startswith(os.getcwd()):            raise DistutilsOptionError('`file:` directive can not access %s' % filepath)    @staticmethod    def _read_file(filepath):        with io.open(filepath, encoding='utf-8') as f:            return f.read()    @classmethod    def _parse_attr(cls, value, package_dir=None):        """Represents value as a module attribute.        Examples:            attr: package.attr            attr: package.module.attr        :param str 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__'        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[''])        with patch_path(parent_path):            try:                # attempt to load value statically                return getattr(StaticModule(module_name), attr_name)            except Exception:                # fallback to simple import                module = importlib.import_module(module_name)        return getattr(module, attr_name)    @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:                pass  # Keep silent for a new option may appear anytime.    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,                # Dots in section names are translated into dunderscores.                ('parse_section%s' % method_postfix).replace('.', '__'),                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)    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_handlerclass ConfigMetadataHandler(ConfigHandler):    section_prefix = 'metadata'    aliases = {        'home_page': 'url',        'summary': 'description',        'classifier': 'classifiers',        'platform': 'platforms',    }    strict_mode = False    """We need to keep it loose, to be partially compatible with    `pbr` and `d2to1` packages which also uses `metadata` section.    """    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': 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': exclude_files_parser('license'),            'license_file': self._deprecated_config_handler(                exclude_files_parser('license_file'),                "The license_file parameter is deprecated, "                "use license_files instead.",                DeprecationWarning,            ),            'license_files': parse_list,            'description': parse_file,            'long_description': parse_file,            'version': self._parse_version,            'project_urls': parse_dict,        }    def _parse_version(self, value):        """Parses `version` option value.        :param value:        :rtype: str        """        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()        if not isinstance(version, str):            if hasattr(version, '__iter__'):                version = '.'.join(map(str, version))            else:                version = '%s' % version        return versionclass 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        parse_cmdclass = self._parse_cmdclass        return {            'zip_safe': parse_bool,            'include_package_data': parse_bool,            'package_dir': parse_dict,            '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,            'py_modules': parse_list,            'python_requires': SpecifierSet,            'cmdclass': parse_cmdclass,        }    def _parse_cmdclass(self, value):        def resolve_class(qualified_class_name):            idx = qualified_class_name.rfind('.')            class_name = qualified_class_name[idx + 1 :]            pkg_name = qualified_class_name[:idx]            module = __import__(pkg_name)            return getattr(module, class_name)        return {k: resolve_class(v) for k, v in self._parse_dict(value).items()}    def _parse_packages(self, value):        """Parses `packages` option value.        :param value:        :rtype: list        """        find_directives = ['find:', 'find_namespace:']        trimmed_value = value.strip()        if trimmed_value not in find_directives:            return self._parse_list(value)        findns = trimmed_value == find_directives[1]        # Read function arguments from a dedicated section.        find_kwargs = self.parse_section_packages__find(            self.sections.get('packages.find', {})        )        if findns:            from setuptools import find_namespace_packages as find_packages        else:            from setuptools import find_packages        return find_packages(**find_kwargs)    def parse_section_packages__find(self, section_options):        """Parses `packages.find` configuration file section.        To be used in conjunction with _parse_packages().        :param dict section_options:        """        section_data = self._parse_section_to_dict(section_options, self._parse_list)        valid_keys = ['where', 'include', 'exclude']        find_kwargs = dict(            [(k, v) for k, v in section_data.items() if k in valid_keys and v]        )        where = find_kwargs.get('where')        if where is not None:            find_kwargs['where'] = where[0]  # cast list to single val        return find_kwargs    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        )    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_glob)        self['data_files'] = [(k, v) for k, v in parsed.items()]
 |