Repository: pantsbuild/pants

Issue:
We should export a `pants_contrib_requirement` in the spirit of `pants_requirement`.
This makes plugin authors lives easier. The new macro should also support environment markers as outlined in #6358.


Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/python/pants_requirement.py b/src/python/pants/backend/python/pants_requirement.py
--- a/src/python/pants/backend/python/pants_requirement.py
+++ b/src/python/pants/backend/python/pants_requirement.py
@@ -9,6 +9,9 @@
 
 from pants.backend.python.python_requirement import PythonRequirement
 from pants.base.build_environment import pants_version
+from pants.base.exceptions import TargetDefinitionException
+from pants.build_graph.address import Address
+from pants.util.meta import classproperty
 
 
 class PantsRequirement(object):
@@ -24,21 +27,37 @@ class PantsRequirement(object):
   :API: public
   """
 
+  @classproperty
+  def alias(self):
+    return 'pants_requirement'
+
   def __init__(self, parse_context):
     self._parse_context = parse_context
 
-  def __call__(self, name=None):
+  def __call__(self, name=None, dist=None):
     """
-    :param string name: The name to use for the target, defaults to the parent dir name.
+    :param string name: The name to use for the target, defaults to the dist name if specified and
+                        otherwise the parent dir name.
+    :param string dist: The pants dist to create a requirement for. This must be a
+                        'pantsbuild.pants*' distribution; eg:
+                        'pantsbuild.pants.contrib.python.checks'.
     """
-    name = name or os.path.basename(self._parse_context.rel_path)
+    name = name or dist or os.path.basename(self._parse_context.rel_path)
+    dist = dist or 'pantsbuild.pants'
+    if not (dist == 'pantsbuild.pants' or dist.startswith('pantsbuild.pants.')):
+      target = Address(spec_path=self._parse_context.rel_path, target_name=name)
+      raise TargetDefinitionException(target=target,
+                                      msg='The {} target only works for pantsbuild.pants '
+                                          'distributions, given {}'.format(self.alias, dist))
 
     # TODO(John Sirois): Modify to constraint to >=3.5,<4 as part of
     # https://github.com/pantsbuild/pants/issues/6062
     env_marker = "python_version>='2.7' and python_version<'3'"
 
-    requirement = PythonRequirement(requirement="pantsbuild.pants=={version} ; {env_marker}"
-                                    .format(version=pants_version(), env_marker=env_marker))
+    requirement = PythonRequirement(requirement="{key}=={version} ; {env_marker}"
+                                    .format(key=dist,
+                                            version=pants_version(),
+                                            env_marker=env_marker))
 
     self._parse_context.create_object('python_requirement_library',
                                       name=name,
diff --git a/src/python/pants/backend/python/register.py b/src/python/pants/backend/python/register.py
--- a/src/python/pants/backend/python/register.py
+++ b/src/python/pants/backend/python/register.py
@@ -53,7 +53,7 @@ def build_file_aliases():
     },
     context_aware_object_factories={
       'python_requirements': PythonRequirements,
-      'pants_requirement': PantsRequirement,
+      PantsRequirement.alias: PantsRequirement,
     }
   )
 


Test patch:
diff --git a/tests/python/pants_test/backend/python/test_pants_requirement.py b/tests/python/pants_test/backend/python/test_pants_requirement.py
--- a/tests/python/pants_test/backend/python/test_pants_requirement.py
+++ b/tests/python/pants_test/backend/python/test_pants_requirement.py
@@ -8,6 +8,7 @@
 from pants.backend.python.register import build_file_aliases
 from pants.backend.python.targets.python_requirement_library import PythonRequirementLibrary
 from pants.base.build_environment import pants_version
+from pants.build_graph.address_lookup_error import AddressLookupError
 from pants_test.test_base import TestBase
 
 
@@ -17,9 +18,10 @@ def alias_groups(cls):
     # NB: We use aliases and BUILD files to test proper registration of the pants_requirement macro.
     return build_file_aliases()
 
-  def assert_pants_requirement(self, python_requirement_library):
+  def assert_pants_requirement(self, python_requirement_library, expected_dist='pantsbuild.pants'):
     self.assertIsInstance(python_requirement_library, PythonRequirementLibrary)
-    expected = PythonRequirement('pantsbuild.pants=={}'.format(pants_version()))
+    expected = PythonRequirement('{key}=={version}'
+                                 .format(key=expected_dist, version=pants_version()))
 
     def key(python_requirement):
       return (python_requirement.requirement.key,
@@ -48,3 +50,34 @@ def test_custom_name(self):
 
     python_requirement_library = self.target('3rdparty/python/pants:pantsbuild.pants')
     self.assert_pants_requirement(python_requirement_library)
+
+  def test_dist(self):
+    self.add_to_build_file('3rdparty/python/pants', "pants_requirement(dist='pantsbuild.pants')")
+
+    python_requirement_library = self.target('3rdparty/python/pants:pantsbuild.pants')
+    self.assert_pants_requirement(python_requirement_library)
+
+  def test_contrib(self):
+    self.add_to_build_file('3rdparty/python/pants',
+                           "pants_requirement(dist='pantsbuild.pants.contrib.bob')")
+
+    python_requirement_library = self.target('3rdparty/python/pants:pantsbuild.pants.contrib.bob')
+    self.assert_pants_requirement(python_requirement_library,
+                                  expected_dist='pantsbuild.pants.contrib.bob')
+
+  def test_custom_name_contrib(self):
+    self.add_to_build_file('3rdparty/python/pants',
+                           "pants_requirement(name='bob', dist='pantsbuild.pants.contrib.bob')")
+
+    python_requirement_library = self.target('3rdparty/python/pants:bob')
+    self.assert_pants_requirement(python_requirement_library,
+                                  expected_dist='pantsbuild.pants.contrib.bob')
+
+  def test_bad_dist(self):
+    self.add_to_build_file('3rdparty/python/pants',
+                           "pants_requirement(name='jane', dist='pantsbuild.pantsish')")
+
+    with self.assertRaises(AddressLookupError):
+      # The pants_requirement should raise on the invalid dist name of pantsbuild.pantsish making
+      # the target at 3rdparty/python/pants:jane fail to exist.
+      self.target('3rdparty/python/pants:jane')