text: # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
  # Licensed under the Apache License, Version 2.0 (see LICENSE).
  
  from __future__ import absolute_import
  
  import os
  import re
  from abc import abstractmethod, abstractproperty
  
  from pex.base import maybe_requirement, requirement_is_exact
  from pex.compatibility import AbstractClass
  from pex.compatibility import string as compatibility_string
  from pex.installer import Packager, SetuptoolsInstallerBase
  from pex.package import Package
  from pex.resolver_options import ResolverOptionsBuilder, ResolverOptionsInterface
  from pex.third_party.pkg_resources import Requirement, safe_extra
  
  # Extract extras as specified per "declaring extras":
  # https://pythonhosted.org/setuptools/setuptools.html
  _EXTRAS_PATTERN = re.compile(r'(?P<main>.*)\[(?P<extras>.*)\]$')
  
  
  def strip_extras(resolvable_string):
    match = _EXTRAS_PATTERN.match(resolvable_string)
    if match:
      resolvable_string, extras = match.groupdict()['main'], match.groupdict()['extras']
      extras = [safe_extra(extra.strip()) for extra in extras.split(',')]
    else:
      extras = []
    return resolvable_string, extras
  
  
  class Resolvable(AbstractClass):
    """An entity that can be resolved into a package."""
  
    class Error(Exception): pass
    class InvalidRequirement(Error): pass
  
    _REGISTRY = []
  
    @property
    def is_constraint(self):
      if hasattr(self, "_is_constraint"):
        return self._is_constraint
      return False
  
    @is_constraint.setter
    def is_constraint(self, value):
      self._is_constraint = value
  
    @classmethod
    def register(cls, implementation):
      """Register an implementation of a Resolvable.
  
      :param implementation: The resolvable implementation.
      :type implementation: :class:`Resolvable`
      """
      cls._REGISTRY.append(implementation)
  
    @classmethod
    def get(cls, resolvable_string, options_builder=None, interpreter=None):
      """Get a :class:`Resolvable` from a string.
  
      :returns: A :class:`Resolvable` or ``None`` if no implementation was appropriate.
      """
      options_builder = options_builder or ResolverOptionsBuilder()
      for resolvable_impl in cls._REGISTRY:
        try:
          return resolvable_impl.from_string(resolvable_string,
                                             options_builder,
                                             interpreter=interpreter)
        except cls.InvalidRequirement:
          continue
      raise cls.InvalidRequirement('Unknown requirement type: %s' % resolvable_string)
  
    # @abstractmethod - Only available in Python 3.3+
    @classmethod
    def from_string(cls, requirement_string, options_builder, interpreter=None):
      """Produce a resolvable from this requirement string.
  
      :returns: Instance of the particular Resolvable implementation.
      :raises InvalidRequirement: If requirement_string is not a valid string representation
        of the resolvable.
      """
      raise cls.InvalidRequirement('Resolvable is abstract.')
  
    def __init__(self, options):
      if not isinstance(options, ResolverOptionsInterface):
        raise TypeError('Resolvable must be initialized with a ResolverOptionsInterface, got %s' % (
            type(options)))
      self._options = options
  
    @property
    def options(self):
      """The ResolverOptions for this Resolvable."""
      return self._options
  
    @abstractmethod
    def compatible(self, iterator):
      """Given a finder of type :class:`Iterator` (possibly ignored), determine which packages
         are compatible with this resolvable.
  
      :returns: An iterable of compatible :class:`Package` objects.
      """
  
    @abstractmethod
    def packages(self):
      """Return a list of :class:`Package` objects that this resolvable resolves.
  
      :returns: An iterable of compatible :class:`Package` objects.
      """
  
    @abstractproperty
    def name(self):
      """The distribution key associated with this resolvable, i.e. the name of the packages
         this resolvable will produce."""
  
    # TODO(wickman) Call this "cacheable" instead?
    @abstractproperty
    def exact(self):
      """Whether or not this resolvable specifies an exact (cacheable) requirement."""
  
    def extras(self):
      """Return the "extras" tags associated with this resolvable if any."""
      return []
  
  
  class ResolvableRepository(Resolvable):
    """A VCS repository resolvable, e.g. 'git+', 'svn+', 'hg+', 'bzr+' packages."""
  
    COMPATIBLE_VCS = frozenset(['git', 'svn', 'hg', 'bzr'])
  
    @classmethod
    def from_string(cls, requirement_string, options_builder, interpreter=None):
      if any(requirement_string.startswith('%s+' % vcs) for vcs in cls.COMPATIBLE_VCS):
        # further delegate
        pass
  
      # TODO(wickman) Implement: Issue #93.
      raise cls.InvalidRequirement('Versioning system URLs not supported.')
  
    def __init__(self, options):
      super(ResolvableRepository, self).__init__(options)
  
    def compatible(self, iterator):
      return []
  
    def packages(self):
      return []
  
    @property
    def name(self):
      raise NotImplementedError
  
    @property
    def exact(self):
      return True
  
  
  class ResolvablePackage(Resolvable):
    """A package (.tar.gz, .egg, .whl, etc) resolvable."""
  
    # TODO(wickman) Implement extras parsing for ResolvablePackage
    @classmethod
    def from_string(cls, requirement_string, options_builder, interpreter=None):
      requirement_string, extras = strip_extras(requirement_string)
      package = Package.from_href(requirement_string)
      if package is None:
        raise cls.InvalidRequirement('Requirement string does not appear to be a package.')
      return cls(package, options_builder.build(package.name), extras=extras)
  
    def __init__(self, package, options, extras=None):
      self.package = package
      self._extras = extras
      super(ResolvablePackage, self).__init__(options)
  
    def compatible(self, iterator):
      return []
  
    def packages(self):
      return [self.package]
  
    @property
    def name(self):
      return self.package.name
  
    @property
    def exact(self):
      return True
  
    def extras(self):
      return self._extras
  
    def __eq__(self, other):
      return isinstance(other, ResolvablePackage) and self.package == other.package
  
    def __hash__(self):
      return hash(self.package)
  
    def __str__(self):
      return str(self.package)
  
  
  class ResolvableRequirement(Resolvable):
    """A requirement (e.g. 'setuptools', 'Flask>=0.8,<0.9', 'pex[whl]')."""
  
    @classmethod
    def from_string(cls, requirement_string, options_builder, interpreter=None):
      try:
        req = maybe_requirement(requirement_string)
      except ValueError:
        raise cls.InvalidRequirement('%s does not appear to be a requirement string.' %
            requirement_string)
      return cls(req, options_builder.build(req.key))
  
    def __init__(self, requirement, options):
      self.requirement = requirement
      super(ResolvableRequirement, self).__init__(options)
  
    def compatible(self, iterator):
      sorter = self.options.get_sorter()
      return sorter.sort(package for package in iterator.iter(self.requirement))
  
    def packages(self):
      iterator = self.options.get_iterator()
      sorter = self.options.get_sorter()
      return sorter.sort(iterator.iter(self.requirement))
  
    @property
    def name(self):
      return self.requirement.key
  
    @property
    def exact(self):
      return requirement_is_exact(self.requirement)
  
    def extras(self):
      return list(self.requirement.extras)
  
    def __eq__(self, other):
      return isinstance(other, ResolvableRequirement) and self.requirement == other.requirement
  
    def __hash__(self):
      return hash(self.requirement)
  
    def __str__(self):
      return str(self.requirement)
  
  
  class ResolvableDirectory(ResolvablePackage):
    """A source directory (with setup.py) resolvable."""
  
    @classmethod
    def is_installable(cls, requirement_string):
      if not os.path.isdir(requirement_string):
        return False
      return os.path.isfile(os.path.join(requirement_string, 'setup.py'))
  
    @classmethod
    def from_string(cls, requirement_string, options_builder, interpreter=None):
      requirement_string, extras = strip_extras(requirement_string)
      if cls.is_installable(requirement_string):
        if interpreter is None:
          raise cls.InvalidRequirement('%s is not an installable directory because we were called '
                                       'without an interpreter to use to execute setup.py.'
                                       % requirement_string)
        packager = Packager(requirement_string, interpreter=interpreter)
        try:
          sdist = packager.sdist()
        except SetuptoolsInstallerBase.Error:
          raise cls.InvalidRequirement('Could not create source distribution for %s'
                                       % requirement_string)
        package = Package.from_href(sdist)
        return ResolvablePackage(package, options_builder.build(package.name), extras=extras)
      else:
        raise cls.InvalidRequirement('%s does not appear to be an installable directory.'
                                     % requirement_string)
  
  
  Resolvable.register(ResolvableDirectory)
  Resolvable.register(ResolvableRepository)
  Resolvable.register(ResolvablePackage)
  Resolvable.register(ResolvableRequirement)
  
  
  # TODO(wickman) Because we explicitly acknowledge all implementations of Resolvable here,
  # perhaps move away from a registry pattern and integrate into Resolvable classmethod.
  def resolvables_from_iterable(iterable, builder, interpreter=None):
    """Given an iterable of resolvable-like objects, return list of Resolvable objects.
  
    :param iterable: An iterable of :class:`Resolvable`, :class:`Requirement`, :class:`Package`,
        or `str` to map into an iterable of :class:`Resolvable` objects.
    :returns: A list of :class:`Resolvable` objects.
    """
  
    def translate(obj):
      if isinstance(obj, Resolvable):
        return obj
      elif isinstance(obj, Requirement):
        return ResolvableRequirement(obj, builder.build(obj.key))
      elif isinstance(obj, Package):
        return ResolvablePackage(obj, builder.build(obj.name))
      elif isinstance(obj, compatibility_string):
        return Resolvable.get(obj, builder, interpreter=interpreter)
      else:
        raise ValueError('Do not know how to resolve %s' % type(obj))
    return list(map(translate, iterable))
  