comparison planemo/lib/python3.7/site-packages/pip/_internal/vcs/versioncontrol.py @ 1:56ad4e20f292 draft

"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author guerler
date Fri, 31 Jul 2020 00:32:28 -0400
parents
children
comparison
equal deleted inserted replaced
0:d30785e31577 1:56ad4e20f292
1 """Handles all VCS (version control) support"""
2 from __future__ import absolute_import
3
4 import errno
5 import logging
6 import os
7 import shutil
8 import sys
9
10 from pip._vendor import pkg_resources
11 from pip._vendor.six.moves.urllib import parse as urllib_parse
12
13 from pip._internal.exceptions import BadCommand
14 from pip._internal.utils.misc import (
15 ask_path_exists, backup_dir, call_subprocess, display_path, rmtree,
16 )
17 from pip._internal.utils.typing import MYPY_CHECK_RUNNING
18
19 if MYPY_CHECK_RUNNING:
20 from typing import (
21 Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type
22 )
23 from pip._internal.utils.ui import SpinnerInterface
24
25 AuthInfo = Tuple[Optional[str], Optional[str]]
26
27 __all__ = ['vcs']
28
29
30 logger = logging.getLogger(__name__)
31
32
33 def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None):
34 """
35 Return the URL for a VCS requirement.
36
37 Args:
38 repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+").
39 project_name: the (unescaped) project name.
40 """
41 egg_project_name = pkg_resources.to_filename(project_name)
42 req = '{}@{}#egg={}'.format(repo_url, rev, egg_project_name)
43 if subdir:
44 req += '&subdirectory={}'.format(subdir)
45
46 return req
47
48
49 class RemoteNotFoundError(Exception):
50 pass
51
52
53 class RevOptions(object):
54
55 """
56 Encapsulates a VCS-specific revision to install, along with any VCS
57 install options.
58
59 Instances of this class should be treated as if immutable.
60 """
61
62 def __init__(
63 self,
64 vc_class, # type: Type[VersionControl]
65 rev=None, # type: Optional[str]
66 extra_args=None, # type: Optional[List[str]]
67 ):
68 # type: (...) -> None
69 """
70 Args:
71 vc_class: a VersionControl subclass.
72 rev: the name of the revision to install.
73 extra_args: a list of extra options.
74 """
75 if extra_args is None:
76 extra_args = []
77
78 self.extra_args = extra_args
79 self.rev = rev
80 self.vc_class = vc_class
81
82 def __repr__(self):
83 return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev)
84
85 @property
86 def arg_rev(self):
87 # type: () -> Optional[str]
88 if self.rev is None:
89 return self.vc_class.default_arg_rev
90
91 return self.rev
92
93 def to_args(self):
94 # type: () -> List[str]
95 """
96 Return the VCS-specific command arguments.
97 """
98 args = [] # type: List[str]
99 rev = self.arg_rev
100 if rev is not None:
101 args += self.vc_class.get_base_rev_args(rev)
102 args += self.extra_args
103
104 return args
105
106 def to_display(self):
107 # type: () -> str
108 if not self.rev:
109 return ''
110
111 return ' (to revision {})'.format(self.rev)
112
113 def make_new(self, rev):
114 # type: (str) -> RevOptions
115 """
116 Make a copy of the current instance, but with a new rev.
117
118 Args:
119 rev: the name of the revision for the new object.
120 """
121 return self.vc_class.make_rev_options(rev, extra_args=self.extra_args)
122
123
124 class VcsSupport(object):
125 _registry = {} # type: Dict[str, VersionControl]
126 schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
127
128 def __init__(self):
129 # type: () -> None
130 # Register more schemes with urlparse for various version control
131 # systems
132 urllib_parse.uses_netloc.extend(self.schemes)
133 # Python >= 2.7.4, 3.3 doesn't have uses_fragment
134 if getattr(urllib_parse, 'uses_fragment', None):
135 urllib_parse.uses_fragment.extend(self.schemes)
136 super(VcsSupport, self).__init__()
137
138 def __iter__(self):
139 return self._registry.__iter__()
140
141 @property
142 def backends(self):
143 # type: () -> List[VersionControl]
144 return list(self._registry.values())
145
146 @property
147 def dirnames(self):
148 # type: () -> List[str]
149 return [backend.dirname for backend in self.backends]
150
151 @property
152 def all_schemes(self):
153 # type: () -> List[str]
154 schemes = [] # type: List[str]
155 for backend in self.backends:
156 schemes.extend(backend.schemes)
157 return schemes
158
159 def register(self, cls):
160 # type: (Type[VersionControl]) -> None
161 if not hasattr(cls, 'name'):
162 logger.warning('Cannot register VCS %s', cls.__name__)
163 return
164 if cls.name not in self._registry:
165 self._registry[cls.name] = cls()
166 logger.debug('Registered VCS backend: %s', cls.name)
167
168 def unregister(self, name):
169 # type: (str) -> None
170 if name in self._registry:
171 del self._registry[name]
172
173 def get_backend_for_dir(self, location):
174 # type: (str) -> Optional[VersionControl]
175 """
176 Return a VersionControl object if a repository of that type is found
177 at the given directory.
178 """
179 for vcs_backend in self._registry.values():
180 if vcs_backend.controls_location(location):
181 logger.debug('Determine that %s uses VCS: %s',
182 location, vcs_backend.name)
183 return vcs_backend
184 return None
185
186 def get_backend(self, name):
187 # type: (str) -> Optional[VersionControl]
188 """
189 Return a VersionControl object or None.
190 """
191 name = name.lower()
192 return self._registry.get(name)
193
194
195 vcs = VcsSupport()
196
197
198 class VersionControl(object):
199 name = ''
200 dirname = ''
201 repo_name = ''
202 # List of supported schemes for this Version Control
203 schemes = () # type: Tuple[str, ...]
204 # Iterable of environment variable names to pass to call_subprocess().
205 unset_environ = () # type: Tuple[str, ...]
206 default_arg_rev = None # type: Optional[str]
207
208 @classmethod
209 def should_add_vcs_url_prefix(cls, remote_url):
210 """
211 Return whether the vcs prefix (e.g. "git+") should be added to a
212 repository's remote url when used in a requirement.
213 """
214 return not remote_url.lower().startswith('{}:'.format(cls.name))
215
216 @classmethod
217 def get_subdirectory(cls, repo_dir):
218 """
219 Return the path to setup.py, relative to the repo root.
220 """
221 return None
222
223 @classmethod
224 def get_requirement_revision(cls, repo_dir):
225 """
226 Return the revision string that should be used in a requirement.
227 """
228 return cls.get_revision(repo_dir)
229
230 @classmethod
231 def get_src_requirement(cls, repo_dir, project_name):
232 """
233 Return the requirement string to use to redownload the files
234 currently at the given repository directory.
235
236 Args:
237 project_name: the (unescaped) project name.
238
239 The return value has a form similar to the following:
240
241 {repository_url}@{revision}#egg={project_name}
242 """
243 repo_url = cls.get_remote_url(repo_dir)
244 if repo_url is None:
245 return None
246
247 if cls.should_add_vcs_url_prefix(repo_url):
248 repo_url = '{}+{}'.format(cls.name, repo_url)
249
250 revision = cls.get_requirement_revision(repo_dir)
251 subdir = cls.get_subdirectory(repo_dir)
252 req = make_vcs_requirement_url(repo_url, revision, project_name,
253 subdir=subdir)
254
255 return req
256
257 @staticmethod
258 def get_base_rev_args(rev):
259 """
260 Return the base revision arguments for a vcs command.
261
262 Args:
263 rev: the name of a revision to install. Cannot be None.
264 """
265 raise NotImplementedError
266
267 @classmethod
268 def make_rev_options(cls, rev=None, extra_args=None):
269 # type: (Optional[str], Optional[List[str]]) -> RevOptions
270 """
271 Return a RevOptions object.
272
273 Args:
274 rev: the name of a revision to install.
275 extra_args: a list of extra options.
276 """
277 return RevOptions(cls, rev, extra_args=extra_args)
278
279 @classmethod
280 def _is_local_repository(cls, repo):
281 # type: (str) -> bool
282 """
283 posix absolute paths start with os.path.sep,
284 win32 ones start with drive (like c:\\folder)
285 """
286 drive, tail = os.path.splitdrive(repo)
287 return repo.startswith(os.path.sep) or bool(drive)
288
289 def export(self, location, url):
290 """
291 Export the repository at the url to the destination location
292 i.e. only download the files, without vcs informations
293
294 :param url: the repository URL starting with a vcs prefix.
295 """
296 raise NotImplementedError
297
298 @classmethod
299 def get_netloc_and_auth(cls, netloc, scheme):
300 """
301 Parse the repository URL's netloc, and return the new netloc to use
302 along with auth information.
303
304 Args:
305 netloc: the original repository URL netloc.
306 scheme: the repository URL's scheme without the vcs prefix.
307
308 This is mainly for the Subversion class to override, so that auth
309 information can be provided via the --username and --password options
310 instead of through the URL. For other subclasses like Git without
311 such an option, auth information must stay in the URL.
312
313 Returns: (netloc, (username, password)).
314 """
315 return netloc, (None, None)
316
317 @classmethod
318 def get_url_rev_and_auth(cls, url):
319 # type: (str) -> Tuple[str, Optional[str], AuthInfo]
320 """
321 Parse the repository URL to use, and return the URL, revision,
322 and auth info to use.
323
324 Returns: (url, rev, (username, password)).
325 """
326 scheme, netloc, path, query, frag = urllib_parse.urlsplit(url)
327 if '+' not in scheme:
328 raise ValueError(
329 "Sorry, {!r} is a malformed VCS url. "
330 "The format is <vcs>+<protocol>://<url>, "
331 "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url)
332 )
333 # Remove the vcs prefix.
334 scheme = scheme.split('+', 1)[1]
335 netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme)
336 rev = None
337 if '@' in path:
338 path, rev = path.rsplit('@', 1)
339 url = urllib_parse.urlunsplit((scheme, netloc, path, query, ''))
340 return url, rev, user_pass
341
342 @staticmethod
343 def make_rev_args(username, password):
344 """
345 Return the RevOptions "extra arguments" to use in obtain().
346 """
347 return []
348
349 def get_url_rev_options(self, url):
350 # type: (str) -> Tuple[str, RevOptions]
351 """
352 Return the URL and RevOptions object to use in obtain() and in
353 some cases export(), as a tuple (url, rev_options).
354 """
355 url, rev, user_pass = self.get_url_rev_and_auth(url)
356 username, password = user_pass
357 extra_args = self.make_rev_args(username, password)
358 rev_options = self.make_rev_options(rev, extra_args=extra_args)
359
360 return url, rev_options
361
362 @staticmethod
363 def normalize_url(url):
364 # type: (str) -> str
365 """
366 Normalize a URL for comparison by unquoting it and removing any
367 trailing slash.
368 """
369 return urllib_parse.unquote(url).rstrip('/')
370
371 @classmethod
372 def compare_urls(cls, url1, url2):
373 # type: (str, str) -> bool
374 """
375 Compare two repo URLs for identity, ignoring incidental differences.
376 """
377 return (cls.normalize_url(url1) == cls.normalize_url(url2))
378
379 def fetch_new(self, dest, url, rev_options):
380 """
381 Fetch a revision from a repository, in the case that this is the
382 first fetch from the repository.
383
384 Args:
385 dest: the directory to fetch the repository to.
386 rev_options: a RevOptions object.
387 """
388 raise NotImplementedError
389
390 def switch(self, dest, url, rev_options):
391 """
392 Switch the repo at ``dest`` to point to ``URL``.
393
394 Args:
395 rev_options: a RevOptions object.
396 """
397 raise NotImplementedError
398
399 def update(self, dest, url, rev_options):
400 """
401 Update an already-existing repo to the given ``rev_options``.
402
403 Args:
404 rev_options: a RevOptions object.
405 """
406 raise NotImplementedError
407
408 @classmethod
409 def is_commit_id_equal(cls, dest, name):
410 """
411 Return whether the id of the current commit equals the given name.
412
413 Args:
414 dest: the repository directory.
415 name: a string name.
416 """
417 raise NotImplementedError
418
419 def obtain(self, dest, url):
420 # type: (str, str) -> None
421 """
422 Install or update in editable mode the package represented by this
423 VersionControl object.
424
425 :param dest: the repository directory in which to install or update.
426 :param url: the repository URL starting with a vcs prefix.
427 """
428 url, rev_options = self.get_url_rev_options(url)
429
430 if not os.path.exists(dest):
431 self.fetch_new(dest, url, rev_options)
432 return
433
434 rev_display = rev_options.to_display()
435 if self.is_repository_directory(dest):
436 existing_url = self.get_remote_url(dest)
437 if self.compare_urls(existing_url, url):
438 logger.debug(
439 '%s in %s exists, and has correct URL (%s)',
440 self.repo_name.title(),
441 display_path(dest),
442 url,
443 )
444 if not self.is_commit_id_equal(dest, rev_options.rev):
445 logger.info(
446 'Updating %s %s%s',
447 display_path(dest),
448 self.repo_name,
449 rev_display,
450 )
451 self.update(dest, url, rev_options)
452 else:
453 logger.info('Skipping because already up-to-date.')
454 return
455
456 logger.warning(
457 '%s %s in %s exists with URL %s',
458 self.name,
459 self.repo_name,
460 display_path(dest),
461 existing_url,
462 )
463 prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ',
464 ('s', 'i', 'w', 'b'))
465 else:
466 logger.warning(
467 'Directory %s already exists, and is not a %s %s.',
468 dest,
469 self.name,
470 self.repo_name,
471 )
472 # https://github.com/python/mypy/issues/1174
473 prompt = ('(i)gnore, (w)ipe, (b)ackup ', # type: ignore
474 ('i', 'w', 'b'))
475
476 logger.warning(
477 'The plan is to install the %s repository %s',
478 self.name,
479 url,
480 )
481 response = ask_path_exists('What to do? %s' % prompt[0], prompt[1])
482
483 if response == 'a':
484 sys.exit(-1)
485
486 if response == 'w':
487 logger.warning('Deleting %s', display_path(dest))
488 rmtree(dest)
489 self.fetch_new(dest, url, rev_options)
490 return
491
492 if response == 'b':
493 dest_dir = backup_dir(dest)
494 logger.warning(
495 'Backing up %s to %s', display_path(dest), dest_dir,
496 )
497 shutil.move(dest, dest_dir)
498 self.fetch_new(dest, url, rev_options)
499 return
500
501 # Do nothing if the response is "i".
502 if response == 's':
503 logger.info(
504 'Switching %s %s to %s%s',
505 self.repo_name,
506 display_path(dest),
507 url,
508 rev_display,
509 )
510 self.switch(dest, url, rev_options)
511
512 def unpack(self, location, url):
513 # type: (str, str) -> None
514 """
515 Clean up current location and download the url repository
516 (and vcs infos) into location
517
518 :param url: the repository URL starting with a vcs prefix.
519 """
520 if os.path.exists(location):
521 rmtree(location)
522 self.obtain(location, url=url)
523
524 @classmethod
525 def get_remote_url(cls, location):
526 """
527 Return the url used at location
528
529 Raises RemoteNotFoundError if the repository does not have a remote
530 url configured.
531 """
532 raise NotImplementedError
533
534 @classmethod
535 def get_revision(cls, location):
536 """
537 Return the current commit id of the files at the given location.
538 """
539 raise NotImplementedError
540
541 @classmethod
542 def run_command(
543 cls,
544 cmd, # type: List[str]
545 show_stdout=True, # type: bool
546 cwd=None, # type: Optional[str]
547 on_returncode='raise', # type: str
548 extra_ok_returncodes=None, # type: Optional[Iterable[int]]
549 command_desc=None, # type: Optional[str]
550 extra_environ=None, # type: Optional[Mapping[str, Any]]
551 spinner=None # type: Optional[SpinnerInterface]
552 ):
553 # type: (...) -> Text
554 """
555 Run a VCS subcommand
556 This is simply a wrapper around call_subprocess that adds the VCS
557 command name, and checks that the VCS is available
558 """
559 cmd = [cls.name] + cmd
560 try:
561 return call_subprocess(cmd, show_stdout, cwd,
562 on_returncode=on_returncode,
563 extra_ok_returncodes=extra_ok_returncodes,
564 command_desc=command_desc,
565 extra_environ=extra_environ,
566 unset_environ=cls.unset_environ,
567 spinner=spinner)
568 except OSError as e:
569 # errno.ENOENT = no such file or directory
570 # In other words, the VCS executable isn't available
571 if e.errno == errno.ENOENT:
572 raise BadCommand(
573 'Cannot find command %r - do you have '
574 '%r installed and in your '
575 'PATH?' % (cls.name, cls.name))
576 else:
577 raise # re-raise exception if a different error occurred
578
579 @classmethod
580 def is_repository_directory(cls, path):
581 # type: (str) -> bool
582 """
583 Return whether a directory path is a repository directory.
584 """
585 logger.debug('Checking in %s for %s (%s)...',
586 path, cls.dirname, cls.name)
587 return os.path.exists(os.path.join(path, cls.dirname))
588
589 @classmethod
590 def controls_location(cls, location):
591 # type: (str) -> bool
592 """
593 Check if a location is controlled by the vcs.
594 It is meant to be overridden to implement smarter detection
595 mechanisms for specific vcs.
596
597 This can do more than is_repository_directory() alone. For example,
598 the Git override checks that Git is actually available.
599 """
600 return cls.is_repository_directory(location)