Repository: scikit-learn/scikit-learn
Pull request: [MRG] Fixes cross-reference and no reference issues in sphinx
<!--
Thanks for contributing a pull request! Please ensure you have taken a look at
the contribution guidelines: https://github.com/scikit-learn/scikit-learn/blob/master/CONTRIBUTING.md#pull-request-checklist
-->

#### Reference Issues/PRs
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->
Fixes https://github.com/scikit-learn/scikit-learn/issues/12802

#### What does this implement/fix? Explain your changes.
Overrides the default `ReferencesResolver` with a `CustomReferencesResolver`. The custom resolver does two things:

1. If running in the context of a python class, it places the `py` domain above the `std` domain, allowing class methods to be referenced first.
2. Removes the logging when there is more than one reference.

#### Any other comments?
This together with https://github.com/scikit-learn/scikit-learn/pull/12970 should resolve most of the warnings that appear in sphinx when we switched the default role to "any".

<!--
Please be aware that we are a loose team of volunteers so patience is
necessary; assistance handling other issues is very welcome. We value
all user contributions, no matter how minor they are. If we are slow to
review, either the pull request needs some benchmarking, tinkering,
convincing, etc. or more likely the reviewers are simply busy. In either
case, we ask for your understanding during the review process.
For more information, see our FAQ on this topic:
http://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->

Problem statement:
Fix 'any' doc build warnings
Changing the default role to ``any`` in sphinx created loads of warnings, which makes it hard to see actual warnings from sphinx.
We should see if we can fix this.
One way would be to use single backticks only if there is actually something to reference, the other is to somehow silence the warnings.

Hints from issue discussion:
+1 for working out how to silence the warnings. A warnings.filterwarnings
in conf.py matching the message might suffice?? Or does shpinx log its
warnings by some other mechanism?

Commit message: RFC: Adds early exit

Patch to review:
diff --git a/doc/conf.py b/doc/conf.py
index a8a2a58089dce..7b84d72c75c1a 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -38,6 +38,7 @@
     'sphinx.ext.imgconverter',
     'sphinx_gallery.gen_gallery',
     'sphinx_issues',
+    'custom_references_resolver'
 ]
 
 # this is needed for some reason...
diff --git a/doc/sphinxext/custom_references_resolver.py b/doc/sphinxext/custom_references_resolver.py
new file mode 100644
index 0000000000000..0ee49983bad47
--- /dev/null
+++ b/doc/sphinxext/custom_references_resolver.py
@@ -0,0 +1,83 @@
+"""Adapted from
+sphinx.transforms.post_transforms.ReferencesResolver.resolve_anyref
+
+If 'py' is one of the domains and `py:class` is defined,
+the Python domain will be processed before the 'std' domain.
+"""
+from contextlib import suppress
+
+from docutils import nodes
+from sphinx.transforms.post_transforms import ReferencesResolver
+
+
+class CustomReferencesResolver(ReferencesResolver):
+    def resolve_anyref(self, refdoc, node, contnode):
+        """Resolve reference generated by the "any" role."""
+        stddomain = self.env.get_domain('std')
+        target = node['reftarget']
+
+        # first, try resolving as :doc:
+        doc_ref = stddomain.resolve_xref(self.env, refdoc, self.app.builder,
+                                         'doc', target, node, contnode)
+        if doc_ref:
+            return self.create_node(('doc', doc_ref))
+
+        # process 'py' domain first for python classes
+        if "py:class" in node:
+            with suppress(KeyError):
+                py_domain = self.env.domains['py']
+                py_ref = py_domain.resolve_any_xref(
+                    self.env, refdoc, self.app.builder, target, node, contnode)
+                if py_ref:
+                    return self.create_node(py_ref[0])
+
+        # next, do the standard domain (makes this a priority)
+        std_ref = stddomain.resolve_any_xref(
+            self.env, refdoc, self.app.builder, target, node, contnode)
+        if std_ref:
+            return self.create_node(std_ref[0])
+
+        for domain in self.env.domains.values():
+            try:
+                ref = domain.resolve_any_xref(
+                    self.env, refdoc, self.app.builder, target, node, contnode)
+                if ref:
+                    return self.create_node(ref[0])
+            except NotImplementedError:
+                # the domain doesn't yet support the new interface
+                # we have to manually collect possible references (SLOW)
+                for role in domain.roles:
+                    res = domain.resolve_xref(self.env, refdoc,
+                                              self.app.builder, role, target,
+                                              node, contnode)
+                    if res and isinstance(res[0], nodes.Element):
+                        result = ('%s:%s' % (domain.name, role), res)
+                        return self.create_node(result)
+        # no results
+        return None
+
+    def create_node(self, result):
+        res_role, newnode = result
+        # Override "any" class with the actual role type to get the styling
+        # approximately correct.
+        res_domain = res_role.split(':')[0]
+        if (len(newnode) > 0 and isinstance(newnode[0], nodes.Element)
+                and newnode[0].get('classes')):
+            newnode[0]['classes'].append(res_domain)
+            newnode[0]['classes'].append(res_role.replace(':', '-'))
+        return newnode
+
+
+def setup(app):
+
+    if (hasattr(app.registry, "get_post_transforms")
+            and callable(app.registry.get_post_transforms)):
+        post_transforms = app.registry.get_post_transforms()
+    else:
+        # Support sphinx 1.6.*
+        post_transforms = app.post_transforms
+
+    for i, transform_class in enumerate(post_transforms):
+        if transform_class == ReferencesResolver:
+            post_transforms[i] = CustomReferencesResolver
+            break
diff --git a/examples/cluster/plot_dict_face_patches.py b/examples/cluster/plot_dict_face_patches.py
index 6d33f01e6a7cb..ac144e7213cc5 100644
--- a/examples/cluster/plot_dict_face_patches.py
+++ b/examples/cluster/plot_dict_face_patches.py
@@ -9,7 +9,8 @@
 to use the online API of the scikit-learn to process a very large
 dataset by chunks. The way we proceed is that we load an image at a time
 and extract randomly 50 patches from this image. Once we have accumulated
-500 of these patches (using 10 images), we run the `partial_fit` method
+500 of these patches (using 10 images), we run the
+:func:`~sklearn.cluster.MiniBatchKMeans.partial_fit` method
 of the online KMeans object, MiniBatchKMeans.
 
 The verbose setting on the MiniBatchKMeans enables us to see that some


Code review comments:
diff_hunk: @@ -0,0 +1,83 @@
+"""Adapted from
+sphinx.transforms.post_transforms.ReferencesResolver.resolve_anyref
+
+If 'py' is one of the domains and `py:class` is defined,
+the Python domain will be processed before the 'std' domain.
+"""
+from contextlib import suppress
+
+from docutils import nodes
+from sphinx.transforms.post_transforms import ReferencesResolver
+
+
+class CustomReferencesResolver(ReferencesResolver):
+    def resolve_anyref(self, refdoc, node, contnode):
+        """Resolve reference generated by the "any" role."""
+        stddomain = self.env.get_domain('std')
+        target = node['reftarget']
+
+        # first, try resolving as :doc:
+        doc_ref = stddomain.resolve_xref(self.env, refdoc, self.app.builder,
path: doc/sphinxext/custom_references_resolver.py
text: @user1:
I'm not sure we really ever want to link to `:doc:`, do we?