Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intersphinx Mapping link not working with Plotly objects in signature #473

Closed
jeertmans opened this issue Aug 26, 2024 · 9 comments · Fixed by #474
Closed

Intersphinx Mapping link not working with Plotly objects in signature #473

jeertmans opened this issue Aug 26, 2024 · 9 comments · Fixed by #474

Comments

@jeertmans
Copy link

jeertmans commented Aug 26, 2024

The issue was originally posted on sphinx-doc/sphinx#12360 (comment), and they suggested that I post my issue here.

Hello,

For some (unknown) reasons, it appears that Sphinx does not correctly link to the objects if they were defined in the signature. I would love to have all my external references actually work.

The following code

def process_plotly_kwargs(
    kwargs: MutableMapping[str, Any],
) -> Figure:
    """
    Process keyword arguments passed to some Plotly plotting utility.

    Args:
        kwargs: A mutable mapping of keyword arguments passed to
            Plotly plotting.

            .. warning::

                The keys specified below will be removed from the mapping.

    Keyword Args:
        figure (:py:class:`Figure<plotly.graph_objects.Figure>`):
            The figure that draws contents of the scene.

    Return:
        The figure used to display contents.
    """

produces:

image

As you can see, the handwritten reference to Figure is clickable, but not the one from the signature.

This is also in other online documentation, see plot_terminator_improvement from the Neptuna Python package:

image

In this function, I return one of three possible plot types, and we can clearly see that the only one failing is the Plotly one:

@contextmanager
def reuse(**kwargs: Any) -> Iterator[SceneCanvas | MplFigure | Figure]:
    """Create a context manager that will automatically reuse the current canvas / figure.

    Args:
        args: Positional arguments passed to
            :py:func:`set_defaults`.
        kwargs: Keywords arguments passed to
            :py:func:`set_defaults`.

    Return:
        The canvas or figure that is reused for this context.
    """
    ...

image

See original doc: https://differt.eertmans.be/v0.0.12/reference/differt.plotting.html#differt.plotting.process_plotly_kwargs

@hoodmane
Copy link
Collaborator

Are there supposed to be images in this?

@jeertmans
Copy link
Author

Are there supposed to be images in this?

Yes… they rendered correctly in my web browser, don’t know why it does not here. I will re-upload them

@hoodmane
Copy link
Collaborator

Could you also provide your conf.py or at least the sphinx-autodoc-typehints settings and the intersphinx settings? It would be good to have a reproducer.

@hoodmane
Copy link
Collaborator

You should set nitpicky = True in your sphinx conf.py, then it will warn about broken references. In this case, it will say:

WARNING: py:class reference target not found: plotly.graph_objs._figure.Figure

The problem is that Figure is defined in plotly.graph_objs._figure but it is documented as being exported from plotly.graph_objs, so Figure.__module__ is plotly.graph_objs._figure and introspection gives the wrong path. One possible solution would be to fix up the path like so:

--- a/packages/python/plotly/_plotly_utils/importers.py
+++ b/packages/python/plotly/_plotly_utils/importers.py
@@ -34,7 +34,10 @@ def relative_import(parent_name, rel_modules=(), rel_classes=()):
             rel_module = ".".join(rel_path_parts[:-1])
             class_name = import_name
             class_module = importlib.import_module(rel_module, parent_name)
-            return getattr(class_module, class_name)
+            cls = getattr(class_module, class_name)
+            cls.__module__ = parent_name
+            return cls
+
 
         raise AttributeError(
             "module {__name__!r} has no attribute {name!r}".format(

@jeertmans
Copy link
Author

Could you also provide your conf.py or at least the sphinx-autodoc-typehints settings and the intersphinx settings? It would be good to have a reproducer.

Sure, you can find the original conf.py file here: https://github.com/jeertmans/DiffeRT/blob/v0.0.12/docs/source/conf.py

@jeertmans
Copy link
Author

You should set nitpicky = True in your sphinx conf.py, then it will warn about broken references. In this case, it will say:

WARNING: py:class reference target not found: plotly.graph_objs._figure.Figure

The problem is that Figure is defined in plotly.graph_objs._figure but it is documented as being exported from plotly.graph_objs, so Figure.__module__ is plotly.graph_objs._figure and introspection gives the wrong path. One possible solution would be to fix up the path like so:

--- a/packages/python/plotly/_plotly_utils/importers.py
+++ b/packages/python/plotly/_plotly_utils/importers.py
@@ -34,7 +34,10 @@ def relative_import(parent_name, rel_modules=(), rel_classes=()):
             rel_module = ".".join(rel_path_parts[:-1])
             class_name = import_name
             class_module = importlib.import_module(rel_module, parent_name)
-            return getattr(class_module, class_name)
+            cls = getattr(class_module, class_name)
+            cls.__module__ = parent_name
+            return cls
+
 
         raise AttributeError(
             "module {__name__!r} has no attribute {name!r}".format(

Oh I see, but then I would need to patch Plotly's code, not mine. But thanks for helping! I guess the way they import classes is quite convoluted...

@hoodmane
Copy link
Collaborator

hoodmane commented Aug 28, 2024

Maybe you should open an issue on plotly about this. I personally think it is a bug in my libraries if SomeClass.__module__ doesn't point to the public, documented import path for SomeClass.

But inevitably there are going to be a lot of libraries that define objects in private modules and reexport them without fixing up their names like this. On our side, I think it would make sense to add a hook to sphinx_autodoc_typehints that can fix up the module name. So we could do something like

module = module_name_fixup_hook(get_annotation_module(annotation))

and module_name_fixup_hook can default to the identity function.

@hoodmane
Copy link
Collaborator

hoodmane commented Aug 28, 2024

Looks like function configs are not very well supported by sphinx's caching system though, so setting such a configuration option to a nondefault value would mean that it always rebuilds.
sphinx-doc/sphinx#12300

hoodmane added a commit to hoodmane/sphinx-autodoc-typehints that referenced this issue Aug 28, 2024
This adds a configuration option that allows the user to rewrite module names if
needed. Resolves tox-dev#473. Note that we also rewrite the names of `_io` and
`typing_extensions` internally. One benefit of this hook is that if other
similar rewrites are needed, users will be able to immediately add them without
having to patch sphinx_autodoc_typehints itself.

One disadvantage is that by default, having a function in the config prevents
caching. I think this can be handled with the following slightly inelegant hack:

```py
class ModuleNameRewriteHook:
   version: int

   def __eq__(self, other):
      return type(self) == type(other) and self.version == other.version

   def __init__(self):
      self.version = 2

   def __call__(self, module):
      # logic here
      # Make sure to bump version if you edit this so that sphinx will rerun.
      return module

typehints_fixup_module_name = ModuleNameRewriteHook
```

See sphinx-doc/sphinx#12300.
hoodmane added a commit to hoodmane/sphinx-autodoc-typehints that referenced this issue Aug 28, 2024
This adds a configuration option that allows the user to rewrite module names if
needed. Resolves tox-dev#473. Note that we also rewrite the names of `_io` and
`typing_extensions` internally. One benefit of this hook is that if other
similar rewrites are needed, users will be able to immediately add them without
having to patch sphinx_autodoc_typehints itself.

One disadvantage is that by default, having a function in the config prevents
caching. I think this can be handled with the following slightly inelegant hack:

```py
class ModuleNameRewriteHook:
   version: int

   def __eq__(self, other):
      return type(self) == type(other) and self.version == other.version

   def __init__(self):
      self.version = 2

   def __call__(self, module):
      # logic here
      # Make sure to bump version if you edit this so that sphinx will rerun.
      return module

typehints_fixup_module_name = ModuleNameRewriteHook
```

See sphinx-doc/sphinx#12300.
@jeertmans
Copy link
Author

Thanks for your help @hoodmane :-) !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants