Skip to content

Prepare to turn matplotlib.style into a plain module. #30163

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/api/next_api_changes/deprecations/30163-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
``matplotlib.style.core``
~~~~~~~~~~~~~~~~~~~~~~~~~
The ``matplotlib.style.core`` module is deprecated. All APIs intended for
public use are now available in `matplotlib.style` directly (including
``USER_LIBRARY_PATHS``, which was previously not reexported).

The following APIs of ``matplotlib.style.core`` have been deprecated with no
replacement: ``BASE_LIBRARY_PATH``, ``STYLE_EXTENSION``, ``STYLE_BLACKLIST``,
``update_user_library``, ``read_style_directory``, ``update_nested_dict``.
252 changes: 250 additions & 2 deletions lib/matplotlib/style/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,252 @@
from .core import available, context, library, reload_library, use
"""
Core functions and attributes for the matplotlib style library:

``use``
Select style sheet to override the current matplotlib settings.
``context``
Context manager to use a style sheet temporarily.
``available``
List available style sheets.
``library``
A dictionary of style names and matplotlib settings.
"""

__all__ = ["available", "context", "library", "reload_library", "use"]
import contextlib
import importlib.resources
import logging
import os
from pathlib import Path
import warnings

import matplotlib as mpl
from matplotlib import _api, _docstring, rc_params_from_file, rcParamsDefault

_log = logging.getLogger(__name__)

__all__ = ['use', 'context', 'available', 'library', 'reload_library']


_BASE_LIBRARY_PATH = os.path.join(mpl.get_data_path(), 'stylelib')
# Users may want multiple library paths, so store a list of paths.
USER_LIBRARY_PATHS = [os.path.join(mpl.get_configdir(), 'stylelib')]
_STYLE_EXTENSION = 'mplstyle'
# A list of rcParams that should not be applied from styles
_STYLE_BLACKLIST = {
'interactive', 'backend', 'webagg.port', 'webagg.address',
'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback',
'toolbar', 'timezone', 'figure.max_open_warning',
'figure.raise_window', 'savefig.directory', 'tk.window_focus',
'docstring.hardcopy', 'date.epoch'}


@_docstring.Substitution(
"\n".join(map("- {}".format, sorted(_STYLE_BLACKLIST, key=str.lower)))
)
def use(style):
"""
Use Matplotlib style settings from a style specification.

The style name of 'default' is reserved for reverting back to
the default style settings.

.. note::

This updates the `.rcParams` with the settings from the style.
`.rcParams` not defined in the style are kept.

Parameters
----------
style : str, dict, Path or list

A style specification. Valid options are:

str
- One of the style names in `.style.available` (a builtin style or
a style installed in the user library path).

- A dotted name of the form "package.style_name"; in that case,
"package" should be an importable Python package name, e.g. at
``/path/to/package/__init__.py``; the loaded style file is
``/path/to/package/style_name.mplstyle``. (Style files in
subpackages are likewise supported.)

- The path or URL to a style file, which gets loaded by
`.rc_params_from_file`.

dict
A mapping of key/value pairs for `matplotlib.rcParams`.

Path
The path to a style file, which gets loaded by
`.rc_params_from_file`.

list
A list of style specifiers (str, Path or dict), which are applied
from first to last in the list.

Notes
-----
The following `.rcParams` are not related to style and will be ignored if
found in a style specification:

%s
"""
if isinstance(style, (str, Path)) or hasattr(style, 'keys'):
# If name is a single str, Path or dict, make it a single element list.
styles = [style]
else:
styles = style

style_alias = {'mpl20': 'default', 'mpl15': 'classic'}

for style in styles:
if isinstance(style, str):
style = style_alias.get(style, style)
if style == "default":
# Deprecation warnings were already handled when creating
# rcParamsDefault, no need to reemit them here.
with _api.suppress_matplotlib_deprecation_warning():
# don't trigger RcParams.__getitem__('backend')
style = {k: rcParamsDefault[k] for k in rcParamsDefault
if k not in _STYLE_BLACKLIST}
elif style in library:
style = library[style]
elif "." in style:
pkg, _, name = style.rpartition(".")
try:
path = importlib.resources.files(pkg) / f"{name}.{_STYLE_EXTENSION}"
style = rc_params_from_file(path, use_default_template=False)
except (ModuleNotFoundError, OSError, TypeError) as exc:
# There is an ambiguity whether a dotted name refers to a
# package.style_name or to a dotted file path. Currently,
# we silently try the first form and then the second one;
# in the future, we may consider forcing file paths to
# either use Path objects or be prepended with "./" and use
# the slash as marker for file paths.
pass
if isinstance(style, (str, Path)):
try:
style = rc_params_from_file(style, use_default_template=False)
except OSError as err:
raise OSError(

Check warning on line 131 in lib/matplotlib/style/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/style/__init__.py#L130-L131

Added lines #L130 - L131 were not covered by tests
f"{style!r} is not a valid package style, path of style "
f"file, URL of style file, or library style name (library "
f"styles are listed in `style.available`)") from err
filtered = {}
for k in style: # don't trigger RcParams.__getitem__('backend')
if k in _STYLE_BLACKLIST:
_api.warn_external(

Check warning on line 138 in lib/matplotlib/style/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/style/__init__.py#L138

Added line #L138 was not covered by tests
f"Style includes a parameter, {k!r}, that is not "
f"related to style. Ignoring this parameter.")
else:
filtered[k] = style[k]
mpl.rcParams.update(filtered)


@contextlib.contextmanager
def context(style, after_reset=False):
"""
Context manager for using style settings temporarily.

Parameters
----------
style : str, dict, Path or list
A style specification. Valid options are:

str
- One of the style names in `.style.available` (a builtin style or
a style installed in the user library path).

- A dotted name of the form "package.style_name"; in that case,
"package" should be an importable Python package name, e.g. at
``/path/to/package/__init__.py``; the loaded style file is
``/path/to/package/style_name.mplstyle``. (Style files in
subpackages are likewise supported.)

- The path or URL to a style file, which gets loaded by
`.rc_params_from_file`.
dict
A mapping of key/value pairs for `matplotlib.rcParams`.

Path
The path to a style file, which gets loaded by
`.rc_params_from_file`.

list
A list of style specifiers (str, Path or dict), which are applied
from first to last in the list.

after_reset : bool
If True, apply style after resetting settings to their defaults;
otherwise, apply style on top of the current settings.
"""
with mpl.rc_context():
if after_reset:
mpl.rcdefaults()

Check warning on line 185 in lib/matplotlib/style/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/style/__init__.py#L185

Added line #L185 was not covered by tests
use(style)
yield


def _update_user_library(library):
"""Update style library with user-defined rc files."""
for stylelib_path in map(os.path.expanduser, USER_LIBRARY_PATHS):
styles = _read_style_directory(stylelib_path)
_update_nested_dict(library, styles)
return library


@_api.deprecated("3.11")
def update_user_library(library):
return _update_user_library(library)

Check warning on line 200 in lib/matplotlib/style/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/style/__init__.py#L200

Added line #L200 was not covered by tests


def _read_style_directory(style_dir):
"""Return dictionary of styles defined in *style_dir*."""
styles = dict()
for path in Path(style_dir).glob(f"*.{_STYLE_EXTENSION}"):
with warnings.catch_warnings(record=True) as warns:
styles[path.stem] = rc_params_from_file(path, use_default_template=False)
for w in warns:
_log.warning('In %s: %s', path, w.message)

Check warning on line 210 in lib/matplotlib/style/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/style/__init__.py#L210

Added line #L210 was not covered by tests
return styles


@_api.deprecated("3.11")
def read_style_directory(style_dir):
return _read_style_directory(style_dir)

Check warning on line 216 in lib/matplotlib/style/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/style/__init__.py#L216

Added line #L216 was not covered by tests


def _update_nested_dict(main_dict, new_dict):
"""
Update nested dict (only level of nesting) with new values.

Unlike `dict.update`, this assumes that the values of the parent dict are
dicts (or dict-like), so you shouldn't replace the nested dict if it
already exists. Instead you should update the sub-dict.
"""
# update named styles specified by user
for name, rc_dict in new_dict.items():
main_dict.setdefault(name, {}).update(rc_dict)
return main_dict


@_api.deprecated("3.11")
def update_nested_dict(main_dict, new_dict):
return _update_nested_dict(main_dict, new_dict)

Check warning on line 235 in lib/matplotlib/style/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/style/__init__.py#L235

Added line #L235 was not covered by tests


# Load style library
# ==================
_base_library = _read_style_directory(_BASE_LIBRARY_PATH)
library = {}
available = []


def reload_library():
"""Reload the style library."""
library.clear()
library.update(_update_user_library(_base_library))
available[:] = sorted(library.keys())


reload_library()
20 changes: 20 additions & 0 deletions lib/matplotlib/style/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from collections.abc import Generator
import contextlib

from matplotlib import RcParams
from matplotlib.typing import RcStyleType

USER_LIBRARY_PATHS: list[str] = ...

def use(style: RcStyleType) -> None: ...
@contextlib.contextmanager
def context(
style: RcStyleType, after_reset: bool = ...
) -> Generator[None, None, None]: ...

library: dict[str, RcParams]
available: list[str]

def reload_library() -> None: ...

__all__ = ['use', 'context', 'available', 'library', 'reload_library']
Loading
Loading