Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

__init__.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. """Handles all VCS (version control) support"""
  2. from __future__ import absolute_import
  3. import errno
  4. import logging
  5. import os
  6. import shutil
  7. import sys
  8. from pip._vendor.six.moves.urllib import parse as urllib_parse
  9. from pip._internal.exceptions import BadCommand
  10. from pip._internal.utils.misc import (
  11. display_path, backup_dir, call_subprocess, rmtree, ask_path_exists,
  12. )
  13. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  14. if MYPY_CHECK_RUNNING:
  15. from typing import Dict, Optional, Tuple # noqa: F401
  16. from pip._internal.cli.base_command import Command # noqa: F401
  17. __all__ = ['vcs', 'get_src_requirement']
  18. logger = logging.getLogger(__name__)
  19. class RevOptions(object):
  20. """
  21. Encapsulates a VCS-specific revision to install, along with any VCS
  22. install options.
  23. Instances of this class should be treated as if immutable.
  24. """
  25. def __init__(self, vcs, rev=None, extra_args=None):
  26. """
  27. Args:
  28. vcs: a VersionControl object.
  29. rev: the name of the revision to install.
  30. extra_args: a list of extra options.
  31. """
  32. if extra_args is None:
  33. extra_args = []
  34. self.extra_args = extra_args
  35. self.rev = rev
  36. self.vcs = vcs
  37. def __repr__(self):
  38. return '<RevOptions {}: rev={!r}>'.format(self.vcs.name, self.rev)
  39. @property
  40. def arg_rev(self):
  41. if self.rev is None:
  42. return self.vcs.default_arg_rev
  43. return self.rev
  44. def to_args(self):
  45. """
  46. Return the VCS-specific command arguments.
  47. """
  48. args = []
  49. rev = self.arg_rev
  50. if rev is not None:
  51. args += self.vcs.get_base_rev_args(rev)
  52. args += self.extra_args
  53. return args
  54. def to_display(self):
  55. if not self.rev:
  56. return ''
  57. return ' (to revision {})'.format(self.rev)
  58. def make_new(self, rev):
  59. """
  60. Make a copy of the current instance, but with a new rev.
  61. Args:
  62. rev: the name of the revision for the new object.
  63. """
  64. return self.vcs.make_rev_options(rev, extra_args=self.extra_args)
  65. class VcsSupport(object):
  66. _registry = {} # type: Dict[str, Command]
  67. schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
  68. def __init__(self):
  69. # Register more schemes with urlparse for various version control
  70. # systems
  71. urllib_parse.uses_netloc.extend(self.schemes)
  72. # Python >= 2.7.4, 3.3 doesn't have uses_fragment
  73. if getattr(urllib_parse, 'uses_fragment', None):
  74. urllib_parse.uses_fragment.extend(self.schemes)
  75. super(VcsSupport, self).__init__()
  76. def __iter__(self):
  77. return self._registry.__iter__()
  78. @property
  79. def backends(self):
  80. return list(self._registry.values())
  81. @property
  82. def dirnames(self):
  83. return [backend.dirname for backend in self.backends]
  84. @property
  85. def all_schemes(self):
  86. schemes = []
  87. for backend in self.backends:
  88. schemes.extend(backend.schemes)
  89. return schemes
  90. def register(self, cls):
  91. if not hasattr(cls, 'name'):
  92. logger.warning('Cannot register VCS %s', cls.__name__)
  93. return
  94. if cls.name not in self._registry:
  95. self._registry[cls.name] = cls
  96. logger.debug('Registered VCS backend: %s', cls.name)
  97. def unregister(self, cls=None, name=None):
  98. if name in self._registry:
  99. del self._registry[name]
  100. elif cls in self._registry.values():
  101. del self._registry[cls.name]
  102. else:
  103. logger.warning('Cannot unregister because no class or name given')
  104. def get_backend_name(self, location):
  105. """
  106. Return the name of the version control backend if found at given
  107. location, e.g. vcs.get_backend_name('/path/to/vcs/checkout')
  108. """
  109. for vc_type in self._registry.values():
  110. if vc_type.controls_location(location):
  111. logger.debug('Determine that %s uses VCS: %s',
  112. location, vc_type.name)
  113. return vc_type.name
  114. return None
  115. def get_backend(self, name):
  116. name = name.lower()
  117. if name in self._registry:
  118. return self._registry[name]
  119. def get_backend_from_location(self, location):
  120. vc_type = self.get_backend_name(location)
  121. if vc_type:
  122. return self.get_backend(vc_type)
  123. return None
  124. vcs = VcsSupport()
  125. class VersionControl(object):
  126. name = ''
  127. dirname = ''
  128. # List of supported schemes for this Version Control
  129. schemes = () # type: Tuple[str, ...]
  130. # Iterable of environment variable names to pass to call_subprocess().
  131. unset_environ = () # type: Tuple[str, ...]
  132. default_arg_rev = None # type: Optional[str]
  133. def __init__(self, url=None, *args, **kwargs):
  134. self.url = url
  135. super(VersionControl, self).__init__(*args, **kwargs)
  136. def get_base_rev_args(self, rev):
  137. """
  138. Return the base revision arguments for a vcs command.
  139. Args:
  140. rev: the name of a revision to install. Cannot be None.
  141. """
  142. raise NotImplementedError
  143. def make_rev_options(self, rev=None, extra_args=None):
  144. """
  145. Return a RevOptions object.
  146. Args:
  147. rev: the name of a revision to install.
  148. extra_args: a list of extra options.
  149. """
  150. return RevOptions(self, rev, extra_args=extra_args)
  151. def _is_local_repository(self, repo):
  152. """
  153. posix absolute paths start with os.path.sep,
  154. win32 ones start with drive (like c:\\folder)
  155. """
  156. drive, tail = os.path.splitdrive(repo)
  157. return repo.startswith(os.path.sep) or drive
  158. def export(self, location):
  159. """
  160. Export the repository at the url to the destination location
  161. i.e. only download the files, without vcs informations
  162. """
  163. raise NotImplementedError
  164. def get_netloc_and_auth(self, netloc, scheme):
  165. """
  166. Parse the repository URL's netloc, and return the new netloc to use
  167. along with auth information.
  168. Args:
  169. netloc: the original repository URL netloc.
  170. scheme: the repository URL's scheme without the vcs prefix.
  171. This is mainly for the Subversion class to override, so that auth
  172. information can be provided via the --username and --password options
  173. instead of through the URL. For other subclasses like Git without
  174. such an option, auth information must stay in the URL.
  175. Returns: (netloc, (username, password)).
  176. """
  177. return netloc, (None, None)
  178. def get_url_rev_and_auth(self, url):
  179. """
  180. Parse the repository URL to use, and return the URL, revision,
  181. and auth info to use.
  182. Returns: (url, rev, (username, password)).
  183. """
  184. scheme, netloc, path, query, frag = urllib_parse.urlsplit(url)
  185. if '+' not in scheme:
  186. raise ValueError(
  187. "Sorry, {!r} is a malformed VCS url. "
  188. "The format is <vcs>+<protocol>://<url>, "
  189. "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url)
  190. )
  191. # Remove the vcs prefix.
  192. scheme = scheme.split('+', 1)[1]
  193. netloc, user_pass = self.get_netloc_and_auth(netloc, scheme)
  194. rev = None
  195. if '@' in path:
  196. path, rev = path.rsplit('@', 1)
  197. url = urllib_parse.urlunsplit((scheme, netloc, path, query, ''))
  198. return url, rev, user_pass
  199. def make_rev_args(self, username, password):
  200. """
  201. Return the RevOptions "extra arguments" to use in obtain().
  202. """
  203. return []
  204. def get_url_rev_options(self, url):
  205. """
  206. Return the URL and RevOptions object to use in obtain() and in
  207. some cases export(), as a tuple (url, rev_options).
  208. """
  209. url, rev, user_pass = self.get_url_rev_and_auth(url)
  210. username, password = user_pass
  211. extra_args = self.make_rev_args(username, password)
  212. rev_options = self.make_rev_options(rev, extra_args=extra_args)
  213. return url, rev_options
  214. def normalize_url(self, url):
  215. """
  216. Normalize a URL for comparison by unquoting it and removing any
  217. trailing slash.
  218. """
  219. return urllib_parse.unquote(url).rstrip('/')
  220. def compare_urls(self, url1, url2):
  221. """
  222. Compare two repo URLs for identity, ignoring incidental differences.
  223. """
  224. return (self.normalize_url(url1) == self.normalize_url(url2))
  225. def fetch_new(self, dest, url, rev_options):
  226. """
  227. Fetch a revision from a repository, in the case that this is the
  228. first fetch from the repository.
  229. Args:
  230. dest: the directory to fetch the repository to.
  231. rev_options: a RevOptions object.
  232. """
  233. raise NotImplementedError
  234. def switch(self, dest, url, rev_options):
  235. """
  236. Switch the repo at ``dest`` to point to ``URL``.
  237. Args:
  238. rev_options: a RevOptions object.
  239. """
  240. raise NotImplementedError
  241. def update(self, dest, url, rev_options):
  242. """
  243. Update an already-existing repo to the given ``rev_options``.
  244. Args:
  245. rev_options: a RevOptions object.
  246. """
  247. raise NotImplementedError
  248. def is_commit_id_equal(self, dest, name):
  249. """
  250. Return whether the id of the current commit equals the given name.
  251. Args:
  252. dest: the repository directory.
  253. name: a string name.
  254. """
  255. raise NotImplementedError
  256. def obtain(self, dest):
  257. """
  258. Install or update in editable mode the package represented by this
  259. VersionControl object.
  260. Args:
  261. dest: the repository directory in which to install or update.
  262. """
  263. url, rev_options = self.get_url_rev_options(self.url)
  264. if not os.path.exists(dest):
  265. self.fetch_new(dest, url, rev_options)
  266. return
  267. rev_display = rev_options.to_display()
  268. if self.is_repository_directory(dest):
  269. existing_url = self.get_url(dest)
  270. if self.compare_urls(existing_url, url):
  271. logger.debug(
  272. '%s in %s exists, and has correct URL (%s)',
  273. self.repo_name.title(),
  274. display_path(dest),
  275. url,
  276. )
  277. if not self.is_commit_id_equal(dest, rev_options.rev):
  278. logger.info(
  279. 'Updating %s %s%s',
  280. display_path(dest),
  281. self.repo_name,
  282. rev_display,
  283. )
  284. self.update(dest, url, rev_options)
  285. else:
  286. logger.info('Skipping because already up-to-date.')
  287. return
  288. logger.warning(
  289. '%s %s in %s exists with URL %s',
  290. self.name,
  291. self.repo_name,
  292. display_path(dest),
  293. existing_url,
  294. )
  295. prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ',
  296. ('s', 'i', 'w', 'b'))
  297. else:
  298. logger.warning(
  299. 'Directory %s already exists, and is not a %s %s.',
  300. dest,
  301. self.name,
  302. self.repo_name,
  303. )
  304. prompt = ('(i)gnore, (w)ipe, (b)ackup ', ('i', 'w', 'b'))
  305. logger.warning(
  306. 'The plan is to install the %s repository %s',
  307. self.name,
  308. url,
  309. )
  310. response = ask_path_exists('What to do? %s' % prompt[0], prompt[1])
  311. if response == 'a':
  312. sys.exit(-1)
  313. if response == 'w':
  314. logger.warning('Deleting %s', display_path(dest))
  315. rmtree(dest)
  316. self.fetch_new(dest, url, rev_options)
  317. return
  318. if response == 'b':
  319. dest_dir = backup_dir(dest)
  320. logger.warning(
  321. 'Backing up %s to %s', display_path(dest), dest_dir,
  322. )
  323. shutil.move(dest, dest_dir)
  324. self.fetch_new(dest, url, rev_options)
  325. return
  326. # Do nothing if the response is "i".
  327. if response == 's':
  328. logger.info(
  329. 'Switching %s %s to %s%s',
  330. self.repo_name,
  331. display_path(dest),
  332. url,
  333. rev_display,
  334. )
  335. self.switch(dest, url, rev_options)
  336. def unpack(self, location):
  337. """
  338. Clean up current location and download the url repository
  339. (and vcs infos) into location
  340. """
  341. if os.path.exists(location):
  342. rmtree(location)
  343. self.obtain(location)
  344. def get_src_requirement(self, dist, location):
  345. """
  346. Return a string representing the requirement needed to
  347. redownload the files currently present in location, something
  348. like:
  349. {repository_url}@{revision}#egg={project_name}-{version_identifier}
  350. """
  351. raise NotImplementedError
  352. def get_url(self, location):
  353. """
  354. Return the url used at location
  355. """
  356. raise NotImplementedError
  357. def get_revision(self, location):
  358. """
  359. Return the current commit id of the files at the given location.
  360. """
  361. raise NotImplementedError
  362. def run_command(self, cmd, show_stdout=True, cwd=None,
  363. on_returncode='raise',
  364. command_desc=None,
  365. extra_environ=None, spinner=None):
  366. """
  367. Run a VCS subcommand
  368. This is simply a wrapper around call_subprocess that adds the VCS
  369. command name, and checks that the VCS is available
  370. """
  371. cmd = [self.name] + cmd
  372. try:
  373. return call_subprocess(cmd, show_stdout, cwd,
  374. on_returncode,
  375. command_desc, extra_environ,
  376. unset_environ=self.unset_environ,
  377. spinner=spinner)
  378. except OSError as e:
  379. # errno.ENOENT = no such file or directory
  380. # In other words, the VCS executable isn't available
  381. if e.errno == errno.ENOENT:
  382. raise BadCommand(
  383. 'Cannot find command %r - do you have '
  384. '%r installed and in your '
  385. 'PATH?' % (self.name, self.name))
  386. else:
  387. raise # re-raise exception if a different error occurred
  388. @classmethod
  389. def is_repository_directory(cls, path):
  390. """
  391. Return whether a directory path is a repository directory.
  392. """
  393. logger.debug('Checking in %s for %s (%s)...',
  394. path, cls.dirname, cls.name)
  395. return os.path.exists(os.path.join(path, cls.dirname))
  396. @classmethod
  397. def controls_location(cls, location):
  398. """
  399. Check if a location is controlled by the vcs.
  400. It is meant to be overridden to implement smarter detection
  401. mechanisms for specific vcs.
  402. This can do more than is_repository_directory() alone. For example,
  403. the Git override checks that Git is actually available.
  404. """
  405. return cls.is_repository_directory(location)
  406. def get_src_requirement(dist, location):
  407. version_control = vcs.get_backend_from_location(location)
  408. if version_control:
  409. try:
  410. return version_control().get_src_requirement(dist,
  411. location)
  412. except BadCommand:
  413. logger.warning(
  414. 'cannot determine version of editable source in %s '
  415. '(%s command not found in path)',
  416. location,
  417. version_control.name,
  418. )
  419. return dist.as_requirement()
  420. logger.warning(
  421. 'cannot determine version of editable source in %s (is not SVN '
  422. 'checkout, Git clone, Mercurial clone or Bazaar branch)',
  423. location,
  424. )
  425. return dist.as_requirement()