tox-dev / tox

@@ -17,7 +17,7 @@
Loading
17 17
18 18
19 19
def build_parser(cli_only: bool) -> ArgumentParser:
20 -
    parser = _OurArgumentParser(add_help=False, prog="")
20 +
    parser = _OurArgumentParser(add_help=False, prog="", allow_abbrev=False)
21 21
    _global_options(parser)
22 22
    _req_options(parser, cli_only)
23 23
    return parser

@@ -100,7 +100,7 @@
Loading
100 100
                    missing_requirement = set(old["requirements"]) - set(new_requirements)
101 101
                    if missing_requirement:
102 102
                        raise Recreate(f"requirements removed: {' '.join(missing_requirement)}")
103 -
                args = arguments.as_args()
103 +
                args = arguments.as_root_args
104 104
                if args:
105 105
                    self._execute_installer(args, of_type)
106 106

@@ -18,7 +18,7 @@
Loading
18 18
    result.assert_success()
19 19
    deps = result.state.conf.get_env("py")["deps"]
20 20
    assert deps.unroll() == ([], ["alpha", "beta", "pytest"])
21 -
    assert deps.as_args() == ["-r", "path.txt", "-r", str(project.path / "path2.txt"), "pytest"]
21 +
    assert deps.as_root_args == ["pytest", "-r", "path.txt", "-r", str(project.path / "path2.txt")]
22 22
    assert str(deps) == f"-r {project.path / 'tox.ini'}"
23 23
24 24

@@ -9,6 +9,6 @@
Loading
9 9
def test_legacy_requirement_file(tmp_path: Path, legacy_flag: str) -> None:
10 10
    python_deps = PythonDeps(f"{legacy_flag}a.txt", tmp_path)
11 11
    (tmp_path / "a.txt").write_text("b")
12 -
    assert python_deps.as_args() == [legacy_flag, "a.txt"]
12 +
    assert python_deps.as_root_args == [legacy_flag, "a.txt"]
13 13
    assert vars(python_deps.options) == {}
14 14
    assert [str(i) for i in python_deps.requirements] == ["b" if legacy_flag == "-r" else "-c b"]

@@ -7,7 +7,7 @@
Loading
7 7
import urllib.parse
8 8
from argparse import ArgumentParser, Namespace
9 9
from pathlib import Path
10 -
from typing import IO, Any, Dict, Iterator, List, Optional, Tuple, Union
10 +
from typing import IO, Any, Dict, Iterator, List, Optional, Tuple, Union, cast
11 11
from urllib.request import urlopen
12 12
13 13
import chardet
@@ -26,6 +26,8 @@
Loading
26 26
_EXTRA_ELEMENT = re.compile(r"[a-zA-Z0-9]*[-._a-zA-Z0-9]")
27 27
ReqFileLines = Iterator[Tuple[int, str]]
28 28
29 +
DEFAULT_INDEX_URL = "https://pypi.org/simple"
30 +
29 31
30 32
class ParsedRequirement:
31 33
    def __init__(self, req: str, options: Dict[str, Any], from_file: str, lineno: int) -> None:
@@ -93,8 +95,13 @@
Loading
93 95
            result.extend(("--hash", hash_value))
94 96
        return " ".join(result)
95 97
98 +
    def as_args(self) -> Iterator[str]:
99 +
        if self.options.get("is_editable"):
100 +
            yield "-e"
101 +
        yield str(self._requirement)
102 +
96 103
97 -
class _ParsedLine:
104 +
class ParsedLine:
98 105
    def __init__(self, filename: str, lineno: int, args: str, opts: Namespace, constraint: bool) -> None:
99 106
        self.filename = filename
100 107
        self.lineno = lineno
@@ -118,8 +125,8 @@
Loading
118 125
        self._path = path
119 126
        self._is_constraint: bool = constraint
120 127
        self._opt = Namespace()
121 -
        self._result: List[ParsedRequirement] = []
122 -
        self._loaded = False
128 +
        self._requirements: Optional[List[ParsedRequirement]] = None
129 +
        self._as_root_args: Optional[List[str]] = None
123 130
        self._parser_private: Optional[ArgumentParser] = None
124 131
125 132
    def __str__(self) -> str:
@@ -135,13 +142,13 @@
Loading
135 142
136 143
    @property
137 144
    def options(self) -> Namespace:
138 -
        self._parse_requirements()
145 +
        self._ensure_requirements_parsed()
139 146
        return self._opt
140 147
141 148
    @property
142 149
    def requirements(self) -> List[ParsedRequirement]:
143 -
        self._parse_requirements()
144 -
        return self._result
150 +
        self._ensure_requirements_parsed()
151 +
        return cast(List[ParsedRequirement], self._requirements)
145 152
146 153
    @property
147 154
    def _parser(self) -> ArgumentParser:
@@ -149,31 +156,34 @@
Loading
149 156
            self._parser_private = build_parser(False)
150 157
        return self._parser_private
151 158
152 -
    def _parse_requirements(self) -> None:
153 -
        if self._loaded:
154 -
            return
155 -
        self._result, found = [], set()
156 -
        for parsed_line in self._parse_and_recurse(str(self._path), self.is_constraint):
157 -
            parsed_req = self._handle_line(parsed_line)
158 -
            if parsed_req is not None:
159 +
    def _ensure_requirements_parsed(self) -> None:
160 +
        if self._requirements is None:
161 +
            self._requirements = self._parse_requirements(opt=self._opt, recurse=True)
162 +
163 +
    def _parse_requirements(self, opt: Namespace, recurse: bool) -> List[ParsedRequirement]:
164 +
        result, found = [], set()
165 +
        for parsed_line in self._parse_and_recurse(str(self._path), self.is_constraint, recurse):
166 +
            if parsed_line.is_requirement:
167 +
                parsed_req = self._handle_requirement_line(parsed_line)
159 168
                key = str(parsed_req)
160 169
                if key not in found:
161 170
                    found.add(key)
162 -
                    self._result.append(parsed_req)
163 -
164 -
        def key_func(line: ParsedRequirement) -> Tuple[int, Tuple[int, str, str]]:
165 -
            of_type = {Requirement: 0, Path: 1, str: 2}[type(line.requirement)]
166 -
            between = of_type, str(line.requirement).lower(), str(line.options)
167 -
            if "is_constraint" in line.options:
168 -
                return 2, between
169 -
            if "is_editable" in line.options:
170 -
                return 1, between
171 -
            return 0, between
