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.

constructors.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. """Backing implementation for InstallRequirement's various constructors
  2. The idea here is that these formed a major chunk of InstallRequirement's size
  3. so, moving them and support code dedicated to them outside of that class
  4. helps creates for better understandability for the rest of the code.
  5. These are meant to be used elsewhere within pip to create instances of
  6. InstallRequirement.
  7. """
  8. import logging
  9. import os
  10. import re
  11. from pip._vendor.packaging.markers import Marker
  12. from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
  13. from pip._vendor.packaging.specifiers import Specifier
  14. from pip._vendor.pkg_resources import RequirementParseError, parse_requirements
  15. from pip._internal.download import (
  16. is_archive_file, is_url, path_to_url, url_to_path,
  17. )
  18. from pip._internal.exceptions import InstallationError
  19. from pip._internal.models.index import PyPI, TestPyPI
  20. from pip._internal.models.link import Link
  21. from pip._internal.pyproject import make_pyproject_path
  22. from pip._internal.req.req_install import InstallRequirement
  23. from pip._internal.utils.misc import is_installable_dir
  24. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  25. from pip._internal.vcs import vcs
  26. from pip._internal.wheel import Wheel
  27. if MYPY_CHECK_RUNNING:
  28. from typing import ( # noqa: F401
  29. Optional, Tuple, Set, Any, Union, Text, Dict,
  30. )
  31. from pip._internal.cache import WheelCache # noqa: F401
  32. __all__ = [
  33. "install_req_from_editable", "install_req_from_line",
  34. "parse_editable"
  35. ]
  36. logger = logging.getLogger(__name__)
  37. operators = Specifier._operators.keys()
  38. def _strip_extras(path):
  39. # type: (str) -> Tuple[str, Optional[str]]
  40. m = re.match(r'^(.+)(\[[^\]]+\])$', path)
  41. extras = None
  42. if m:
  43. path_no_extras = m.group(1)
  44. extras = m.group(2)
  45. else:
  46. path_no_extras = path
  47. return path_no_extras, extras
  48. def parse_editable(editable_req):
  49. # type: (str) -> Tuple[Optional[str], str, Optional[Set[str]]]
  50. """Parses an editable requirement into:
  51. - a requirement name
  52. - an URL
  53. - extras
  54. - editable options
  55. Accepted requirements:
  56. svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
  57. .[some_extra]
  58. """
  59. url = editable_req
  60. # If a file path is specified with extras, strip off the extras.
  61. url_no_extras, extras = _strip_extras(url)
  62. if os.path.isdir(url_no_extras):
  63. if not os.path.exists(os.path.join(url_no_extras, 'setup.py')):
  64. msg = (
  65. 'File "setup.py" not found. Directory cannot be installed '
  66. 'in editable mode: {}'.format(os.path.abspath(url_no_extras))
  67. )
  68. pyproject_path = make_pyproject_path(url_no_extras)
  69. if os.path.isfile(pyproject_path):
  70. msg += (
  71. '\n(A "pyproject.toml" file was found, but editable '
  72. 'mode currently requires a setup.py based build.)'
  73. )
  74. raise InstallationError(msg)
  75. # Treating it as code that has already been checked out
  76. url_no_extras = path_to_url(url_no_extras)
  77. if url_no_extras.lower().startswith('file:'):
  78. package_name = Link(url_no_extras).egg_fragment
  79. if extras:
  80. return (
  81. package_name,
  82. url_no_extras,
  83. Requirement("placeholder" + extras.lower()).extras,
  84. )
  85. else:
  86. return package_name, url_no_extras, None
  87. for version_control in vcs:
  88. if url.lower().startswith('%s:' % version_control):
  89. url = '%s+%s' % (version_control, url)
  90. break
  91. if '+' not in url:
  92. raise InstallationError(
  93. '%s should either be a path to a local project or a VCS url '
  94. 'beginning with svn+, git+, hg+, or bzr+' %
  95. editable_req
  96. )
  97. vc_type = url.split('+', 1)[0].lower()
  98. if not vcs.get_backend(vc_type):
  99. error_message = 'For --editable=%s only ' % editable_req + \
  100. ', '.join([backend.name + '+URL' for backend in vcs.backends]) + \
  101. ' is currently supported'
  102. raise InstallationError(error_message)
  103. package_name = Link(url).egg_fragment
  104. if not package_name:
  105. raise InstallationError(
  106. "Could not detect requirement name for '%s', please specify one "
  107. "with #egg=your_package_name" % editable_req
  108. )
  109. return package_name, url, None
  110. def deduce_helpful_msg(req):
  111. # type: (str) -> str
  112. """Returns helpful msg in case requirements file does not exist,
  113. or cannot be parsed.
  114. :params req: Requirements file path
  115. """
  116. msg = ""
  117. if os.path.exists(req):
  118. msg = " It does exist."
  119. # Try to parse and check if it is a requirements file.
  120. try:
  121. with open(req, 'r') as fp:
  122. # parse first line only
  123. next(parse_requirements(fp.read()))
  124. msg += " The argument you provided " + \
  125. "(%s) appears to be a" % (req) + \
  126. " requirements file. If that is the" + \
  127. " case, use the '-r' flag to install" + \
  128. " the packages specified within it."
  129. except RequirementParseError:
  130. logger.debug("Cannot parse '%s' as requirements \
  131. file" % (req), exc_info=True)
  132. else:
  133. msg += " File '%s' does not exist." % (req)
  134. return msg
  135. # ---- The actual constructors follow ----
  136. def install_req_from_editable(
  137. editable_req, # type: str
  138. comes_from=None, # type: Optional[str]
  139. use_pep517=None, # type: Optional[bool]
  140. isolated=False, # type: bool
  141. options=None, # type: Optional[Dict[str, Any]]
  142. wheel_cache=None, # type: Optional[WheelCache]
  143. constraint=False # type: bool
  144. ):
  145. # type: (...) -> InstallRequirement
  146. name, url, extras_override = parse_editable(editable_req)
  147. if url.startswith('file:'):
  148. source_dir = url_to_path(url)
  149. else:
  150. source_dir = None
  151. if name is not None:
  152. try:
  153. req = Requirement(name)
  154. except InvalidRequirement:
  155. raise InstallationError("Invalid requirement: '%s'" % name)
  156. else:
  157. req = None
  158. return InstallRequirement(
  159. req, comes_from, source_dir=source_dir,
  160. editable=True,
  161. link=Link(url),
  162. constraint=constraint,
  163. use_pep517=use_pep517,
  164. isolated=isolated,
  165. options=options if options else {},
  166. wheel_cache=wheel_cache,
  167. extras=extras_override or (),
  168. )
  169. def install_req_from_line(
  170. name, # type: str
  171. comes_from=None, # type: Optional[Union[str, InstallRequirement]]
  172. use_pep517=None, # type: Optional[bool]
  173. isolated=False, # type: bool
  174. options=None, # type: Optional[Dict[str, Any]]
  175. wheel_cache=None, # type: Optional[WheelCache]
  176. constraint=False # type: bool
  177. ):
  178. # type: (...) -> InstallRequirement
  179. """Creates an InstallRequirement from a name, which might be a
  180. requirement, directory containing 'setup.py', filename, or URL.
  181. """
  182. if is_url(name):
  183. marker_sep = '; '
  184. else:
  185. marker_sep = ';'
  186. if marker_sep in name:
  187. name, markers_as_string = name.split(marker_sep, 1)
  188. markers_as_string = markers_as_string.strip()
  189. if not markers_as_string:
  190. markers = None
  191. else:
  192. markers = Marker(markers_as_string)
  193. else:
  194. markers = None
  195. name = name.strip()
  196. req_as_string = None
  197. path = os.path.normpath(os.path.abspath(name))
  198. link = None
  199. extras_as_string = None
  200. if is_url(name):
  201. link = Link(name)
  202. else:
  203. p, extras_as_string = _strip_extras(path)
  204. looks_like_dir = os.path.isdir(p) and (
  205. os.path.sep in name or
  206. (os.path.altsep is not None and os.path.altsep in name) or
  207. name.startswith('.')
  208. )
  209. if looks_like_dir:
  210. if not is_installable_dir(p):
  211. raise InstallationError(
  212. "Directory %r is not installable. Neither 'setup.py' "
  213. "nor 'pyproject.toml' found." % name
  214. )
  215. link = Link(path_to_url(p))
  216. elif is_archive_file(p):
  217. if not os.path.isfile(p):
  218. logger.warning(
  219. 'Requirement %r looks like a filename, but the '
  220. 'file does not exist',
  221. name
  222. )
  223. link = Link(path_to_url(p))
  224. # it's a local file, dir, or url
  225. if link:
  226. # Handle relative file URLs
  227. if link.scheme == 'file' and re.search(r'\.\./', link.url):
  228. link = Link(
  229. path_to_url(os.path.normpath(os.path.abspath(link.path))))
  230. # wheel file
  231. if link.is_wheel:
  232. wheel = Wheel(link.filename) # can raise InvalidWheelFilename
  233. req_as_string = "%s==%s" % (wheel.name, wheel.version)
  234. else:
  235. # set the req to the egg fragment. when it's not there, this
  236. # will become an 'unnamed' requirement
  237. req_as_string = link.egg_fragment
  238. # a requirement specifier
  239. else:
  240. req_as_string = name
  241. if extras_as_string:
  242. extras = Requirement("placeholder" + extras_as_string.lower()).extras
  243. else:
  244. extras = ()
  245. if req_as_string is not None:
  246. try:
  247. req = Requirement(req_as_string)
  248. except InvalidRequirement:
  249. if os.path.sep in req_as_string:
  250. add_msg = "It looks like a path."
  251. add_msg += deduce_helpful_msg(req_as_string)
  252. elif ('=' in req_as_string and
  253. not any(op in req_as_string for op in operators)):
  254. add_msg = "= is not a valid operator. Did you mean == ?"
  255. else:
  256. add_msg = ""
  257. raise InstallationError(
  258. "Invalid requirement: '%s'\n%s" % (req_as_string, add_msg)
  259. )
  260. else:
  261. req = None
  262. return InstallRequirement(
  263. req, comes_from, link=link, markers=markers,
  264. use_pep517=use_pep517, isolated=isolated,
  265. options=options if options else {},
  266. wheel_cache=wheel_cache,
  267. constraint=constraint,
  268. extras=extras,
  269. )
  270. def install_req_from_req_string(
  271. req_string, # type: str
  272. comes_from=None, # type: Optional[InstallRequirement]
  273. isolated=False, # type: bool
  274. wheel_cache=None, # type: Optional[WheelCache]
  275. use_pep517=None # type: Optional[bool]
  276. ):
  277. # type: (...) -> InstallRequirement
  278. try:
  279. req = Requirement(req_string)
  280. except InvalidRequirement:
  281. raise InstallationError("Invalid requirement: '%s'" % req)
  282. domains_not_allowed = [
  283. PyPI.file_storage_domain,
  284. TestPyPI.file_storage_domain,
  285. ]
  286. if req.url and comes_from.link.netloc in domains_not_allowed:
  287. # Explicitly disallow pypi packages that depend on external urls
  288. raise InstallationError(
  289. "Packages installed from PyPI cannot depend on packages "
  290. "which are not also hosted on PyPI.\n"
  291. "%s depends on %s " % (comes_from.name, req)
  292. )
  293. return InstallRequirement(
  294. req, comes_from, isolated=isolated, wheel_cache=wheel_cache,
  295. use_pep517=use_pep517
  296. )