Skip to content

PREVIEW: Define the supported rcParams as code #26088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

timhoffm
Copy link
Member

@timhoffm timhoffm commented Jun 8, 2023

Disclaimer: This is very much work in progress and may substantially change before sent to review. But I want to give interested folks the opportunity to already have an early look.

Motivation

Until now, the parameter definition was dispersed: valid names and defaults are loaded from the canonical matplotlibrc data file. Docs are only available as comments in there. Validators are attached ad-hoc in rcsetup.py.

This makes for a more formal definition of parameters, including meta-information, like validators, docs in a single place. It will simplify the rcParams related code in matplotlib.__init__.py and matplotlib.rcsetup.py. It will also enable us to generate sphinx documentation on the parameters.

Related: #23531, #4394

Scope / context

This can be seen in the context of the larger idea of re-designing the config system. However, for now the direct goal is to:

  • replace matplotlibrc as source of trough
  • simplify assignment of the validators in rcsetup
  • simplify
    def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False):
    """
    Construct a `RcParams` instance from file *fname*.
    Unlike `rc_params_from_file`, the configuration class only contains the
    parameters specified in the file (i.e. default values are not filled in).
    Parameters
    ----------
    fname : path-like
    The loaded file.
    transform : callable, default: the identity function
    A function called on each individual line of the file to transform it,
    before further parsing.
    fail_on_error : bool, default: False
    Whether invalid entries should result in an exception or a warning.
    """
    import matplotlib as mpl
    rc_temp = {}
    with _open_file_or_url(fname) as fd:
    try:
    for line_no, line in enumerate(fd, 1):
    line = transform(line)
    strippedline = cbook._strip_comment(line)
    if not strippedline:
    continue
    tup = strippedline.split(':', 1)
    if len(tup) != 2:
    _log.warning('Missing colon in file %r, line %d (%r)',
    fname, line_no, line.rstrip('\n'))
    continue
    key, val = tup
    key = key.strip()
    val = val.strip()
    if val.startswith('"') and val.endswith('"'):
    val = val[1:-1] # strip double quotes
    if key in rc_temp:
    _log.warning('Duplicate key in file %r, line %d (%r)',
    fname, line_no, line.rstrip('\n'))
    rc_temp[key] = (val, line, line_no)
    except UnicodeDecodeError:
    _log.warning('Cannot decode configuration file %r as utf-8.',
    fname)
    raise
    config = RcParams()
    for key, (val, line, line_no) in rc_temp.items():
    if key in rcsetup._validators:
    if fail_on_error:
    config[key] = val # try to convert to proper type or raise
    else:
    try:
    config[key] = val # try to convert to proper type or skip
    except Exception as msg:
    _log.warning('Bad value in file %r, line %d (%r): %s',
    fname, line_no, line.rstrip('\n'), msg)
    elif key in _deprecated_ignore_map:
    version, alt_key = _deprecated_ignore_map[key]
    _api.warn_deprecated(
    version, name=key, alternative=alt_key, obj_type='rcparam',
    addendum="Please update your matplotlibrc.")
    else:
    # __version__ must be looked up as an attribute to trigger the
    # module-level __getattr__.
    version = ('main' if '.post' in mpl.__version__
    else f'v{mpl.__version__}')
    _log.warning("""
    Bad key %(key)s in file %(fname)s, line %(line_no)s (%(line)r)
    You probably need to get an updated matplotlibrc file from
    https://github.com/matplotlib/matplotlib/blob/%(version)s/lib/matplotlib/mpl-data/matplotlibrc
    or from the matplotlib source distribution""",
    dict(key=key, fname=fname, line_no=line_no,
    line=line.rstrip('\n'), version=version))
    return config
    def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
    """
    Construct a `RcParams` from file *fname*.
    Parameters
    ----------
    fname : str or path-like
    A file with Matplotlib rc settings.
    fail_on_error : bool
    If True, raise an error when the parser fails to convert a parameter.
    use_default_template : bool
    If True, initialize with default parameters before updating with those
    in the given file. If False, the configuration class only contains the
    parameters specified in the file. (Useful for updating dicts.)
    """
    config_from_file = _rc_params_in_file(fname, fail_on_error=fail_on_error)
    if not use_default_template:
    return config_from_file
    with _api.suppress_matplotlib_deprecation_warning():
    config = RcParams({**rcParamsDefault, **config_from_file})
    if "".join(config['text.latex.preamble']):
    _log.info("""
    *****************************************************************
    You have the following UNSUPPORTED LaTeX preamble customizations:
    %s
    Please do not ask for support with these customizations active.
    *****************************************************************
    """, '\n'.join(config['text.latex.preamble']))
    _log.debug('loaded rc file %s', fname)
    return config
    # When constructing the global instances, we need to perform certain updates
    # by explicitly calling the superclass (dict.update, dict.items) to avoid
    # triggering resolution of _auto_backend_sentinel.
    rcParamsDefault = _rc_params_in_file(
    cbook._get_data_path("matplotlibrc"),
    # Strip leading comment.
    transform=lambda line: line[1:] if line.startswith("#") else line,
    fail_on_error=True)
    dict.update(rcParamsDefault, rcsetup._hardcoded_defaults)
    # Normally, the default matplotlibrc file contains *no* entry for backend (the
    # corresponding line starts with ##, not #; we fill on _auto_backend_sentinel
    # in that case. However, packagers can set a different default backend
    # (resulting in a normal `#backend: foo` line) in which case we should *not*
    # fill in _auto_backend_sentinel.
    dict.setdefault(rcParamsDefault, "backend", rcsetup._auto_backend_sentinel)
    rcParams = RcParams() # The global instance.
    dict.update(rcParams, dict.items(rcParamsDefault))
    dict.update(rcParams, _rc_params_in_file(matplotlib_fname()))
    rcParamsOrig = rcParams.copy()
    with _api.suppress_matplotlib_deprecation_warning():
    # This also checks that all rcParams are indeed listed in the template.
    # Assigning to rcsetup.defaultParams is left only for backcompat.
    defaultParams = rcsetup.defaultParams = {
    # We want to resolve deprecated rcParams, but not backend...
    key: [(rcsetup._auto_backend_sentinel if key == "backend" else
    rcParamsDefault[key]),
    validator]
    for key, validator in rcsetup._validators.items()}
    if rcParams['axes.formatter.use_locale']:
    locale.setlocale(locale.LC_ALL, '')
    def rc(group, **kwargs):
    """
    Set the current `.rcParams`. *group* is the grouping for the rc, e.g.,
    for ``lines.linewidth`` the group is ``lines``, for
    ``axes.facecolor``, the group is ``axes``, and so on. Group may
    also be a list or tuple of group names, e.g., (*xtick*, *ytick*).
    *kwargs* is a dictionary attribute name/value pairs, e.g.,::
    rc('lines', linewidth=2, color='r')
    sets the current `.rcParams` and is equivalent to::
    rcParams['lines.linewidth'] = 2
    rcParams['lines.color'] = 'r'
    The following aliases are available to save typing for interactive users:
    ===== =================
    Alias Property
    ===== =================
    'lw' 'linewidth'
    'ls' 'linestyle'
    'c' 'color'
    'fc' 'facecolor'
    'ec' 'edgecolor'
    'mew' 'markeredgewidth'
    'aa' 'antialiased'
    ===== =================
    Thus you could abbreviate the above call as::
    rc('lines', lw=2, c='r')
    Note you can use python's kwargs dictionary facility to store
    dictionaries of default parameters. e.g., you can customize the
    font rc as follows::
    font = {'family' : 'monospace',
    'weight' : 'bold',
    'size' : 'larger'}
    rc('font', **font) # pass in the font dict as kwargs
    This enables you to easily switch between several configurations. Use
    ``matplotlib.style.use('default')`` or :func:`~matplotlib.rcdefaults` to
    restore the default `.rcParams` after changes.
    Notes
    -----
    Similar functionality is available by using the normal dict interface, i.e.
    ``rcParams.update({"lines.linewidth": 2, ...})`` (but ``rcParams.update``
    does not support abbreviations or grouping).
    """
    aliases = {
    'lw': 'linewidth',
    'ls': 'linestyle',
    'c': 'color',
    'fc': 'facecolor',
    'ec': 'edgecolor',
    'mew': 'markeredgewidth',
    'aa': 'antialiased',
    }
    if isinstance(group, str):
    group = (group,)
    for g in group:
    for k, v in kwargs.items():
    name = aliases.get(k) or k
    key = f'{g}.{name}'
    try:
    rcParams[key] = v
    except KeyError as err:
    raise KeyError(('Unrecognized key "%s" for group "%s" and '
    'name "%s"') % (key, g, name)) from err
    def rcdefaults():
    """
    Restore the `.rcParams` from Matplotlib's internal default style.
    Style-blacklisted `.rcParams` (defined in
    ``matplotlib.style.core.STYLE_BLACKLIST``) are not updated.
    See Also
    --------
    matplotlib.rc_file_defaults
    Restore the `.rcParams` from the rc file originally loaded by
    Matplotlib.
    matplotlib.style.use
    Use a specific style file. Call ``style.use('default')`` to restore
    the default style.
    """
    # Deprecation warnings were already handled when creating rcParamsDefault,
    # no need to reemit them here.
    with _api.suppress_matplotlib_deprecation_warning():
    from .style.core import STYLE_BLACKLIST
    rcParams.clear()
    rcParams.update({k: v for k, v in rcParamsDefault.items()
    if k not in STYLE_BLACKLIST})
    def rc_file_defaults():
    """
    Restore the `.rcParams` from the original rc file loaded by Matplotlib.
    Style-blacklisted `.rcParams` (defined in
    ``matplotlib.style.core.STYLE_BLACKLIST``) are not updated.
    """
    # Deprecation warnings were already handled when creating rcParamsOrig, no
    # need to reemit them here.
    with _api.suppress_matplotlib_deprecation_warning():
    from .style.core import STYLE_BLACKLIST
    rcParams.update({k: rcParamsOrig[k] for k in rcParamsOrig
    if k not in STYLE_BLACKLIST})
    def rc_file(fname, *, use_default_template=True):
    """
    Update `.rcParams` from file.
    Style-blacklisted `.rcParams` (defined in
    ``matplotlib.style.core.STYLE_BLACKLIST``) are not updated.
    Parameters
    ----------
    fname : str or path-like
    A file with Matplotlib rc settings.
    use_default_template : bool
    If True, initialize with default parameters before updating with those
    in the given file. If False, the current configuration persists
    and only the parameters specified in the file are updated.
    """
    # Deprecation warnings were already handled in rc_params_from_file, no need
    # to reemit them here.
    with _api.suppress_matplotlib_deprecation_warning():
    from .style.core import STYLE_BLACKLIST
    rc_from_file = rc_params_from_file(
    fname, use_default_template=use_default_template)
    rcParams.update({k: rc_from_file[k] for k in rc_from_file
    if k not in STYLE_BLACKLIST})
    @contextlib.contextmanager
    def rc_context(rc=None, fname=None):
    """
    Return a context manager for temporarily changing rcParams.
    The :rc:`backend` will not be reset by the context manager.
    rcParams changed both through the context manager invocation and
    in the body of the context will be reset on context exit.
    Parameters
    ----------
    rc : dict
    The rcParams to temporarily set.
    fname : str or path-like
    A file with Matplotlib rc settings. If both *fname* and *rc* are given,
    settings from *rc* take precedence.
    See Also
    --------
    :ref:`customizing-with-matplotlibrc-files`
    Examples
    --------
    Passing explicit values via a dict::
    with mpl.rc_context({'interactive': False}):
    fig, ax = plt.subplots()
    ax.plot(range(3), range(3))
    fig.savefig('example.png')
    plt.close(fig)
    Loading settings from a file::
    with mpl.rc_context(fname='print.rc'):
    plt.plot(x, y) # uses 'print.rc'
    Setting in the context body::
    with mpl.rc_context():
    # will be reset
    mpl.rcParams['lines.linewidth'] = 5
    plt.plot(x, y)
    """
    orig = dict(rcParams.copy())
    del orig['backend']
    try:
    if fname:
    rc_file(fname)
    if rc:
    rcParams.update(rc)
    yield
    finally:
    dict.update(rcParams, orig) # Revert to the original rcs.

