conftest.py 9.1 KB
Newer Older
F
Frost Ming 已提交
1
import collections
F
frostming 已提交
2
import functools
F
frostming 已提交
3
import json
F
Frost Ming 已提交
4 5
import os
import shutil
F
Frost Ming 已提交
6
import sys
F
frostming 已提交
7
from distutils.dir_util import copy_tree
F
frostming 已提交
8 9
from io import BytesIO
from pathlib import Path
F
frostming 已提交
10
from typing import Callable, Iterable, List, Optional, Tuple
F
frostming 已提交
11 12
from urllib.parse import urlparse

F
Frost Ming 已提交
13
import pytest
F
frostming 已提交
14
from click.testing import CliRunner
F
Frost Ming 已提交
15 16
from pip._internal.vcs import versioncontrol
from pip._vendor import requests
F
frostming 已提交
17
from pip._vendor.pkg_resources import safe_name
F
Frost Ming 已提交
18

F
linting  
Frost Ming 已提交
19
from pdm._types import CandidateInfo
F
Frost Ming 已提交
20
from pdm.cli.actions import do_init, do_use
21
from pdm.core import main
F
frostming 已提交
22
from pdm.exceptions import CandidateInfoNotFound
23
from pdm.iostream import stream
F
frostming 已提交
24
from pdm.models.candidates import Candidate
F
Frost Ming 已提交
25
from pdm.models.environment import Environment
F
frostming 已提交
26 27 28
from pdm.models.repositories import BaseRepository
from pdm.models.requirements import Requirement
from pdm.models.specifiers import PySpecSet
F
frostming 已提交
29
from pdm.project import Project
F
frostming 已提交
30 31
from pdm.project.config import Config
from pdm.utils import cached_property, get_finder
F
frostming 已提交
32 33
from tests import FIXTURES

34
stream.disable_colors()
F
frostming 已提交
35

F
frostming 已提交
36

F
frostming 已提交
37
class LocalFileAdapter(requests.adapters.BaseAdapter):
F
frostming 已提交
38 39 40 41 42
    def __init__(self, base_path):
        super().__init__()
        self.base_path = base_path
        self._opened_files = []

F
Frost Ming 已提交
43 44 45 46 47 48
    def send(
        self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
    ):
        file_path = self.base_path / urlparse(request.url).path.lstrip(
            "/"
        )  # type: Path
F
Frost Ming 已提交
49
        response = requests.models.Response()
F
frostming 已提交
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
        response.request = request
        if not file_path.exists():
            response.status_code = 404
            response.reason = "Not Found"
            response.raw = BytesIO(b"Not Found")
        else:
            response.status_code = 200
            response.reason = "OK"
            response.raw = file_path.open("rb")
        self._opened_files.append(response.raw)
        return response

    def close(self):
        for fp in self._opened_files:
            fp.close()
        self._opened_files.clear()


F
Frost Ming 已提交
68 69 70 71 72 73 74
class MockVersionControl(versioncontrol.VersionControl):
    def obtain(self, dest, url):
        url, _ = self.get_url_rev_options(url)
        path = os.path.splitext(os.path.basename(urlparse(str(url)).path))[0]
        mocked_path = FIXTURES / "projects" / path
        shutil.copytree(mocked_path, dest)

F
frostming 已提交
75 76 77 78
    @classmethod
    def get_revision(cls, location):
        return "1234567890abcdef"

F
Frost Ming 已提交
79

F
frostming 已提交
80
class TestRepository(BaseRepository):
F
frostming 已提交
81 82
    def __init__(self, sources, environment):
        super().__init__(sources, environment)
F
frostming 已提交
83 84 85 86 87 88 89 90 91 92 93 94 95 96
        self._pypi_data = {}
        self.load_fixtures()

    def add_candidate(self, name, version, requires_python=""):
        pypi_data = self._pypi_data.setdefault(safe_name(name), {}).setdefault(
            version, {}
        )
        pypi_data["requires_python"] = requires_python

    def add_dependencies(self, name, version, requirements):
        pypi_data = self._pypi_data[safe_name(name)][version]
        pypi_data.setdefault("dependencies", []).extend(requirements)

    def _get_dependencies_from_fixture(
F
Frost Ming 已提交
97
        self, candidate: Candidate
F
frostming 已提交
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    ) -> Tuple[List[str], str, str]:
        try:
            pypi_data = self._pypi_data[candidate.req.key][candidate.version]
        except KeyError:
            raise CandidateInfoNotFound(candidate)
        deps = pypi_data.get("dependencies", [])
        for extra in candidate.req.extras or ():
            deps.extend(pypi_data.get("extras_require", {}).get(extra, []))
        return deps, pypi_data.get("requires_python", ""), ""

    def dependency_generators(self) -> Iterable[Callable[[Candidate], CandidateInfo]]:
        return (
            self._get_dependencies_from_cache,
            self._get_dependencies_from_fixture,
            self._get_dependencies_from_metadata,
        )

    def get_hashes(self, candidate: Candidate) -> None:
        candidate.hashes = {}
F
frostming 已提交
117

F
Frost Ming 已提交
118 119 120
    def _find_named_matches(
        self,
        requirement: Requirement,
F
frostming 已提交
121 122 123
        requires_python: PySpecSet = PySpecSet(),
        allow_prereleases: Optional[bool] = None,
        allow_all: bool = False,
F
Frost Ming 已提交
124
    ) -> List[Candidate]:
F
frostming 已提交
125 126 127 128 129 130
        if allow_prereleases is None:
            allow_prereleases = requirement.allow_prereleases

        cans = []
        for version, candidate in self._pypi_data.get(requirement.key, {}).items():
            c = Candidate(
F
frostming 已提交
131 132 133 134
                requirement,
                self.environment,
                name=requirement.project_name,
                version=version,
F
frostming 已提交
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
            )
            c._requires_python = PySpecSet(candidate.get("requires_python", ""))
            cans.append(c)

        sorted_cans = sorted(
            (
                c
                for c in cans
                if requirement.specifier.contains(c.version, allow_prereleases)
            ),
            key=lambda c: c.version,
        )
        if not allow_all:
            sorted_cans = [
                can
                for can in sorted_cans
                if requires_python.is_subset(can.requires_python)
            ]
        if not sorted_cans and allow_prereleases is None:
            # No non-pre-releases is found, force pre-releases now
            sorted_cans = sorted(
                (c for c in cans if requirement.specifier.contains(c.version, True)),
                key=lambda c: c.version,
            )
        return sorted_cans
F
frostming 已提交
160

F
frostming 已提交
161 162 163 164
    def load_fixtures(self):
        json_file = FIXTURES / "pypi.json"
        self._pypi_data = json.loads(json_file.read_text())

F
frostming 已提交
165

F
frostming 已提交
166
class TestProject(Project):
F
frostming 已提交
167 168 169 170 171 172 173
    def __init__(self, root_path):
        self.GLOBAL_PROJECT = Path(root_path) / ".pdm-home" / "global-project"
        super().__init__(root_path)

    @cached_property
    def global_config(self):
        return Config(self.root / ".pdm-home" / "config.toml", is_global=True)
F
frostming 已提交
174 175


F
frostming 已提交
176 177 178 179 180 181 182 183
class Distribution:
    def __init__(self, key, version):
        self.key = key
        self.version = version
        self.dependencies = []

    def requires(self, extras=()):
        return self.dependencies
F
Frost Ming 已提交
184 185 186 187 188 189 190


class MockWorkingSet(collections.abc.MutableMapping):
    def __init__(self, *args, **kwargs):
        self.pkg_ws = None
        self._data = {}

F
frostming 已提交
191 192
    def add_distribution(self, dist):
        self._data[dist.key] = dist
F
Frost Ming 已提交
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210

    def __getitem__(self, key):
        return self._data[key]

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]


@pytest.fixture()
F
frostming 已提交
211
def working_set(mocker, repository):
F
Frost Ming 已提交
212 213
    from pip._internal.utils import logging

