diff --git a/.gitignore b/.gitignore index c74e7438a387..bb332f00e77b 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,8 @@ Thumbs.db ################################### lib/matplotlib/mpl-data/matplotlib.conf lib/matplotlib/mpl-data/matplotlibrc +tutorials/intermediate/CL01.png +tutorials/intermediate/CL02.png # Documentation generated files # ################################# diff --git a/INSTALL.rst b/INSTALL.rst index 9e7528b68bd0..087ff0dd2e3a 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -112,13 +112,6 @@ file will be particularly useful to those packaging Matplotlib. .. _setup.cfg: https://raw.githubusercontent.com/matplotlib/matplotlib/master/setup.cfg.template -If you have installed prerequisites to nonstandard places and need to -inform Matplotlib where they are, edit ``setupext.py`` and add the base -dirs to the ``basedir`` dictionary entry for your ``sys.platform``; -e.g., if the header of some required library is in -``/some/path/include/someheader.h``, put ``/some/path`` in the -``basedir`` list for your platform. - .. _install_requirements: Dependencies diff --git a/doc/_static/mpl.css b/doc/_static/mpl.css index ac4bde0486f9..8cdc2e5668d9 100644 --- a/doc/_static/mpl.css +++ b/doc/_static/mpl.css @@ -594,6 +594,10 @@ span.linkdescr { font-size: 90%; } +.highlight span.c1 span.highlighted { + background-color: #fce5a6; +} + ul.search { margin: 10px 0 0 20px; padding: 0; diff --git a/doc/api/ticker_api.rst b/doc/api/ticker_api.rst index 8da9ac2988f0..652050dafedc 100644 --- a/doc/api/ticker_api.rst +++ b/doc/api/ticker_api.rst @@ -6,3 +6,7 @@ :members: :undoc-members: :show-inheritance: + + +.. inheritance-diagram:: matplotlib.ticker + :parts: 1 diff --git a/doc/devel/documenting_mpl.rst b/doc/devel/documenting_mpl.rst index 26b019af7ea7..98d2fd23ad88 100644 --- a/doc/devel/documenting_mpl.rst +++ b/doc/devel/documenting_mpl.rst @@ -500,15 +500,8 @@ section of the docstring. rcParams ~~~~~~~~ rcParams can be referenced with the custom ``:rc:`` role: -:literal:`:rc:\`foo\`` yields ``rcParams["foo"]``. Use `= [default-val]` -to indicate the default value of the parameter. The default value should be -literal, i.e. enclosed in double backticks. For simplicity these may be -omitted for string default values. - -.. code-block:: rst - - If not provided, defaults to :rc:`figure.figsize` = ``[6.4, 4.8]``. - If not provided, defaults to :rc:`figure.facecolor` = 'w'. +:literal:`:rc:\`foo\`` yields ``rcParams["foo"] = 'default'``, which is a link +to the :file:`matplotlibrc` file description. Deprecated formatting conventions --------------------------------- diff --git a/doc/index.rst b/doc/index.rst index bfb42dc74cca..1abbd0092ebe 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -182,12 +182,25 @@ Open source style="float:right; margin-left:20px" /> + +Matplotlib is a Sponsored Project of NumFOCUS, a 501(c)(3) nonprofit +charity in the United States. NumFOCUS provides Matplotlib with +fiscal, legal, and administrative support to help ensure the health +and sustainability of the project. Visit `numfocus.org `_ for more +information. + +Donations to Matplotlib are managed by NumFOCUS. For donors in the +United States, your gift is tax-deductible to the extent provided by +law. As with any donation, you should consult with your tax adviser +about your particular tax situation. + Please consider `donating to the Matplotlib project `_ through the Numfocus organization or to the `John Hunter Technology Fellowship `_. .. _donating: https://numfocus.salsalabs.org/donate-to-matplotlib/index.html .. _jdh-fellowship: https://www.numfocus.org/programs/john-hunter-technology-fellowship/ +.. _nf: https://numfocus.org The :doc:`Matplotlib license ` is based on the `Python Software Foundation (PSF) license `_. diff --git a/doc/sphinxext/custom_roles.py b/doc/sphinxext/custom_roles.py index 2232f1032424..88dddb6a2e4b 100644 --- a/doc/sphinxext/custom_roles.py +++ b/doc/sphinxext/custom_roles.py @@ -1,16 +1,19 @@ from docutils import nodes from os.path import sep +from matplotlib import rcParamsDefault def rcparam_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - rendered = nodes.Text('rcParams["{}"]'.format(text)) + param = rcParamsDefault.get(text) + rendered = nodes.Text(f'rcParams["{text}"] = {param!r}') source = inliner.document.attributes['source'].replace(sep, '/') rel_source = source.split('/doc/', 1)[1] levels = rel_source.count('/') refuri = ('../' * levels + - 'tutorials/introductory/customizing.html#matplotlib-rcparams') + 'tutorials/introductory/customizing.html' + + f"?highlight={text}#a-sample-matplotlibrc-file") ref = nodes.reference(rawtext, rendered, refuri=refuri) return [nodes.literal('', '', ref)], [] diff --git a/examples/lines_bars_and_markers/horizontal_barchart_distribution.py b/examples/lines_bars_and_markers/horizontal_barchart_distribution.py index 73367e4b1d60..c851997ad29a 100644 --- a/examples/lines_bars_and_markers/horizontal_barchart_distribution.py +++ b/examples/lines_bars_and_markers/horizontal_barchart_distribution.py @@ -70,6 +70,7 @@ def survey(results, category_names): survey(results, category_names) +plt.show() ############################################################################# # diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index a2a70fdc0212..a79b61be6754 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -280,7 +280,11 @@ def _make_layout_margins(ax, renderer, h_pad, w_pad): invTransFig = fig.transFigure.inverted().transform_bbox pos = ax.get_position(original=True) tightbbox = ax.get_tightbbox(renderer=renderer) - bbox = invTransFig(tightbbox) + if tightbbox is None: + bbox = pos + else: + bbox = invTransFig(tightbbox) + # this can go wrong: if not (np.isfinite(bbox.width) and np.isfinite(bbox.height)): # just abort, this is likely a bad set of co-ordinates that diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c14a01fa3a98..89fc3ba576d6 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2163,12 +2163,12 @@ def _convert_dx(dx, x0, xconv, convert): # removes the units from unit packages like `pint` that # wrap numpy arrays. try: - x0 = x0[0] + x0 = cbook.safe_first_element(x0) except (TypeError, IndexError, KeyError): x0 = x0 try: - x = xconv[0] + x = cbook.safe_first_element(xconv) except (TypeError, IndexError, KeyError): x = xconv diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ff5aaf032f51..c2a4d2f42c81 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3267,7 +3267,8 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, reverse = left > right left, right = self.xaxis.get_major_locator().nonsingular(left, right) left, right = self.xaxis.limit_range_for_scale(left, right) - left, right = sorted([left, right], reverse=reverse) + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + left, right = sorted([left, right], reverse=bool(reverse)) self.viewLim.intervalx = (left, right) if auto is not None: @@ -3649,7 +3650,8 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, reverse = bottom > top bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - bottom, top = sorted([bottom, top], reverse=reverse) + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + bottom, top = sorted([bottom, top], reverse=bool(reverse)) self.viewLim.intervaly = (bottom, top) if auto is not None: diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index a4cdffc55e14..e23b5748cf7c 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -2156,7 +2156,8 @@ def get_minpos(self): def set_inverted(self, inverted): # docstring inherited a, b = self.get_view_interval() - self.axes.set_xlim(sorted((a, b), reverse=inverted), auto=None) + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + self.axes.set_xlim(sorted((a, b), reverse=bool(inverted)), auto=None) def set_default_intervals(self): # docstring inherited @@ -2463,7 +2464,8 @@ def get_minpos(self): def set_inverted(self, inverted): # docstring inherited a, b = self.get_view_interval() - self.axes.set_ylim(sorted((a, b), reverse=inverted), auto=None) + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + self.axes.set_ylim(sorted((a, b), reverse=bool(inverted)), auto=None) def set_default_intervals(self): # docstring inherited diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 08357ab0b99a..0be54ae14fd7 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1532,6 +1532,20 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): self.key = key +def _is_non_interactive_terminal_ipython(ip): + """ + Return whether we are in a a terminal IPython, but non interactive. + + When in _terminal_ IPython, ip.parent will have and `interact` attribute, + if this attribute is False we do not setup eventloop integration as the + user will _not_ interact with IPython. In all other case (ZMQKernel, or is + interactive), we do. + """ + return (hasattr(ip, 'parent') + and (ip.parent is not None) + and getattr(ip.parent, 'interact', None) is False) + + class FigureCanvasBase(object): """ The canvas the figure renders into. @@ -1620,15 +1634,8 @@ def _fix_ipython_backend2gui(cls): backend2gui_rif = {"qt5": "qt", "qt4": "qt", "gtk3": "gtk3", "wx": "wx", "macosx": "osx"}.get(rif) if backend2gui_rif: - pt.backend2gui[get_backend()] = backend2gui_rif - # Work around pylabtools.find_gui_and_backend always reading from - # rcParamsOrig. - orig_origbackend = mpl.rcParamsOrig["backend"] - try: - mpl.rcParamsOrig["backend"] = mpl.rcParams["backend"] - ip.enable_matplotlib() - finally: - mpl.rcParamsOrig["backend"] = orig_origbackend + if _is_non_interactive_terminal_ipython(ip): + ip.enable_gui(backend2gui_rif) @contextmanager def _idle_draw_cntx(self): diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 1e60ec14dab9..d7a4fd0f0ca0 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -492,7 +492,8 @@ def draw_idle(self): # current event loop in order to ensure thread affinity and to # accumulate multiple draw requests from event handling. # TODO: queued signal connection might be safer than singleShot - if not (self._draw_pending or self._is_drawing): + if not (getattr(self, '_draw_pending', False) or + getattr(self, '._is_drawing', False)): self._draw_pending = True QtCore.QTimer.singleShot(0, self._draw_idle) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 4dec3b9cb0c4..f1fc3555863d 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -38,16 +38,21 @@ def paintEvent(self, event): painter = QtGui.QPainter(self) - rect = event.rect() - left = rect.left() - top = rect.top() - width = rect.width() - height = rect.height() # See documentation of QRect: bottom() and right() are off by 1, so use # left() + width() and top() + height(). - bbox = Bbox( - [[left, self.renderer.height - (top + height * self._dpi_ratio)], - [left + width * self._dpi_ratio, self.renderer.height - top]]) + rect = event.rect() + # scale rect dimensions using the screen dpi ratio to get + # correct values for the Figure coordinates (rather than QT5's coords) + width = rect.width() * self._dpi_ratio + height = rect.height() * self._dpi_ratio + left, top = self.mouseEventCoords(rect.topLeft()) + # shift the "top" by the height of the image to get the + # correct corner for our coordinate system + bottom = top - height + # same with the right side of the image + right = left + width + # create a buffer using the image bounding box + bbox = Bbox([[left, bottom], [right, top]]) reg = self.copy_from_bbox(bbox) buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32( memoryview(reg)) @@ -60,8 +65,9 @@ def paintEvent(self, event): if hasattr(qimage, 'setDevicePixelRatio'): # Not available on Qt4 or some older Qt5. qimage.setDevicePixelRatio(self._dpi_ratio) - origin = QtCore.QPoint(left, top) - painter.drawImage(origin / self._dpi_ratio, qimage) + # set origin using original QT coordinates + origin = QtCore.QPoint(rect.left(), rect.top()) + painter.drawImage(origin, qimage) # Adjust the buf reference count to work around a memory # leak bug in QImage under PySide on Python 3. if QT_API in ('PySide', 'PySide2'): diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 663d280b0347..54f903ddc6b3 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -836,7 +836,7 @@ def __init__(self, locator, tz=None, formats=None, offset_formats=None, # like 1 Jan can just be labled "Jan". 02:02:00 can # just be labeled 02:02. if zero_formats: - if len(formats) != 6: + if len(zero_formats) != 6: raise ValueError('zero_formats argument must be a list of ' '6 format strings (or None)') self.zero_formats = zero_formats diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index cadb906ab065..782b6897204f 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -790,7 +790,7 @@ def axes(arg=None, **kwargs): Parameters ---------- - arg : { None, 4-tuple, Axes } + arg : None or 4-tuple The exact behavior of this function depends on the type: - *None*: A new full window axes is added using @@ -798,13 +798,6 @@ def axes(arg=None, **kwargs): - 4-tuple of floats *rect* = ``[left, bottom, width, height]``. A new axes is added with dimensions *rect* in normalized (0, 1) units using `~.Figure.add_axes` on the current figure. - - `~.axes.Axes`: This is equivalent to `.pyplot.sca`. - It sets the current axes to *arg*. Note: This implicitly - changes the current figure to the parent of *arg*. - - .. note:: The use of an `.axes.Axes` as an argument is deprecated - and will be removed in v3.0. Please use `.pyplot.sca` - instead. projection : {None, 'aitoff', 'hammer', 'lambert', 'mollweide', \ 'polar', 'rectilinear', str}, optional diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 17d5d7a6dc47..31f755e4375a 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -461,6 +461,19 @@ def validate_fontsize(s): validate_fontsizelist = _listify_validator(validate_fontsize) +def validate_fontweight(s): + weights = [ + 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman', + 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black'] + # Note: Historically, weights have been case-sensitive in Matplotlib + if s in weights: + return s + try: + return int(s) + except (ValueError, TypeError): + raise ValueError(f'{s} is not a valid font weight. %s') + + def validate_font_properties(s): parse_fontconfig_pattern(s) return s @@ -1113,7 +1126,7 @@ def _validate_linestyle(ls): 'font.style': ['normal', validate_string], 'font.variant': ['normal', validate_string], 'font.stretch': ['normal', validate_string], - 'font.weight': ['normal', validate_string], + 'font.weight': ['normal', validate_fontweight], 'font.size': [10, validate_float], # Base font size in points 'font.serif': [['DejaVu Serif', 'Bitstream Vera Serif', 'Computer Modern Roman', @@ -1190,7 +1203,7 @@ def _validate_linestyle(ls): 'axes.titlesize': ['large', validate_fontsize], # fontsize of the # axes title - 'axes.titleweight': ['normal', validate_string], # font weight of axes title + 'axes.titleweight': ['normal', validate_fontweight], # font weight of axes title 'axes.titlepad': [6.0, validate_float], # pad from axes top to title in points 'axes.grid': [False, validate_bool], # display grid or not 'axes.grid.which': ['major', validate_axis_locator], # set whether the gid are by @@ -1202,7 +1215,7 @@ def _validate_linestyle(ls): 'axes.labelsize': ['medium', validate_fontsize], # fontsize of the # x any y labels 'axes.labelpad': [4.0, validate_float], # space between label and axis - 'axes.labelweight': ['normal', validate_string], # fontsize of the x any y labels + 'axes.labelweight': ['normal', validate_fontweight], # fontsize of the x any y labels 'axes.labelcolor': ['black', validate_color], # color of axis label 'axes.formatter.limits': [[-7, 7], validate_nseq_int(2)], # use scientific notation if log10 @@ -1340,7 +1353,7 @@ def _validate_linestyle(ls): ## figure props # figure title 'figure.titlesize': ['large', validate_fontsize], - 'figure.titleweight': ['normal', validate_string], + 'figure.titleweight': ['normal', validate_fontweight], # figure size in inches: width by height 'figure.figsize': [[6.4, 4.8], validate_nseq_float(2)], diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 8d9996c13830..5ca7b8e66d30 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -367,7 +367,7 @@ def remove_coding(text): TEMPLATE = """ {{ source_code }} -{{ only_html }} +.. only:: html {% if source_link or (html_show_formats and not multi_image) %} ( @@ -403,27 +403,15 @@ def remove_coding(text): {{ caption }} {% endfor %} -{{ only_latex }} +.. only:: not html {% for img in images %} - {% if 'pdf' in img.formats -%} - .. figure:: {{ build_dir }}/{{ img.basename }}.pdf + .. figure:: {{ build_dir }}/{{ img.basename }}.* {% for option in options -%} {{ option }} {% endfor %} {{ caption }} - {% endif -%} - {% endfor %} - -{{ only_texinfo }} - - {% for img in images %} - .. image:: {{ build_dir }}/{{ img.basename }}.png - {% for option in options -%} - {{ option }} - {% endfor %} - {% endfor %} """ @@ -794,10 +782,6 @@ def run(arguments, content, options, state_machine, state, lineno): ':%s: %s' % (key, val) for key, val in options.items() if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] - only_html = ".. only:: html" - only_latex = ".. only:: latex" - only_texinfo = ".. only:: texinfo" - # Not-None src_link signals the need for a source link in the generated # html if j == 0 and config.plot_html_show_source_link: @@ -811,9 +795,6 @@ def run(arguments, content, options, state_machine, state, lineno): build_dir=build_dir_link, source_link=src_link, multi_image=len(images) > 1, - only_html=only_html, - only_latex=only_latex, - only_texinfo=only_texinfo, options=opts, images=images, source_code=source_code, diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 5cdabee201f7..8ff5ac45d27d 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1592,6 +1592,14 @@ def test_bar_pandas(pd): ax.plot(dates, baseline, color='orange', lw=4) +def test_bar_pandas_indexed(pd): + # Smoke test for indexed pandas + df = pd.DataFrame({"x": [1., 2., 3.], "width": [.2, .4, .6]}, + index=[1, 2, 3]) + fig, ax = plt.subplots() + ax.bar(df.x, 1., width=df.width) + + @image_comparison(baseline_images=['hist_log'], remove_text=True) def test_hist_log(): diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index f6493cfb9c38..0d4f1bec56a6 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -307,3 +307,23 @@ def test_figureoptions(): "matplotlib.backends.qt_editor._formlayout.FormDialog.exec_", lambda self: None): fig.canvas.manager.toolbar.edit_parameters() + + +@pytest.mark.backend("Qt5Agg") +def test_canvas_reinit(): + import matplotlib.pyplot as plt + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg + from functools import partial + + called = False + + def crashing_callback(fig, stale): + nonlocal called + fig.canvas.draw_idle() + called = True + + fig, ax = plt.subplots() + fig.stale_callback = crashing_callback + # this should not raise + canvas = FigureCanvasQTAgg(fig) + assert called diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index df3e5cf18def..8769fcec3537 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -404,3 +404,16 @@ def test_colorbar_location(): fig.colorbar(pcm, ax=axs[-2, 3:], shrink=0.5, location='top') fig.colorbar(pcm, ax=axs[0, 0], shrink=0.5, location='left') fig.colorbar(pcm, ax=axs[1:3, 2], shrink=0.5, location='right') + + +def test_hidden_axes(): + # test that if we make an axes not visible that constrained_layout + # still works. Note the axes still takes space in the layout + # (as does a gridspec slot that is empty) + fig, axs = plt.subplots(2, 2, constrained_layout=True) + axs[0, 1].set_visible(False) + fig.canvas.draw() + extents1 = np.copy(axs[0, 0].get_position().extents) + + np.testing.assert_allclose(extents1, + [0.045552, 0.548288, 0.47319, 0.982638], rtol=1e-5) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index ae803000ddee..22cd78d46820 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -989,3 +989,18 @@ def test_deprecation(): with pytest.warns(MatplotlibDeprecationWarning): # Enough arguments to pass "shape" positionally. obj.imshow(data, *[None] * 10) + + +def test_image_cursor_formatting(): + fig, ax = plt.subplots() + # Create a dummy image to be able to call format_cursor_data + im = ax.imshow(np.zeros((4, 4))) + + data = np.ma.masked_array([0], mask=[True]) + assert im.format_cursor_data(data) == '[]' + + data = np.ma.masked_array([0], mask=[False]) + assert im.format_cursor_data(data) == '[0]' + + data = np.nan + assert im.format_cursor_data(data) == '[nan]' diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 123ffe4ae3c3..d8fe73cebc2a 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -17,6 +17,7 @@ validate_colorlist, validate_color, validate_bool, + validate_fontweight, validate_nseq_int, validate_nseq_float, validate_cycler, @@ -412,6 +413,26 @@ def test_validator_invalid(validator, arg, exception_type): validator(arg) +@pytest.mark.parametrize('weight, parsed_weight', [ + ('bold', 'bold'), + ('BOLD', ValueError), # weight is case-sensitive + (100, 100), + ('100', 100), + (np.array(100), 100), + # fractional fontweights are not defined. This should actually raise a + # ValueError, but historically did not. + (20.6, 20), + ('20.6', ValueError), + ([100], ValueError), +]) +def test_validate_fontweight(weight, parsed_weight): + if parsed_weight is ValueError: + with pytest.raises(ValueError): + validate_fontweight(weight) + else: + assert validate_fontweight(weight) == parsed_weight + + def test_keymaps(): key_list = [k for k in mpl.rcParams if 'keymap' in k] for k in key_list: diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 3f6130164814..d1aa0c7fd112 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -181,10 +181,11 @@ 'LogFormatterExponent', 'LogFormatterMathtext', 'IndexFormatter', 'LogFormatterSciNotation', 'LogitFormatter', 'EngFormatter', 'PercentFormatter', + 'OldScalarFormatter', 'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator', 'LinearLocator', 'LogLocator', 'AutoLocator', 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator', - 'SymmetricalLogLocator', 'LogitLocator') + 'SymmetricalLogLocator', 'LogitLocator', 'OldAutoLocator') def _mathdefault(s): @@ -628,6 +629,8 @@ def format_data_short(self, value): """ if self._useLocale: return locale.format_string('%-12g', (value,)) + elif isinstance(value, np.ma.MaskedArray) and value.mask: + return '' else: return '%-12g' % value diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 4202be9a1138..047234b365f5 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -626,7 +626,8 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False, reverse = left > right left, right = self.xaxis.get_major_locator().nonsingular(left, right) left, right = self.xaxis.limit_range_for_scale(left, right) - left, right = sorted([left, right], reverse=reverse) + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + left, right = sorted([left, right], reverse=bool(reverse)) self.xy_viewLim.intervalx = (left, right) if auto is not None: