예제 #1
0
class Builder:
    """Base class for building and distributing a package from given path."""

    DEFAULT_EXCLUDES = ["build"]

    def __init__(
        self,
        location: Union[str, Path],
        config_settings: Optional[Mapping[str, Any]] = None,
    ) -> None:
        self._old_cwd: Optional[str] = None
        self.location = Path(location).absolute()
        self.config_settings = config_settings
        self._meta: Optional[Metadata] = None

    @property
    def meta(self) -> Metadata:
        if not self._meta:
            self._meta = Metadata(self.location / "pyproject.toml")
            # Open the validation for next release
            self._meta.validate(False)
        return self._meta

    @property
    def meta_version(self) -> str:
        meta_version = self.meta.version
        if meta_version is None:
            return "0.0.0"
        return to_filename(safe_version(meta_version))

    def __enter__(self: T) -> T:
        self._old_cwd = os.getcwd()
        os.chdir(self.location)
        return self

    def __exit__(self, *args: Any) -> None:
        assert self._old_cwd
        os.chdir(self._old_cwd)

    def build(self, build_dir: str, **kwargs: Any) -> str:
        raise NotImplementedError

    def _get_include_and_exclude_paths(self,
                                       for_sdist: bool = False
                                       ) -> Tuple[List[str], List[str]]:
        includes = set()
        excludes = set(self.DEFAULT_EXCLUDES)

        meta_excludes = list(self.meta.excludes)
        source_includes = self.meta.source_includes or ["tests"]
        if not for_sdist:
            # exclude source-includes for non-sdist builds
            meta_excludes.extend(source_includes)

        if not self.meta.includes:
            top_packages = _find_top_packages(self.meta.package_dir or ".")
            if top_packages:
                includes.update(top_packages)
            else:
                includes.add(f"{self.meta.package_dir or '.'}/*.py")
        else:
            includes.update(self.meta.includes)

        includes.update(source_includes)
        excludes.update(meta_excludes)

        include_globs = {
            os.path.normpath(path): key
            for key in includes for path in glob.iglob(key, recursive=True)
        }

        excludes_globs = {
            os.path.normpath(path): key
            for key in excludes for path in glob.iglob(key, recursive=True)
        }

        include_paths, exclude_paths = _merge_globs(include_globs,
                                                    excludes_globs)
        return sorted(include_paths), sorted(exclude_paths)

    def _is_excluded(self, path: str, exclude_paths: List[str]) -> bool:
        return any(
            is_same_or_descendant_path(path, exclude_path)
            for exclude_path in exclude_paths)

    def _find_files_iter(self, for_sdist: bool = False) -> Iterator[str]:
        includes, excludes = self._get_include_and_exclude_paths(for_sdist)
        for include_path in includes:
            path = Path(include_path)
            if path.is_file():
                yield include_path
                continue
            # The path is a directory name
            for path in path.glob("**/*"):
                if not path.is_file():
                    continue

                rel_path = path.absolute().relative_to(
                    self.location).as_posix()
                if path.name.endswith(".pyc") or self._is_excluded(
                        rel_path, excludes):
                    continue

                yield rel_path

        if not for_sdist:
            return

        if self.meta.build and os.path.isfile(self.meta.build):
            yield self.meta.build

        for pat in ("COPYING", "LICENSE"):
            for p in glob.glob(pat + "*"):
                if os.path.isfile(p):
                    yield p

        if self.meta.readme and os.path.isfile(self.meta.readme):
            yield self.meta.readme

        if self.meta.filepath.exists():
            yield self.meta.filepath.name

    def find_files_to_add(self, for_sdist: bool = False) -> List[Path]:
        """Traverse the project path and return a list of file names
        that should be included in a sdist distribution.
        If for_sdist is True, will include files like LICENSE, README and pyproject
        Produce a paths list relative to the source dir.
        """
        return sorted({Path(p) for p in self._find_files_iter(for_sdist)})

    def format_setup_py(self) -> str:
        before, extra, after = [], [], []
        meta = self.meta
        kwargs = {
            "name": meta.name,
            "version": meta.version,
            "author": meta.author,
            "license": meta.license_type,
            "author_email": meta.author_email,
            "maintainer": meta.maintainer,
            "maintainer_email": meta.maintainer_email,
            "description": meta.description,
            "url": (meta.project_urls or {}).get("homepage", ""),
        }

        if meta.build:
            # The build script must contain a `build(setup_kwargs)`, we just import
            # and execute it.
            after.extend([
                "from {} import build\n".format(meta.build.split(".")[0]),
                "build(setup_kwargs)\n",
            ])

        package_paths = meta.convert_package_paths()
        if package_paths["packages"]:
            extra.append("    'packages': {},\n".format(
                _format_list(package_paths["packages"], 8)))
        if package_paths["package_dir"]:
            extra.append("    'package_dir': {!r},\n".format(
                package_paths["package_dir"]))
        if package_paths["package_data"]:
            extra.append("    'package_data': {!r},\n".format(
                package_paths["package_data"]))
        if package_paths["exclude_package_data"]:
            extra.append("    'exclude_package_data': {!r},\n".format(
                package_paths["exclude_package_data"]))
        if meta.readme:
            before.append(OPEN_README.format(readme=meta.readme))
        elif meta.long_description:
            before.append("long_description = '''{}'''\n".format(
                repr(meta.long_description)[1:-1]))
        else:
            before.append("long_description = None\n")
        if meta.long_description_content_type:
            extra.append("    'long_description_content_type': {!r},\n".format(
                meta.long_description_content_type))

        if meta.keywords:
            extra.append(f"    'keywords': {meta.keywords!r},\n")
        if meta.classifiers:
            extra.append(
                f"    'classifiers': {_format_list(meta.classifiers, 8)},\n")
        if meta.dependencies:
            before.append(
                f"INSTALL_REQUIRES = {_format_list(meta.dependencies)}\n")
            extra.append("    'install_requires': INSTALL_REQUIRES,\n")
        if meta.optional_dependencies:
            before.append("EXTRAS_REQUIRE = {}\n".format(
                _format_dict_list(meta.optional_dependencies)))
            extra.append("    'extras_require': EXTRAS_REQUIRE,\n")
        if meta.requires_python:
            extra.append(f"    'python_requires': {meta.requires_python!r},\n")
        if meta.entry_points:
            before.append(
                f"ENTRY_POINTS = {_format_dict_list(meta.entry_points)}\n")
            extra.append("    'entry_points': ENTRY_POINTS,\n")
        return SETUP_FORMAT.format(before="".join(before),
                                   after="".join(after),
                                   extra="".join(extra),
                                   **kwargs)

    def format_pkginfo(self, full: bool = True) -> str:
        meta = self.meta
        content = METADATA_BASE.format(
            name=meta.name or "UNKNOWN",
            version=meta.version or "UNKNOWN",
            license=meta.license_type or "UNKNOWN",
            description=meta.description or "UNKNOWN",
        )

        # Optional fields
        if meta.keywords:
            content += "Keywords: {}\n".format(",".join(meta.keywords))

        if meta.author:
            content += f"Author: {meta.author}\n"

        if meta.author_email:
            content += f"Author-email: {meta.author_email}\n"

        if meta.maintainer:
            content += f"Maintainer: {meta.maintainer}\n"

        if meta.maintainer_email:
            content += f"Maintainer-email: {meta.maintainer_email}\n"

        if meta.requires_python:
            content += f"Requires-Python: {meta.requires_python}\n"

        for classifier in meta.classifiers or []:
            content += f"Classifier: {classifier}\n"

        if full:
            for dep in sorted(meta.dependencies):
                content += f"Requires-Dist: {dep}\n"

        for extra, reqs in sorted(meta.requires_extra.items()):
            content += f"Provides-Extra: {extra}\n"
            if full:
                for dep in reqs:
                    content += f"Requires-Dist: {dep}\n"

        for url in sorted(meta.project_urls or {}):
            content += f"Project-URL: {url}, {meta.project_urls[url]}\n"

        if meta.long_description_content_type:
            content += "Description-Content-Type: {}\n".format(
                meta.long_description_content_type)
        if meta.long_description:
            readme = meta.long_description
            if full:
                content += "\n" + readme + "\n"
            else:
                content += "Description: {}\n".format(
                    textwrap.indent(readme, " " * 8,
                                    lambda line: True).lstrip())

        return content

    def ensure_setup_py(self, clean: bool = True) -> Path:
        """Ensures the requirement has a setup.py ready."""
        # XXX: Currently only handle PDM project, and do nothing if not.

        setup_py_path = self.location.joinpath("setup.py")
        if setup_py_path.is_file():
            return setup_py_path

        setup_py_path.write_text(self.format_setup_py(), encoding="utf-8")

        # Clean this temp file when process exits
        def cleanup() -> None:
            try:
                setup_py_path.unlink()
            except OSError:
                pass

        if clean:
            atexit.register(cleanup)
        return setup_py_path
