Skip to content

Conversation

@luismavs
Copy link
Contributor

@luismavs luismavs commented Jul 18, 2025

Allow execution without plotly.

Fixes #2398

Handles calls to .visualize... methods with a pass and a logger.warning

Before submitting

  • This PR fixes a typo or improves the docs (if yes, ignore all other checks!).
  • Did you read the contributor guideline?
  • Was this discussed/approved via a Github issue? Please add a link to it if that's the case.
  • Did you make sure to update the documentation with your changes (if applicable)?
  • Did you write any new necessary tests?

Copy link
Owner

@MaartenGr MaartenGr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR! I left some comments here and there. Most importantly, I think this solution is more complex than is currently needed. We only need to mock the plotting as we can do the type-hinting of go.Figure like so: "go.Figure".

As an example, we can reduce the complexity to this:

import importlib.util
import logging
from typing import Any


class MockPlotlyModule:
    """Mock module that raises informative errors when plotly functions are called."""
  
    def __getattr__(self, name: str) -> Any:
        def mock_function(*args, **kwargs):
            raise ImportError(
                f"Plotly is required to use '{name}'. "
                "Install it with: pip install plotly"
            )
        return mock_function

# Clean import logic using importlib
if importlib.util.find_spec("plotly") is None:
    logger.warning("Plotly is not installed. Plotting functions will raise errors when called.")
    
    # Mock modules
    plotting, go = MockPlotlyModule(), None
    
else:
    from bertopic import plotting
    import plotly.graph_objects as go

I'm also wondering if we should do the logger.warning here as we cannot control that verbosity. There might be use-cases where you do not want that logging.


except ModuleNotFoundError as e:
if "No module named 'plotly'" in str(e):
logger.warning("Plotly is not installed. Please install it to use the plotting functions.")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps also add a way to install it? A one-liner would suffice, something like "For example with pip install plotly or uv pip install plotly". That way, users would not have to search how to install plotly but can do it immediately.

import plotly.graph_objects as go

except ModuleNotFoundError as e:
if "No module named 'plotly'" in str(e):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether this is a good enough check to see whether plotly is installed. Checking the string might be unstable. Perhaps use importlib instead?

plotly_available = importlib.util.find_spec("plotly") is not None

Comment on lines 238 to 243
def __getattr__(self, name):
def mock_function(*args, **kwargs):
self.logger.warning(f"Plotly is not installed. Cannot use {name} visualization function.")
return MockFigure()

return mock_function
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's simplify this by raising an error instead.

Comment on lines 246 to 250
class MockFigure:
"""Mock class for plotly.graph_objects.Figure when plotly is not installed."""

def __init__(self, *args, **kwargs):
pass
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary when we can use def some_function(self) -> "go.Figure" instead. This would reduce the code quite a bit.

Copy link
Contributor Author

@luismavs luismavs Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Will use plotly.graph_objs instead of plotly.graph_objects because only the later is available when typing.TYPE_CHECKING is True.

@luismavs
Copy link
Contributor Author

luismavs commented Aug 5, 2025

Thank you for the PR! I left some comments here and there. Most importantly, I think this solution is more complex than is currently needed. We only need to mock the plotting as we can do the type-hinting of go.Figure like so: "go.Figure".

As an example, we can reduce the complexity to this:

import importlib.util
import logging
from typing import Any


class MockPlotlyModule:
    """Mock module that raises informative errors when plotly functions are called."""
  
    def __getattr__(self, name: str) -> Any:
        def mock_function(*args, **kwargs):
            raise ImportError(
                f"Plotly is required to use '{name}'. "
                "Install it with: pip install plotly"
            )
        return mock_function

I like this overall approach, simpler indeed.

The visualize methods now return string literal type hint "go.Figure" which works fine (as far as the type checker goes) in both cases - with and without plotly installed.

I've also gone ahead and fixed visualize_document_datamap method too, to have return type hint matplotlib Figure

Clean import logic using importlib

if importlib.util.find_spec("plotly") is None:
logger.warning("Plotly is not installed. Plotting functions will raise errors when called.")

Fine. For consistency, the check for HDBSCAN on line 41 should use something similar.

# Mock modules
plotting, go = MockPlotlyModule(), None

else:
from bertopic import plotting
import plotly.graph_objects as go


I'm also wondering if we should do the logger.warning here as we cannot control that verbosity. There might be use-cases where you do not want that logging.

I'm ok with removing that logger.warning - there is already a clear exception if the user tries to use visualizations without plotly present. Also, ideally BERTopic should have a way to configure the log level more dynamically rather than the hardcoded WARNING level.

@MaartenGr
Copy link
Owner

Apologies for the delay and thank you for the updated code. I agree with the logger, it should be handled a bit more elegantly than it is now (mostly handled in the main class and not easily changed). Either way, I'll go ahead and merge this, so thank you for your work on this. It is greatly appreciated!

@MaartenGr MaartenGr merged commit 7724551 into MaartenGr:master Aug 17, 2025
6 checks passed
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 this pull request may close these issues.

Allow execution without plotly

2 participants