Skip to content

Commit 1858d3a

Browse files
committed
Load style files from third-party packages.
1 parent c54afff commit 1858d3a

File tree

10 files changed

+99
-21
lines changed

10 files changed

+99
-21
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.

environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies:
1212
- contourpy>=1.0.1
1313
- cycler>=0.10.0
1414
- fonttools>=4.22.0
15+
- importlib-resources>=3.2.0
1516
- kiwisolver>=1.0.1
1617
- numpy>=1.19
1718
- pillow>=6.2

lib/matplotlib/style/core.py

+45-16
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@
1515
import logging
1616
import os
1717
from pathlib import Path
18+
import sys
1819
import warnings
1920

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

@@ -82,20 +90,27 @@ def use(style):
8290
Parameters
8391
----------
8492
style : str, dict, Path or list
85-
A style specification. Valid options are:
8693
87-
+------+-------------------------------------------------------------+
88-
| str | The name of a style or a path/URL to a style file. For a |
89-
| | list of available style names, see `.style.available`. |
90-
+------+-------------------------------------------------------------+
91-
| dict | Dictionary with valid key/value pairs for |
92-
| | `matplotlib.rcParams`. |
93-
+------+-------------------------------------------------------------+
94-
| Path | A path-like object which is a path to a style file. |
95-
+------+-------------------------------------------------------------+
96-
| list | A list of style specifiers (str, Path or dict) applied from |
97-
| | first to last in the list. |
98-
+------+-------------------------------------------------------------+
94+
A style specification.
95+
96+
- If a str, this can be one of the style names in `.style.available`
97+
(a builtin style or a style installed in the user library path).
98+
99+
This can also be a dotted name of the form "package.name.style_name";
100+
in that case, "package.name" should be an importable Python package
101+
name, e.g. at ``/path/to/package/name/__init__.py``; the loaded style
102+
file is ``/path/to/package/name/style_name.mplstyle``.
103+
104+
This can also be the path or URL to a style file, which gets loaded
105+
by `.rc_params_from_file`.
106+
107+
- If a dict, this is a mapping of key/value pairs for `.rcParams`.
108+
109+
- If a Path, this is the path to a style file, which gets loaded by
110+
`.rc_params_from_file`.
111+
112+
- If a list, this is a list of style specifiers (str, Path or dict),
113+
which get applied from first to last in the list.
99114
100115
Notes
101116
-----
@@ -127,14 +142,28 @@ def use(style):
127142
if k not in STYLE_BLACKLIST}
128143
elif style in library:
129144
style = library[style]
145+
elif "." in style:
146+
pkg, _, name = style.rpartition(".")
147+
try:
148+
path = (importlib_resources.files(pkg)
149+
/ f"{name}.{STYLE_EXTENSION}")
150+
style = _rc_params_in_file(path)
151+
except (ModuleNotFoundError, IOError) as exc:
152+
# There is an ambiguity whether a dotted name refers to a
153+
# package.style_name or to a dotted file path. Currently,
154+
# we silently try the first form and then the second one;
155+
# in the future, we may consider forcing file paths to
156+
# either use Path objects or be prepended with "./" and use
157+
# the slash as marker for file paths.
158+
pass
130159
if isinstance(style, (str, Path)):
131160
try:
132161
style = _rc_params_in_file(style)
133162
except IOError as err:
134163
raise IOError(
135-
f"{style!r} not found in the style library and input is "
136-
f"not a valid URL or path; see `style.available` for the "
137-
f"list of available styles") from err
164+
f"{style!r} is not a valid package style, path of style "
165+
f"file, URL of style file, or library style name (library "
166+
f"styles are listed in `style.avaible`)") from err
138167
filtered = {}
139168
for k in style: # don't trigger RcParams.__getitem__('backend')
140169
if k in STYLE_BLACKLIST:

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
@@ -334,6 +334,11 @@ def make_release_tree(self, base_dir, files):
334334
os.environ.get("CIBUILDWHEEL", "0") != "1"
335335
) else []
336336
),
337+
extras_require={
338+
':python_version<"3.10"': [
339+
"importlib-resources>=3.2.0",
340+
],
341+
},
337342
use_scm_version={
338343
"version_scheme": "release-branch-semver",
339344
"local_scheme": "node-and-date",

tutorials/introductory/customizing.py

+14-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,17 @@ 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 mypackage``, with a ``mypackage/__init__.py`` module, and you add
147+
# a ``mypackage/presentation.mplstyle`` style sheet, then it can be used as
148+
# ``plt.style.use("mypackage.presentation")``. Subpackages (e.g.
149+
# ``dotted.package.name``) are also supported.
150+
#
140151
# Alternatively, you can make your style known to Matplotlib by placing
141152
# your ``<style-name>.mplstyle`` file into ``mpl_configdir/stylelib``. You
142153
# can then load your custom style sheet with a call to

0 commit comments

Comments
 (0)