Skip to content

Commit a884a3d

Browse files
committed
Load style files from third-party packages.
1 parent ca2db5e commit a884a3d

File tree

9 files changed

+86
-15
lines changed

9 files changed

+86
-15
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,10 @@
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 my.package``, with
6+
a ``my/package/__init__.py`` module. Then a
7+
``my/package/presentation.mplstyle`` style sheet can be used as
8+
``plt.style.use("my.package.presentation")``. (Note that the implementation
9+
does not actually import ``my.package``, making this process safe against
10+
possible import-time side effects.)

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

@@ -63,20 +71,27 @@ def use(style):
6371
Parameters
6472
----------
6573
style : str, dict, Path or list
66-
A style specification. Valid options are:
6774
68-
+------+-------------------------------------------------------------+
69-
| str | The name of a style or a path/URL to a style file. For a |
70-
| | list of available style names, see `.style.available`. |
71-
+------+-------------------------------------------------------------+
72-
| dict | Dictionary with valid key/value pairs for |
73-
| | `matplotlib.rcParams`. |
74-
+------+-------------------------------------------------------------+
75-
| Path | A path-like object which is a path to a style file. |
76-
+------+-------------------------------------------------------------+
77-
| list | A list of style specifiers (str, Path or dict) applied from |
78-
| | first to last in the list. |
79-
+------+-------------------------------------------------------------+
75+
A style specification.
76+
77+
- If a str, this can be one of the style names in `.style.available`
78+
(a builtin style or a style installed in the user library path).
79+
80+
This can also be a dotted name of the form "package.name.style_name";
81+
in that case, "package.name" should be an importable Python package
82+
name, e.g. at ``/path/to/package/name/__init__.py``; the loaded style
83+
file is ``/path/to/package/name/style_name.mplstyle``.
84+
85+
This can also be the path or URL to a style file, which gets loaded
86+
by `.rc_params_from_file`.
87+
88+
- If a dict, this is a mapping of key/value pairs for `.rcParams`.
89+
90+
- If a Path, this is the path to a style file, which gets loaded by
91+
`.rc_params_from_file`.
92+
93+
- If a list, this is a a list of style specifiers (str, Path or dict),
94+
which get applied from first to last in the list.
8095
8196
Notes
8297
-----
@@ -130,6 +145,20 @@ def use(style):
130145
if k not in STYLE_BLACKLIST}
131146
elif style in library:
132147
style = library[style]
148+
elif "." in style:
149+
pkg, name = style.rsplit(".")
150+
try:
151+
path = (importlib_resources.files(pkg)
152+
/ f"{name}.{STYLE_EXTENSION}")
153+
style = _rc_params_in_file(path)
154+
except (ModuleNotFoundError, IOError) as exc:
155+
# There is an ambiguity whether a dotted name refers to a
156+
# package.style_name or to a dotted file path. Currently,
157+
# we silently try the first form and then the second one;
158+
# in the future, we may consider forcing file paths to
159+
# either use Path objects or be prepended with "./" and use
160+
# the slash as marker for file paths.
161+
pass
133162
if isinstance(style, (str, Path)):
134163
try:
135164
style = _rc_params_in_file(style)

lib/matplotlib/tests/test_style.py

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

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

+6
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ def plotting_function():
137137
# >>> import matplotlib.pyplot as plt
138138
# >>> plt.style.use('./images/presentation.mplstyle')
139139
#
140+
# You can include style sheets into standard importable Python packages (which
141+
# can be e.g. distributed on PyPI). If your package is importable as
142+
# ``import my.package``, with a ``my/package/__init__.py`` module, and you add
143+
# a ``my/package/presentation.mplstyle`` style sheet, then it can be used as
144+
# ``plt.style.use("my.package.presentation")``.
145+
#
140146
# Alternatively, you can make your style known to Matplotlib by placing
141147
# your ``<style-name>.mplstyle`` file into ``mpl_configdir/stylelib``. You
142148
# can then load your custom style sheet with a call to

0 commit comments

Comments
 (0)