172 -
173 -
        self._result.sort(key=key_func)
174 -
        self._loaded = True
175 -
176 -
    def _parse_and_recurse(self, filename: str, constraint: bool) -> Iterator[_ParsedLine]:
171 +
                    result.append(parsed_req)
172 +
            else:
173 +
                self._merge_option_line(opt, parsed_line.opts, parsed_line.filename)
174 +
        result.sort(key=self._key_func)
175 +
        return result
176 +
177 +
    def _key_func(self, line: ParsedRequirement) -> Tuple[int, Tuple[int, str, str]]:  # noqa
178 +
        of_type = {Requirement: 0, Path: 1, str: 2}[type(line.requirement)]
179 +
        between = of_type, str(line.requirement).lower(), str(line.options)
180 +
        if "is_constraint" in line.options:
181 +
            return 2, between
182 +
        if "is_editable" in line.options:
183 +
            return 1, between
184 +
        return 0, between
185 +
186 +
    def _parse_and_recurse(self, filename: str, constraint: bool, recurse: bool) -> Iterator[ParsedLine]:
177 187
        for line in self._parse_file(filename, constraint):
178 188
            if not line.is_requirement and (line.opts.requirements or line.opts.constraints):
179 189
                if line.opts.requirements:  # parse a nested requirements file
@@ -185,15 +195,19 @@
Loading
185 195
                elif not _SCHEME_RE.search(req_path):  # original file and nested file are paths
186 196
                    # do a join so relative paths work
187 197
                    req_path = os.path.join(os.path.dirname(filename), req_path)
188 -
                yield from self._parse_and_recurse(req_path, nested_constraint)
198 +
                if recurse:
199 +
                    yield from self._parse_and_recurse(req_path, nested_constraint, recurse)
200 +
                else:
201 +
                    line.filename = req_path
202 +
                    yield line
189 203
            else:
190 204
                yield line
191 205
192 -
    def _parse_file(self, url: str, constraint: bool) -> Iterator[_ParsedLine]:
206 +
    def _parse_file(self, url: str, constraint: bool) -> Iterator[ParsedLine]:
193 207
        content = self._get_file_content(url)
194 208
        for line_number, line in self._pre_process(content):
195 209
            args_str, opts = self._parse_line(line)
196 -
            yield _ParsedLine(url, line_number, args_str, opts, constraint)
210 +
            yield ParsedLine(url, line_number, args_str, opts, constraint)
197 211
198 212
    def _get_file_content(self, url: str) -> str:
199 213
        """
@@ -242,30 +256,8 @@
Loading
242 256
        opts = self._parser.parse_args(args)
243 257
        return args_str, opts
244 258
245 -
    def _handle_line(self, line: _ParsedLine) -> Optional[ParsedRequirement]:
246 -
        """
247 -
        Handle a single parsed requirements line; This can result in creating/yielding requirements or updating options.
248 -
249 -
        :param line: The parsed line to be processed.
250 -
251 -
        Returns a ParsedRequirement object if the line is a requirement line, otherwise returns None.
252 -
253 -
        For lines that contain requirements, the only options that have an effect are from SUPPORTED_OPTIONS_REQ, and
254 -
        they are scoped to the requirement. Other options from SUPPORTED_OPTIONS may be present, but are ignored.
255 -
256 -
        For lines that do not contain requirements, the only options that have an effect are from SUPPORTED_OPTIONS.
257 -
        Options from SUPPORTED_OPTIONS_REQ may be present, but are ignored. These lines may contain multiple options
258 -
        (although our docs imply only one is supported), and all our parsed and affect the finder.
