Skip to content

Load style files from third-party packages. #24257

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

Merged
merged 2 commits into from
Nov 4, 2022
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ jobs:

# Install dependencies from PyPI.
python -m pip install --upgrade $PRE \
'contourpy>=1.0.1' cycler fonttools kiwisolver numpy packaging \
pillow pyparsing python-dateutil setuptools-scm \
'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \
numpy packaging pillow pyparsing python-dateutil setuptools-scm \
-r requirements/testing/all.txt \
${{ matrix.extra-requirements }}

Expand Down
2 changes: 2 additions & 0 deletions doc/api/next_api_changes/development/24257-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
importlib_resources>=2.3.0 is now required on Python<3.10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 changes: 3 additions & 0 deletions doc/devel/dependencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ reference.
* `Pillow <https://pillow.readthedocs.io/en/latest/>`_ (>= 6.2)
* `pyparsing <https://pypi.org/project/pyparsing/>`_ (>= 2.3.1)
* `setuptools <https://setuptools.readthedocs.io/en/latest/>`_
* `pyparsing <https://pypi.org/project/pyparsing/>`_ (>= 2.3.1)
* `importlib-resources <https://pypi.org/project/importlib-resources/>`_
(>= 3.2.0; only required on Python < 3.10)


.. _optional_dependencies:
Expand Down
11 changes: 11 additions & 0 deletions doc/users/next_whats_new/styles_from_packages.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Style files can be imported from third-party packages
-----------------------------------------------------

Third-party packages can now distribute style files that are globally available
as follows. Assume that a package is importable as ``import mypackage``, with
a ``mypackage/__init__.py`` module. Then a ``mypackage/presentation.mplstyle``
style sheet can be used as ``plt.style.use("mypackage.presentation")``.

The implementation does not actually import ``mypackage``, making this process
safe against possible import-time side effects. Subpackages (e.g.
``dotted.package.name``) are also supported.
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies:
- contourpy>=1.0.1
- cycler>=0.10.0
- fonttools>=4.22.0
- importlib-resources>=3.2.0
- kiwisolver>=1.0.1
- numpy>=1.19
- pillow>=6.2
Expand Down
129 changes: 73 additions & 56 deletions lib/matplotlib/style/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@
import logging
import os
from pathlib import Path
import sys
import warnings

if sys.version_info >= (3, 10):
import importlib.resources as importlib_resources
else:
# Even though Py3.9 has importlib.resources, it doesn't properly handle
# modules added in sys.path.
import importlib_resources

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

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,23 +72,6 @@
"directly use the seaborn API instead.")


def _remove_blacklisted_style_params(d, warn=True):
o = {}
for key in d: # prevent triggering RcParams.__getitem__('backend')
if key in STYLE_BLACKLIST:
if warn:
_api.warn_external(
f"Style includes a parameter, {key!r}, that is not "
"related to style. Ignoring this parameter.")
else:
o[key] = d[key]
return o


def _apply_style(d, warn=True):
mpl.rcParams.update(_remove_blacklisted_style_params(d, warn=warn))


@_docstring.Substitution(
"\n".join(map("- {}".format, sorted(STYLE_BLACKLIST, key=str.lower)))
)
Expand All @@ -99,20 +90,28 @@ def use(style):
Parameters
----------
style : str, dict, Path or list
A style specification. Valid options are:

+------+-------------------------------------------------------------+
| str | The name of a style or a path/URL to a style file. For a |
| | list of available style names, see `.style.available`. |
+------+-------------------------------------------------------------+
| dict | Dictionary with valid key/value pairs for |
| | `matplotlib.rcParams`. |
+------+-------------------------------------------------------------+
| Path | A path-like object which is a path to a style file. |
+------+-------------------------------------------------------------+
| list | A list of style specifiers (str, Path or dict) applied from |
| | first to last in the list. |
+------+-------------------------------------------------------------+
A style specification.

- If a str, this can be one of the style names in `.style.available`
(a builtin style or a style installed in the user library path).

This can also be 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.)

This can also be the path or URL to a style file, which gets loaded
by `.rc_params_from_file`.

- If a dict, this is a mapping of key/value pairs for `.rcParams`.

- If a Path, this is the path to a style file, which gets loaded by
`.rc_params_from_file`.

- If a list, this is a list of style specifiers (str, Path or dict),
which get applied from first to last in the list.

Notes
-----
Expand All @@ -129,33 +128,52 @@ def use(style):

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

def fix_style(s):
if isinstance(s, str):
s = style_alias.get(s, s)
if s in _DEPRECATED_SEABORN_STYLES:
for style in styles:
if isinstance(style, str):
style = style_alias.get(style, style)
if style in _DEPRECATED_SEABORN_STYLES:
_api.warn_deprecated("3.6", message=_DEPRECATED_SEABORN_MSG)
s = _DEPRECATED_SEABORN_STYLES[s]
return s

for style in map(fix_style, styles):
if not isinstance(style, (str, Path)):
_apply_style(style)
elif style == 'default':
# Deprecation warnings were already handled when creating
# rcParamsDefault, no need to reemit them here.
with _api.suppress_matplotlib_deprecation_warning():
_apply_style(rcParamsDefault, warn=False)
elif style in library:
_apply_style(library[style])
else:
style = _DEPRECATED_SEABORN_STYLES[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_in_file(path)
except (ModuleNotFoundError, IOError) 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:
rc = rc_params_from_file(style, use_default_template=False)
_apply_style(rc)
style = _rc_params_in_file(style)
except IOError as err:
raise IOError(
"{!r} not found in the style library and input is not a "
"valid URL or path; see `style.available` for list of "
"available styles".format(style)) from err
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(
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
Expand Down Expand Up @@ -205,8 +223,7 @@ def read_style_directory(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)
styles[path.stem] = _rc_params_in_file(path)
for w in warns:
_log.warning('In %s: %s', path, w.message)
return styles
Expand Down
15 changes: 15 additions & 0 deletions lib/matplotlib/tests/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,18 @@ def test_deprecated_seaborn_styles():

def test_up_to_date_blacklist():
assert mpl.style.core.STYLE_BLACKLIST <= {*mpl.rcsetup._validators}


def test_style_from_module(tmp_path, monkeypatch):
monkeypatch.syspath_prepend(tmp_path)
monkeypatch.chdir(tmp_path)
pkg_path = tmp_path / "mpl_test_style_pkg"
pkg_path.mkdir()
(pkg_path / "test_style.mplstyle").write_text(
"lines.linewidth: 42", encoding="utf-8")
pkg_path.with_suffix(".mplstyle").write_text(
"lines.linewidth: 84", encoding="utf-8")
mpl.style.use("mpl_test_style_pkg.test_style")
assert mpl.rcParams["lines.linewidth"] == 42
mpl.style.use("mpl_test_style_pkg.mplstyle")
assert mpl.rcParams["lines.linewidth"] == 84
1 change: 1 addition & 0 deletions requirements/testing/minver.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
contourpy==1.0.1
cycler==0.10
kiwisolver==1.0.1
importlib-resources==3.2.0
numpy==1.19.0
packaging==20.0
pillow==6.2.1
Expand Down
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ def make_release_tree(self, base_dir, files):
os.environ.get("CIBUILDWHEEL", "0") != "1"
) else []
),
extras_require={
Copy link
Member

Choose a reason for hiding this comment

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

I think this is not doing what you would expect it to.

Python version specific markers can be achieved with

importlib-resources>=3.2.0; python_version<"3.10"

as per https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#platform-specific-dependencies.

This should be in the main dependencies section, not as an extra.

Copy link
Member

Choose a reason for hiding this comment

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

This does actually work, I checked when I had the same thought previously. It is a bit outdated, and what you linked is the more recommended way, but this is valid (if perhaps not fully future-proof, as it is not well documented any more).

':python_version<"3.10"': [
"importlib-resources>=3.2.0",
],
},
use_scm_version={
"version_scheme": "release-branch-semver",
"local_scheme": "node-and-date",
Expand Down
17 changes: 14 additions & 3 deletions tutorials/introductory/customizing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

There are three ways to customize Matplotlib:

1. :ref:`Setting rcParams at runtime<customizing-with-dynamic-rc-settings>`.
2. :ref:`Using style sheets<customizing-with-style-sheets>`.
3. :ref:`Changing your matplotlibrc file<customizing-with-matplotlibrc-files>`.
1. :ref:`Setting rcParams at runtime<customizing-with-dynamic-rc-settings>`.
2. :ref:`Using style sheets<customizing-with-style-sheets>`.
3. :ref:`Changing your matplotlibrc file<customizing-with-matplotlibrc-files>`.

Setting rcParams at runtime takes precedence over style sheets, style
sheets take precedence over :file:`matplotlibrc` files.
Expand Down Expand Up @@ -137,6 +137,17 @@ def plotting_function():
# >>> import matplotlib.pyplot as plt
# >>> plt.style.use('./images/presentation.mplstyle')
#
#
# Distributing styles
# -------------------
#
# You can include style sheets into standard importable Python packages (which
# can be e.g. distributed on PyPI). If your package is importable as
# ``import mypackage``, with a ``mypackage/__init__.py`` module, and you add
# a ``mypackage/presentation.mplstyle`` style sheet, then it can be used as
# ``plt.style.use("mypackage.presentation")``. Subpackages (e.g.
# ``dotted.package.name``) are also supported.
#
# Alternatively, you can make your style known to Matplotlib by placing
# your ``<style-name>.mplstyle`` file into ``mpl_configdir/stylelib``. You
# can then load your custom style sheet with a call to
Expand Down