# Source code for pex.package

    # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
    # Licensed under the Apache License, Version 2.0 (see LICENSE).

    import os

    from pkg_resources import EGG_NAME, parse_version, safe_name, safe_version

    from .archiver import Archiver
    from .base import maybe_requirement
    from .link import Link
    from .pep425tags import get_supported
    from .util import Memoizer

    class Package(Link):
      """Base class for named Python binary packages (e.g. source, egg, wheel)."""

      class Error(Exception): pass
      class InvalidPackage(Error): pass

      # The registry of concrete implementations
      _REGISTRY = set()

      # The cache of packages that we have already constructed.
      _HREF_TO_PACKAGE_CACHE = Memoizer()

      @classmethod
      def register(cls, package_type):
        """Register a concrete implementation of a Package to be recognized by pex."""
        if not issubclass(package_type, cls):
          raise TypeError('package_type must be a subclass of Package.')
        cls._REGISTRY.add(package_type)

      @classmethod
      def from_href(cls, href, **kw):
        """Convert from a url to Package.

        :param href: The url to parse
        :type href: string
        :returns: A Package object if a valid concrete implementation exists, otherwise None.
        """
        package = cls._HREF_TO_PACKAGE_CACHE.get(href)
        if package is not None:
          return package
        link_href = Link.wrap(href)
        for package_type in cls._REGISTRY:
          try:
            package = package_type(link_href.url, **kw)
            break
          except package_type.InvalidPackage:
            continue
        if package is not None:
          cls._HREF_TO_PACKAGE_CACHE.store(href, package)
        return package

      @property
      def name(self):
        return NotImplementedError

      @property
      def raw_version(self):
        return NotImplementedError

      @property
      def version(self):
        return parse_version(self.raw_version)

      def satisfies(self, requirement, allow_prereleases=None):
        """Determine whether this package matches the requirement.

        :param requirement: The requirement to compare this Package against
        :type requirement: string or :class:`pkg_resources.Requirement`
        :param Optional[bool] allow_prereleases: Whether to allow prereleases to satisfy
          the `requirement`.
        :returns: True if the package matches the requirement, otherwise False
        """
        requirement = maybe_requirement(requirement)
        link_name = safe_name(self.name).lower()
        if link_name != requirement.key:
          return False

        # NB: If we upgrade to setuptools>=34 the SpecifierSet used here (requirement.specifier) will
        # come from a non-vendored `packaging` package and pex's bootstrap code in `PEXBuilder` will
        # need an update.
        return requirement.specifier.contains(self.raw_version, prereleases=allow_prereleases)

      def compatible(self, supported_tags):
        """Is this package compatible with the given tag set?

        :param supported_tags: A list of tags that is supported by the target
                               interpeter, as generated by
                               :func:`pex.pep425tags.get_supported`.
        :type supported_tags: list of 3-tuples
        """
        raise NotImplementedError()

    class SourcePackage(Package):
      """A Package representing an uncompiled/unbuilt source distribution."""

      @classmethod
      def split_fragment(cls, fragment):
        """A heuristic used to split a string into version name/fragment:

           >>> SourcePackage.split_fragment('pysolr-2.1.0-beta')
           ('pysolr', '2.1.0-beta')
           >>> SourcePackage.split_fragment('cElementTree-1.0.5-20051216')
           ('cElementTree', '1.0.5-20051216')
           >>> SourcePackage.split_fragment('pil-1.1.7b1-20090412')
           ('pil', '1.1.7b1-20090412')
           >>> SourcePackage.split_fragment('django-plugin-2-2.3')
           ('django-plugin-2', '2.3')
        """
        def likely_version_component(enumerated_fragment):
          return sum(bool(v and v[0].isdigit()) for v in enumerated_fragment[1].split('.'))
        fragments = fragment.split('-')
        if len(fragments) == 1:
          return fragment, ''
        max_index, _ = max(enumerate(fragments), key=likely_version_component)
        return '-'.join(fragments[0:max_index]), '-'.join(fragments[max_index:])

      def __init__(self, url, **kw):
        super(SourcePackage, self).__init__(url, **kw)

        ext = Archiver.get_extension(self.filename)
        if ext is None:
          raise self.InvalidPackage('%s is not a recognized archive format.' % self.filename)

        fragment = self.filename[:-len(ext)]
        self._name, self._raw_version = self.split_fragment(fragment)

      @property
      def name(self):
        return safe_name(self._name)

      @property
      def raw_version(self):
        return safe_version(self._raw_version)

      # SourcePackages are always compatible as they can be translated to a distribution.
      def compatible(self, supported_tags):
        return True

    class BinaryPackage(Package):
      """A Package representing a binary distribution which can be e.g. platform specific."""

      def __init__(self, *args, **kwargs):
        super(BinaryPackage, self).__init__(*args, **kwargs)
        self._supported_tags = None

      @property
      def supported_tags(self):
        if not self._supported_tags:
          self._supported_tags = frozenset(self._iter_tags())
        return self._supported_tags

      def _iter_tags(self):
        raise NotImplementedError()

      def compatible(self, supported_tags):
        """Is this package compatible with the given tag set?

        :param supported_tags: A list of tags that is supported by the target
                               interpeter, as generated by
                               :func:`pex.pep425tags.get_supported`.
        :type supported_tags: list of 3-tuples
        """
        return not set(supported_tags).isdisjoint(self.supported_tags)

    class EggPackage(BinaryPackage):
      """A Package representing a built egg."""

      def __init__(self, url, **kw):
        super(EggPackage, self).__init__(url, **kw)
        filename, ext = os.path.splitext(self.filename)
        if ext.lower() != '.egg':
          raise self.InvalidPackage('Not an egg: %s' % filename)
        matcher = EGG_NAME(filename)
        if not matcher:
          raise self.InvalidPackage('Could not match egg: %s' % filename)

        self._name, self._raw_version, self._py_version, self._platform = matcher.group(
            'name', 'ver', 'pyver', 'plat'
        )

        if self._raw_version is None or self._py_version is None:
          raise self.InvalidPackage('url with .egg extension but bad name: %s' % url)

      def __hash__(self):
        return hash((self.name, self.version, self.py_version, self.platform))

      @property
      def name(self):
        return safe_name(self._name)

      @property
      def raw_version(self):
        return safe_version(self._raw_version)

      @property
      def py_version(self):
        return self._py_version

      @property
      def platform(self):
        return self._platform

      def _iter_tags(self):
        abi_tag = 'none'
        tag_platform = (self._platform or 'any').replace('-', '_').replace('.', '_')
        suffix_maj = self._py_version.split('.', 1)[0]
        suffix_min = self._py_version.replace('.', '')

        for prefix in ('py', 'cp'):
          for suffix in (suffix_maj, suffix_min):
            impl = ''.join((prefix, suffix))
            yield (impl, abi_tag, tag_platform)

        # Work around PyPy versions being weird in pep425tags.get_supported
        if self.py_version == '2.7':
          yield ('pp2', abi_tag, tag_platform)
        elif self.py_version == '3.2':
          yield ('pp321', abi_tag, tag_platform)
        elif self.py_version == '3.3':
          # N.B. PyPy 3.3 maps to `pp352` because of PyPy versioning.
          # see e.g. http://doc.pypy.org/en/latest/release-pypy3.3-v5.2-alpha1.html
          yield ('pp352', abi_tag, tag_platform)
        elif self.py_version == '3.5':
          yield ('pp357', abi_tag, tag_platform)

    class WheelPackage(BinaryPackage):
      """A Package representing a built wheel."""

      def __init__(self, url, **kw):
        super(WheelPackage, self).__init__(url, **kw)
        filename, ext = os.path.splitext(self.filename)
        if ext.lower() != '.whl':
          raise self.InvalidPackage('Not a wheel: %s' % filename)
        try:
          self._name, self._raw_version, self._py_tag, self._abi_tag, self._arch_tag = (
              filename.rsplit('-', 4))
        except ValueError:
          raise self.InvalidPackage('Wheel filename malformed.')
        # See https://github.com/pypa/pip/issues/1150 for why this is unavoidable.
        self._name = self._name.replace('_', '-')
        self._raw_version = self._raw_version.replace('_', '-')

      @property
      def name(self):
        return self._name

      @property
      def raw_version(self):
        return self._raw_version

      def _iter_tags(self):
        for py in self._py_tag.split('.'):
          for abi in self._abi_tag.split('.'):
            for arch in self._arch_tag.split('.'):
              yield (py, abi, arch)

      def __eq__(self, other):
        return (self._name == other._name and
                self._raw_version == other._raw_version and
                self._py_tag == other._py_tag and
                self._abi_tag == other._abi_tag and
                self._arch_tag == other._arch_tag)

      def __hash__(self):
        return hash((self._name, self._raw_version, self._py_tag, self._abi_tag, self._arch_tag))

    Package.register(SourcePackage)
    Package.register(EggPackage)
    Package.register(WheelPackage)

    def distribution_compatible(dist, supported_tags=None):
      """Is this distribution compatible with the given interpreter/platform combination?

      :param supported_tags: A list of tag tuples specifying which tags are supported
        by the platform in question.
      :returns: True if the distribution is compatible, False if it is unrecognized or incompatible.
      """
      if supported_tags is None:
        supported_tags = get_supported()
      package = Package.from_href(dist.location)
      if not package:
        return False
      return package.compatible(supported_tags)