예제 #2
0
class Builder:
    """Base class for building and distributing a package from given path."""

    DEFAULT_EXCLUDES = ["ez_setup", "*__pycache__", "tests", "tests.*"]

    def __init__(self, location: Union[str, Path]) -> None:
        self._old_cwd = None
        self.location = Path(location).absolute()
        self._meta = None

    @property
    def meta(self) -> Metadata:
        if not self._meta:
            self._meta = Metadata(self.location / "pyproject.toml")
            # Open the validation for next release
            self._meta.validate(False)
        return self._meta

    def __enter__(self) -> "Builder":
        self._old_cwd = os.getcwd()
        os.chdir(self.location)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        os.chdir(self._old_cwd)

    def build(self, build_dir: str, **kwargs) -> str:
        raise NotImplementedError

    def _find_files_iter(self, for_sdist: bool = False) -> Iterator[str]:
        includes = set()
        find_froms = set()
        excludes = set()
        dont_find_froms = set()
        source_includes = self.meta.source_includes or ["tests"]
        meta_includes = itertools.chain(self.meta.includes, source_includes)
        meta_excludes = list(self.meta.excludes)

        if not for_sdist:
            # exclude source-includes for non-sdist builds
            meta_excludes.extend(source_includes)

        for pat in meta_includes:
            if os.path.basename(pat) == "*":
                pat = pat[:-2]
            if "*" in pat or os.path.isfile(pat):
                includes.add(pat)
            else:
                find_froms.add(pat)
        if not self.meta.includes:
            top_packages = _find_top_packages(self.meta.package_dir or ".")
            if top_packages:
                find_froms.update(top_packages)
            else:
                includes.add(f"{self.meta.package_dir or '.'}/*.py")

        for pat in meta_excludes:
            if "*" in pat or os.path.isfile(pat):
                excludes.add(pat)
            else:
                dont_find_froms.add(pat)

        include_globs = {
            path: key
            for key in includes for path in glob.glob(key)
        }
        excludes_globs = {
            path: key
            for key in excludes for path in glob.glob(key)
        }

        includes, excludes = _merge_globs(include_globs, excludes_globs)
        for path in find_froms:
            if any(_match_path(path, item) for item in dont_find_froms):
                continue
            path_base = os.path.dirname(path)
            if not path_base or path_base == ".":
                # the path is top level itself
                path_base = path

            for root, dirs, filenames in os.walk(path):

                for filename in filenames:
                    if filename.endswith(".pyc") or any(
                            _match_path(os.path.join(root, filename), item)
                            for item in excludes):
                        continue
                    yield os.path.join(root, filename)

        for path in includes:
            if os.path.isfile(path):
                yield path
        if not for_sdist:
            return

        if self.meta.build and os.path.isfile(self.meta.build):
            yield self.meta.build

        for pat in ("COPYING", "LICENSE"):
            for path in glob.glob(pat + "*"):
                if os.path.isfile(path):
                    yield path

        if self.meta.readme and os.path.isfile(self.meta.readme):
            yield self.meta.readme

        if self.meta.filepath.exists():
            yield "pyproject.toml"

    def find_files_to_add(self, for_sdist: bool = False) -> List[Path]:
        """Traverse the project path and return a list of file names
        that should be included in a sdist distribution.
        If for_sdist is True, will include files like LICENSE, README and pyproject
        Produce a paths list relative to the source dir.
        """
        return sorted(set(Path(p) for p in self._find_files_iter(for_sdist)))

    def format_setup_py(self) -> str:
        before, extra, after = [], [], []
        meta = self.meta
        kwargs = {
            "name": meta.name,
            "version": meta.version,
            "author": meta.author,
            "license": meta.license_type,
            "author_email": meta.author_email,
            "maintainer": meta.maintainer,
            "maintainer_email": meta.maintainer_email,
            "description": meta.description,
            "url": (meta.project_urls or {}).get("homepage", ""),
        }

        if meta.build:
            # The build script must contain a `build(setup_kwargs)`, we just import
            # and execute it.
            after.extend([
                "from {} import build\n".format(meta.build.split(".")[0]),
                "build(setup_kwargs)\n",
            ])

        package_paths = meta.convert_package_paths()
        if package_paths["packages"]:
            extra.append("    'packages': {},\n".format(
                _format_list(package_paths["packages"], 8)))
        if package_paths["package_dir"]:
            extra.append("    'package_dir': {!r},\n".format(
                package_paths["package_dir"]))
        if package_paths["package_data"]:
            extra.append("    'package_data': {!r},\n".format(
                package_paths["package_data"]))
        if package_paths["exclude_package_data"]:
            extra.append("    'exclude_package_data': {!r},\n".format(
                package_paths["exclude_package_data"]))
        if meta.readme:
            before.append(OPEN_README.format(readme=meta.readme))
        elif meta.long_description:
            before.append("long_description = '''{}'''\n".format(
                repr(meta.long_description)[1:-1]))
        else:
            before.append("long_description = None\n")
        if meta.long_description_content_type:
            extra.append("    'long_description_content_type': {!r},\n".format(
                meta.long_description_content_type))

        if meta.keywords:
            extra.append("    'keywords': {!r},\n".format(meta.keywords))
        if meta.classifiers:
            extra.append("    'classifiers': {},\n".format(
                _format_list(meta.classifiers, 8)))
        if meta.dependencies:
            before.append("INSTALL_REQUIRES = {}\n".format(
                _format_list(meta.dependencies)))
            extra.append("    'install_requires': INSTALL_REQUIRES,\n")
        if meta.optional_dependencies:
            before.append("EXTRAS_REQUIRE = {}\n".format(
                _format_dict_list(meta.optional_dependencies)))
            extra.append("    'extras_require': EXTRAS_REQUIRE,\n")
        if meta.requires_python:
            extra.append("    'python_requires': {!r},\n".format(
                meta.requires_python))
        if meta.entry_points:
            before.append("ENTRY_POINTS = {}\n".format(
                _format_dict_list(meta.entry_points)))
            extra.append("    'entry_points': ENTRY_POINTS,\n")
        return SETUP_FORMAT.format(before="".join(before),
                                   after="".join(after),
                                   extra="".join(extra),
                                   **kwargs)

    def format_pkginfo(self, full=True) -> str:
        meta = self.meta
        content = METADATA_BASE.format(
            name=meta.name or "UNKNOWN",
            version=meta.version or "UNKNOWN",
            license=meta.license_type or "UNKNOWN",
            description=meta.description or "UNKNOWN",
        )

        # Optional fields
        if meta.keywords:
            content += "Keywords: {}\n".format(",".join(meta.keywords))

        if meta.author:
            content += "Author: {}\n".format(meta.author)

        if meta.author_email:
            content += "Author-email: {}\n".format(meta.author_email)

        if meta.maintainer:
            content += "Maintainer: {}\n".format(meta.maintainer)

        if meta.maintainer_email:
            content += "Maintainer-email: {}\n".format(meta.maintainer_email)

        if meta.requires_python:
            content += "Requires-Python: {}\n".format(meta.requires_python)

        for classifier in meta.classifiers or []:
            content += "Classifier: {}\n".format(classifier)

        if full:
            for dep in sorted(meta.dependencies):
                content += "Requires-Dist: {}\n".format(dep)

        for extra, reqs in sorted(meta.requires_extra.items()):
            content += "Provides-Extra: {}\n".format(extra)
            if full:
                for dep in reqs:
                    content += "Requires-Dist: {}\n".format(dep)

        for url in sorted(meta.project_urls or {}):
            content += "Project-URL: {}, {}\n".format(url,
                                                      meta.project_urls[url])

        if meta.long_description_content_type:
            content += "Description-Content-Type: {}\n".format(
                meta.long_description_content_type)
        if meta.long_description:
            readme = meta.long_description
            if full:
                content += "\n" + readme + "\n"
            else:
                content += "Description: {}\n".format(
                    textwrap.indent(readme, " " * 8,
                                    lambda line: True).lstrip())

        return content

    def ensure_setup_py(self, clean: bool = True) -> Path:
        """Ensures the requirement has a setup.py ready."""
        # XXX: Currently only handle PDM project, and do nothing if not.

        setup_py_path = self.location.joinpath("setup.py")
        if setup_py_path.is_file():
            return setup_py_path

        setup_py_path.write_text(self.format_setup_py(), encoding="utf-8")

        # Clean this temp file when process exits
        def cleanup():
            try:
                setup_py_path.unlink()
            except OSError:
                pass

        if clean:
            atexit.register(cleanup)
        return setup_py_path