diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 663ff4b70536..cd07687d873b 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -14,11 +14,14 @@ """ import ast +from dataclasses import dataclass from functools import lru_cache, reduce from numbers import Real import operator import os import re +import textwrap +from typing import Any, Callable import numpy as np @@ -1290,3 +1293,131 @@ def _convert_validator_spec(key, conv): } _validators = {k: _convert_validator_spec(k, conv) for k, conv in _validators.items()} + + +@dataclass +class Param: + name: str + default: Any + validator: Callable[[Any], Any] + desc: str = None + + def to_matplotlibrc(self): + return f"{self.name}: {self.default} # {self.desc}\n" + + +class Comment: + def __init__(self, text): + self.text = textwrap.dedent(text).strip("\n") + + def to_matplotlibrc(self): + return self.text + "\n" + + +class RcParamsDefinition: + """ + Definition of config parameters. + + Parameters + ---------- + content: list of Param and Comment objects + This contains: + + - Param objects specifying config parameters + - Comment objects specifying comments to be included in the generated + matplotlibrc file + """ + + def __init__(self, content): + self._content = content + self._params = {item.name: item for item in content if isinstance(item, Param)} + + def create_default_rcparams(self): + """ + Return a RcParams object with the default values. + + Note: The result is equivalent to ``matplotlib.rcParamsDefault``, but + generated from this definition and not from a matplotlibrc file. + """ + from matplotlib import RcParams # TODO: avoid circular import + return RcParams({ + name: param.default for name, param in self._params.items() + }) + + def write_default_matplotlibrc(self, filename): + """ + Write the default matplotlibrc file. + + Note: This aspires to fully generate lib/matplotlib/mpl-data/matplotlibrc. + """ + with open(filename, "w") as f: + for item in self._content: + f.write(item.to_matplotlibrc()) + + def read_matplotlibrc(self, filename): + """ + Read a matplotlibrc file and return a dict with the values. + + Note: This is the core of file-reading. + ``RcParams(read_matplotlibrc(filename))`` is equivalent to + `matplotlib._rc_params_in_file` (modulo handling of the + additional parameters. + + Also note that we have the validation in here, and currently + again in RcParams itself. The validators are currently necessary + to transform the string representation of the values into their + actual type. We may split transformation and validation into + separate steps in the future. + """ + params = {} + with open(filename) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + name, value = line.split(":", 1) + name = name.strip() + value = value.split("#", 1)[0].strip() + try: + param_def = self._params[name] + except KeyError: + raise ValueError(f"Unknown rcParam {name!r} in {filename!r}") + else: + params[name] = param_def.validator(value) + return params + + +# This is how the valid rcParams will be defined in the future. +# The Comment sections are a bit ad-hoc, but are necessary for now if we want the +# definition to be the leading source of truth and also be able to generate the +# current matplotlibrc file from it. +rc_def = RcParamsDefinition([ + Comment(""" + # *************************************************************************** + # * LINES * + # *************************************************************************** + # See https://matplotlib.org/stable/api/artist_api.html#module-matplotlib.lines + # for more information on line properties. + """), + Param("lines.linewidth", 1.5, validate_float, "line width in points"), + Param("lines.linestyle", "-", validate_string, "solid line"), + Param("lines.color", "C0", validate_string, "has no affect on plot(); see axes.prop_cycle"), + Param("lines.marker", "None", validate_string, "the default marker"), + Param("lines.markerfacecolor", "auto", validate_string, "the default marker face color"), + Param("lines.markeredgecolor", "auto", validate_string, "the default marker edge color"), + Param("lines.markeredgewidth", 1.0, validate_float, "the line width around the marker symbol"), + Param("lines.markersize", 6, validate_float, "marker size, in points"), + Param("lines.dash_joinstyle", "round", validate_string, "{miter, round, bevel}"), + Param("lines.dash_capstyle", "butt", validate_string, "{butt, round, projecting}"), + Param("lines.solid_joinstyle", "round", validate_string, "{miter, round, bevel}"), + Param("lines.solid_capstyle", "projecting", validate_string, "{butt, round, projecting}"), + Param("lines.antialiased", True, validate_bool, "render lines in antialiased (no jaggies)"), + Comment(""" + + # The three standard dash patterns. These are scaled by the linewidth. + """), + Param("lines.dashed_pattern", [3.7, 1.6], validate_floatlist), + Param("lines.dashdot_pattern", [6.4, 1.6, 1, 1.6], validate_floatlist), + Param("lines.dotted_pattern", [1, 1.65], validate_floatlist), + Param("lines.scale_dashes", True, validate_bool), +]) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index ce20f57ef665..32537696ae6f 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -1,5 +1,6 @@ import copy import os +import textwrap from pathlib import Path import re import subprocess @@ -27,9 +28,13 @@ validate_hist_bins, validate_int, validate_markevery, + validate_string, validate_stringlist, _validate_linestyle, - _listify_validator) + _listify_validator, + RcParamsDefinition, + Param, Comment, +) def test_rcparams(tmpdir): @@ -604,3 +609,77 @@ def test_rcparams_legend_loc(): match_str = f"{value} is not a valid value for legend.loc;" with pytest.raises(ValueError, match=re.escape(match_str)): mpl.RcParams({'legend.loc': value}) + + +class TestRcParamsDefinition: + + # sample definition for testing + rc_def = RcParamsDefinition([ + Comment("""\ + # *************************************************************************** + # * LINES * + # *************************************************************************** + # See https://matplotlib.org/stable/api/artist_api.html#module-matplotlib.lines + # for more information on line properties. + """), + Param("lines.linewidth", 1.5, validate_float, "line width in points"), + Param("lines.linestyle", "-", validate_string, "solid line"), + Param("lines.antialiased", True, validate_bool, "antialiasing on lines"), + ]) + + @staticmethod + def unindented(text): + """Helper function to be able to use indented multi-line strings.""" + return textwrap.dedent(text).lstrip() + + def test_create_default_rcparams(self): + params = self.rc_def.create_default_rcparams() + assert type(params) is mpl.RcParams + # TODO: check why rcParams.items() returns the elements in an order + # different from the order given at setup - for now, just sort + assert sorted(params.items()) == sorted([ + ("lines.linewidth", 1.5), + ("lines.linestyle", "-"), + ("lines.antialiased", True), + ]) + + def test_write_default_matplotlibrc(self, tmp_path): + self.rc_def.write_default_matplotlibrc(tmp_path / "matplotlibrc") + + lines = (tmp_path / "matplotlibrc").read_text() + assert lines == self.unindented(""" + # *************************************************************************** + # * LINES * + # *************************************************************************** + # See https://matplotlib.org/stable/api/artist_api.html#module-matplotlib.lines + # for more information on line properties. + lines.linewidth: 1.5 # line width in points + lines.linestyle: - # solid line + lines.antialiased: True # antialiasing on lines + """) + + def test_read_matplotlibrc(self, tmp_path): + (tmp_path / "matplotlibrc").write_text(self.unindented(""" + lines.linewidth: 2 # line width in points + lines.linestyle: -- + """)) + params = self.rc_def.read_matplotlibrc(tmp_path / "matplotlibrc") + assert params == { + "lines.linewidth": 2, + "lines.linestyle": "--", + } + + def test_read_matplotlibrc_unknown_key(self, tmp_path): + (tmp_path / "matplotlibrc").write_text(self.unindented(""" + lines.linewidth: 2 # line width in points + lines.unkown_param: "fail" + """)) + with pytest.raises(ValueError, match="Unknown rcParam 'lines.unkown_param'"): + self.rc_def.read_matplotlibrc(tmp_path / "matplotlibrc") + + def test_read_matplotlibrc_invalid_value(self, tmp_path): + (tmp_path / "matplotlibrc").write_text(self.unindented(""" + lines.linewidth: two # line width in points + """)) + with pytest.raises(ValueError, match="Could not convert 'two' to float"): + self.rc_def.read_matplotlibrc(tmp_path / "matplotlibrc") 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