Skip to content

Commit 01c8743

Browse files
committed
Load style files from third-party packages.
1 parent 31d4f24 commit 01c8743

File tree

9 files changed

+94
-18
lines changed

9 files changed

+94
-18
lines changed

.github/workflows/tests.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ jobs:
162162
163163
# Install dependencies from PyPI.
164164
python -m pip install --upgrade $PRE \
165-
'contourpy>=1.0.1' cycler fonttools kiwisolver numpy packaging \
166-
pillow pyparsing python-dateutil setuptools-scm \
165+
'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \
166+
numpy packaging pillow pyparsing python-dateutil setuptools-scm \
167167
-r requirements/testing/all.txt \
168168
${{ matrix.extra-requirements }}
169169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
importlib_resources>=2.3.0 is now required on Python<3.10
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

doc/devel/dependencies.rst

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ reference.
2626
* `Pillow <https://pillow.readthedocs.io/en/latest/>`_ (>= 6.2)
2727
* `pyparsing <https://pypi.org/project/pyparsing/>`_ (>= 2.3.1)
2828
* `setuptools <https://setuptools.readthedocs.io/en/latest/>`_
29+
* `pyparsing <https://pypi.org/project/pyparsing/>`_ (>= 2.3.1)
30+
* `importlib-resources <https://pypi.org/project/importlib-resources/>`_
31+
(>= 3.2.0; only required on Python < 3.10)
2932

3033

3134
.. _optional_dependencies:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Style files can be imported from third-party packages
2+
-----------------------------------------------------
3+
4+
Third-party packages can now distribute style files that are globally available
5+
as follows. Assume that a package is importable as ``import mypackage``, with
6+
a ``mypackage/__init__.py`` module. Then a ``mypackage/presentation.mplstyle``
7+
style sheet can be used as ``plt.style.use("mypackage.presentation")``.
8+
9+
The implementation does not actually import ``mypackage``, making this process
10+
safe against possible import-time side effects. Subpackages (e.g.
11+
``dotted.package.name`` are also supported).

lib/matplotlib/style/core.py

+42-13
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@
1616
import os
1717
from pathlib import Path
1818
import re
19+
import sys
1920
import warnings
2021

22+
if sys.version_info >= (3, 10):
23+
import importlib.resources as importlib_resources
24+
else:
25+
# Even though Py3.9 has importlib.resources, it doesn't properly handle
26+
# modules added in sys.path.
27+
import importlib_resources
28+
2129
import matplotlib as mpl
2230
from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault
2331

@@ -89,20 +97,27 @@ def use(style):
8997
Parameters
9098
----------
9199
style : str, dict, Path or list
92-
A style specification. Valid options are:
93100
94-
+------+-------------------------------------------------------------+
95-
| str | The name of a style or a path/URL to a style file. For a |
96-
| | list of available style names, see `.style.available`. |
97-
+------+-------------------------------------------------------------+
98-
| dict | Dictionary with valid key/value pairs for |
99-
| | `matplotlib.rcParams`. |
100-
+------+-------------------------------------------------------------+
101-
| Path | A path-like object which is a path to a style file. |
102-
+------+-------------------------------------------------------------+
103-
| list | A list of style specifiers (str, Path or dict) applied from |
104-
| | first to last in the list. |
105-
+------+-------------------------------------------------------------+
101+
A style specification.
102+
103+
- If a str, this can be one of the style names in `.style.available`
104+
(a builtin style or a style installed in the user library path).
105+
106+
This can also be a dotted name of the form "package.name.style_name";
107+
in that case, "package.name" should be an importable Python package
108+
name, e.g. at ``/path/to/package/name/__init__.py``; the loaded style
109+
file is ``/path/to/package/name/style_name.mplstyle``.
110+
111+
This can also be the path or URL to a style file, which gets loaded
112+
by `.rc_params_from_file`.
113+
114+
- If a dict, this is a mapping of key/value pairs for `.rcParams`.
115+
116+
- If a Path, this is the path to a style file, which gets loaded by
117+
`.rc_params_from_file`.
118+
119+
- If a list, this is a a list of style specifiers (str, Path or dict),
120+
which get applied from first to last in the list.
106121
107122
Notes
108123
-----
@@ -134,6 +149,20 @@ def use(style):
134149
if k not in STYLE_BLACKLIST}
135150
elif style in library:
136151
style = library[style]
152+
elif "." in style:
153+
pkg, _, name = style.rpartition(".")
154+
try:
155+
path = (importlib_resources.files(pkg)
156+
/ f"{name}.{STYLE_EXTENSION}")
157+
style = _rc_params_in_file(path)
158+
except (ModuleNotFoundError, IOError) as exc:
159+
# There is an ambiguity whether a dotted name refers to a
160+
# package.style_name or to a dotted file path. Currently,
161+
# we silently try the first form and then the second one;
162+
# in the future, we may consider forcing file paths to
163+
# either use Path objects or be prepended with "./" and use
164+
# the slash as marker for file paths.
165+
pass
137166
if isinstance(style, (str, Path)):
138167
try:
139168
style = _rc_params_in_file(style)