This can still be done with the current RcParams object in place.

This definition will likely become private and users will still use rcParams, rcParamsDefault etc. for the time being.

Until now, the parameter definition was dispersed:
valid names and defaults are loaded from the
canonical `matplotlibrc` data file. Docs are only
available as comments in there. Validators are
attached ad-hoc in `rcsetup.py`.

This makes for a more formal definition of parameters,
including meta-information, like validators, docs in
a single place. It will simplify the rcParams related
code in `matplotlib.__init__.py` and `matplotlib.rcsetup.py`.
It will also enable us to generate sphinx documentation
on the parameters.
@ksunden ksunden marked this pull request as draft June 8, 2023 00:25
Comment on lines +1410 to +1413
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}"),
Copy link
Member

@story645 story645 Jun 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I'm biased towards doing type based validation - Param is of type capstyle so it gets validated by a function that lives in/as part of a capstyle type definition. That way it's consistent here and throughout the library, rather than here where you're defining the set of valid styles twice. Honestly I'd even prefer something as basic as a lines.capstyles constant that holds the set.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect this to be a larger project with multiple development steps, possibly over multple PRs. As a first step here, I'm only consolidating the existing structure into a single place. Changing the validation logic at the same time would be too much.
Rethinking validation is on the plan and will become easier once this first step is taken.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there be comments marking each section?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

@chahak13
Copy link
Contributor

chahak13 commented Jun 8, 2023

This is definitely useful, I think. I had a few notes on this while working on #25617 and will keep an eye on this too. Thanks!

@oscargus
Copy link
Member

oscargus commented Jun 8, 2023

Is this related to the documentation of the rcParams? That is, is the "comment" a comment or a doc-string? I guess that the idea right now is to recreate the style file template?

I haven't fully got my head around it, but since I think that rcParam docs is a quite crucial issues, I'm thinking if it can be added here as well? Although not hard to add at a later stage, it may be useful to be able to generate documentation for each rcParam.

@timhoffm
Copy link
Member Author

timhoffm commented Jun 8, 2023

That is, is the "comment" a comment or a doc-string? I guess that the idea right now is to recreate the style file template?

For now (step 1) it's only to recreate the template. I plan to add the possibility for proper documentation later, which then can also be extracted into sphinx pages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants