Repository: sloria/sphinx-issues

Issue:
Add :pr: role as alias for :issue:?
Some contributors to scikit-learn have presumed that in the changelog `:issue:` is strictly an issue, while often we would rather reference PRs (this helps us checking that everything is covered in the changelog). Perhaps (optionally) adding a `:pr:` alias would be a good idea.

Comments:

Patch:
diff --git a/README.rst b/README.rst
index a7e7aad..efa129f 100644
--- a/README.rst
+++ b/README.rst
@@ -5,20 +5,22 @@ sphinx-issues
 .. image:: https://travis-ci.org/sloria/sphinx-issues.svg?branch=master
     :target: https://travis-ci.org/sloria/sphinx-issues
 
-A Sphinx extension for linking to your project's issue tracker. Includes roles for linking to both issues and user profiles, with built-in support for GitHub (though this works with other services).
+A Sphinx extension for linking to your project's issue tracker. Includes roles for linking to issues, pull requests, user profiles, with built-in support for GitHub (though this works with other services).
 
 Example
 *******
 
-For an example usage, check out `marshmallow's changelog <http://marshmallow.readthedocs.org/en/latest/changelog.html#changelog>`_, which makes use of the roles in this library.
+For an example usage, check out `marshmallow's changelog <http://marshmallow.readthedocs.org/en/latest/changelog.html>`_, which makes use of the roles in this library.
 
 Installation and Configuration
 ******************************
-::
 
-    $ pip install sphinx-issues
+.. code-block:: console
 
-Add ``sphinx_issues`` to ``extensions`` in your ``conf.py``. If your project is on Github, add the ``issues_github_path`` config variable. Otherwise, use ``issues_uri``.
+    pip install sphinx-issues
+
+
+Add ``sphinx_issues`` to ``extensions`` in your ``conf.py``. If your project is on GitHub, add the ``issues_github_path`` config variable. Otherwise, use ``issues_uri`` and ``issues_pr_uri``.
 
 .. code-block:: python
 
@@ -35,11 +37,12 @@ Add ``sphinx_issues`` to ``extensions`` in your ``conf.py``. If your project is
 
     # equivalent to
     issues_uri = 'https://github.com/sloria/marshmallow/issues/{issue}'
+    issues_pr_uri = 'https://github.com/sloria/marshmallow/pull/{pr}'
 
 Usage
 *****
 
-Use the ``:issue:`` role in your docs like so:
+Use the ``:issue:``  and ``:pr:`` roles in your docs like so:
 
 .. code-block:: rst
 
@@ -47,6 +50,8 @@ Use the ``:issue:`` role in your docs like so:
 
     See issues :issue:`12,13`
 
+    See PR :pr:`58`
+
 
 Use the ``:user:`` role in your docs to link to user profiles (Github by default, but can be configured via the ``issues_user_uri`` config variable).
 
@@ -77,6 +82,7 @@ Changelog
 1.0.0 (unreleased)
 ------------------
 
+- Add ``:pr:`` role. Thanks @jnotham for the suggestion.
 - Drop support for Python 3.4.
 
 0.4.0 (2017-11-25)