259 -
        """
260 -
        if line.is_requirement:
261 -
            parsed_req = self._handle_requirement_line(line)
262 -
            return parsed_req
263 -
        else:
264 -
            self._handle_option_line(line.opts, line.filename)
265 -
            return None
266 -
267 259
    @staticmethod
268 -
    def _handle_requirement_line(line: _ParsedLine) -> ParsedRequirement:
260 +
    def _handle_requirement_line(line: ParsedLine) -> ParsedRequirement:
269 261
        # For editable requirements, we don't support per-requirement options, so just return the parsed requirement.
270 262
        # get the options that apply to requirements
271 263
        req_options = {}
@@ -278,50 +270,66 @@
Loading
278 270
            req_options["hash"] = hash_values
279 271
        return ParsedRequirement(line.requirement, req_options, line.filename, line.lineno)
280 272
281 -
    def _handle_option_line(self, opts: Namespace, filename: str) -> None:  # noqa: C901
273 +
    @staticmethod
274 +
    def _merge_option_line(base_opt: Namespace, opt: Namespace, filename: str) -> None:  # noqa: C901
282 275
        # percolate options upward
283 -
        if opts.require_hashes:
284 -
            self._opt.require_hashes = True
285 -
        if opts.features_enabled:
286 -
            if not hasattr(self._opt, "features_enabled"):
287 -
                self._opt.features_enabled = []
288 -
            for feature in opts.features_enabled:
289 -
                if feature not in self._opt.features_enabled:
290 -
                    self._opt.features_enabled.append(feature)
291 -
            self._opt.features_enabled.sort()
292 -
        if opts.index_url:
293 -
            if not hasattr(self._opt, "index_url"):
294 -
                self._opt.index_url = []
295 -
            self._opt.index_url = [opts.index_url]
296 -
        if opts.no_index is True:
297 -
            self._opt.index_url = []
298 -
        if opts.extra_index_url:
299 -
            if not hasattr(self._opt, "index_url"):
300 -
                self._opt.index_url = []
301 -
            for url in opts.extra_index_url:
302 -
                if url not in self._opt.index_url:
303 -
                    self._opt.index_url.extend(opts.extra_index_url)
304 -
        if opts.find_links:
276 +
        if opt.requirements:
277 +
            if not hasattr(base_opt, "requirements"):
278 +
                base_opt.requirements = []
279 +
            if opt.requirements[0] not in base_opt.requirements:
280 +
                base_opt.requirements.append(opt.requirements[0])
281 +
        if opt.constraints:
282 +
            if not hasattr(base_opt, "constraints"):
283 +
                base_opt.constraints = []
284 +
            if opt.constraints[0] not in base_opt.constraints:
285 +
                base_opt.constraints.append(opt.constraints[0])
286 +
        if opt.require_hashes:
287 +
            base_opt.require_hashes = True
288 +
        if opt.features_enabled:
289 +
            if not hasattr(base_opt, "features_enabled"):
290 +
                base_opt.features_enabled = []
291 +
            for feature in opt.features_enabled:
292 +
                if feature not in base_opt.features_enabled:
293 +
                    base_opt.features_enabled.append(feature)
294 +
            base_opt.features_enabled.sort()
295 +
        if opt.index_url:
296 +
            if getattr(base_opt, "index_url", []):
297 +
                base_opt.index_url[0] = opt.index_url
298 +
            else:
299 +
                base_opt.index_url = [opt.index_url]
300 +
        if opt.no_index is True:
301 +
            base_opt.index_url = []
302 +
        if opt.extra_index_url:
303 +
            if not getattr(base_opt, "index_url", []):
304 +
                base_opt.index_url = [DEFAULT_INDEX_URL]
305 +
            for url in opt.extra_index_url:
306 +
                if url not in base_opt.index_url:
307 +
                    base_opt.index_url.extend(opt.extra_index_url)
308 +
        if opt.find_links:
305 309
            # FIXME: it would be nice to keep track of the source of the find_links: support a find-links local path
306 310
            # relative to a requirements file.
307 -
            if not hasattr(self._opt, "index_url"):  # pragma: no branch
308 -
                self._opt.find_links = []
309 -
            value = opts.find_links[0]
311 +
            if not hasattr(base_opt, "index_url"):  # pragma: no branch
312 +
                base_opt.find_links = []
313 +
            value = opt.find_links[0]
310 314
            req_dir = os.path.dirname(os.path.abspath(filename))
311 315
            relative_to_reqs_file = os.path.join(req_dir, value)
312 316
            if os.path.exists(relative_to_reqs_file):
313 317
                value = relative_to_reqs_file  # pragma: no cover
314 -
            if value not in self._opt.find_links:  # pragma: no branch
315 -
                self._opt.find_links.append(value)
316 -
        if opts.pre:
317 -
            self._opt.pre = True
318 -
        if opts.prefer_binary:
319 -
            self._opt.prefer_binary = True
320 -
        for host in opts.trusted_host or []:
321 -
            if not hasattr(self._opt, "trusted_hosts"):
322 -
                self._opt.trusted_hosts = []
323 -
            if host not in self._opt.trusted_hosts:
324 -
                self._opt.trusted_hosts.append(host)
318 +
            if value not in base_opt.find_links:  # pragma: no branch
319 +
                base_opt.find_links.append(value)
320 +
        if opt.pre:
321 +
            base_opt.pre = True
322 +
        if opt.prefer_binary:
323 +
            base_opt.prefer_binary = True
324 +
        for host in opt.trusted_host or []:
325 +
            if not hasattr(base_opt, "trusted_hosts"):
326 +
                base_opt.trusted_hosts = []
327 +
            if host not in base_opt.trusted_hosts:
328 +
                base_opt.trusted_hosts.append(host)
329 +
        if opt.no_binary:
330 +
            base_opt.no_binary = opt.no_binary
331 +
        if opt.only_binary:
332 +
            base_opt.only_binary = opt.only_binary
325 333
326 334
    @staticmethod
327 335
    def _break_args_options(line: str) -> Tuple[str, str]:
@@ -399,6 +407,53 @@
Loading
399 407
                line = line.replace(env_var, value)
400 408
            yield line_number, line
401 409
410 +
    @property
411 +
    def as_root_args(self) -> List[str]:
412 +
        if self._as_root_args is None:
413 +
            opt = Namespace()
414 +
            result: List[str] = []
415 +
            for req in self._parse_requirements(opt=opt, recurse=False):
416 +
                result.extend(req.as_args())
417 +
            option_args = self._option_to_args(opt)
418 +
            result.extend(option_args)
419 +
420 +
            self._as_root_args = result
421 +
        return self._as_root_args
422 +
423 +
    @staticmethod
424 +
    def _option_to_args(opt: Namespace) -> List[str]:
425 +
        result: List[str] = []
426 +
        for req in getattr(opt, "requirements", []):
427 +
            result.extend(("-r", req))
428 +
        for req in getattr(opt, "constraints", []):
429 +
            result.extend(("-c", req))
430 +
        index_url = getattr(opt, "index_url", None)
431 +
        if index_url is not None:
432 +
            if index_url:
433 +
                if index_url[0] != DEFAULT_INDEX_URL:
434 +
                    result.extend(("-i", index_url[0]))
435 +
                for url in index_url[1:]:
436 +
                    result.extend(("--extra-index-url", url))
437 +
            else:
438 +
                result.append("--no-index")
439 +
        for link in getattr(opt, "find_links", []):
440 +
            result.extend(("-f", link))
441 +
        if hasattr(opt, "pre"):
442 +
            result.append("--pre")
443 +
        for host in getattr(opt, "trusted_hosts", []):
444 +
            result.extend(("--trusted-host", host))
445 +
        if hasattr(opt, "prefer_binary"):
446 +
            result.append("--prefer-binary")
447 +
        if hasattr(opt, "require_hashes"):
448 +
            result.append("--require-hashes")
449 +
        for feature in getattr(opt, "features_enabled", []):
450 +
            result.extend(("--use-feature", feature))
451 +
        if hasattr(opt, "no_binary"):
452 +
            result.extend(("--no-binary", opt.no_binary))
453 +
        if hasattr(opt, "only_binary"):
454 +
            result.extend(("--only-binary", opt.only_binary))
455 +
        return result
456 +
402 457
403 458
__all__ = (
404 459
    "RequirementsFile",

@@ -10,208 +10,270 @@
Loading
10 10
from tox.pytest import CaptureFixture, MonkeyPatch
11 11
from tox.tox_env.python.pip.req.file import ParsedRequirement, RequirementsFile
12 12
13 -
14 -
@pytest.mark.parametrize(
15 -
    ("req", "opts", "requirements"),
16 -
    [
17 -
        pytest.param("--pre", {"pre": True}, [], id="pre"),
18 -
        pytest.param("--no-index", {"index_url": []}, [], id="no-index"),
19 -
        pytest.param("--no-index\n-i a\n--no-index", {"index_url": []}, [], id="no-index overwrites index"),
20 -
        pytest.param("--prefer-binary", {"prefer_binary": True}, [], id="prefer-binary"),
21 -
        pytest.param("--require-hashes", {"require_hashes": True}, [], id="requires-hashes"),
22 -
        pytest.param("--pre ", {"pre": True}, [], id="space after"),
23 -
        pytest.param(" --pre", {"pre": True}, [], id="space before"),
24 -
        pytest.param("--pre\\\n", {"pre": True}, [], id="newline after"),
25 -
        pytest.param("--pre # magic", {"pre": True}, [], id="comment after space"),
26 -
        pytest.param("--pre\t# magic", {"pre": True}, [], id="comment after tab"),
27 -
        pytest.param(
28 -
            "--find-links /my/local/archives",
29 -
            {"find_links": ["/my/local/archives"]},
30 -
            [],
31 -
            id="find-links path",
32 -
        ),
33 -
        pytest.param(
34 -
            "--find-links /my/local/archives --find-links /my/local/archives",
35 -
            {"find_links": ["/my/local/archives"]},
36 -
            [],
37 -
            id="find-links duplicate same line",
38 -
        ),
39 -
        pytest.param(
40 -
            "--find-links /my/local/archives\n--find-links /my/local/archives",
41 -
            {"find_links": ["/my/local/archives"]},
42 -
            [],
43 -
            id="find-links duplicate different line",
44 -
        ),
45 -
        pytest.param(
46 -
            "--find-links \\\n/my/local/archives",
47 -
            {"find_links": ["/my/local/archives"]},
48 -
            [],
49 -
            id="find-links newline path",
50 -
        ),
51 -
        pytest.param(
52 -
            "--find-links http://some.archives.com/archives",
53 -
            {"find_links": ["http://some.archives.com/archives"]},
54 -
            [],
55 -
            id="find-links url",
56 -
        ),
57 -
        pytest.param("-i a", {"index_url": ["a"]}, [], id="index url short"),
58 -
        pytest.param("--index-url a", {"index_url": ["a"]}, [], id="index url long"),
59 -
        pytest.param("-i a -i b\n-i c", {"index_url": ["c"]}, [], id="index url multiple"),
60 -
        pytest.param("--extra-index-url a", {"index_url": ["a"]}, [], id="extra-index-url"),
61 -
        pytest.param(
62 -
            "--extra-index-url a --extra-index-url a", {"index_url": ["a"]}, [], id="extra-index-url dup same line"
63 -
        ),
64 -
        pytest.param(
65 -
            "--extra-index-url a\n--extra-index-url a",
66 -
            {"index_url": ["a"]},
67 -
            [],
68 -
            id="extra-index-url dup different line",
69 -
        ),
70 -
        pytest.param("-e a", {}, ["-e a"], id="e"),
71 -
        pytest.param("--editable a", {}, ["-e a"], id="editable"),
72 -
        pytest.param("--editable .[2,1]", {}, ["-e .[1,2]"], id="editable extra"),
73 -
        pytest.param(".[\t, a1. , B2-\t, C3_, ]", {}, [".[B2-,C3_,a1.]"], id="path with extra"),
74 -
        pytest.param(".[a.1]", {}, [".[a.1]"], id="path with invalid extra is path"),
75 -
        pytest.param("-f a", {"find_links": ["a"]}, [], id="f"),
76 -
        pytest.param("--find-links a", {"find_links": ["a"]}, [], id="find-links"),
77 -
        pytest.param("--trusted-host a", {"trusted_hosts": ["a"]}, [], id="trusted-host"),
78 -
        pytest.param(
79 -
            "--trusted-host a --trusted-host a", {"trusted_hosts": ["a"]}, [], id="trusted-host dup same line"
80 -
        ),
81 -
        pytest.param(
82 -
            "--trusted-host a\n--trusted-host a", {"trusted_hosts": ["a"]}, [], id="trusted-host dup different line"
83 -
        ),
84 -
        pytest.param(
85 -
            "--use-feature 2020-resolver", {"features_enabled": ["2020-resolver"]}, [], id="use-feature space"
86 -
        ),
87 -
        pytest.param("--use-feature=fast-deps", {"features_enabled": ["fast-deps"]}, [], id="use-feature equal"),
88 -
        pytest.param(
89 -
            "--use-feature=fast-deps --use-feature 2020-resolver",
90 -
            {"features_enabled": ["2020-resolver", "fast-deps"]},
91 -
            [],
92 -
            id="use-feature multiple same line",
93 -
        ),
94 -
        pytest.param(
95 -
            "--use-feature=fast-deps\n--use-feature 2020-resolver",
96 -
            {"features_enabled": ["2020-resolver", "fast-deps"]},
97 -
            [],
98 -
            id="use-feature multiple different line",
99 -
        ),
100 -
        pytest.param(
101 -
            "--use-feature=fast-deps\n--use-feature 2020-resolver\n" * 2,
102 -
            {"features_enabled": ["2020-resolver", "fast-deps"]},
103 -
            [],
104 -
            id="use-feature multiple duplicate different line",
105 -
        ),
106 -
        pytest.param("--no-binary :all:", {}, [], id="no-binary"),
107 -
        pytest.param("--only-binary :all:", {}, [], id="only-binary space"),
108 -
        pytest.param("--only-binary=:all:", {}, [], id="only-binary equal"),
109 -
        pytest.param("####### example-requirements.txt #######", {}, [], id="comment"),
110 -
        pytest.param("\t##### Requirements without Version Specifiers ######", {}, [], id="tab and comment"),
111 -
        pytest.param("  # start", {}, [], id="space and comment"),
112 -
        pytest.param("nose", {}, ["nose"], id="req"),
113 -
        pytest.param("nose\nnose", {}, ["nose"], id="req dup"),
114 -
        pytest.param(
115 -
            "numpy[2,1]  @ file://./downloads/numpy-1.9.2-cp34-none-win32.whl",
116 -
            {},
117 -
            ["numpy[1,2]@ file://./downloads/numpy-1.9.2-cp34-none-win32.whl"],
118 -
            id="path with name-extra-protocol",
119 -
        ),
120 -
        pytest.param(
121 -
            "docopt == 0.6.1             # Version Matching. Must be version 0.6.1",
122 -
            {},
123 -
            ["docopt==0.6.1"],
124 -
            id="req equal comment",
125 -
        ),
126 -
        pytest.param(
127 -
            "keyring >= 4.1.1            # Minimum version 4.1.1",
128 -
            {},
129 -
            ["keyring>=4.1.1"],
130 -
            id="req ge comment",
131 -
        ),
132 -
        pytest.param(
133 -
            "coverage != 3.5             # Version Exclusion. Anything except version 3.5",
134 -
            {},
135 -
            ["coverage!=3.5"],
136 -
            id="req ne comment",
137 -
        ),
138 -
        pytest.param(
139 -
            "Mopidy-Dirble ~= 1.1        # Compatible release. Same as >= 1.1, == 1.*",
140 -
            {},
141 -
            ["Mopidy-Dirble~=1.1"],
142 -
            id="req approx comment",
143 -
        ),
144 -
        pytest.param("b==1.3", {}, ["b==1.3"], id="req eq"),
145 -
        pytest.param("c >=1.2,<2.0", {}, ["c<2.0,>=1.2"], id="req ge lt"),
146 -
        pytest.param("d[bar,foo]", {}, ["d[bar,foo]"], id="req extras"),
147 -
        pytest.param("d[foo, bar]", {}, ["d[bar,foo]"], id="req extras space"),
148 -
        pytest.param("d[foo,\tbar]", {}, ["d[bar,foo]"], id="req extras tab"),
149 -
        pytest.param("e~=1.4.2", {}, ["e~=1.4.2"], id="req approx"),
150 -
        pytest.param(
151 -
            "f ==5.4 ; python_version < '2.7'", {}, ['f==5.4; python_version < "2.7"'], id="python version filter"
152 -
        ),
153 -
        pytest.param("g; sys_platform == 'win32'", {}, ['g; sys_platform == "win32"'], id="platform filter"),
154 -
        pytest.param(
155 -
            "http://w.org/w_P-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl",
156 -
            {},
157 -
            ["http://w.org/w_P-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl"],
158 -
            id="http URI",
159 -
        ),
160 -
        pytest.param(
161 -
            "git+https://git.example.com/MyProject#egg=MyProject",
162 -
            {},
163 -
            ["git+https://git.example.com/MyProject#egg=MyProject"],
164 -
            id="vcs with https",
165 -
        ),
166 -
        pytest.param(
167 -
            "git+ssh://git.example.com/MyProject#egg=MyProject",
168 -
            {},
169 -
            ["git+ssh://git.example.com/MyProject#egg=MyProject"],
170 -
            id="vcs with ssh",
171 -
        ),
172 -
        pytest.param(
173 -
            "git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject",
174 -
            {},
175 -
            ["git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject"],
176 -
            id="vcs with commit hash pin",
177 -
        ),
178 -
        pytest.param(
179 -
            "attrs --hash sha384:142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5e2ff2c528ecae04"
180 -
            "912224782ab\t--hash=sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152 # ok",
181 -
            {},
182 -
            [
183 -
                "attrs --hash sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152 --hash sha384:"
184 -
                "142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5e2ff2c528ecae04912224782ab"
185 -
            ],
186 -
            id="hash",
187 -
        ),
188 -
        pytest.param(
189 -
            "attrs --hash=sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152\\\n "
190 -
            "--hash sha384:142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5"
191 -
            "e2ff2c528ecae04912224782ab\n",
192 -
            {},
193 -
            [
194 -
                "attrs --hash sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152 --hash sha384:"
195 -
                "142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5e2ff2c528ecae04912224782ab"
196 -
            ],
197 -
            id="hash with escaped newline",
198 -
        ),
199 -
        pytest.param(
200 -
            "attrs --hash=sha512:7a91e5a3d1a1238525e477385ef5ee6cecdc8f8fcc2a79d1b35a9f57ad15c814"
201 -
            "dada670026f41fdd62e5e10b3fd75d6112704a9521c3df105f0b6f3bb11b128a",
202 -
            {},
203 -
            [
204 -
                "attrs --hash sha512:7a91e5a3d1a1238525e477385ef5ee6cecdc8f8fcc2a79d1b35a9f57ad15c814"
205 -
                "dada670026f41fdd62e5e10b3fd75d6112704a9521c3df105f0b6f3bb11b128a"
206 -
            ],
207 -
            id="sha512 hash is supported",
208 -
        ),
209 -
    ],
210 -
)
211 -
def test_req_file(tmp_path: Path, req: str, opts: Dict[str, Any], requirements: List[str]) -> None:
13 +
_REQ_FILE_TEST_CASES = [
14 +
    pytest.param("--pre", {"pre": True}, [], ["--pre"], id="pre"),
15 +
    pytest.param("--no-index", {"index_url": []}, [], ["--no-index"], id="no-index"),
16 +
    pytest.param("--no-index\n-i a\n--no-index", {"index_url": []}, [], ["--no-index"], id="no-index overwrites index"),
17 +
    pytest.param("--prefer-binary", {"prefer_binary": True}, [], ["--prefer-binary"], id="prefer-binary"),
18 +
    pytest.param("--require-hashes", {"require_hashes": True}, [], ["--require-hashes"], id="requires-hashes"),
19 +
    pytest.param("--pre ", {"pre": True}, [], ["--pre"], id="space after"),
20 +
    pytest.param(" --pre", {"pre": True}, [], ["--pre"], id="space before"),
21 +
    pytest.param("--pre\\\n", {"pre": True}, [], ["--pre"], id="newline after"),
22 +
    pytest.param("--pre # magic", {"pre": True}, [], ["--pre"], id="comment after space"),
23 +
    pytest.param("--pre\t# magic", {"pre": True}, [], ["--pre"], id="comment after tab"),
24 +
    pytest.param(
25 +
        "--find-links /my/local/archives",
26 +
        {"find_links": ["/my/local/archives"]},
27 +
        [],
28 +
        ["-f", "/my/local/archives"],
29 +
        id="find-links path",
30 +
    ),
31 +
    pytest.param(
32 +
        "--find-links /my/local/archives --find-links /my/local/archives",
33 +
        {"find_links": ["/my/local/archives"]},
34 +
        [],
35 +
        ["-f", "/my/local/archives"],
36 +
        id="find-links duplicate same line",
37 +
    ),
38 +
    pytest.param(
39 +
        "--find-links /my/local/archives\n--find-links /my/local/archives",
40 +
        {"find_links": ["/my/local/archives"]},
41 +
        [],
42 +
        ["-f", "/my/local/archives"],
43 +
        id="find-links duplicate different line",
44 +
    ),
45 +
    pytest.param(
46 +
        "--find-links \\\n/my/local/archives",
47 +
        {"find_links": ["/my/local/archives"]},
48 +
        [],
49 +
        ["-f", "/my/local/archives"],
50 +
        id="find-links newline path",
51 +
    ),
52 +
    pytest.param(
53 +
        "--find-links http://some.archives.com/archives",
54 +
        {"find_links": ["http://some.archives.com/archives"]},
55 +
        [],
56 +
        ["-f", "http://some.archives.com/archives"],
57 +
        id="find-links url",
58 +
    ),
59 +
    pytest.param("-i a", {"index_url": ["a"]}, [], ["-i", "a"], id="index url short"),
60 +
    pytest.param("--index-url a", {"index_url": ["a"]}, [], ["-i", "a"], id="index url long"),
61 +
    pytest.param("-i a -i b\n-i c", {"index_url": ["c"]}, [], ["-i", "c"], id="index url multiple"),
62 +
    pytest.param(
63 +
        "--extra-index-url a",
64 +
        {"index_url": ["https://pypi.org/simple", "a"]},
65 +
        [],
66 +
        ["--extra-index-url", "a"],
67 +
        id="extra-index-url",
68 +
    ),
69 +
    pytest.param(
70 +
        "--extra-index-url a --extra-index-url a",
71 +
        {"index_url": ["https://pypi.org/simple", "a"]},
72 +
        [],
73 +
        ["--extra-index-url", "a"],
74 +
        id="extra-index-url dup same line",
75 +
    ),
76 +
    pytest.param(
77 +
        "--extra-index-url a\n--extra-index-url a",
78 +
        {"index_url": ["https://pypi.org/simple", "a"]},
79 +
        [],
80 +
        ["--extra-index-url", "a"],
81 +
        id="extra-index-url dup different line",
82 +
    ),
83 +
    pytest.param("-e a", {}, ["-e a"], ["-e", "a"], id="e"),
84 +
    pytest.param("--editable a", {}, ["-e a"], ["-e", "a"], id="editable"),
85 +
    pytest.param("--editable .[2,1]", {}, ["-e .[1,2]"], ["-e", ".[1,2]"], id="editable extra"),
86 +
    pytest.param(".[\t, a1. , B2-\t, C3_, ]", {}, [".[B2-,C3_,a1.]"], [".[B2-,C3_,a1.]"], id="path with extra"),
87 +
    pytest.param(".[a.1]", {}, [".[a.1]"], [".[a.1]"], id="path with invalid extra is path"),
88 +
    pytest.param("-f a", {"find_links": ["a"]}, [], ["-f", "a"], id="f"),
89 +
    pytest.param("--find-links a", {"find_links": ["a"]}, [], ["-f", "a"], id="find-links"),
90 +
    pytest.param("--trusted-host a", {"trusted_hosts": ["a"]}, [], ["--trusted-host", "a"], id="trusted-host"),
91 +
    pytest.param(
92 +
        "--trusted-host a --trusted-host a",
93 +
        {"trusted_hosts": ["a"]},
94 +
        [],
95 +
        ["--trusted-host", "a"],
96 +
        id="trusted-host dup same line",
97 +
    ),
98 +
    pytest.param(
99 +
        "--trusted-host a\n--trusted-host a",
100 +
        {"trusted_hosts": ["a"]},
101 +
        [],
102 +
        ["--trusted-host", "a"],
103 +
        id="trusted-host dup different line",
104 +
    ),
105 +
    pytest.param(
106 +
        "--use-feature 2020-resolver",
107 +
        {"features_enabled": ["2020-resolver"]},
108 +
        [],
109 +
        ["--use-feature", "2020-resolver"],
110 +
        id="use-feature space",
111 +
    ),
112 +
    pytest.param(
113 +
        "--use-feature=fast-deps",
114 +
        {"features_enabled": ["fast-deps"]},
115 +
        [],
116 +
        ["--use-feature", "fast-deps"],
117 +
        id="use-feature equal",
118 +
    ),
119 +
    pytest.param(
120 +
        "--use-feature=fast-deps --use-feature 2020-resolver",
121 +
        {"features_enabled": ["2020-resolver", "fast-deps"]},
122 +
        [],
123 +
        ["--use-feature", "2020-resolver", "--use-feature", "fast-deps"],
124 +
        id="use-feature multiple same line",
125 +
    ),
126 +
    pytest.param(
127 +
        "--use-feature=fast-deps\n--use-feature 2020-resolver",
128 +
        {"features_enabled": ["2020-resolver", "fast-deps"]},
129 +
        [],
130 +
        ["--use-feature", "2020-resolver", "--use-feature", "fast-deps"],
131 +
        id="use-feature multiple different line",
132 +
    ),
133 +
    pytest.param(
134 +
        "--use-feature=fast-deps\n--use-feature 2020-resolver\n" * 2,
135 +
        {"features_enabled": ["2020-resolver", "fast-deps"]},
136 +
        [],
137 +
        ["--use-feature", "2020-resolver", "--use-feature", "fast-deps"],
138 +
        id="use-feature multiple duplicate different line",
139 +
    ),
140 +
    pytest.param("--no-binary :all:", {"no_binary": ":all:"}, [], ["--no-binary", ":all:"], id="no-binary all"),
141 +
    pytest.param("--no-binary :none:", {"no_binary": ":none:"}, [], ["--no-binary", ":none:"], id="no-binary none"),
142 +
    pytest.param("--only-binary :all:", {"only_binary": ":all:"}, [], ["--only-binary", ":all:"], id="only-binary all"),
143 +
    pytest.param(
144 +
        "--only-binary :none:", {"only_binary": ":none:"}, [], ["--only-binary", ":none:"], id="only-binary none"
145 +
    ),
146 +
    pytest.param("####### example-requirements.txt #######", {}, [], [], id="comment"),
147 +
    pytest.param("\t##### Requirements without Version Specifiers ######", {}, [], [], id="tab and comment"),
148 +
    pytest.param("  # start", {}, [], [], id="space and comment"),
149 +
    pytest.param("nose", {}, ["nose"], ["nose"], id="req"),
150 +
    pytest.param("nose\nnose", {}, ["nose"], ["nose"], id="req dup"),
151 +
    pytest.param(
152 +
        "numpy[2,1]  @ file://./downloads/numpy-1.9.2-cp34-none-win32.whl",
153 +
        {},
154 +
        ["numpy[1,2]@ file://./downloads/numpy-1.9.2-cp34-none-win32.whl"],
155 +
        ["numpy[1,2]@ file://./downloads/numpy-1.9.2-cp34-none-win32.whl"],
156 +
        id="path with name-extra-protocol",
157 +
    ),
158 +
    pytest.param(
159 +
        "docopt == 0.6.1             # Version Matching. Must be version 0.6.1",
160 +
        {},
161 +
        ["docopt==0.6.1"],
162 +
        ["docopt==0.6.1"],
163 +
        id="req equal comment",
164 +
    ),
165 +
    pytest.param(
166 +
        "keyring >= 4.1.1            # Minimum version 4.1.1",
167 +
        {},
168 +
        ["keyring>=4.1.1"],
169 +
        ["keyring>=4.1.1"],
170 +
        id="req ge comment",
171 +
    ),
172 +
    pytest.param(
173 +
        "coverage != 3.5             # Version Exclusion. Anything except version 3.5",
174 +
        {},
175 +
        ["coverage!=3.5"],
176 +
        ["coverage!=3.5"],
177 +
        id="req ne comment",
178 +
    ),
179 +
    pytest.param(
180 +
        "Mopidy-Dirble ~= 1.1        # Compatible release. Same as >= 1.1, == 1.*",
181 +
        {},
182 +
        ["Mopidy-Dirble~=1.1"],
183 +
        ["Mopidy-Dirble~=1.1"],
184 +
        id="req approx comment",
185 +
    ),
186 +
    pytest.param("b==1.3", {}, ["b==1.3"], ["b==1.3"], id="req eq"),
187 +
    pytest.param("c >=1.2,<2.0", {}, ["c<2.0,>=1.2"], ["c<2.0,>=1.2"], id="req ge lt"),
188 +
    pytest.param("d[bar,foo]", {}, ["d[bar,foo]"], ["d[bar,foo]"], id="req extras"),
189 +
    pytest.param("d[foo, bar]", {}, ["d[bar,foo]"], ["d[bar,foo]"], id="req extras space"),
190 +
    pytest.param("d[foo,\tbar]", {}, ["d[bar,foo]"], ["d[bar,foo]"], id="req extras tab"),
191 +
    pytest.param("e~=1.4.2", {}, ["e~=1.4.2"], ["e~=1.4.2"], id="req approx"),
192 +
    pytest.param(
193 +
        "f ==5.4 ; python_version < '2.7'",
194 +
        {},
195 +
        ['f==5.4; python_version < "2.7"'],
196 +
        ['f==5.4; python_version < "2.7"'],
197 +
        id="python version filter",
198 +
    ),
199 +
    pytest.param(
200 +
        "g; sys_platform == 'win32'",
201 +
        {},
202 +
        ['g; sys_platform == "win32"'],
203 +
        ['g; sys_platform == "win32"'],
204 +
        id="platform filter",
205 +
    ),
206 +
    pytest.param(
207 +
        "http://w.org/w_P-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl",
208 +
        {},
209 +
        ["http://w.org/w_P-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl"],
210 +
        ["http://w.org/w_P-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl"],
211 +
        id="http URI",
212 +
    ),
213 +
    pytest.param(
214 +
        "git+https://git.example.com/MyProject#egg=MyProject",
215 +
        {},
216 +
        ["git+https://git.example.com/MyProject#egg=MyProject"],
217 +
        ["git+https://git.example.com/MyProject#egg=MyProject"],
218 +
        id="vcs with https",
219 +
    ),
220 +
    pytest.param(
221 +
        "git+ssh://git.example.com/MyProject#egg=MyProject",
222 +
        {},
223 +
        ["git+ssh://git.example.com/MyProject#egg=MyProject"],
224 +
        ["git+ssh://git.example.com/MyProject#egg=MyProject"],
225 +
        id="vcs with ssh",
226 +
    ),
227 +
    pytest.param(
228 +
        "git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject",
229 +
        {},
230 +
        ["git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject"],
231 +
        ["git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject"],
232 +
        id="vcs with commit hash pin",
233 +
    ),
234 +
    pytest.param(
235 +
        "attrs --hash sha384:142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5e2ff2c528ecae04"
236 +
        "912224782ab\t--hash=sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152 # ok",
237 +
        {},
238 +
        [
239 +
            "attrs --hash sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152 --hash sha384:"
240 +
            "142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5e2ff2c528ecae04912224782ab"
241 +
        ],
242 +
        ["attrs"],
243 +
        id="hash",
244 +
    ),
245 +
    pytest.param(
246 +
        "attrs --hash=sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152\\\n "
247 +
        "--hash sha384:142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5"
248 +
        "e2ff2c528ecae04912224782ab\n",
249 +
        {},
250 +
        [
251 +
            "attrs --hash sha256:af957b369adcd07e5b3c64d2cdb76d6808c5e0b16c35ca41c79c8eee34808152 --hash sha384:"
252 +
            "142d9b02f3f4511ccabf6c14bd34d2b0a9ed043a898228b48343cfdf4eb10856ef7ad5e2ff2c528ecae04912224782ab"
253 +
        ],
254 +
        ["attrs"],
255 +
        id="hash with escaped newline",
256 +
    ),
257 +
    pytest.param(
258 +
        "attrs --hash=sha512:7a91e5a3d1a1238525e477385ef5ee6cecdc8f8fcc2a79d1b35a9f57ad15c814"
259 +
        "dada670026f41fdd62e5e10b3fd75d6112704a9521c3df105f0b6f3bb11b128a",
260 +
        {},
261 +
        [
262 +
            "attrs --hash sha512:7a91e5a3d1a1238525e477385ef5ee6cecdc8f8fcc2a79d1b35a9f57ad15c814"
263 +
            "dada670026f41fdd62e5e10b3fd75d6112704a9521c3df105f0b6f3bb11b128a"
264 +
        ],
265 +
        ["attrs"],
266 +
        id="sha512 hash is supported",
267 +
    ),
268 +
]
269 +
270 +
271 +
@pytest.mark.parametrize(("req", "opts", "requirements", "as_args"), _REQ_FILE_TEST_CASES)
272 +
def test_req_file(tmp_path: Path, req: str, opts: Dict[str, Any], requirements: List[str], as_args: List[str]) -> None:
212 273
    requirements_txt = tmp_path / "req.txt"
213 274
    requirements_txt.write_text(req)
214 275
    req_file = RequirementsFile(requirements_txt, constraint=False)
276 +
    assert req_file.as_root_args == as_args
215 277
    assert str(req_file) == f"-r {requirements_txt}"
216 278
    assert vars(req_file.options) == opts
217 279
    found = [str(i) for i in req_file.requirements]
@@ -243,8 +305,10 @@
Loading
243 305
    other_req = tmp_path / "other-requirements.txt"
244 306
    other_req.write_text("magic\nmagical")
245 307
    requirements_file = tmp_path / "req.txt"
246 -
    requirements_file.write_text(f"{flag} other-requirements.txt")
308 +
    requirements_file.write_text(f"{flag} other-requirements.txt\n{flag} other-requirements.txt")
247 309
    req_file = RequirementsFile(requirements_file, constraint=False)
310 +
    assert req_file.as_root_args == ["-r", "other-requirements.txt"]
311 +
    assert req_file.as_root_args is req_file.as_root_args  # check it's cached
248 312
    assert vars(req_file.options) == {}
249 313
    found = [str(i) for i in req_file.requirements]
250 314
    assert found == ["magic", "magical"]
@@ -284,8 +348,9 @@
Loading
284 348
    other_req = tmp_path / "other.txt"
285 349
    other_req.write_text("magic\nmagical\n-i a")
286 350
    requirements_file = tmp_path / "req.txt"
287 -
    requirements_file.write_text(f"{flag} other.txt")
351 +
    requirements_file.write_text(f"{flag} other.txt\n{flag} other.txt")
288 352
    req_file = RequirementsFile(requirements_file, constraint=True)
353 +
    assert req_file.as_root_args == ["-c", "other.txt"]
289 354
    assert vars(req_file.options) == {"index_url": ["a"]}
290 355
    found = [str(i) for i in req_file.requirements]
291 356
    assert found == ["-c magic", "-c magical"]

@@ -57,7 +57,7 @@
Loading
57 57
    ("content", "args"),
58 58
    [
59 59
        pytest.param("-e .", ["-e", "."], id="short editable"),
60 -
        pytest.param("--editable .", ["--editable", "."], id="long editable"),
60 +
        pytest.param("--editable .", ["-e", "."], id="long editable"),
61 61
        pytest.param(
62 62
            "git+ssh://git.example.com/MyProject\\#egg=MyProject",
63 63
            ["git+ssh://git.example.com/MyProject#egg=MyProject"],
@@ -87,7 +87,7 @@
Loading
87 87
    result_second = proj.run("r")
88 88
    result_second.assert_success()
89 89
    assert execute_calls.call_count == 1
90 -
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install"] + args + ["a"]
90 +
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "a"] + args
91 91
92 92
93 93
def test_pip_req_path(tox_project: ToxProjectCreator) -> None:
@@ -185,7 +185,7 @@
Loading
185 185
    result_second = proj.run("r")
186 186
    result_second.assert_success()
187 187
    assert execute_calls.call_count == 1
188 -
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "-r", "r.txt", "b"]
188 +
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "b", "-r", "r.txt"]
189 189
190 190
    # if the requirement file changes recreate
191 191
    (proj.path / "r.txt").write_text("c\nd")
@@ -194,7 +194,7 @@
Loading
194 194
    result_third.assert_success()
195 195
    assert "py: recreate env because requirements removed: a" in result_third.out, result_third.out
196 196
    assert execute_calls.call_count == 1
197 -
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "-r", "r.txt", "b"]
197 +
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "b", "-r", "r.txt"]
198 198
199 199
200 200
def test_pip_install_constraint_file_create_change(tox_project: ToxProjectCreator) -> None:
@@ -203,7 +203,7 @@
Loading
203 203
    result = proj.run("r")
204 204
    result.assert_success()
205 205
    assert execute_calls.call_count == 1
206 -
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "-c", "c.txt", "a"]
206 +
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "a", "-c", "c.txt"]
207 207
208 208
    # a new dependency triggers an install
209 209
    (proj.path / "tox.ini").write_text("[testenv]\ndeps=-c c.txt\n a\n d\nskip_install=true")
@@ -211,7 +211,7 @@
Loading
211 211
    result_second = proj.run("r")
212 212
    result_second.assert_success()
213 213
    assert execute_calls.call_count == 1
214 -
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "-c", "c.txt", "a", "d"]
214 +
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "a", "d", "-c", "c.txt"]
215 215
216 216
    # a new constraints triggers a recreate
217 217
    (proj.path / "c.txt").write_text("")
@@ -220,7 +220,7 @@
Loading
220 220
    result_third.assert_success()
221 221
    assert "py: recreate env because changed constraint(s) removed b" in result_third.out, result_third.out
222 222
    assert execute_calls.call_count == 1
223 -
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "-c", "c.txt", "a", "d"]
223 +
    assert execute_calls.call_args[0][3].cmd == ["python", "-I", "-m", "pip", "install", "a", "d", "-c", "c.txt"]
224 224
225 225
226 226
def test_pip_install_constraint_file_new(tox_project: ToxProjectCreator) -> None:

@@ -1,6 +1,4 @@
Loading
1 1
import re
2 -
import shlex
3 -
import sys
4 2
from argparse import ArgumentParser
5 3
from pathlib import Path
6 4
from typing import List, Optional, Tuple
@@ -16,10 +14,13 @@
Loading
16 14
        self._unroll: Optional[Tuple[List[str], List[str]]] = None
17 15
18 16
    def _get_file_content(self, url: str) -> str:
19 -
        if url == str(self._path):
17 +
        if self._is_url_self(url):
20 18
            return self._raw
21 19
        return super()._get_file_content(url)
22 20
21 +
    def _is_url_self(self, url: str) -> bool:
22 +
        return url == str(self._path)
23 +
23 24
    def _pre_process(self, content: str) -> ReqFileLines:
24 25
        for at, line in super()._pre_process(content):
25 26
            if line.startswith("-r") or line.startswith("-c") and line[2].isalpha():
@@ -32,12 +33,6 @@
Loading
32 33
            self._parser_private = build_parser(cli_only=True)  # e.g. no --hash for cli only
33 34
        return self._parser_private
34 35
35 -
    def as_args(self) -> List[str]:
36 -
        result = []
37 -
        for line in self.lines():
38 -
            result.extend(shlex.split(line, posix=sys.platform != "win32"))
39 -
        return result
40 -
41 36
    def lines(self) -> List[str]:
42 37
        return self._raw.splitlines()
43 38
Files Coverage
src/tox 100.00%
tests 100.00%
Project Totals (150 files) 100.00%
3.6 - MacOs
Build #744046438 -
tests
3.10.0-alpha.7 - MacOs
Build #744046438 -
tests

No yaml found.

Create your codecov.yml to customize your Codecov experience

Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading