Mercurial > repos > guerler > hhblits
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 ) |