From c54afff1b61baff7afe87e100f7f3396b2ab9a0b Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 23 Oct 2022 12:26:45 +0200 Subject: [PATCH 1/2] Inline style application logic into mpl.style.use. ... and use the shorter _rc_params_in_file. --- lib/matplotlib/style/core.py | 73 +++++++++++++++--------------------- 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 646b39dcc5df..5a510a337a31 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -18,7 +18,7 @@ import warnings 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__) @@ -64,23 +64,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))) ) @@ -129,33 +112,38 @@ 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] + 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} not found in the style library and input is " + f"not a valid URL or path; see `style.available` for the " + f"list of available styles") 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 @@ -205,8 +193,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 From 24be7785cb19d6796171fc37955086d9728ec04a Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 23 Oct 2022 13:22:51 +0200 Subject: [PATCH 2/2] Load style files from third-party packages. --- .github/workflows/tests.yml | 4 +- .../next_api_changes/development/24257-AL.rst | 2 + doc/devel/dependencies.rst | 3 + .../next_whats_new/styles_from_packages.rst | 11 ++++ environment.yml | 1 + lib/matplotlib/style/core.py | 62 ++++++++++++++----- lib/matplotlib/tests/test_style.py | 15 +++++ requirements/testing/minver.txt | 1 + setup.py | 5 ++ tutorials/introductory/customizing.py | 17 ++++- 10 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 doc/api/next_api_changes/development/24257-AL.rst create mode 100644 doc/users/next_whats_new/styles_from_packages.rst diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 020f09df4010..eddb14be0bc6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 }} diff --git a/doc/api/next_api_changes/development/24257-AL.rst b/doc/api/next_api_changes/development/24257-AL.rst new file mode 100644 index 000000000000..584420df8fd7 --- /dev/null +++ b/doc/api/next_api_changes/development/24257-AL.rst @@ -0,0 +1,2 @@ +importlib_resources>=2.3.0 is now required on Python<3.10 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/devel/dependencies.rst b/doc/devel/dependencies.rst index fb76857898a4..365c062364e2 100644 --- a/doc/devel/dependencies.rst +++ b/doc/devel/dependencies.rst @@ -26,6 +26,9 @@ reference. * `Pillow `_ (>= 6.2) * `pyparsing `_ (>= 2.3.1) * `setuptools `_ +* `pyparsing `_ (>= 2.3.1) +* `importlib-resources `_ + (>= 3.2.0; only required on Python < 3.10) .. _optional_dependencies: diff --git a/doc/users/next_whats_new/styles_from_packages.rst b/doc/users/next_whats_new/styles_from_packages.rst new file mode 100644 index 000000000000..d129bb356fa7 --- /dev/null +++ b/doc/users/next_whats_new/styles_from_packages.rst @@ -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. diff --git a/environment.yml b/environment.yml index 28ff3a1b2c34..c9b7aa610720 100644 --- a/environment.yml +++ b/environment.yml @@ -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 diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 5a510a337a31..ed5cd4f63bc5 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -15,8 +15,16 @@ 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_in_file, rcParamsDefault @@ -82,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 ----- @@ -127,14 +143,28 @@ def use(style): 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: style = _rc_params_in_file(style) except IOError as err: raise IOError( - f"{style!r} not found in the style library and input is " - f"not a valid URL or path; see `style.available` for the " - f"list of available styles") 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: diff --git a/lib/matplotlib/tests/test_style.py b/lib/matplotlib/tests/test_style.py index c788c45920ae..7d1ed94ea236 100644 --- a/lib/matplotlib/tests/test_style.py +++ b/lib/matplotlib/tests/test_style.py @@ -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 diff --git a/requirements/testing/minver.txt b/requirements/testing/minver.txt index d932b0aa34e7..82301e900f52 100644 --- a/requirements/testing/minver.txt +++ b/requirements/testing/minver.txt @@ -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 diff --git a/setup.py b/setup.py index 365de0c0b5a2..2f1fb84f8cc6 100644 --- a/setup.py +++ b/setup.py @@ -334,6 +334,11 @@ def make_release_tree(self, base_dir, files): os.environ.get("CIBUILDWHEEL", "0") != "1" ) else [] ), + extras_require={ + ':python_version<"3.10"': [ + "importlib-resources>=3.2.0", + ], + }, use_scm_version={ "version_scheme": "release-branch-semver", "local_scheme": "node-and-date", diff --git a/tutorials/introductory/customizing.py b/tutorials/introductory/customizing.py index ea6b501e99ea..10fc21d2187b 100644 --- a/tutorials/introductory/customizing.py +++ b/tutorials/introductory/customizing.py @@ -9,9 +9,9 @@ There are three ways to customize Matplotlib: - 1. :ref:`Setting rcParams at runtime`. - 2. :ref:`Using style sheets`. - 3. :ref:`Changing your matplotlibrc file`. +1. :ref:`Setting rcParams at runtime`. +2. :ref:`Using style sheets`. +3. :ref:`Changing your matplotlibrc file`. Setting rcParams at runtime takes precedence over style sheets, style sheets take precedence over :file:`matplotlibrc` files. @@ -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 ``.mplstyle`` file into ``mpl_configdir/stylelib``. You # can then load your custom style sheet with a call to pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy