constructors.py 14 KB

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