lib/matplotlib/tests/test_style.py

+15
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,18 @@ def test_deprecated_seaborn_styles():
190190

191191
def test_up_to_date_blacklist():
192192
assert mpl.style.core.STYLE_BLACKLIST <= {*mpl.rcsetup._validators}
193+
194+
195+
def test_style_from_module(tmp_path, monkeypatch):
196+
monkeypatch.syspath_prepend(tmp_path)
197+
monkeypatch.chdir(tmp_path)
198+
pkg_path = tmp_path / "mpl_test_style_pkg"
199+
pkg_path.mkdir()
200+
(pkg_path / "test_style.mplstyle").write_text(
201+
"lines.linewidth: 42", encoding="utf-8")
202+
pkg_path.with_suffix(".mplstyle").write_text(
203+
"lines.linewidth: 84", encoding="utf-8")
204+
mpl.style.use("mpl_test_style_pkg.test_style")
205+
assert mpl.rcParams["lines.linewidth"] == 42
206+
mpl.style.use("mpl_test_style_pkg.mplstyle")
207+
assert mpl.rcParams["lines.linewidth"] == 84

requirements/testing/minver.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
contourpy==1.0.1
44
cycler==0.10
55
kiwisolver==1.0.1
6+
importlib-resources==3.2.0
67
numpy==1.19.0
78
packaging==20.0
89
pillow==6.2.1

setup.py

+5
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ def make_release_tree(self, base_dir, files):
327327
os.environ.get("CIBUILDWHEEL", "0") != "1"
328328
) else []
329329
),
330+
extras_require={
331+
':python_version<"3.10"': [
332+
"importlib-resources>=3.2.0",
333+
],
334+
},
330335
use_scm_version={
331336
"version_scheme": "release-branch-semver",
332337
"local_scheme": "node-and-date",

tutorials/introductory/customizing.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
1010
There are three ways to customize Matplotlib:
1111
12-
1. :ref:`Setting rcParams at runtime<customizing-with-dynamic-rc-settings>`.
13-
2. :ref:`Using style sheets<customizing-with-style-sheets>`.
14-
3. :ref:`Changing your matplotlibrc file<customizing-with-matplotlibrc-files>`.
12+
1. :ref:`Setting rcParams at runtime<customizing-with-dynamic-rc-settings>`.
13+
2. :ref:`Using style sheets<customizing-with-style-sheets>`.
14+
3. :ref:`Changing your matplotlibrc file<customizing-with-matplotlibrc-files>`.
1515
1616
Setting rcParams at runtime takes precedence over style sheets, style
1717
sheets take precedence over :file:`matplotlibrc` files.
@@ -137,6 +137,16 @@ def plotting_function():
137137
# >>> import matplotlib.pyplot as plt
138138
# >>> plt.style.use('./images/presentation.mplstyle')
139139
#
140+
#
141+
# Distributing styles
142+
# -------------------
143+
#
144+
# You can include style sheets into standard importable Python packages (which
145+
# can be e.g. distributed on PyPI). If your package is importable as
146+
# ``import my.package``, with a ``my/package/__init__.py`` module, and you add
147+
# a ``my/package/presentation.mplstyle`` style sheet, then it can be used as
148+
# ``plt.style.use("my.package.presentation")``.
149+
#
140150
# Alternatively, you can make your style known to Matplotlib by placing
141151
# your ``<style-name>.mplstyle`` file into ``mpl_configdir/stylelib``. You
142152
# can then load your custom style sheet with a call to

0 commit comments

Comments
 (0)