conftest.py 8.8 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
10
from typing import Callable, Iterable, List, 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
F
frostming 已提交
23
from pdm.models.candidates import Candidate
F
Frost Ming 已提交
24
from pdm.models.environment import Environment
F
frostming 已提交
25
from pdm.models.repositories import BaseRepository
F
frostming 已提交
26
from pdm.models.requirements import Requirement, filter_requirements_with_extras
F
frostming 已提交
27
from pdm.project import Project
F
frostming 已提交
28
from pdm.project.config import Config
F
Frost Ming 已提交
29
from pdm.utils import cached_property, get_finder, temp_environ
F
frostming 已提交
30 31
from tests import FIXTURES

F
Frost Ming 已提交
32
os.environ["CI"] = "1"
F
frostming 已提交
33

F
frostming 已提交
34

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

F
Frost Ming 已提交
41 42 43 44 45 46
    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 已提交
47
        response = requests.models.Response()
F
frostming 已提交
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
        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 已提交
66 67 68 69 70 71 72
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 已提交
73 74 75 76
    @classmethod
    def get_revision(cls, location):
        return "1234567890abcdef"

F
Frost Ming 已提交
77

78 79 80 81
class _FakeLink:
    is_wheel = False


F
frostming 已提交
82
class TestRepository(BaseRepository):
F
frostming 已提交
83 84
    def __init__(self, sources, environment):
        super().__init__(sources, environment)
F
frostming 已提交
85 86 87 88 89 90 91 92 93 94 95 96 97 98
        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 已提交
99
        self, candidate: Candidate
F
frostming 已提交
100 101 102 103 104 105 106 107
    ) -> 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, []))
F
frostming 已提交
108
        deps = filter_requirements_with_extras(deps, candidate.req.extras or ())
F
frostming 已提交
109 110 111 112 113 114 115 116 117 118 119
        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 已提交
120

121
    def _find_candidates(self, requirement: Requirement) -> Iterable[Candidate]:
F
frostming 已提交
122 123
        for version, candidate in self._pypi_data.get(requirement.key, {}).items():
            c = Candidate(
F
frostming 已提交
124 125 126 127
                requirement,
                self.environment,
                name=requirement.project_name,
                version=version,
F
frostming 已提交
128
            )
F
Frost Ming 已提交
129
            c.requires_python = candidate.get("requires_python", "")
130 131
            c.link = _FakeLink()
            yield c
F
frostming 已提交
132

F
frostming 已提交
133 134 135 136
    def load_fixtures(self):
        json_file = FIXTURES / "pypi.json"
        self._pypi_data = json.loads(json_file.read_text())

F
frostming 已提交
137

F
frostming 已提交
138
class TestProject(Project):
F
frostming 已提交
139 140 141 142 143 144 145
    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 已提交
146 147


F
frostming 已提交
148
class Distribution:
149
    def __init__(self, key, version, editable=False):
F
frostming 已提交
150 151
        self.key = key
        self.version = version
152
        self.editable = editable
F
frostming 已提交
153 154 155 156
        self.dependencies = []

    def requires(self, extras=()):
        return self.dependencies
F
Frost Ming 已提交
157 158 159 160 161 162 163


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

F
frostming 已提交
164 165
    def add_distribution(self, dist):
        self._data[dist.key] = dist
F
Frost Ming 已提交
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183

    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 已提交
184
def working_set(mocker, repository):
F
Frost Ming 已提交
185
    from pdm.models.pip_shims import pip_logging
F
Frost Ming 已提交
186

F
Frost Ming 已提交
187 188 189 190
    rv = MockWorkingSet()
    mocker.patch.object(Environment, "get_working_set", return_value=rv)

    def install(candidate):
F
Frost Ming 已提交
191
        pip_logging._log_state.indentation = 0
F
frostming 已提交
192 193
        dependencies = repository.get_dependencies(candidate)[0]
        key = safe_name(candidate.name).lower()
194
        dist = Distribution(key, candidate.version, candidate.req.editable)
F
frostming 已提交
195 196
        dist.dependencies = dependencies
        rv.add_distribution(dist)
F
Frost Ming 已提交
197

F
Frost Ming 已提交
198 199
    def uninstall(dist):
        del rv[dist.key]
F
Frost Ming 已提交
200 201 202 203

    installer = mocker.MagicMock()
    installer.install.side_effect = install
    installer.uninstall.side_effect = uninstall
F
Frost Ming 已提交
204
    mocker.patch("pdm.installers.synchronizers.Installer", return_value=installer)
F
frostming 已提交
205
    mocker.patch("pdm.installers.Installer", return_value=installer)
F
Frost Ming 已提交
206 207

    yield rv
F
frostming 已提交
208 209


F
frostming 已提交
210 211 212 213
def get_local_finder(*args, **kwargs):
    finder = get_finder(*args, **kwargs)
    finder.session.mount("http://fixtures.test/", LocalFileAdapter(FIXTURES))
    return finder
F
frostming 已提交
214 215


F
frostming 已提交
216 217
@pytest.fixture(autouse=True)
def pip_global_tempdir_manager():
F
Frost Ming 已提交
218
    from pdm.models.pip_shims import global_tempdir_manager
F
frostming 已提交
219 220 221 222 223

    with global_tempdir_manager():
        yield


F
frostming 已提交
224
@pytest.fixture()
F
frostming 已提交
225
def project_no_init(tmp_path, mocker):
F
frostming 已提交
226
    p = TestProject(tmp_path.as_posix())
227
    p.core = main
F
frostming 已提交
228 229
    mocker.patch("pdm.utils.get_finder", get_local_finder)
    mocker.patch("pdm.models.environment.get_finder", get_local_finder)
F
frostming 已提交
230
    mocker.patch("pdm.project.core.Config.HOME_CONFIG", tmp_path)
F
Frost Ming 已提交
231
    old_config_map = Config._config_map.copy()
F
frostming 已提交
232
    p.global_config["cache_dir"] = tmp_path.joinpath("caches").as_posix()
233
    do_use(p, getattr(sys, "_base_executable", sys.executable))
F
Frost Ming 已提交
234 235
    with temp_environ():
        os.environ.pop("VIRTUAL_ENV", None)
F
Frost Ming 已提交
236
        os.environ.pop("PYTHONPATH", None)
F
Frost Ming 已提交
237
        yield p
F
Frost Ming 已提交
238 239
    # Restore the config items
    Config._config_map = old_config_map
F
Frost Ming 已提交
240 241


F
Frost Ming 已提交
242
@pytest.fixture()
F
frostming 已提交
243 244
def project(project_no_init):
    do_init(project_no_init, "test_project", "0.0.0")
F
Frost Ming 已提交
245 246
    # Clean the cached property
    project_no_init.__dict__.pop("environment", None)
F
frostming 已提交
247
    return project_no_init
F
Frost Ming 已提交
248 249


F
frostming 已提交
250 251 252 253 254 255
@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 已提交
256
        copy_tree(source.as_posix(), project_no_init.root.as_posix())
F
Frost Ming 已提交
257
        project_no_init._pyproject = None
F
frostming 已提交
258 259 260 261 262
        return project_no_init

    return func


F
frostming 已提交
263
@pytest.fixture()
264
def repository(project, mocker):
F
frostming 已提交
265
    rv = TestRepository([], project.environment)
266
    mocker.patch.object(project, "get_repository", return_value=rv)
F
frostming 已提交
267 268 269
    return rv


F
Frost Ming 已提交
270 271 272 273 274 275
@pytest.fixture()
def vcs(mocker):
    ret = MockVersionControl()
    mocker.patch(
        "pip._internal.vcs.versioncontrol.VcsSupport.get_backend", return_value=ret
    )
F
frostming 已提交
276 277 278 279
    mocker.patch(
        "pip._internal.vcs.versioncontrol.VcsSupport.get_backend_for_scheme",
        return_value=ret,
    )
F
Frost Ming 已提交
280 281 282 283 284 285
    yield ret


@pytest.fixture(params=[False, True])
def is_editable(request):
    return request.param
F
frostming 已提交
286 287 288 289 290


@pytest.fixture(params=[False, True])
def is_dev(request):
    return request.param
F
frostming 已提交
291 292 293 294


@pytest.fixture()
def invoke():
F
Frost Ming 已提交
295
    runner = CliRunner(mix_stderr=False)
F
frostming 已提交
296
    return functools.partial(runner.invoke, main, prog_name="pdm")