|
26 | 26 | into) is the first in the list. |
27 | 27 | BACKEND_WARNING : str or None |
28 | 28 | The warning to be issued upon trying to create an interactive or |
29 | | - animated plot, if any. This is set under two conditions: |
| 29 | + animated plot, if any. This is set under three conditions: |
30 | 30 | 1. No compatible interactive backends are available |
31 | | - 2. Hypertools was imported into a notebook and the notebook-native |
32 | | - interactive backend (nbAgg) is not available. This should never |
| 31 | + 2. Hypertools was imported into a notebook that uses the "classic" |
| 32 | + notebook JS frontend, and the notebook-native interactive |
| 33 | + plotting backend (nbAgg) is not available. This should never |
33 | 34 | happen, but theoretically could if the |
34 | 35 | `ipython`/`jupyter`/`jupyter-core`/`notebook` installation is |
35 | 36 | faulty. |
| 37 | + 3. Hypertools was imported into a notebook that uses the |
| 38 | + JupyterLab JS frontend, the notebook server was launched from a |
| 39 | + different Python environment than is used by the IPython |
| 40 | + kernel, and the "ipympl" (a.k.a. "widget") interactive plotting |
| 41 | + backend is likely to not work properly because the `ipympl` |
| 42 | + package is not installed in the server environment. `ipympl` is |
| 43 | + a dependency of Hypertools, but when the notebook kernel and |
| 44 | + server environments are different, (a compatible version of) it |
| 45 | + must also be installed in the server environment to provide the |
| 46 | + various JS components needed to display interactive plots. |
36 | 47 | HYPERTOOLS_BACKEND : str |
37 | 48 | The `matplotlib` backend used to create interactive and animated |
38 | 49 | plots. |
|
73 | 84 | import inspect |
74 | 85 | import os |
75 | 86 | import shlex |
| 87 | +import shutil |
76 | 88 | import sys |
77 | 89 | import traceback |
78 | 90 | import warnings |
@@ -542,20 +554,91 @@ def _init_backend(): |
542 | 554 |
|
543 | 555 | else: |
544 | 556 | IS_NOTEBOOK = True |
545 | | - # if running in a notebook, should almost always use nbAgg. May |
546 | | - # eventually let user override this with environment variable |
547 | | - # (e.g., to use ipympl, widget, or WXAgg in JupyterLab), but for |
548 | | - # now this can be changed manually with |
549 | | - # `hypertools.set_interactive_backend` or the `mpl_backend` |
550 | | - # kwarg to `hypertools.plot` |
| 557 | + # if running in a notebook, the backend to use depends on the |
| 558 | + # user's Jupyter version and frontend. In "classic" Jupyter |
| 559 | + # notebooks (i.e., notebook < 7.0), use `nbAgg` since it's the |
| 560 | + # most stable, best supported, available out-of-the-box, etc. |
| 561 | + # However, that backend no longer works with the new JupyterLab |
| 562 | + # JS frontend (which is also used by notebook >= 7.0), so in |
| 563 | + # those cases, use the "ipympl"/"widget" plotting backend |
| 564 | + # instead. |
| 565 | + # |
| 566 | + # The user can technically override this by setting the |
| 567 | + # HYPERTOOLS_BACKEND environment variable, but for now this is |
| 568 | + # undocumented and the two officially supported methods of |
| 569 | + # changing the backend hypertools uses for interactive plots are |
| 570 | + # to (1) manually set the desired backend after importing |
| 571 | + # hypertools with `hypertools.set_interactive_backend`, or (2) |
| 572 | + # pass the desired backend identifier to the `mpl_backend` kwarg |
| 573 | + # of `hypertools.plot` |
| 574 | + # |
| 575 | + # Note: the "ipympl"/"widget" backend requires the `ipympl` |
| 576 | + # library be installed in both the notebook kernel environment |
| 577 | + # AND the notebook server environment, if the two aren't the |
| 578 | + # same. The former is guaranteed by hypertools's dependencies, |
| 579 | + # but the latter is not, so we must check for it manually. |
| 580 | + notebook_frontend = _get_jupyter_frontend() |
| 581 | + if notebook_frontend == 'lab': |
| 582 | + notebook_backend = 'module://ipympl.backend_nbagg' |
| 583 | + else: |
| 584 | + notebook_backend = 'nbAgg' |
| 585 | + |
551 | 586 | try: |
552 | | - mpl.use('nbAgg') |
553 | | - working_backend = 'nbAgg' |
554 | | - except ImportError: |
555 | | - BACKEND_WARNING = ("Failed to switch to interactive notebook " |
556 | | - "backend ('nbAgg'). Falling back to inline " |
557 | | - "static plots.") |
| 587 | + mpl.use(notebook_backend) |
| 588 | + except (ImportError, ModuleNotFoundError): |
| 589 | + BACKEND_WARNING = ( |
| 590 | + "Failed to switch to interactive notebook backend " |
| 591 | + f"('{notebook_backend}'). Falling back to inline static plots." |
| 592 | + ) |
558 | 593 | working_backend = 'inline' |
| 594 | + else: |
| 595 | + working_backend = notebook_backend |
| 596 | + if notebook_frontend == 'lab': |
| 597 | + # if the notebook uses the JupyterLab JS frontend and |
| 598 | + # the ipympl plotting backend was successfully found in |
| 599 | + # the notebook kernel environment (`try` block above), |
| 600 | + # determine whether the notebook server environment is |
| 601 | + # the same or different |
| 602 | + kernel_python = sys.executable |
| 603 | + server_python = shutil.which('python') |
| 604 | + if server_python != kernel_python: |
| 605 | + # if they're different, check whether `ipympl` is |
| 606 | + # installed in the server environment |
| 607 | + import IPython # guaranteed to be installed at this point |
| 608 | + |
| 609 | + with open(os.devnull, 'w') as devnull, redirect_stdout(devnull): |
| 610 | + retcode = IPython.utils.process.system('pip show ipympl') |
| 611 | + if retcode != 0: |
| 612 | + # NOTE: this is not currently checked, but the |
| 613 | + # `ipympl` versions installed in the server and |
| 614 | + # kernel environments must also be compatible |
| 615 | + # with each other. See |
| 616 | + # https://matplotlib.org/ipympl/installing.html#compatibility-table |
| 617 | + # NOTE: this check is imperfect and will result |
| 618 | + # in a false positive if the user has installed |
| 619 | + # just the carved-off JS components from |
| 620 | + # `ipympl` via the `jupyter-matplotlib` |
| 621 | + # extension instead of installing the full |
| 622 | + # package. We *could* account for this by |
| 623 | + # additionally checking the output of |
| 624 | + # `jupyter labextension check jupyter-matplotlib`, |
| 625 | + # but then we'd get into differentiating |
| 626 | + # the extension not being installed at all vs. |
| 627 | + # being installed but not enabled, etc. |
| 628 | + # Ultimately this is a pretty minor edge case |
| 629 | + # and the cost of a false positive is pretty |
| 630 | + # low (a harmless warning message IF the user |
| 631 | + # creates an interactive plot), so IMO it's |
| 632 | + # not worth the extra overhead of running more |
| 633 | + # shell commands every time hypertools is |
| 634 | + # imported. |
| 635 | + BACKEND_WARNING = ( |
| 636 | + "The `ipympl` package is not installed in the " |
| 637 | + "Jupyter server's environment. Interactive and " |
| 638 | + "animated plots may not appear. To fix this, " |
| 639 | + "pip-install `ipympl` and restart the Jupyer " |
| 640 | + "server." |
| 641 | + ) |
559 | 642 |
|
560 | 643 | switch_backend = _switch_backend_notebook |
561 | 644 | reset_backend = _reset_backend_notebook |
|
0 commit comments