Source code for setuptools_wrapper.setup

"""
A simpler setuptools-based package definition.
"""

# built-in
from contextlib import contextmanager
import os
import shutil
from sys import platform, version_info
import tempfile
from typing import Any, Dict, Iterator, List, Set, Union, cast

# third-party
import setuptools

SELF = "setuptools-wrapper"


[docs] @contextmanager def inject_self( working_dir: str, curr_pkg_slug: str, pkg: str = SELF, force_copy: bool = False, ) -> Iterator[None]: """ Copy this entire package into the caller's source distribution. This is the only way to avoid a pointless requirement to already have this package installed to install anything else, and also generally not requiring an explicit install of this package except for command-line use. """ added = False pkg = pkg.replace("-", "_") # inject sources into a package with a different name, otherwise # installation becomes very broken and messed up to_create = os.path.join(working_dir, f"{curr_pkg_slug}_bootstrap") try: # do nothing if we are building ourselves (but allow forcing this) if (force_copy or pkg not in curr_pkg_slug) and not os.path.isdir( to_create ): os.mkdir(to_create) # copy our sources into their package to_copy = [ "__init__.py", "setup.py", "py.typed", ] vmklib_dir = os.path.dirname(__file__) for fname in to_copy: dest = os.path.join(to_create, fname) src = os.path.join(vmklib_dir, fname) if not os.path.isfile(dest) and os.path.isfile(src): shutil.copyfile(src, dest) added = True yield finally: if added: shutil.rmtree(to_create)
[docs] def get_long_description(desc_filename: str = "README.md") -> str: """Get a package's long-description data from a file.""" try: with open(desc_filename, "r", encoding="utf-8") as desc_file: long_description = desc_file.read() return long_description except FileNotFoundError: return ""
[docs] def default_requirements_file(directory: str) -> str: """Default location to look for the requirements file.""" return os.path.join(directory, "requirements.txt")
[docs] def get_requirements(reqs_filename: str) -> Set[str]: """Get a package's requirements based on its requirements file.""" try: with open(reqs_filename, "r", encoding="utf-8") as reqs_file: return set(x.strip() for x in reqs_file) except FileNotFoundError: return set()
[docs] def get_data_files(pkg_name: str, data_dir: str = "data") -> List[str]: """ Get the non-code sources under a package directory's data directory. """ data_files = [] for root, _, files in os.walk(os.path.join(pkg_name, data_dir)): for fname in files: rel_name = os.path.join(root, fname).replace(pkg_name + os.sep, "") data_files.append(rel_name) return data_files
[docs] class PythonVersionCompare: """Python version string comparison object.""" def __init__( self, major: int = None, minor: int = None, micro: int = None ) -> None: """Initialize this instance.""" if major is None: major = version_info.major if minor is None: minor = version_info.minor if micro is None: micro = version_info.micro self.major = major self.minor = minor self.micro = micro
[docs] @staticmethod def from_str(data: str) -> "PythonVersionCompare": """Create a version comparison instance from a string.""" parts = data.strip().split(".") major = None minor = None micro = None for part in parts: as_int = int(part) if major is None: major = as_int elif minor is None: minor = as_int elif micro is None: micro = as_int return PythonVersionCompare(major=major, minor=minor, micro=micro)
[docs] def cmp(self, other) -> int: """Compare to an object.""" if isinstance(other, str): other = PythonVersionCompare.from_str(other) assert isinstance(other, PythonVersionCompare) diff = self.major - other.major if diff != 0: return diff diff = self.minor - other.minor if diff != 0: return diff return self.micro - other.micro
def __lt__(self, other) -> bool: """Compare to an object.""" return self.cmp(other) < 0 def __le__(self, other) -> bool: """Compare to an object.""" return self.cmp(other) <= 0 def __eq__(self, other) -> bool: """Compare to an object.""" return self.cmp(other) == 0 def __ne__(self, other) -> bool: """Compare to an object.""" return self.cmp(other) != 0 def __gt__(self, other) -> bool: """Compare to an object.""" return self.cmp(other) > 0 def __ge__(self, other) -> bool: """Compare to an object.""" return self.cmp(other) >= 0
[docs] def process_requirements(requirements: Set[str]) -> Set[str]: """Process conditional statements in requirement declarations.""" new_reqs: Set[str] = set() for requirement in requirements: parts = [x.strip() for x in requirement.split(";")] if len(parts) == 1: new_reqs.add(parts[0]) elif eval( # pylint: disable=eval-used parts[1], {}, { "sys_platform": platform, "python_version": PythonVersionCompare(), }, ): new_reqs.add(parts[0]) return new_reqs
# pylint: disable=too-many-arguments
[docs] def setup( pkg_info: Dict[str, Any], author_info: Dict[str, str], url_override: str = None, classifiers_override: List[str] = None, requirements: Set[str] = None, **kwargs, ) -> None: """ Build a 'setuptools.setup' call with sane defaults and making assumptions about certain aspects of a package's structure. """ defaults: Dict[str, Union[str, List[str], Set[str]]] = { "url_override": ( f"https://github.com/{author_info['username']}/" f"{pkg_info['name']}" ), "classifiers_override": [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], "requirements": set(), } # Resolve defaults if necessary. url_override = cast( str, defaults["url_override"] if url_override is None else url_override ) classifiers_override = cast( List[str], ( defaults["classifiers_override"] if classifiers_override is None else classifiers_override ), ) requirements = cast( Set[str], defaults["requirements"] if requirements is None else requirements, ) for version in pkg_info.get("versions", []): classifiers_override.append( f"Programming Language :: Python :: {version}" ) # Find requirements files inside the package's root directory. req_files = [default_requirements_file(pkg_info["slug"])] for req_file in req_files: requirements |= get_requirements(req_file) with tempfile.TemporaryDirectory() as temp_dir: working_dir = temp_dir dir_contents = os.listdir(os.getcwd()) if pkg_info["slug"] in dir_contents: working_dir = os.getcwd() with inject_self( working_dir, pkg_info["slug"], force_copy=pkg_info.get("force_copy", False), ): setuptools.setup( name=pkg_info["name"], version=pkg_info["version"], author=author_info["name"], author_email=author_info["email"], description=pkg_info["description"], long_description=get_long_description(), long_description_content_type="text/markdown", url=url_override, packages=setuptools.find_namespace_packages( include=[pkg_info["slug"], pkg_info["slug"] + ".*"], exclude=["tests", "tests.*"], ), classifiers=classifiers_override, python_requires=f">={pkg_info.get('versions', ['3.6'])[0]}", install_requires=list(process_requirements(requirements)), package_data={ pkg_info["slug"]: ( get_data_files(pkg_info["slug"]) + ["py.typed", "*.pyi", "*.txt"] ), f"{pkg_info['slug']}_bootstrap": ["py.typed"], "": ["*.pyi"], }, **kwargs, )