Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b9c5152

Browse files
authored
Merge pull request #24257 from anntzer/style
Load style files from third-party packages.
2 parents aca6e9d + 24be778 commit b9c5152

File tree

10 files changed

+127
-61
lines changed

10 files changed

+127
-61
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
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
Lines changed: 2 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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:
Lines changed: 11 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,18 @@
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
21-
from matplotlib import _api, _docstring, rc_params_from_file, rcParamsDefault
29+
from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault
2230

2331
_log = logging.getLogger(__name__)
2432

@@ -64,23 +72,6 @@
6472
"directly use the seaborn API instead.")
6573

6674

67-
def _remove_blacklisted_style_params(d, warn=True):
68-
o = {}
69-
for key in d: # prevent triggering RcParams.__getitem__('backend')
70-
if key in STYLE_BLACKLIST:
71-
if warn:
72-
_api.warn_external(
73-
f"Style includes a parameter, {key!r}, that is not "
74-
"related to style. Ignoring this parameter.")
75-
else:
76-
o[key] = d[key]
77-
return o
78-
79-
80-
def _apply_style(d, warn=True):
81-
mpl.rcParams.update(_remove_blacklisted_style_params(d, warn=warn))
82-
83-
8475
@_docstring.Substitution(
8576
"\n".join(map("- {}".format, sorted(STYLE_BLACKLIST, key=str.lower)))
8677
)
@@ -99,20 +90,28 @@ def use(style):
9990
Parameters
10091
----------
10192
style : str, dict, Path or list
102-
A style specification. Valid options are:
10393
104-
+------+-------------------------------------------------------------+
105-
| str | The name of a style or a path/URL to a style file. For a |
106-
| | list of available style names, see `.style.available`. |
107-
+------+-------------------------------------------------------------+
108-
| dict | Dictionary with valid key/value pairs for |
109-
| | `matplotlib.rcParams`. |
110-
+------+-------------------------------------------------------------+
111-
| Path | A path-like object which is a path to a style file. |
112-
+------+-------------------------------------------------------------+
113-
| list | A list of style specifiers (str, Path or dict) applied from |
114-
| | first to last in the list. |
115-
+------+-------------------------------------------------------------+
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.style_name"; in
100+
that case, "package" should be an importable Python package name,
101+
e.g. at ``/path/to/package/__init__.py``; the loaded style file is
102+
``/path/to/package/style_name.mplstyle``. (Style files in
103+
subpackages are likewise supported.)
104+
105+
This can also be the path or URL to a style file, which gets loaded
106+
by `.rc_params_from_file`.
107+
108+
- If a dict, this is a mapping of key/value pairs for `.rcParams`.
109+
110+
- If a Path, this is the path to a style file, which gets loaded by
111+
`.rc_params_from_file`.
112+
113+
- If a list, this is a list of style specifiers (str, Path or dict),
114+
which get applied from first to last in the list.
116115
117116
Notes
118117
-----
@@ -129,33 +128,52 @@ def use(style):
129128

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

132-
def fix_style(s):
133-
if isinstance(s, str):
134-
s = style_alias.get(s, s)
135-
if s in _DEPRECATED_SEABORN_STYLES:
131+
for style in styles:
132+
if isinstance(style, str):
133+
style = style_alias.get(style, style)
134+
if style in _DEPRECATED_SEABORN_STYLES:
136135
_api.warn_deprecated("3.6", message=_DEPRECATED_SEABORN_MSG)
137-
s = _DEPRECATED_SEABORN_STYLES[s]
138-
return s
139-
140-
for style in map(fix_style, styles):
141-
if not isinstance(style, (str, Path)):
142-
_apply_style(style)
143-
elif style == 'default':
144-
# Deprecation warnings were already handled when creating
145-
# rcParamsDefault, no need to reemit them here.
146-
with _api.suppress_matplotlib_deprecation_warning():
147-
_apply_style(rcParamsDefault, warn=False)
148-
elif style in library:
149-
_apply_style(library[style])
150-
else:
136+
style = _DEPRECATED_SEABORN_STYLES[style]
137+
if style == "default":
138+
# Deprecation warnings were already handled when creating
139+
# rcParamsDefault, no need to reemit them here.
140+
with _api.suppress_matplotlib_deprecation_warning():
141+
# don't trigger RcParams.__getitem__('backend')
142+
style = {k: rcParamsDefault[k] for k in rcParamsDefault
143+
if k not in STYLE_BLACKLIST}
144+
elif style in library:
145+
style = library[style]
146+
elif "." in style:
147+
pkg, _, name = style.rpartition(".")
148+
try:
149+
path = (importlib_resources.files(pkg)
150+
/ f"{name}.{STYLE_EXTENSION}")
151+
style = _rc_params_in_file(path)
152+
except (ModuleNotFoundError, IOError) as exc:
153+
# There is an ambiguity whether a dotted name refers to a
154+
# package.style_name or to a dotted file path. Currently,
155+
# we silently try the first form and then the second one;
156+
# in the future, we may consider forcing file paths to
157+
# either use Path objects or be prepended with "./" and use
158+
# the slash as marker for file paths.
159+
pass
160+
if isinstance(style, (str, Path)):
151161
try:
152-
rc = rc_params_from_file(style, use_default_template=False)
153-
_apply_style(rc)
162+
style = _rc_params_in_file(style)
154163
except IOError as err:
155164
raise IOError(
156-
"{!r} not found in the style library and input is not a "
157-
"valid URL or path; see `style.available` for list of "
158-
"available styles".format(style)) from err
165+
f"{style!r} is not a valid package style, path of style "
166+
f"file, URL of style file, or library style name (library "
167+
f"styles are listed in `style.available`)") from err
168+
filtered = {}
169+
for k in style: # don't trigger RcParams.__getitem__('backend')
170+
if k in STYLE_BLACKLIST:
171+
_api.warn_external(
172+
f"Style includes a parameter, {k!r}, that is not "
173+
f"related to style. Ignoring this parameter.")
174+
else:
175+
filtered[k] = style[k]
176+
mpl.rcParams.update(filtered)
159177

160178

161179
@contextlib.contextmanager
@@ -205,8 +223,7 @@ def read_style_directory(style_dir):
205223
styles = dict()
206224
for path in Path(style_dir).glob(f"*.{STYLE_EXTENSION}"):
207225
with warnings.catch_warnings(record=True) as warns:
208-
styles[path.stem] = rc_params_from_file(
209-
path, use_default_template=False)
226+
styles[path.stem] = _rc_params_in_file(path)
210227
for w in warns:
211228
_log.warning('In %s: %s', path, w.message)
212229
return styles

lib/matplotlib/tests/test_style.py

Lines changed: 15 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 14 additions & 3 deletions
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)