F
Frost Ming 已提交
214 215 216 217
    rv = MockWorkingSet()
    mocker.patch.object(Environment, "get_working_set", return_value=rv)

    def install(candidate):
F
Frost Ming 已提交
218
        logging._log_state.indentation = 0
F
frostming 已提交
219 220 221 222 223
        dependencies = repository.get_dependencies(candidate)[0]
        key = safe_name(candidate.name).lower()
        dist = Distribution(key, candidate.version)
        dist.dependencies = dependencies
        rv.add_distribution(dist)
F
Frost Ming 已提交
224

F
Frost Ming 已提交
225 226
    def uninstall(dist):
        del rv[dist.key]
F
Frost Ming 已提交
227 228 229 230 231 232 233

    installer = mocker.MagicMock()
    installer.install.side_effect = install
    installer.uninstall.side_effect = uninstall
    mocker.patch("pdm.installers.Installer", return_value=installer)

    yield rv
F
frostming 已提交
234 235


F
frostming 已提交
236 237 238 239
def get_local_finder(*args, **kwargs):
    finder = get_finder(*args, **kwargs)
    finder.session.mount("http://fixtures.test/", LocalFileAdapter(FIXTURES))
    return finder
F
frostming 已提交
240 241 242


@pytest.fixture()
F
frostming 已提交
243
def project_no_init(tmp_path, mocker):
F
frostming 已提交
244
    p = TestProject(tmp_path.as_posix())
245
    p.core = main
F
frostming 已提交
246 247
    mocker.patch("pdm.utils.get_finder", get_local_finder)
    mocker.patch("pdm.models.environment.get_finder", get_local_finder)
F
frostming 已提交
248
    mocker.patch("pdm.project.core.Config.HOME_CONFIG", tmp_path)
F
Frost Ming 已提交
249
    old_config_map = Config._config_map.copy()
F
frostming 已提交
250
    p.global_config["cache_dir"] = tmp_path.joinpath("caches").as_posix()
F
Frost Ming 已提交
251
    do_use(p, sys.executable)
F
Frost Ming 已提交
252 253 254
    yield p
    # Restore the config items
    Config._config_map = old_config_map
F
Frost Ming 已提交
255 256


F
Frost Ming 已提交
257
@pytest.fixture()
F
frostming 已提交
258 259 260
def project(project_no_init):
    do_init(project_no_init, "test_project", "0.0.0")
    return project_no_init
F
Frost Ming 已提交
261 262


F
frostming 已提交
263 264 265 266 267 268
@pytest.fixture()
def fixture_project(project_no_init):
    """Initailize a project from a fixture project"""

    def func(project_name):
        source = FIXTURES / "projects" / project_name
F
frostming 已提交
269
        copy_tree(source.as_posix(), project_no_init.root.as_posix())
F
frostming 已提交
270 271 272 273 274
        return project_no_init

    return func


F
frostming 已提交
275
@pytest.fixture()
276
def repository(project, mocker):
F
frostming 已提交
277
    rv = TestRepository([], project.environment)
278
    mocker.patch.object(project, "get_repository", return_value=rv)
F
frostming 已提交
279 280 281
    return rv


F
Frost Ming 已提交
282 283 284 285 286 287
@pytest.fixture()
def vcs(mocker):
    ret = MockVersionControl()
    mocker.patch(
        "pip._internal.vcs.versioncontrol.VcsSupport.get_backend", return_value=ret
    )
F
frostming 已提交
288 289 290 291
    mocker.patch(
        "pip._internal.vcs.versioncontrol.VcsSupport.get_backend_for_scheme",
        return_value=ret,
    )
F
Frost Ming 已提交
292 293 294 295 296 297
    yield ret


@pytest.fixture(params=[False, True])
def is_editable(request):
    return request.param
F
frostming 已提交
298 299 300 301 302


@pytest.fixture(params=[False, True])
def is_dev(request):
    return request.param
F
frostming 已提交
303 304 305 306 307 308


@pytest.fixture()
def invoke():
    runner = CliRunner()
    return functools.partial(runner.invoke, main, prog_name="pdm")