diff --git a/sphinx_issues.py b/sphinx_issues.py
index 24b9386..532858e 100644
--- a/sphinx_issues.py
+++ b/sphinx_issues.py
@@ -41,59 +41,89 @@ def user_role(name, rawtext, text, lineno,
     link = nodes.reference(text=text, refuri=ref, **options)
     return [link], []
 
-
-def _make_issue_node(issue_no, config, options=None):
-    options = options or {}
-    if issue_no not in ('-', '0'):
-        if config.issues_uri:
-            ref = config.issues_uri.format(issue=issue_no)
-        elif config.issues_github_path:
-            ref = 'https://github.com/{0}/issues/{1}'.format(
-                config.issues_github_path, issue_no
-            )
+class IssueRole(object):
+    def __init__(self, uri_config_option, format_kwarg, github_uri_template):
+        self.uri_config_option = uri_config_option
+        self.format_kwarg = format_kwarg
+        self.github_uri_template = github_uri_template
+
+    def make_node(self, issue_no, config, options=None):
+        options = options or {}
+        if issue_no not in ('-', '0'):
+            uri_template = getattr(config, self.uri_config_option, None)
+            if uri_template:
+                ref = uri_template.format(**{self.format_kwarg: issue_no})
+            elif config.issues_github_path:
+                ref = self.github_uri_template.format(
+                    issues_github_path=config.issues_github_path,
+                    n=issue_no,
+                )
+            else:
+                raise ValueError(
+                    'Neither {} nor issues_github_path '
+                    'is set'.format(self.uri_config_option)
+                )
+            issue_text = '#{0}'.format(issue_no)
+            link = nodes.reference(text=issue_text, refuri=ref, **options)
         else:
-            raise ValueError('Neither issues_uri nor issues_github_path is set')
-        issue_text = '#{0}'.format(issue_no)
-        link = nodes.reference(text=issue_text, refuri=ref, **options)
-    else:
-        link = None
-    return link
-
-
-def issue_role(name, rawtext, text, lineno,
-               inliner, options=None, content=None):
-    """Sphinx role for linking to an issue. Must have
-    `issues_uri` or `issues_github_path` configured in ``conf.py``.
-
-    Examples: ::
-
-        :issue:`123`
-        :issue:`42,45`
-    """
-    options = options or {}
-    content = content or []
-    issue_nos = [each.strip() for each in utils.unescape(text).split(',')]
-    config = inliner.document.settings.env.app.config
-    ret = []
-    for i, issue_no in enumerate(issue_nos):
-        node = _make_issue_node(issue_no, config, options=options)
-        ret.append(node)
-        if i != len(issue_nos) - 1:
-            sep = nodes.raw(text=', ', format='html')
-            ret.append(sep)
-    return ret, []
+            link = None
+        return link
+
+    def __call__(self, name, rawtext, text, lineno,
+                 inliner, options=None, content=None):
+        options = options or {}
+        content = content or []
+        issue_nos = [each.strip() for each in utils.unescape(text).split(',')]
+        config = inliner.document.settings.env.app.config
+        ret = []
+        for i, issue_no in enumerate(issue_nos):
+            node = self.make_node(issue_no, config, options=options)
+            ret.append(node)
+            if i != len(issue_nos) - 1:
+                sep = nodes.raw(text=', ', format='html')
+                ret.append(sep)
+        return ret, []
+
+
+"""Sphinx role for linking to an issue. Must have
+`issues_uri` or `issues_github_path` configured in ``conf.py``.
+Examples: ::
+    :issue:`123`
+    :issue:`42,45`
+"""
+issue_role = IssueRole(
+    uri_config_option='issues_uri',
+    format_kwarg='issue',
+    github_uri_template='https://github.com/{issues_github_path}/issues/{n}'
+)
+
+"""Sphinx role for linking to a pull request. Must have
+`issues_pr_uri` or `issues_github_path` configured in ``conf.py``.
+Examples: ::
+    :pr:`123`
+    :pr:`42,45`
+"""
+pr_role = IssueRole(
+    uri_config_option='issues_pr_uri',
+    format_kwarg='pr',
+    github_uri_template='https://github.com/{issues_github_path}/pull/{n}'
+)
 
 
 def setup(app):
     # Format template for issues URI
     # e.g. 'https://github.com/sloria/marshmallow/issues/{issue}
     app.add_config_value('issues_uri', default=None, rebuild='html')
+    # Format template for PR URI
+    # e.g. 'https://github.com/sloria/marshmallow/pull/{issue}
+    app.add_config_value('issues_pr_uri', default=None, rebuild='html')
     # Shortcut for Github, e.g. 'sloria/marshmallow'
     app.add_config_value('issues_github_path', default=None, rebuild='html')
     # Format template for user profile URI
     # e.g. 'https://github.com/{user}'
     app.add_config_value('issues_user_uri', default=None, rebuild='html')
     app.add_role('issue', issue_role)
+    app.add_role('pr', pr_role)
     app.add_role('user', user_role)
     return {
         'version': __version__,


Test patch:
diff --git a/test_sphinx_issues.py b/test_sphinx_issues.py
index c7f466c..c45ae3c 100644
--- a/test_sphinx_issues.py
+++ b/test_sphinx_issues.py
@@ -10,6 +10,7 @@ from sphinx.application import Sphinx
 from sphinx_issues import (
     issue_role,
     user_role,
+    pr_role,
     setup as issues_setup
 )
 
@@ -18,8 +19,11 @@ import pytest
 
 @pytest.yield_fixture(params=[
     # Parametrize config
-    {'issues_github_path': 'sloria/marshmallow'},
-    {'issues_uri': 'https://github.com/sloria/marshmallow/issues/{issue}'}
+    {'issues_github_path': 'marshmallow-code/marshmallow'},
+    {
+        'issues_uri': 'https://github.com/marshmallow-code/marshmallow/issues/{issue}',
+        'issues_pr_uri': 'https://github.com/marshmallow-code/marshmallow/pull/{pr}',
+    }
 ])
 def app(request):
     src, doctree, confdir, outdir = [mkdtemp() for _ in range(4)]
@@ -57,7 +61,7 @@ def test_issue_role(inliner):
     )
     link = result[0][0]
     assert link.astext() == '#42'
-    issue_url = 'https://github.com/sloria/marshmallow/issues/42'
+    issue_url = 'https://github.com/marshmallow-code/marshmallow/issues/42'
     assert link.attributes['refuri'] == issue_url
 
 
@@ -71,7 +75,7 @@ def test_issue_role_multiple(inliner):
     )
     link1 = result[0][0]
     assert link1.astext() == '#42'
-    issue_url = 'https://github.com/sloria/marshmallow/issues/'
+    issue_url = 'https://github.com/marshmallow-code/marshmallow/issues/'
     assert link1.attributes['refuri'] == issue_url + '42'
 
     sep = result[0][1]
@@ -106,3 +110,17 @@ def test_user_role_explicit_name(inliner):
     link = result[0][0]
     assert link.astext() == 'Steven Loria'
     assert link.attributes['refuri'] == 'https://github.com/sloria'
+
+
+def test_pr_role(inliner):
+    result = pr_role(
+        name=None,
+        rawtext='',
+        text='42',
+        lineno=None,
+        inliner=inliner
+    )
+    link = result[0][0]
+    assert link.astext() == '#42'
+    issue_url = 'https://github.com/marshmallow-code/marshmallow/pull/42'
+    assert link.attributes['refuri'] == issue_url