12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202 |
- """Routines related to PyPI, indexes"""
- from __future__ import absolute_import
- import logging
- import cgi
- import sys
- import os
- import re
- import mimetypes
- import posixpath
- import warnings
- from pip._vendor.six.moves.urllib import parse as urllib_parse
- from pip._vendor.six.moves.urllib import request as urllib_request
- from pip.compat import ipaddress
- from pip.utils import (
- Inf, cached_property, normalize_name, splitext, normalize_path)
- from pip.utils.deprecation import RemovedInPip7Warning, RemovedInPip8Warning
- from pip.utils.logging import indent_log
- from pip.exceptions import (
- DistributionNotFound, BestVersionAlreadyInstalled, InvalidWheelFilename,
- UnsupportedWheel,
- )
- from pip.download import url_to_path, path_to_url
- from pip.models import PyPI
- from pip.wheel import Wheel, wheel_ext
- from pip.pep425tags import supported_tags, supported_tags_noarch, get_platform
- from pip.req.req_requirement import InstallationCandidate
- from pip._vendor import html5lib, requests, pkg_resources, six
- from pip._vendor.packaging.version import parse as parse_version
- from pip._vendor.requests.exceptions import SSLError
- __all__ = ['PackageFinder']
- # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC)
- SECURE_ORIGINS = [
- # protocol, hostname, port
- ("https", "*", "*"),
- ("*", "localhost", "*"),
- ("*", "127.0.0.0/8", "*"),
- ("*", "::1/128", "*"),
- ("file", "*", None),
- ]
- logger = logging.getLogger(__name__)
- class PackageFinder(object):
- """This finds packages.
- This is meant to match easy_install's technique for looking for
- packages, by reading pages and looking for appropriate links
- """
- def __init__(self, find_links, index_urls,
- use_wheel=True, allow_external=(), allow_unverified=(),
- allow_all_external=False, allow_all_prereleases=False,
- trusted_hosts=None, process_dependency_links=False,
- session=None):
- if session is None:
- raise TypeError(
- "PackageFinder() missing 1 required keyword argument: "
- "'session'"
- )
- # Build find_links. If an argument starts with ~, it may be
- # a local file relative to a home directory. So try normalizing
- # it and if it exists, use the normalized version.
- # This is deliberately conservative - it might be fine just to
- # blindly normalize anything starting with a ~...
- self.find_links = []
- for link in find_links:
- if link.startswith('~'):
- new_link = normalize_path(link)
- if os.path.exists(new_link):
- link = new_link
- self.find_links.append(link)
- self.index_urls = index_urls
- self.dependency_links = []
- # These are boring links that have already been logged somehow:
- self.logged_links = set()
- self.use_wheel = use_wheel
- # Do we allow (safe and verifiable) externally hosted files?
- self.allow_external = set(normalize_name(n) for n in allow_external)
- # Which names are allowed to install insecure and unverifiable files?
- self.allow_unverified = set(
- normalize_name(n) for n in allow_unverified
- )
- # Anything that is allowed unverified is also allowed external
- self.allow_external |= self.allow_unverified
- # Do we allow all (safe and verifiable) externally hosted files?
- self.allow_all_external = allow_all_external
- # Domains that we won't emit warnings for when not using HTTPS
- self.secure_origins = [
- ("*", host, "*")
- for host in (trusted_hosts if trusted_hosts else [])
- ]
- # Stores if we ignored any external links so that we can instruct
- # end users how to install them if no distributions are available
- self.need_warn_external = False
- # Stores if we ignored any unsafe links so that we can instruct
- # end users how to install them if no distributions are available
- self.need_warn_unverified = False
- # Do we want to allow _all_ pre-releases?
- self.allow_all_prereleases = allow_all_prereleases
- # Do we process dependency links?
- self.process_dependency_links = process_dependency_links
- # The Session we'll use to make requests
- self.session = session
- def add_dependency_links(self, links):
- # # FIXME: this shouldn't be global list this, it should only
- # # apply to requirements of the package that specifies the
- # # dependency_links value
- # # FIXME: also, we should track comes_from (i.e., use Link)
- if self.process_dependency_links:
- warnings.warn(
- "Dependency Links processing has been deprecated and will be "
- "removed in a future release.",
- RemovedInPip7Warning,
- )
- self.dependency_links.extend(links)
- def _sort_locations(self, locations):
- """
- Sort locations into "files" (archives) and "urls", and return
- a pair of lists (files,urls)
- """
- files = []
- urls = []
- # puts the url for the given file path into the appropriate list
- def sort_path(path):
- url = path_to_url(path)
- if mimetypes.guess_type(url, strict=False)[0] == 'text/html':
- urls.append(url)
- else:
- files.append(url)
- for url in locations:
- is_local_path = os.path.exists(url)
- is_file_url = url.startswith('file:')
- is_find_link = url in self.find_links
- if is_local_path or is_file_url:
- if is_local_path:
- path = url
- else:
- path = url_to_path(url)
- if is_find_link and os.path.isdir(path):
- path = os.path.realpath(path)
- for item in os.listdir(path):
- sort_path(os.path.join(path, item))
- elif is_file_url and os.path.isdir(path):
- urls.append(url)
- elif os.path.isfile(path):
- sort_path(path)
- else:
- urls.append(url)
- return files, urls
- def _candidate_sort_key(self, candidate):
- """
- Function used to generate link sort key for link tuples.
- The greater the return value, the more preferred it is.
- If not finding wheels, then sorted by version only.
- If finding wheels, then the sort order is by version, then:
- 1. existing installs
- 2. wheels ordered via Wheel.support_index_min()
- 3. source archives
- Note: it was considered to embed this logic into the Link
- comparison operators, but then different sdist links
- with the same version, would have to be considered equal
- """
- if self.use_wheel:
- support_num = len(supported_tags)
- if candidate.location == INSTALLED_VERSION:
- pri = 1
- elif candidate.location.is_wheel:
- # can raise InvalidWheelFilename
- wheel = Wheel(candidate.location.filename)
- if not wheel.supported():
- raise UnsupportedWheel(
- "%s is not a supported wheel for this platform. It "
- "can't be sorted." % wheel.filename
- )
- pri = -(wheel.support_index_min())
- else: # sdist
- pri = -(support_num)
- return (candidate.version, pri)
- else:
- return candidate.version
- def _sort_versions(self, applicable_versions):
- """
- Bring the latest version (and wheels) to the front, but maintain the
- existing ordering as secondary. See the docstring for `_link_sort_key`
- for details. This function is isolated for easier unit testing.
- """
- return sorted(
- applicable_versions,
- key=self._candidate_sort_key,
- reverse=True
- )
- def _validate_secure_origin(self, logger, location):
- # Determine if this url used a secure transport mechanism
- parsed = urllib_parse.urlparse(str(location))
- origin = (parsed.scheme, parsed.hostname, parsed.port)
- # Determine if our origin is a secure origin by looking through our
- # hardcoded list of secure origins, as well as any additional ones
- # configured on this PackageFinder instance.
- for secure_origin in (SECURE_ORIGINS + self.secure_origins):
- # Check to see if the protocol matches
- if origin[0] != secure_origin[0] and secure_origin[0] != "*":
- continue
- try:
- # We need to do this decode dance to ensure that we have a
- # unicode object, even on Python 2.x.
- addr = ipaddress.ip_address(
- origin[1]
- if (
- isinstance(origin[1], six.text_type) or
- origin[1] is None
- )
- else origin[1].decode("utf8")
- )
- network = ipaddress.ip_network(
- secure_origin[1]
- if isinstance(secure_origin[1], six.text_type)
- else secure_origin[1].decode("utf8")
- )
- except ValueError:
- # We don't have both a valid address or a valid network, so
- # we'll check this origin against hostnames.
- if origin[1] != secure_origin[1] and secure_origin[1] != "*":
- continue
- else:
- # We have a valid address and network, so see if the address
- # is contained within the network.
- if addr not in network:
- continue
- # Check to see if the port patches
- if (origin[2] != secure_origin[2] and
- secure_origin[2] != "*" and
- secure_origin[2] is not None):
- continue
- # If we've gotten here, then this origin matches the current
- # secure origin and we should break out of the loop and continue
- # on.
- break
- else:
- # If the loop successfully completed without a break, that means
- # that the origin we are testing is not a secure origin.
- logger.warning(
- "This repository located at %s is not a trusted host, if "
- "this repository is available via HTTPS it is recommend to "
- "use HTTPS instead, otherwise you may silence this warning "
- "with '--trusted-host %s'.",
- parsed.hostname,
- parsed.hostname,
- )
- warnings.warn(
- "Implicitly allowing locations which are not hosted at a "
- "secure origin is deprecated and will require the use of "
- "--trusted-host in the future.",
- RemovedInPip7Warning,
- )
- def _get_index_urls_locations(self, project_name):
- """Returns the locations found via self.index_urls
- Checks the url_name on the main (first in the list) index and
- use this url_name to produce all locations
- """
- def mkurl_pypi_url(url):
- loc = posixpath.join(url, project_url_name)
- # For maximum compatibility with easy_install, ensure the path
- # ends in a trailing slash. Although this isn't in the spec
- # (and PyPI can handle it without the slash) some other index
- # implementations might break if they relied on easy_install's
- # behavior.
- if not loc.endswith('/'):
- loc = loc + '/'
- return loc
- project_url_name = urllib_parse.quote(project_name.lower())
- if self.index_urls:
- # Check that we have the url_name correctly spelled:
- # Only check main index if index URL is given
- main_index_url = Link(
- mkurl_pypi_url(self.index_urls[0]),
- trusted=True,
- )
- page = self._get_page(main_index_url)
- if page is None and PyPI.netloc not in str(main_index_url):
- warnings.warn(
- "Failed to find %r at %s. It is suggested to upgrade "
- "your index to support normalized names as the name in "
- "/simple/{name}." % (project_name, main_index_url),
- RemovedInPip8Warning,
- )
- project_url_name = self._find_url_name(
- Link(self.index_urls[0], trusted=True),
- project_url_name,
- ) or project_url_name
- if project_url_name is not None:
- return [mkurl_pypi_url(url) for url in self.index_urls]
- return []
- def _find_all_versions(self, project_name):
- """Find all available versions for project_name
- This checks index_urls, find_links and dependency_links
- All versions found are returned
- See _link_package_versions for details on which files are accepted
- """
- index_locations = self._get_index_urls_locations(project_name)
- file_locations, url_locations = self._sort_locations(index_locations)
- fl_file_loc, fl_url_loc = self._sort_locations(self.find_links)
- file_locations.extend(fl_file_loc)
- url_locations.extend(fl_url_loc)
- _flocations, _ulocations = self._sort_locations(self.dependency_links)
- file_locations.extend(_flocations)
- # We trust every url that the user has given us whether it was given
- # via --index-url or --find-links
- locations = [Link(url, trusted=True) for url in url_locations]
- # We explicitly do not trust links that came from dependency_links
- locations.extend([Link(url) for url in _ulocations])
- logger.debug('%d location(s) to search for versions of %s:',
- len(locations), project_name)
- for location in locations:
- logger.debug('* %s', location)
- self._validate_secure_origin(logger, location)
- find_links_versions = list(self._package_versions(
- # We trust every directly linked archive in find_links
- (Link(url, '-f', trusted=True) for url in self.find_links),
- project_name.lower()
- ))
- page_versions = []
- for page in self._get_pages(locations, project_name):
- logger.debug('Analyzing links from page %s', page.url)
- with indent_log():
- page_versions.extend(
- self._package_versions(page.links, project_name.lower())
- )
- dependency_versions = list(self._package_versions(
- (Link(url) for url in self.dependency_links), project_name.lower()
- ))
- if dependency_versions:
- logger.debug(
- 'dependency_links found: %s',
- ', '.join([
- version.location.url for version in dependency_versions
- ])
- )
- file_versions = list(
- self._package_versions(
- (Link(url) for url in file_locations),
- project_name.lower()
- )
- )
- if file_versions:
- file_versions.sort(reverse=True)
- logger.debug(
- 'Local files found: %s',
- ', '.join([
- url_to_path(candidate.location.url)
- for candidate in file_versions
- ])
- )
- # This is an intentional priority ordering
- return (
- file_versions + find_links_versions + page_versions +
- dependency_versions
- )
- def find_requirement(self, req, upgrade):
- """Try to find an InstallationCandidate for req
- Expects req, an InstallRequirement and upgrade, a boolean
- Returns an InstallationCandidate or None
- May raise DistributionNotFound or BestVersionAlreadyInstalled
- """
- all_versions = self._find_all_versions(req.name)
- # Filter out anything which doesn't match our specifier
- _versions = set(
- req.specifier.filter(
- [x.version for x in all_versions],
- prereleases=(
- self.allow_all_prereleases
- if self.allow_all_prereleases else None
- ),
- )
- )
- applicable_versions = [
- x for x in all_versions if x.version in _versions
- ]
- if req.satisfied_by is not None:
- # Finally add our existing versions to the front of our versions.
- applicable_versions.insert(
- 0,
- InstallationCandidate(
- req.name,
- req.satisfied_by.version,
- INSTALLED_VERSION,
- )
- )
- existing_applicable = True
- else:
- existing_applicable = False
- applicable_versions = self._sort_versions(applicable_versions)
- if not upgrade and existing_applicable:
- if applicable_versions[0].location is INSTALLED_VERSION:
- logger.debug(
- 'Existing installed version (%s) is most up-to-date and '
- 'satisfies requirement',
- req.satisfied_by.version,
- )
- else:
- logger.debug(
- 'Existing installed version (%s) satisfies requirement '
- '(most up-to-date version is %s)',
- req.satisfied_by.version,
- applicable_versions[0][2],
- )
- return None
- if not applicable_versions:
- logger.critical(
- 'Could not find a version that satisfies the requirement %s '
- '(from versions: %s)',
- req,
- ', '.join(
- sorted(
- set(str(i.version) for i in all_versions),
- key=parse_version,
- )
- )
- )
- if self.need_warn_external:
- logger.warning(
- "Some externally hosted files were ignored as access to "
- "them may be unreliable (use --allow-external %s to "
- "allow).",
- req.name,
- )
- if self.need_warn_unverified:
- logger.warning(
- "Some insecure and unverifiable files were ignored"
- " (use --allow-unverified %s to allow).",
- req.name,
- )
- raise DistributionNotFound(
- 'No matching distribution found for %s' % req
- )
- if applicable_versions[0].location is INSTALLED_VERSION:
- # We have an existing version, and its the best version
- logger.debug(
- 'Installed version (%s) is most up-to-date (past versions: '
- '%s)',
- req.satisfied_by.version,
- ', '.join(str(i.version) for i in applicable_versions[1:]) or
- "none",
- )
- raise BestVersionAlreadyInstalled
- if len(applicable_versions) > 1:
- logger.debug(
- 'Using version %s (newest of versions: %s)',
- applicable_versions[0].version,
- ', '.join(str(i.version) for i in applicable_versions)
- )
- selected_version = applicable_versions[0].location
- if (selected_version.verifiable is not None and not
- selected_version.verifiable):
- logger.warning(
- "%s is potentially insecure and unverifiable.", req.name,
- )
- if selected_version._deprecated_regex:
- warnings.warn(
- "%s discovered using a deprecated method of parsing, in the "
- "future it will no longer be discovered." % req.name,
- RemovedInPip7Warning,
- )
- return selected_version
- def _find_url_name(self, index_url, url_name):
- """
- Finds the true URL name of a package, when the given name isn't quite
- correct.
- This is usually used to implement case-insensitivity.
- """
- if not index_url.url.endswith('/'):
- # Vaguely part of the PyPI API... weird but true.
- # FIXME: bad to modify this?
- index_url.url += '/'
- page = self._get_page(index_url)
- if page is None:
- logger.critical('Cannot fetch index base URL %s', index_url)
- return
- norm_name = normalize_name(url_name)
- for link in page.links:
- base = posixpath.basename(link.path.rstrip('/'))
- if norm_name == normalize_name(base):
- logger.debug(
- 'Real name of requirement %s is %s', url_name, base,
- )
- return base
- return None
- def _get_pages(self, locations, project_name):
- """
- Yields (page, page_url) from the given locations, skipping
- locations that have errors, and adding download/homepage links
- """
- all_locations = list(locations)
- seen = set()
- normalized = normalize_name(project_name)
- while all_locations:
- location = all_locations.pop(0)
- if location in seen:
- continue
- seen.add(location)
- page = self._get_page(location)
- if page is None:
- continue
- yield page
- for link in page.rel_links():
- if (normalized not in self.allow_external and not
- self.allow_all_external):
- self.need_warn_external = True
- logger.debug(
- "Not searching %s for files because external "
- "urls are disallowed.",
- link,
- )
- continue
- if (link.trusted is not None and not
- link.trusted and
- normalized not in self.allow_unverified):
- logger.debug(
- "Not searching %s for urls, it is an "
- "untrusted link and cannot produce safe or "
- "verifiable files.",
- link,
- )
- self.need_warn_unverified = True
- continue
- all_locations.append(link)
- _egg_fragment_re = re.compile(r'#egg=([^&]*)')
- _egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)
- _py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
- def _sort_links(self, links):
- """
- Returns elements of links in order, non-egg links first, egg links
- second, while eliminating duplicates
- """
- eggs, no_eggs = [], []
- seen = set()
- for link in links:
- if link not in seen:
- seen.add(link)
- if link.egg_fragment:
- eggs.append(link)
- else:
- no_eggs.append(link)
- return no_eggs + eggs
- def _package_versions(self, links, search_name):
- for link in self._sort_links(links):
- v = self._link_package_versions(link, search_name)
- if v is not None:
- yield v
- def _known_extensions(self):
- extensions = ('.tar.gz', '.tar.bz2', '.tar', '.tgz', '.zip')
- if self.use_wheel:
- return extensions + (wheel_ext,)
- return extensions
- def _link_package_versions(self, link, search_name):
- """Return an InstallationCandidate or None"""
- platform = get_platform()
- version = None
- if link.egg_fragment:
- egg_info = link.egg_fragment
- else:
- egg_info, ext = link.splitext()
- if not ext:
- if link not in self.logged_links:
- logger.debug('Skipping link %s; not a file', link)
- self.logged_links.add(link)
- return
- if egg_info.endswith('.tar'):
- # Special double-extension case:
- egg_info = egg_info[:-4]
- ext = '.tar' + ext
- if ext not in self._known_extensions():
- if link not in self.logged_links:
- logger.debug(
- 'Skipping link %s; unknown archive format: %s',
- link,
- ext,
- )
- self.logged_links.add(link)
- return
- if "macosx10" in link.path and ext == '.zip':
- if link not in self.logged_links:
- logger.debug('Skipping link %s; macosx10 one', link)
- self.logged_links.add(link)
- return
- if ext == wheel_ext:
- try:
- wheel = Wheel(link.filename)
- except InvalidWheelFilename:
- logger.debug(
- 'Skipping %s because the wheel filename is invalid',
- link
- )
- return
- if (pkg_resources.safe_name(wheel.name).lower() !=
- pkg_resources.safe_name(search_name).lower()):
- logger.debug(
- 'Skipping link %s; wrong project name (not %s)',
- link,
- search_name,
- )
- return
- if not wheel.supported():
- logger.debug(
- 'Skipping %s because it is not compatible with this '
- 'Python',
- link,
- )
- return
- # This is a dirty hack to prevent installing Binary Wheels from
- # PyPI unless it is a Windows or Mac Binary Wheel. This is
- # paired with a change to PyPI disabling uploads for the
- # same. Once we have a mechanism for enabling support for
- # binary wheels on linux that deals with the inherent problems
- # of binary distribution this can be removed.
- comes_from = getattr(link, "comes_from", None)
- if (
- (
- not platform.startswith('win') and not
- platform.startswith('macosx') and not
- platform == 'cli'
- ) and
- comes_from is not None and
- urllib_parse.urlparse(
- comes_from.url
- ).netloc.endswith(PyPI.netloc)):
- if not wheel.supported(tags=supported_tags_noarch):
- logger.debug(
- "Skipping %s because it is a pypi-hosted binary "
- "Wheel on an unsupported platform",
- link,
- )
- return
- version = wheel.version
- if not version:
- version = self._egg_info_matches(egg_info, search_name, link)
- if version is None:
- logger.debug(
- 'Skipping link %s; wrong project name (not %s)',
- link,
- search_name,
- )
- return
- if (link.internal is not None and not
- link.internal and not
- normalize_name(search_name).lower()
- in self.allow_external and not
- self.allow_all_external):
- # We have a link that we are sure is external, so we should skip
- # it unless we are allowing externals
- logger.debug("Skipping %s because it is externally hosted.", link)
- self.need_warn_external = True
- return
- if (link.verifiable is not None and not
- link.verifiable and not
- (normalize_name(search_name).lower()
- in self.allow_unverified)):
- # We have a link that we are sure we cannot verify its integrity,
- # so we should skip it unless we are allowing unsafe installs
- # for this requirement.
- logger.debug(
- "Skipping %s because it is an insecure and unverifiable file.",
- link,
- )
- self.need_warn_unverified = True
- return
- match = self._py_version_re.search(version)
- if match:
- version = version[:match.start()]
- py_version = match.group(1)
- if py_version != sys.version[:3]:
- logger.debug(
- 'Skipping %s because Python version is incorrect', link
- )
- return
- logger.debug('Found link %s, version: %s', link, version)
- return InstallationCandidate(search_name, version, link)
- def _egg_info_matches(self, egg_info, search_name, link):
- match = self._egg_info_re.search(egg_info)
- if not match:
- logger.debug('Could not parse version from link: %s', link)
- return None
- name = match.group(0).lower()
- # To match the "safe" name that pkg_resources creates:
- name = name.replace('_', '-')
- # project name and version must be separated by a dash
- look_for = search_name.lower() + "-"
- if name.startswith(look_for):
- return match.group(0)[len(look_for):]
- else:
- return None
- def _get_page(self, link):
- return HTMLPage.get_page(link, session=self.session)
- class HTMLPage(object):
- """Represents one page, along with its URL"""
- # FIXME: these regexes are horrible hacks:
- _homepage_re = re.compile(b'<th>\\s*home\\s*page', re.I)
- _download_re = re.compile(b'<th>\\s*download\\s+url', re.I)
- _href_re = re.compile(
- b'href=(?:"([^"]*)"|\'([^\']*)\'|([^>\\s\\n]*))',
- re.I | re.S
- )
- def __init__(self, content, url, headers=None, trusted=None):
- # Determine if we have any encoding information in our headers
- encoding = None
- if headers and "Content-Type" in headers:
- content_type, params = cgi.parse_header(headers["Content-Type"])
- if "charset" in params:
- encoding = params['charset']
- self.content = content
- self.parsed = html5lib.parse(
- self.content,
- encoding=encoding,
- namespaceHTMLElements=False,
- )
- self.url = url
- self.headers = headers
- self.trusted = trusted
- def __str__(self):
- return self.url
- @classmethod
- def get_page(cls, link, skip_archives=True, session=None):
- if session is None:
- raise TypeError(
- "get_page() missing 1 required keyword argument: 'session'"
- )
- url = link.url
- url = url.split('#', 1)[0]
- # Check for VCS schemes that do not support lookup as web pages.
- from pip.vcs import VcsSupport
- for scheme in VcsSupport.schemes:
- if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
- logger.debug('Cannot look at %s URL %s', scheme, link)
- return None
- try:
- if skip_archives:
- filename = link.filename
- for bad_ext in ['.tar', '.tar.gz', '.tar.bz2', '.tgz', '.zip']:
- if filename.endswith(bad_ext):
- content_type = cls._get_content_type(
- url, session=session,
- )
- if content_type.lower().startswith('text/html'):
- break
- else:
- logger.debug(
- 'Skipping page %s because of Content-Type: %s',
- link,
- content_type,
- )
- return
- logger.debug('Getting page %s', url)
- # Tack index.html onto file:// URLs that point to directories
- (scheme, netloc, path, params, query, fragment) = \
- urllib_parse.urlparse(url)
- if (scheme == 'file' and
- os.path.isdir(urllib_request.url2pathname(path))):
- # add trailing slash if not present so urljoin doesn't trim
- # final segment
- if not url.endswith('/'):
- url += '/'
- url = urllib_parse.urljoin(url, 'index.html')
- logger.debug(' file: URL is directory, getting %s', url)
- resp = session.get(
- url,
- headers={
- "Accept": "text/html",
- "Cache-Control": "max-age=600",
- },
- )
- resp.raise_for_status()
- # The check for archives above only works if the url ends with
- # something that looks like an archive. However that is not a
- # requirement of an url. Unless we issue a HEAD request on every
- # url we cannot know ahead of time for sure if something is HTML
- # or not. However we can check after we've downloaded it.
- content_type = resp.headers.get('Content-Type', 'unknown')
- if not content_type.lower().startswith("text/html"):
- logger.debug(
- 'Skipping page %s because of Content-Type: %s',
- link,
- content_type,
- )
- return
- inst = cls(
- resp.content, resp.url, resp.headers,
- trusted=link.trusted,
- )
- except requests.HTTPError as exc:
- level = 2 if exc.response.status_code == 404 else 1
- cls._handle_fail(link, exc, url, level=level)
- except requests.ConnectionError as exc:
- cls._handle_fail(link, "connection error: %s" % exc, url)
- except requests.Timeout:
- cls._handle_fail(link, "timed out", url)
- except SSLError as exc:
- reason = ("There was a problem confirming the ssl certificate: "
- "%s" % exc)
- cls._handle_fail(link, reason, url, level=2, meth=logger.info)
- else:
- return inst
- @staticmethod
- def _handle_fail(link, reason, url, level=1, meth=None):
- if meth is None:
- meth = logger.debug
- meth("Could not fetch URL %s: %s - skipping", link, reason)
- @staticmethod
- def _get_content_type(url, session):
- """Get the Content-Type of the given url, using a HEAD request"""
- scheme, netloc, path, query, fragment = urllib_parse.urlsplit(url)
- if scheme not in ('http', 'https'):
- # FIXME: some warning or something?
- # assertion error?
- return ''
- resp = session.head(url, allow_redirects=True)
- resp.raise_for_status()
- return resp.headers.get("Content-Type", "")
- @cached_property
- def api_version(self):
- metas = [
- x for x in self.parsed.findall(".//meta")
- if x.get("name", "").lower() == "api-version"
- ]
- if metas:
- try:
- return int(metas[0].get("value", None))
- except (TypeError, ValueError):
- pass
- return None
- @cached_property
- def base_url(self):
- bases = [
- x for x in self.parsed.findall(".//base")
- if x.get("href") is not None
- ]
- if bases and bases[0].get("href"):
- return bases[0].get("href")
- else:
- return self.url
- @property
- def links(self):
- """Yields all links in the page"""
- for anchor in self.parsed.findall(".//a"):
- if anchor.get("href"):
- href = anchor.get("href")
- url = self.clean_link(
- urllib_parse.urljoin(self.base_url, href)
- )
- # Determine if this link is internal. If that distinction
- # doesn't make sense in this context, then we don't make
- # any distinction.
- internal = None
- if self.api_version and self.api_version >= 2:
- # Only api_versions >= 2 have a distinction between
- # external and internal links
- internal = bool(
- anchor.get("rel") and
- "internal" in anchor.get("rel").split()
- )
- yield Link(url, self, internal=internal)
- def rel_links(self):
- for url in self.explicit_rel_links():
- yield url
- for url in self.scraped_rel_links():
- yield url
- def explicit_rel_links(self, rels=('homepage', 'download')):
- """Yields all links with the given relations"""
- rels = set(rels)
- for anchor in self.parsed.findall(".//a"):
- if anchor.get("rel") and anchor.get("href"):
- found_rels = set(anchor.get("rel").split())
- # Determine the intersection between what rels were found and
- # what rels were being looked for
- if found_rels & rels:
- href = anchor.get("href")
- url = self.clean_link(
- urllib_parse.urljoin(self.base_url, href)
- )
- yield Link(url, self, trusted=False)
- def scraped_rel_links(self):
- # Can we get rid of this horrible horrible method?
- for regex in (self._homepage_re, self._download_re):
- match = regex.search(self.content)
- if not match:
- continue
- href_match = self._href_re.search(self.content, pos=match.end())
- if not href_match:
- continue
- url = (
- href_match.group(1) or
- href_match.group(2) or
- href_match.group(3)
- )
- if not url:
- continue
- try:
- url = url.decode("ascii")
- except UnicodeDecodeError:
- continue
- url = self.clean_link(urllib_parse.urljoin(self.base_url, url))
- yield Link(url, self, trusted=False, _deprecated_regex=True)
- _clean_re = re.compile(r'[^a-z0-9$&+,/:;=?@.#%_\\|-]', re.I)
- def clean_link(self, url):
- """Makes sure a link is fully encoded. That is, if a ' ' shows up in
- the link, it will be rewritten to %20 (while not over-quoting
- % or other characters)."""
- return self._clean_re.sub(
- lambda match: '%%%2x' % ord(match.group(0)), url)
- class Link(object):
- def __init__(self, url, comes_from=None, internal=None, trusted=None,
- _deprecated_regex=False):
- # url can be a UNC windows share
- if url != Inf and url.startswith('\\\\'):
- url = path_to_url(url)
- self.url = url
- self.comes_from = comes_from
- self.internal = internal
- self.trusted = trusted
- self._deprecated_regex = _deprecated_regex
- def __str__(self):
- if self.comes_from:
- return '%s (from %s)' % (self.url, self.comes_from)
- else:
- return str(self.url)
- def __repr__(self):
- return '<Link %s>' % self
- def __eq__(self, other):
- if not isinstance(other, Link):
- return NotImplemented
- return self.url == other.url
- def __ne__(self, other):
- if not isinstance(other, Link):
- return NotImplemented
- return self.url != other.url
- def __lt__(self, other):
- if not isinstance(other, Link):
- return NotImplemented
- return self.url < other.url
- def __le__(self, other):
- if not isinstance(other, Link):
- return NotImplemented
- return self.url <= other.url
- def __gt__(self, other):
- if not isinstance(other, Link):
- return NotImplemented
- return self.url > other.url
- def __ge__(self, other):
- if not isinstance(other, Link):
- return NotImplemented
- return self.url >= other.url
- def __hash__(self):
- return hash(self.url)
- @property
- def filename(self):
- _, netloc, path, _, _ = urllib_parse.urlsplit(self.url)
- name = posixpath.basename(path.rstrip('/')) or netloc
- name = urllib_parse.unquote(name)
- assert name, ('URL %r produced no filename' % self.url)
- return name
- @property
- def scheme(self):
- return urllib_parse.urlsplit(self.url)[0]
- @property
- def netloc(self):
- return urllib_parse.urlsplit(self.url)[1]
- @property
- def path(self):
- return urllib_parse.unquote(urllib_parse.urlsplit(self.url)[2])
- def splitext(self):
- return splitext(posixpath.basename(self.path.rstrip('/')))
- @property
- def ext(self):
- return self.splitext()[1]
- @property
- def url_without_fragment(self):
- scheme, netloc, path, query, fragment = urllib_parse.urlsplit(self.url)
- return urllib_parse.urlunsplit((scheme, netloc, path, query, None))
- _egg_fragment_re = re.compile(r'#egg=([^&]*)')
- @property
- def egg_fragment(self):
- match = self._egg_fragment_re.search(self.url)
- if not match:
- return None
- return match.group(1)
- _hash_re = re.compile(
- r'(sha1|sha224|sha384|sha256|sha512|md5)=([a-f0-9]+)'
- )
- @property
- def hash(self):
- match = self._hash_re.search(self.url)
- if match:
- return match.group(2)
- return None
- @property
- def hash_name(self):
- match = self._hash_re.search(self.url)
- if match:
- return match.group(1)
- return None
- @property
- def show_url(self):
- return posixpath.basename(self.url.split('#', 1)[0].split('?', 1)[0])
- @property
- def verifiable(self):
- """
- Returns True if this link can be verified after download, False if it
- cannot, and None if we cannot determine.
- """
- trusted = self.trusted or getattr(self.comes_from, "trusted", None)
- if trusted is not None and trusted:
- # This link came from a trusted source. It *may* be verifiable but
- # first we need to see if this page is operating under the new
- # API version.
- try:
- api_version = getattr(self.comes_from, "api_version", None)
- api_version = int(api_version)
- except (ValueError, TypeError):
- api_version = None
- if api_version is None or api_version <= 1:
- # This link is either trusted, or it came from a trusted,
- # however it is not operating under the API version 2 so
- # we can't make any claims about if it's safe or not
- return
- if self.hash:
- # This link came from a trusted source and it has a hash, so we
- # can consider it safe.
- return True
- else:
- # This link came from a trusted source, using the new API
- # version, and it does not have a hash. It is NOT verifiable
- return False
- elif trusted is not None:
- # This link came from an untrusted source and we cannot trust it
- return False
- @property
- def is_wheel(self):
- return self.ext == wheel_ext
- # An object to represent the "link" for the installed version of a requirement.
- # Using Inf as the url makes it sort higher.
- INSTALLED_VERSION = Link(Inf)
|