Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
test_pip_install_sdist.py224 linesDownload Raw Back to integration
1# https://github.com/python/mypy/issues/169362# mypy: disable-error-code="has-type"3"""Integration tests for setuptools that focus on building packages via pip.4 5The idea behind these tests is not to exhaustively check all the possible6combinations of packages, operating systems, supporting libraries, etc, but7rather check a limited number of popular packages and how they interact with8the exposed public API. This way if any change in API is introduced, we hope to9identify backward compatibility problems before publishing a release.10 11The number of tested packages is purposefully kept small, to minimise duration12and the associated maintenance cost (changes in the way these packages define13their build process may require changes in the tests).14"""15 16import json17import os18import shutil19import sys20from enum import Enum21from glob import glob22from hashlib import md523from urllib.request import urlopen24 25import pytest26from packaging.requirements import Requirement27 28from .helpers import Archive, run29 30pytestmark = pytest.mark.integration31 32 33(LATEST,) = Enum("v", "LATEST")  # type: ignore[misc] # https://github.com/python/mypy/issues/1693634"""Default version to be checked"""35# There are positive and negative aspects of checking the latest version of the36# packages.37# The main positive aspect is that the latest version might have already38# removed the use of APIs deprecated in previous releases of setuptools.39 40 41# Packages to be tested:42# (Please notice the test environment cannot support EVERY library required for43# compiling binary extensions. In Ubuntu/Debian nomenclature, we only assume44# that `build-essential`, `gfortran` and `libopenblas-dev` are installed,45# due to their relevance to the numerical/scientific programming ecosystem)46EXAMPLES = [47    ("pip", LATEST),  # just in case...48    ("pytest", LATEST),  # uses setuptools_scm49    ("mypy", LATEST),  # custom build_py + ext_modules50    # --- Popular packages: https://hugovk.github.io/top-pypi-packages/ ---51    ("botocore", LATEST),52    ("kiwisolver", LATEST),  # build_ext53    ("brotli", LATEST),  # not in the list but used by urllib354    ("pyyaml", LATEST),  # cython + custom build_ext + custom distclass55    ("charset-normalizer", LATEST),  # uses mypyc, used by aiohttp56    ("protobuf", LATEST),57    # ("requests", LATEST),  # XXX: https://github.com/psf/requests/pull/692058    ("celery", LATEST),59    # When adding packages to this list, make sure they expose a `__version__`60    # attribute, or modify the tests below61]62 63 64# Some packages have "optional" dependencies that modify their build behaviour65# and are not listed in pyproject.toml, others still use `setup_requires`66EXTRA_BUILD_DEPS = {67    "pyyaml": ("Cython<3.0",),  # constraint to avoid errors68    "charset-normalizer": ("mypy>=1.4.1",),  # no pyproject.toml available69}70 71EXTRA_ENV_VARS = {72    "pyyaml": {"PYYAML_FORCE_CYTHON": "1"},73    "charset-normalizer": {"CHARSET_NORMALIZER_USE_MYPYC": "1"},74}75 76IMPORT_NAME = {77    "pyyaml": "yaml",78    "protobuf": "google.protobuf",79}80 81 82VIRTUALENV = (sys.executable, "-m", "virtualenv")83 84 85# By default, pip will try to build packages in isolation (PEP 517), which86# means it will download the previous stable version of setuptools.87# `pip` flags can avoid that (the version of setuptools under test88# should be the one to be used)89INSTALL_OPTIONS = (90    "--ignore-installed",91    "--no-build-isolation",92    # Omit "--no-binary :all:" the sdist is supplied directly.93    # Allows dependencies as wheels.94)95# The downside of `--no-build-isolation` is that pip will not download build96# dependencies. The test script will have to also handle that.97 98 99@pytest.fixture100def venv_python(tmp_path):101    run([*VIRTUALENV, str(tmp_path / ".venv")])102    possible_path = (str(p.parent) for p in tmp_path.glob(".venv/*/python*"))103    return shutil.which("python", path=os.pathsep.join(possible_path))104 105 106@pytest.fixture(autouse=True)107def _prepare(tmp_path, venv_python, monkeypatch):108    download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path))109    os.makedirs(download_path, exist_ok=True)110 111    # Environment vars used for building some of the packages112    monkeypatch.setenv("USE_MYPYC", "1")113 114    yield115 116    # Let's provide the maximum amount of information possible in the case117    # it is necessary to debug the tests directly from the CI logs.118    print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")119    print("Temporary directory:")120    map(print, tmp_path.glob("*"))121    print("Virtual environment:")122    run([venv_python, "-m", "pip", "freeze"])123 124 125@pytest.mark.parametrize(("package", "version"), EXAMPLES)126@pytest.mark.uses_network127def test_install_sdist(package, version, tmp_path, venv_python, setuptools_wheel):128    venv_pip = (venv_python, "-m", "pip")129    sdist = retrieve_sdist(package, version, tmp_path)130    deps = build_deps(package, sdist)131    if deps:132        print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")133        print("Dependencies:", deps)134        run([*venv_pip, "install", *deps])135 136    # Use a virtualenv to simulate PEP 517 isolation137    # but install fresh setuptools wheel to ensure the version under development138    env = EXTRA_ENV_VARS.get(package, {})139    run([*venv_pip, "install", "--force-reinstall", setuptools_wheel])140    run([*venv_pip, "install", *INSTALL_OPTIONS, sdist], env)141 142    # Execute a simple script to make sure the package was installed correctly143    pkg = IMPORT_NAME.get(package, package).replace("-", "_")144    script = f"import {pkg}; print(getattr({pkg}, '__version__', 0))"145    run([venv_python, "-c", script])146 147 148# ---- Helper Functions ----149 150 151def retrieve_sdist(package, version, tmp_path):152    """Either use cached sdist file or download it from PyPI"""153    # `pip download` cannot be used due to154    # https://github.com/pypa/pip/issues/1884155    # https://discuss.python.org/t/pep-625-file-name-of-a-source-distribution/4686156    # We have to find the correct distribution file and download it157    download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path))158    dist = retrieve_pypi_sdist_metadata(package, version)159 160    # Remove old files to prevent cache to grow indefinitely161    for file in glob(os.path.join(download_path, f"{package}*")):162        if dist["filename"] != file:163            os.unlink(file)164 165    dist_file = os.path.join(download_path, dist["filename"])166    if not os.path.exists(dist_file):167        download(dist["url"], dist_file, dist["md5_digest"])168    return dist_file169 170 171def retrieve_pypi_sdist_metadata(package, version):172    # https://warehouse.pypa.io/api-reference/json.html173    id_ = package if version is LATEST else f"{package}/{version}"174    with urlopen(f"https://pypi.org/pypi/{id_}/json") as f:175        metadata = json.load(f)176 177    if metadata["info"]["yanked"]:178        raise ValueError(f"Release for {package} {version} was yanked")179 180    version = metadata["info"]["version"]181    release = metadata["releases"][version] if version is LATEST else metadata["urls"]182    (sdist,) = filter(lambda d: d["packagetype"] == "sdist", release)183    return sdist184 185 186def download(url, dest, md5_digest):187    with urlopen(url) as f:188        data = f.read()189 190    assert md5(data).hexdigest() == md5_digest191 192    with open(dest, "wb") as f:193        f.write(data)194 195    assert os.path.exists(dest)196 197 198def build_deps(package, sdist_file):199    """Find out what are the build dependencies for a package.200 201    "Manually" install them, since pip will not install build202    deps with `--no-build-isolation`.203    """204    # delay importing, since pytest discovery phase may hit this file from a205    # testenv without tomli206    from setuptools.compat.py310 import tomllib207 208    archive = Archive(sdist_file)209    info = tomllib.loads(_read_pyproject(archive))210    deps = info.get("build-system", {}).get("requires", [])211    deps += EXTRA_BUILD_DEPS.get(package, [])212    # Remove setuptools from requirements (and deduplicate)213    requirements = {Requirement(d).name: d for d in deps}214    return [v for k, v in requirements.items() if k != "setuptools"]215 216 217def _read_pyproject(archive):218    contents = (219        archive.get_content(member)220        for member in archive221        if os.path.basename(archive.get_name(member)) == "pyproject.toml"222    )223    return next(contents, "")224 
codekingpro/portable-devtools · Team Ai