comparison lib/python3.8/site-packages/pip/_internal/req/constructors.py @ 0:9e54283cc701 draft

"planemo upload commit d12c32a45bcd441307e632fca6d9af7d60289d44"
author guerler
date Mon, 27 Jul 2020 03:47:31 -0400 (2020-07-27)
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:9e54283cc701
1 """Backing implementation for InstallRequirement's various constructors
2
3 The idea here is that these formed a major chunk of InstallRequirement's size
4 so, moving them and support code dedicated to them outside of that class
5 helps creates for better understandability for the rest of the code.
6
7 These are meant to be used elsewhere within pip to create instances of
8 InstallRequirement.
9 """
10
11 # The following comment should be removed at some point in the future.
12 # mypy: strict-optional=False
13
14 import logging
15 import os
16 import re
17
18 from pip._vendor.packaging.markers import Marker
19 from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
20 from pip._vendor.packaging.specifiers import Specifier
21 from pip._vendor.pkg_resources import RequirementParseError, parse_requirements
22
23 from pip._internal.exceptions import InstallationError
24 from pip._internal.models.index import PyPI, TestPyPI
25 from pip._internal.models.link import Link
26 from pip._internal.models.wheel import Wheel
27 from pip._internal.pyproject import make_pyproject_path
28 from pip._internal.req.req_install import InstallRequirement
29 from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS
30 from pip._internal.utils.misc import is_installable_dir, splitext
31 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
32 from pip._internal.utils.urls import path_to_url
33 from pip._internal.vcs import is_url, vcs
34
35 if MYPY_CHECK_RUNNING:
36 from typing import (
37 Any, Dict, Optional, Set, Tuple, Union,
38 )
39 from pip._internal.cache import WheelCache
40
41
42 __all__ = [
43 "install_req_from_editable", "install_req_from_line",
44 "parse_editable"
45 ]
46
47 logger = logging.getLogger(__name__)
48 operators = Specifier._operators.keys()
49
50
51 def is_archive_file(name):
52 # type: (str) -> bool
53 """Return True if `name` is a considered as an archive file."""
54 ext = splitext(name)[1].lower()
55 if ext in ARCHIVE_EXTENSIONS:
56 return True
57 return False
58
59
60 def _strip_extras(path):
61 # type: (str) -> Tuple[str, Optional[str]]
62 m = re.match(r'^(.+)(\[[^\]]+\])$', path)
63 extras = None
64 if m:
65 path_no_extras = m.group(1)
66 extras = m.group(2)
67 else:
68 path_no_extras = path
69
70 return path_no_extras, extras
71
72
73 def convert_extras(extras):
74 # type: (Optional[str]) -> Set[str]
75 if not extras:
76 return set()
77 return Requirement("placeholder" + extras.lower()).extras
78
79
80 def parse_editable(editable_req):
81 # type: (str) -> Tuple[Optional[str], str, Optional[Set[str]]]
82 """Parses an editable requirement into:
83 - a requirement name
84 - an URL
85 - extras
86 - editable options
87 Accepted requirements:
88 svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
89 .[some_extra]
90 """
91
92 url = editable_req
93
94 # If a file path is specified with extras, strip off the extras.
95 url_no_extras, extras = _strip_extras(url)
96
97 if os.path.isdir(url_no_extras):
98 if not os.path.exists(os.path.join(url_no_extras, 'setup.py')):
99 msg = (
100 'File "setup.py" not found. Directory cannot be installed '
101 'in editable mode: {}'.format(os.path.abspath(url_no_extras))
102 )
103 pyproject_path = make_pyproject_path(url_no_extras)
104 if os.path.isfile(pyproject_path):
105 msg += (
106 '\n(A "pyproject.toml" file was found, but editable '
107 'mode currently requires a setup.py based build.)'
108 )
109 raise InstallationError(msg)
110
111 # Treating it as code that has already been checked out
112 url_no_extras = path_to_url(url_no_extras)
113
114 if url_no_extras.lower().startswith('file:'):
115 package_name = Link(url_no_extras).egg_fragment
116 if extras:
117 return (
118 package_name,
119 url_no_extras,
120 Requirement("placeholder" + extras.lower()).extras,
121 )
122 else:
123 return package_name, url_no_extras, None
124
125 for version_control in vcs:
126 if url.lower().startswith('%s:' % version_control):
127 url = '%s+%s' % (version_control, url)
128 break
129
130 if '+' not in url:
131 raise InstallationError(
132 '{} is not a valid editable requirement. '
133 'It should either be a path to a local project or a VCS URL '
134 '(beginning with svn+, git+, hg+, or bzr+).'.format(editable_req)
135 )
136
137 vc_type = url.split('+', 1)[0].lower()
138
139 if not vcs.get_backend(vc_type):
140 error_message = 'For --editable=%s only ' % editable_req + \
141 ', '.join([backend.name + '+URL' for backend in vcs.backends]) + \
142 ' is currently supported'
143 raise InstallationError(error_message)
144
145 package_name = Link(url).egg_fragment
146 if not package_name:
147 raise InstallationError(
148 "Could not detect requirement name for '%s', please specify one "
149 "with #egg=your_package_name" % editable_req
150 )
151 return package_name, url, None
152
153
154 def deduce_helpful_msg(req):
155 # type: (str) -> str
156 """Returns helpful msg in case requirements file does not exist,
157 or cannot be parsed.
158
159 :params req: Requirements file path
160 """
161 msg = ""
162 if os.path.exists(req):
163 msg = " It does exist."
164 # Try to parse and check if it is a requirements file.
165 try:
166 with open(req, 'r') as fp:
167 # parse first line only
168 next(parse_requirements(fp.read()))
169 msg += " The argument you provided " + \
170 "(%s) appears to be a" % (req) + \
171 " requirements file. If that is the" + \
172 " case, use the '-r' flag to install" + \
173 " the packages specified within it."
174 except RequirementParseError:
175 logger.debug("Cannot parse '%s' as requirements \
176 file" % (req), exc_info=True)
177 else:
178 msg += " File '%s' does not exist." % (req)
179 return msg
180
181
182 class RequirementParts(object):
183 def __init__(
184 self,
185 requirement, # type: Optional[Requirement]
186 link, # type: Optional[Link]
187 markers, # type: Optional[Marker]
188 extras, # type: Set[str]
189 ):
190 self.requirement = requirement
191 self.link = link
192 self.markers = markers
193 self.extras = extras
194
195
196 def parse_req_from_editable(editable_req):
197 # type: (str) -> RequirementParts
198 name, url, extras_override = parse_editable(editable_req)
199
200 if name is not None:
201 try:
202 req = Requirement(name)
203 except InvalidRequirement:
204 raise InstallationError("Invalid requirement: '%s'" % name)
205 else:
206 req = None
207
208 link = Link(url)
209
210 return RequirementParts(req, link, None, extras_override)
211
212
213 # ---- The actual constructors follow ----
214
215
216 def install_req_from_editable(
217 editable_req, # type: str
218 comes_from=None, # type: Optional[str]
219 use_pep517=None, # type: Optional[bool]
220 isolated=False, # type: bool
221 options=None, # type: Optional[Dict[str, Any]]
222 wheel_cache=None, # type: Optional[WheelCache]
223 constraint=False # type: bool
224 ):
225 # type: (...) -> InstallRequirement
226
227 parts = parse_req_from_editable(editable_req)
228
229 source_dir = parts.link.file_path if parts.link.scheme == 'file' else None
230
231 return InstallRequirement(
232 parts.requirement, comes_from, source_dir=source_dir,
233 editable=True,
234 link=parts.link,
235 constraint=constraint,
236 use_pep517=use_pep517,
237 isolated=isolated,
238 options=options if options else {},
239 wheel_cache=wheel_cache,
240 extras=parts.extras,
241 )
242
243
244 def _looks_like_path(name):
245 # type: (str) -> bool
246 """Checks whether the string "looks like" a path on the filesystem.
247
248 This does not check whether the target actually exists, only judge from the
249 appearance.
250
251 Returns true if any of the following conditions is true:
252 * a path separator is found (either os.path.sep or os.path.altsep);
253 * a dot is found (which represents the current directory).
254 """
255 if os.path.sep in name:
256 return True
257 if os.path.altsep is not None and os.path.altsep in name:
258 return True
259 if name.startswith("."):
260 return True
261 return False
262
263
264 def _get_url_from_path(path, name):
265 # type: (str, str) -> str
266 """
267 First, it checks whether a provided path is an installable directory
268 (e.g. it has a setup.py). If it is, returns the path.
269
270 If false, check if the path is an archive file (such as a .whl).
271 The function checks if the path is a file. If false, if the path has
272 an @, it will treat it as a PEP 440 URL requirement and return the path.
273 """
274 if _looks_like_path(name) and os.path.isdir(path):
275 if is_installable_dir(path):
276 return path_to_url(path)
277 raise InstallationError(
278 "Directory %r is not installable. Neither 'setup.py' "
279 "nor 'pyproject.toml' found." % name
280 )
281 if not is_archive_file(path):
282 return None
283 if os.path.isfile(path):
284 return path_to_url(path)
285 urlreq_parts = name.split('@', 1)
286 if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
287 # If the path contains '@' and the part before it does not look
288 # like a path, try to treat it as a PEP 440 URL req instead.
289 return None
290 logger.warning(
291 'Requirement %r looks like a filename, but the '
292 'file does not exist',
293 name
294 )
295 return path_to_url(path)
296
297
298 def parse_req_from_line(name, line_source):
299 # type: (str, Optional[str]) -> RequirementParts
300 if is_url(name):
301 marker_sep = '; '
302 else:
303 marker_sep = ';'
304 if marker_sep in name:
305 name, markers_as_string = name.split(marker_sep, 1)
306 markers_as_string = markers_as_string.strip()
307 if not markers_as_string:
308 markers = None
309 else:
310 markers = Marker(markers_as_string)
311 else:
312 markers = None
313 name = name.strip()
314 req_as_string = None
315 path = os.path.normpath(os.path.abspath(name))
316 link = None
317 extras_as_string = None
318
319 if is_url(name):
320 link = Link(name)
321 else:
322 p, extras_as_string = _strip_extras(path)
323 url = _get_url_from_path(p, name)
324 if url is not None:
325 link = Link(url)
326
327 # it's a local file, dir, or url
328 if link:
329 # Handle relative file URLs
330 if link.scheme == 'file' and re.search(r'\.\./', link.url):
331 link = Link(
332 path_to_url(os.path.normpath(os.path.abspath(link.path))))
333 # wheel file
334 if link.is_wheel:
335 wheel = Wheel(link.filename) # can raise InvalidWheelFilename
336 req_as_string = "%s==%s" % (wheel.name, wheel.version)
337 else:
338 # set the req to the egg fragment. when it's not there, this
339 # will become an 'unnamed' requirement
340 req_as_string = link.egg_fragment
341
342 # a requirement specifier
343 else:
344 req_as_string = name
345
346 extras = convert_extras(extras_as_string)
347
348 def with_source(text):
349 # type: (str) -> str
350 if not line_source:
351 return text
352 return '{} (from {})'.format(text, line_source)
353
354 if req_as_string is not None:
355 try:
356 req = Requirement(req_as_string)
357 except InvalidRequirement:
358 if os.path.sep in req_as_string:
359 add_msg = "It looks like a path."
360 add_msg += deduce_helpful_msg(req_as_string)
361 elif ('=' in req_as_string and
362 not any(op in req_as_string for op in operators)):
363 add_msg = "= is not a valid operator. Did you mean == ?"
364 else:
365 add_msg = ''
366 msg = with_source(
367 'Invalid requirement: {!r}'.format(req_as_string)
368 )
369 if add_msg:
370 msg += '\nHint: {}'.format(add_msg)
371 raise InstallationError(msg)
372 else:
373 req = None
374
375 return RequirementParts(req, link, markers, extras)
376
377
378 def install_req_from_line(
379 name, # type: str
380 comes_from=None, # type: Optional[Union[str, InstallRequirement]]
381 use_pep517=None, # type: Optional[bool]
382 isolated=False, # type: bool
383 options=None, # type: Optional[Dict[str, Any]]
384 wheel_cache=None, # type: Optional[WheelCache]
385 constraint=False, # type: bool
386 line_source=None, # type: Optional[str]
387 ):
388 # type: (...) -> InstallRequirement
389 """Creates an InstallRequirement from a name, which might be a
390 requirement, directory containing 'setup.py', filename, or URL.
391
392 :param line_source: An optional string describing where the line is from,
393 for logging purposes in case of an error.
394 """
395 parts = parse_req_from_line(name, line_source)
396
397 return InstallRequirement(
398 parts.requirement, comes_from, link=parts.link, markers=parts.markers,
399 use_pep517=use_pep517, isolated=isolated,
400 options=options if options else {},
401 wheel_cache=wheel_cache,
402 constraint=constraint,
403 extras=parts.extras,
404 )
405
406
407 def install_req_from_req_string(
408 req_string, # type: str
409 comes_from=None, # type: Optional[InstallRequirement]
410 isolated=False, # type: bool
411 wheel_cache=None, # type: Optional[WheelCache]
412 use_pep517=None # type: Optional[bool]
413 ):
414 # type: (...) -> InstallRequirement
415 try:
416 req = Requirement(req_string)
417 except InvalidRequirement:
418 raise InstallationError("Invalid requirement: '%s'" % req_string)
419
420 domains_not_allowed = [
421 PyPI.file_storage_domain,
422 TestPyPI.file_storage_domain,
423 ]
424 if (req.url and comes_from and comes_from.link and
425 comes_from.link.netloc in domains_not_allowed):
426 # Explicitly disallow pypi packages that depend on external urls
427 raise InstallationError(
428 "Packages installed from PyPI cannot depend on packages "
429 "which are not also hosted on PyPI.\n"
430 "%s depends on %s " % (comes_from.name, req)
431 )
432
433 return InstallRequirement(
434 req, comes_from, isolated=isolated, wheel_cache=wheel_cache,
435 use_pep517=use_pep517
436 )