#!/usr/bin/env python # -*- coding: utf-8 -*- import setuptools HAS_DIST_INFO_CMD = False try: import setuptools.command.dist_info HAS_DIST_INFO_CMD = True except ImportError: """Setuptools version is too old.""" ALL_STRING_TYPES = tuple(map(type, ("", b"", u""))) MIN_NATIVE_SETUPTOOLS_VERSION = 34, 4, 0 """Minimal setuptools having good read_configuration implementation.""" RUNTIME_SETUPTOOLS_VERSION = tuple(map(int, setuptools.__version__.split("."))) """Setuptools imported now.""" READ_CONFIG_SHIM_NEEDED = RUNTIME_SETUPTOOLS_VERSION < MIN_NATIVE_SETUPTOOLS_VERSION def str_if_nested_or_str(s): """Turn input into a native string if possible.""" if isinstance(s, ALL_STRING_TYPES): return str(s) if isinstance(s, (list, tuple)): return type(s)(map(str_if_nested_or_str, s)) if isinstance(s, (dict,)): return stringify_dict_contents(s) return s def stringify_dict_contents(dct): """Turn dict keys and values into native strings.""" return {str_if_nested_or_str(k): str_if_nested_or_str(v) for k, v in dct.items()} if not READ_CONFIG_SHIM_NEEDED: from setuptools.config import read_configuration, ConfigOptionsHandler import setuptools.config import setuptools.dist # Set default value for 'use_scm_version' setattr(setuptools.dist.Distribution, "use_scm_version", False) # Attach bool parser to 'use_scm_version' option class ShimConfigOptionsHandler(ConfigOptionsHandler): """Extension class for ConfigOptionsHandler.""" @property def parsers(self): """Return an option mapping with default data type parsers.""" _orig_parsers = super(ShimConfigOptionsHandler, self).parsers return dict(use_scm_version=self._parse_bool, **_orig_parsers) def parse_section_packages__find(self, section_options): find_kwargs = super( ShimConfigOptionsHandler, self ).parse_section_packages__find(section_options) return stringify_dict_contents(find_kwargs) setuptools.config.ConfigOptionsHandler = ShimConfigOptionsHandler else: """This is a shim for setuptools": operator.gt, "<": operator.lt, ">=": operator.ge, "<=": operator.le, "==": operator.eq, "!=": operator.ne, "": operator.eq, }.items(), key=lambda i: len(i[0]), reverse=True, ) ) def is_decimal(s): return type(u"")(s).isdecimal() conditions = map(str.strip, python_requires.split(",")) for c in conditions: for op_sign, op_func in sorted_operators_map: if not c.startswith(op_sign): continue raw_ver = itertools.takewhile( is_decimal, c[len(op_sign) :].strip().split(".") ) ver = tuple(map(int, raw_ver)) yield op_func, ver break def validate_required_python_or_fail(python_requires=None): if python_requires is None: return python_version = sys.version_info preds = parse_predicates(python_requires) for op, v in preds: py_ver_slug = python_version[: max(len(v), 3)] condition_matches = op(py_ver_slug, v) if not condition_matches: raise RuntimeError( "requires Python '{}' but the running Python is {}".format( python_requires, ".".join(map(str, python_version[:3])) ) ) def verify_required_python_runtime(s): @functools.wraps(s) def sw(**attrs): try: validate_required_python_or_fail(attrs.get("python_requires")) except RuntimeError as re: sys.exit("{} {!s}".format(attrs["name"], re)) return s(**attrs) return sw setuptools.setup = ignore_unknown_options(setuptools.setup) setuptools.setup = verify_required_python_runtime(setuptools.setup) try: from configparser import ConfigParser, NoSectionError except ImportError: from ConfigParser import ConfigParser, NoSectionError ConfigParser.read_file = ConfigParser.readfp def maybe_read_files(d): """Read files if the string starts with `file:` marker.""" FILE_FUNC_MARKER = "file:" d = d.strip() if not d.startswith(FILE_FUNC_MARKER): return d descs = [] for fname in map(str.strip, str(d[len(FILE_FUNC_MARKER) :]).split(",")): with io.open(fname, encoding="utf-8") as f: descs.append(f.read()) return "".join(descs) def cfg_val_to_list(v): """Turn config val to list and filter out empty lines.""" return list(filter(bool, map(str.strip, str(v).strip().splitlines()))) def cfg_val_to_dict(v): """Turn config val to dict and filter out empty lines.""" return dict( map( lambda l: list(map(str.strip, l.split("=", 1))), filter(bool, map(str.strip, str(v).strip().splitlines())), ) ) def cfg_val_to_primitive(v): """Parse primitive config val to appropriate data type.""" return json.loads(v.strip().lower()) def read_configuration(filepath): """Read metadata and options from setup.cfg located at filepath.""" cfg = ConfigParser() with io.open(filepath, encoding="utf-8") as f: cfg.read_file(f) md = dict(cfg.items("metadata")) for list_key in "classifiers", "keywords", "project_urls": try: md[list_key] = cfg_val_to_list(md[list_key]) except KeyError: pass try: md["long_description"] = maybe_read_files(md["long_description"]) except KeyError: pass opt = dict(cfg.items("options")) for list_key in "include_package_data", "use_scm_version", "zip_safe": try: opt[list_key] = cfg_val_to_primitive(opt[list_key]) except KeyError: pass for list_key in "scripts", "install_requires", "setup_requires": try: opt[list_key] = cfg_val_to_list(opt[list_key]) except KeyError: pass try: opt["package_dir"] = cfg_val_to_dict(opt["package_dir"]) except KeyError: pass try: opt_package_data = dict(cfg.items("options.package_data")) if not opt_package_data.get("", "").strip(): opt_package_data[""] = opt_package_data["*"] del opt_package_data["*"] except (KeyError, NoSectionError): opt_package_data = {} try: opt_extras_require = dict(cfg.items("options.extras_require")) opt["extras_require"] = {} for k, v in opt_extras_require.items(): opt["extras_require"][k] = cfg_val_to_list(v) except NoSectionError: pass opt["package_data"] = {} for k, v in opt_package_data.items(): opt["package_data"][k] = cfg_val_to_list(v) try: opt_exclude_package_data = dict(cfg.items("options.exclude_package_data")) if ( not opt_exclude_package_data.get("", "").strip() and "*" in opt_exclude_package_data ): opt_exclude_package_data[""] = opt_exclude_package_data["*"] del opt_exclude_package_data["*"] except NoSectionError: pass else: opt["exclude_package_data"] = {} for k, v in opt_exclude_package_data.items(): opt["exclude_package_data"][k] = cfg_val_to_list(v) cur_pkgs = opt.get("packages", "").strip() if "\n" in cur_pkgs: opt["packages"] = cfg_val_to_list(opt["packages"]) elif cur_pkgs.startswith("find:"): opt_packages_find = stringify_dict_contents( dict(cfg.items("options.packages.find")) ) opt["packages"] = setuptools.find_packages(**opt_packages_find) return {"metadata": md, "options": opt} def cut_local_version_on_upload(version): """Generate a PEP440 local version if uploading to PyPI.""" import os import setuptools_scm.version # only present during setup time IS_PYPI_UPLOAD = os.getenv("PYPI_UPLOAD") == "true" # set in tox.ini return ( "" if IS_PYPI_UPLOAD else setuptools_scm.version.get_local_node_and_date(version) ) if HAS_DIST_INFO_CMD: class patched_dist_info(setuptools.command.dist_info.dist_info): def run(self): self.egg_base = str_if_nested_or_str(self.egg_base) return setuptools.command.dist_info.dist_info.run(self) declarative_setup_params = read_configuration("setup.cfg") """Declarative metadata and options as read by setuptools.""" setup_params = {} """Explicit metadata for passing into setuptools.setup() call.""" setup_params = dict(setup_params, **declarative_setup_params["metadata"]) setup_params = dict(setup_params, **declarative_setup_params["options"]) if HAS_DIST_INFO_CMD: setup_params["cmdclass"] = {"dist_info": patched_dist_info} setup_params["use_scm_version"] = {"local_scheme": cut_local_version_on_upload} # Patch incorrectly decoded package_dir option # ``egg_info`` demands native strings failing with unicode under Python 2 # Ref https://github.com/pypa/setuptools/issues/1136 setup_params = stringify_dict_contents(setup_params) if __name__ == "__main__": setuptools.setup(**setup_params)