diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index df170bdf..8665e1d2 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b78e43e1..8483e70c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-docstring-first - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.10.1 hooks: - id: black @@ -17,14 +17,14 @@ repos: - id: napari-plugin-checks - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.6.1 hooks: - id: mypy additional_dependencies: [numpy, matplotlib] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: 'v0.0.285' + rev: 'v0.1.3' hooks: - id: ruff diff --git a/README.md b/README.md index 855c4991..e4551d23 100644 --- a/README.md +++ b/README.md @@ -15,32 +15,7 @@ A plugin to create Matplotlib plots from napari layers ## Introduction `napari-matplotlib` is a bridge between `napari` and `matplotlib`, making it easy to create publication quality `Matplotlib` plots based on the data loaded in `napari` layers. -## Available widgets - -### `Slice` -Plots 1D slices of data along a specified axis. - - -### `Histogram` -Plots histograms of individual image layers, or RGB histograms of an RGB image - - -### `Scatter` -Scatters the values of two similarly sized images layers against each other. - - -## Installation - -You can install `napari-matplotlib` via [pip]: - - pip install napari-matplotlib - - - -To install latest development version : - - pip install git+https://github.com/matplotlib/napari-matplotlib.git - +Documentaiton can be found at https://napari-matplotlib.github.io/ ## Contributing diff --git a/baseline/test_feature_histogram2.png b/baseline/test_feature_histogram2.png deleted file mode 100644 index b7bb19e0..00000000 Binary files a/baseline/test_feature_histogram2.png and /dev/null differ diff --git a/docs/changelog.rst b/docs/changelog.rst index 6f77e0c3..45952311 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,13 @@ Changelog ========= +1.2.0 +----- +Changes +~~~~~~~ +- Dropped support for Python 3.8, and added support for Python 3.11. +- Histogram plots of points and vector layers are now coloured with their napari colourmap. +- Added support for Matplotlib 3.8 + 1.1.0 ----- Additions diff --git a/pyproject.toml b/pyproject.toml index 7c7dbbdd..705b4655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ profile = "black" line_length = 79 [tool.ruff] -target-version = "py38" +target-version = "py39" select = ["I", "UP", "F", "E", "W", "D"] ignore = [ "D100", # Missing docstring in public module @@ -45,7 +45,7 @@ fix = true convention = "numpy" [tool.mypy] -python_version = "3.8" +python_version = "3.9" # Block below are checks that form part of mypy 'strict' mode warn_unused_configs = true warn_redundant_casts = true diff --git a/setup.cfg b/setup.cfg index e1fc9e73..f308412c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ author_email = d.stansby@ucl.ac.uk license = BSD-3-Clause license_files = LICENSE classifiers = - Development Status :: 3 - Alpha + Development Status :: 5 - Production/Stable Framework :: napari Intended Audience :: Developers License :: OSI Approved :: BSD License @@ -31,7 +31,7 @@ install_requires = napari numpy tinycss2 -python_requires = >=3.8 +python_requires = >=3.9 include_package_data = True package_dir = =src @@ -49,6 +49,7 @@ napari.manifest = docs = napari[all]==0.4.17 numpydoc + pydantic<2 pydata-sphinx-theme qtgallery sphinx diff --git a/src/napari_matplotlib/base.py b/src/napari_matplotlib/base.py index 792b5aff..0ff5e389 100644 --- a/src/napari_matplotlib/base.py +++ b/src/napari_matplotlib/base.py @@ -1,12 +1,12 @@ import os from pathlib import Path -from typing import List, Optional, Tuple +from typing import Optional import matplotlib import matplotlib.style as mplstyle import napari -from matplotlib.backends.backend_qtagg import ( - FigureCanvas, +from matplotlib.backends.backend_qtagg import ( # type: ignore[attr-defined] + FigureCanvasQTAgg, NavigationToolbar2QT, ) from matplotlib.figure import Figure @@ -49,12 +49,10 @@ def __init__( # Sets figure.* style with mplstyle.context(self.mpl_style_sheet_path): - self.canvas = FigureCanvas() + self.canvas = FigureCanvasQTAgg() # type: ignore[no-untyped-call] self.canvas.figure.set_layout_engine("constrained") - self.toolbar = NapariNavigationToolbar( - self.canvas, parent=self - ) # type: ignore[no-untyped-call] + self.toolbar = NapariNavigationToolbar(self.canvas, parent=self) self._replace_toolbar_icons() # callback to update when napari theme changed # TODO: this isn't working completely (see issue #140) @@ -97,7 +95,7 @@ def add_single_axes(self) -> None: # Sets axes.* style. # Does not set any text styling set by axes.* keys with mplstyle.context(self.mpl_style_sheet_path): - self.axes = self.figure.subplots() + self.axes = self.figure.add_subplot() def _on_napari_theme_changed(self) -> None: """ @@ -184,7 +182,7 @@ class NapariMPLWidget(BaseNapariMPLWidget): #: Number of layers taken as input n_layers_input = Interval(None, None) #: Type of layer taken as input - input_layer_types: Tuple[napari.layers.Layer, ...] = (napari.layers.Layer,) + input_layer_types: tuple[napari.layers.Layer, ...] = (napari.layers.Layer,) def __init__( self, @@ -193,7 +191,7 @@ def __init__( ): super().__init__(napari_viewer=napari_viewer, parent=parent) self._setup_callbacks() - self.layers: List[napari.layers.Layer] = [] + self.layers: list[napari.layers.Layer] = [] helper_text = self.n_layers_input._helper_text if helper_text is not None: @@ -260,7 +258,7 @@ def _draw(self) -> None: isinstance(layer, self.input_layer_types) for layer in self.layers ): self.draw() - self.canvas.draw() + self.canvas.draw() # type: ignore[no-untyped-call] def clear(self) -> None: """ @@ -309,8 +307,8 @@ def clear(self) -> None: class NapariNavigationToolbar(NavigationToolbar2QT): """Custom Toolbar style for Napari.""" - def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + super().__init__(*args, **kwargs) # type: ignore[no-untyped-call] self.setIconSize( from_napari_css_get_size_of( "QtViewerPushButton", fallback=(28, 28) @@ -319,7 +317,7 @@ def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def] def _update_buttons_checked(self) -> None: """Update toggle tool icons when selected/unselected.""" - super()._update_buttons_checked() + super()._update_buttons_checked() # type: ignore[no-untyped-call] icon_dir = self.parentWidget()._get_path_to_icon() # changes pan/zoom icons depending on state (checked or not) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 66aa7acc..f78a8503 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -1,8 +1,9 @@ -from typing import Any, List, Optional, Tuple +from typing import Any, Optional, cast import napari import numpy as np import numpy.typing as npt +from matplotlib.container import BarContainer from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget from .base import SingleAxesWidget @@ -107,7 +108,7 @@ def _set_axis_keys(self, x_axis_key: str) -> None: self._x_axis_key = x_axis_key self._draw() - def _get_valid_axis_keys(self) -> List[str]: + def _get_valid_axis_keys(self) -> list[str]: """ Get the valid axis keys from the layer FeatureTable. @@ -122,7 +123,7 @@ def _get_valid_axis_keys(self) -> List[str]: else: return self.layers[0].features.keys() - def _get_data(self) -> Tuple[Optional[npt.NDArray[Any]], str]: + def _get_data(self) -> tuple[Optional[npt.NDArray[Any]], str]: """Get the plot data. Returns @@ -162,12 +163,39 @@ def on_update_layers(self) -> None: def draw(self) -> None: """Clear the axes and histogram the currently selected layer/slice.""" + # get the colormap from the layer depending on its type + if isinstance(self.layers[0], napari.layers.Points): + colormap = self.layers[0].face_colormap + self.layers[0].face_color = self.x_axis_key + elif isinstance(self.layers[0], napari.layers.Vectors): + colormap = self.layers[0].edge_colormap + self.layers[0].edge_color = self.x_axis_key + else: + colormap = None + + # apply new colors to the layer + self.viewer.layers[self.layers[0].name].refresh_colors(True) + self.viewer.layers[self.layers[0].name].refresh() + + # Draw the histogram data, x_axis_name = self._get_data() if data is None: return - self.axes.hist(data, bins=50, edgecolor="white", linewidth=0.3) + _, bins, patches = self.axes.hist( + data, bins=50, edgecolor="white", linewidth=0.3 + ) + patches = cast(BarContainer, patches) + + # recolor the histogram plot + if colormap is not None: + self.bins_norm = (bins - bins.min()) / (bins.max() - bins.min()) + colors = colormap.map(self.bins_norm) + + # Set histogram style: + for idx, patch in enumerate(patches): + patch.set_facecolor(colors[idx]) # set ax labels self.axes.set_xlabel(x_axis_name) diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index a4148bd2..67d6896c 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Optional, Union import napari import numpy.typing as npt @@ -40,7 +40,7 @@ def draw(self) -> None: self.axes.set_xlabel(x_axis_name) self.axes.set_ylabel(y_axis_name) - def _get_data(self) -> Tuple[npt.NDArray[Any], npt.NDArray[Any], str, str]: + def _get_data(self) -> tuple[npt.NDArray[Any], npt.NDArray[Any], str, str]: """ Get the plot data. @@ -67,7 +67,7 @@ class ScatterWidget(ScatterBaseWidget): n_layers_input = Interval(2, 2) input_layer_types = (napari.layers.Image,) - def _get_data(self) -> Tuple[npt.NDArray[Any], npt.NDArray[Any], str, str]: + def _get_data(self) -> tuple[npt.NDArray[Any], npt.NDArray[Any], str, str]: """ Get the plot data. @@ -106,7 +106,7 @@ def __init__( self.layout().addLayout(QVBoxLayout()) - self._selectors: Dict[str, QComboBox] = {} + self._selectors: dict[str, QComboBox] = {} for dim in ["x", "y"]: self._selectors[dim] = QComboBox() # Re-draw when combo boxes are updated @@ -147,7 +147,7 @@ def y_axis_key(self, key: str) -> None: self._selectors["y"].setCurrentText(key) self._draw() - def _get_valid_axis_keys(self) -> List[str]: + def _get_valid_axis_keys(self) -> list[str]: """ Get the valid axis keys from the layer FeatureTable. @@ -186,7 +186,7 @@ def draw(self) -> None: if self._ready_to_scatter(): super().draw() - def _get_data(self) -> Tuple[npt.NDArray[Any], npt.NDArray[Any], str, str]: + def _get_data(self) -> tuple[npt.NDArray[Any], npt.NDArray[Any], str, str]: """ Get the plot data from the ``features`` attribute of the first selected layer. diff --git a/src/napari_matplotlib/slice.py b/src/napari_matplotlib/slice.py index 393f2e45..9459fa97 100644 --- a/src/napari_matplotlib/slice.py +++ b/src/napari_matplotlib/slice.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional, Tuple +from typing import Any, Optional import matplotlib.ticker as mticker import napari @@ -99,7 +99,7 @@ def current_dim_index(self) -> int: return self._dim_names.index(self.current_dim_name) @property - def _dim_names(self) -> List[str]: + def _dim_names(self) -> list[str]: """ List of dimension names. This is a property as it varies depending on the dimensionality of the currently selected data. @@ -111,7 +111,7 @@ def _dim_names(self) -> List[str]: else: raise RuntimeError("Don't know how to handle ndim != 2 or 3") - def _get_xy(self) -> Tuple[npt.NDArray[Any], npt.NDArray[Any]]: + def _get_xy(self) -> tuple[npt.NDArray[Any], npt.NDArray[Any]]: """ Get data for plotting. """ diff --git a/src/napari_matplotlib/tests/baseline/test_custom_theme.png b/src/napari_matplotlib/tests/baseline/test_custom_theme.png index c3858522..a668c103 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_custom_theme.png and b/src/napari_matplotlib/tests/baseline/test_custom_theme.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_feature_histogram2.png b/src/napari_matplotlib/tests/baseline/test_feature_histogram2.png deleted file mode 100644 index b7bb19e0..00000000 Binary files a/src/napari_matplotlib/tests/baseline/test_feature_histogram2.png and /dev/null differ diff --git a/src/napari_matplotlib/tests/baseline/test_feature_histogram_points.png b/src/napari_matplotlib/tests/baseline/test_feature_histogram_points.png new file mode 100644 index 00000000..b98a0170 Binary files /dev/null and b/src/napari_matplotlib/tests/baseline/test_feature_histogram_points.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_feature_histogram_vectors.png b/src/napari_matplotlib/tests/baseline/test_feature_histogram_vectors.png new file mode 100644 index 00000000..3b90586e Binary files /dev/null and b/src/napari_matplotlib/tests/baseline/test_feature_histogram_vectors.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_2D.png b/src/napari_matplotlib/tests/baseline/test_histogram_2D.png index b76d1e10..b043bba8 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_histogram_2D.png and b/src/napari_matplotlib/tests/baseline/test_histogram_2D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_3D.png b/src/napari_matplotlib/tests/baseline/test_histogram_3D.png index 2dffdcb2..724314e1 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_histogram_3D.png and b/src/napari_matplotlib/tests/baseline/test_histogram_3D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_slice_2D.png b/src/napari_matplotlib/tests/baseline/test_slice_2D.png index ee3ce3b6..d39920be 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_slice_2D.png and b/src/napari_matplotlib/tests/baseline/test_slice_2D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_slice_3D.png b/src/napari_matplotlib/tests/baseline/test_slice_3D.png index c30211da..cf563de5 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_slice_3D.png and b/src/napari_matplotlib/tests/baseline/test_slice_3D.png differ diff --git a/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png b/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png index 269ebd01..75965607 100644 Binary files a/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png and b/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png differ diff --git a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png index f96b5354..10219106 100644 Binary files a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png and b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png differ diff --git a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png index 27e7d673..3e648eec 100644 Binary files a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png and b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png differ diff --git a/src/napari_matplotlib/tests/scatter/test_scatter_features.py b/src/napari_matplotlib/tests/scatter/test_scatter_features.py index b5a396fd..3ede1e28 100644 --- a/src/napari_matplotlib/tests/scatter/test_scatter_features.py +++ b/src/napari_matplotlib/tests/scatter/test_scatter_features.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import Any, Dict, Tuple +from typing import Any import numpy as np import numpy.typing as npt @@ -34,7 +34,7 @@ def test_features_scatter_widget_2D( def make_labels_layer_with_features() -> ( - Tuple[npt.NDArray[np.uint16], Dict[str, Any]] + tuple[npt.NDArray[np.uint16], dict[str, Any]] ): label_image: npt.NDArray[np.uint16] = np.zeros((100, 100), dtype=np.uint16) for label_value, start_index in enumerate([10, 30, 50], start=1): diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 006c042f..1ceca519 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -38,6 +38,8 @@ def test_histogram_3D(make_napari_viewer, brain_data): def test_feature_histogram(make_napari_viewer): n_points = 1000 random_points = np.random.random((n_points, 3)) * 10 + random_directions = np.random.random((n_points, 3)) * 10 + random_vectors = np.stack([random_points, random_directions], axis=1) feature1 = np.random.random(n_points) feature2 = np.random.normal(size=n_points) @@ -47,10 +49,10 @@ def test_feature_histogram(make_napari_viewer): properties={"feature1": feature1, "feature2": feature2}, name="points1", ) - viewer.add_points( - random_points, + viewer.add_vectors( + random_vectors, properties={"feature1": feature1, "feature2": feature2}, - name="points2", + name="vectors1", ) widget = FeaturesHistogramWidget(viewer) @@ -70,26 +72,42 @@ def test_feature_histogram(make_napari_viewer): @pytest.mark.mpl_image_compare -def test_feature_histogram2(make_napari_viewer): - import numpy as np +def test_feature_histogram_vectors(make_napari_viewer): + n_points = 1000 + np.random.seed(42) + random_points = np.random.random((n_points, 3)) * 10 + random_directions = np.random.random((n_points, 3)) * 10 + random_vectors = np.stack([random_points, random_directions], axis=1) + feature1 = np.random.random(n_points) + + viewer = make_napari_viewer() + viewer.add_vectors( + random_vectors, + properties={"feature1": feature1}, + name="vectors1", + ) + + widget = FeaturesHistogramWidget(viewer) + viewer.window.add_dock_widget(widget) + widget._set_axis_keys("feature1") + fig = FeaturesHistogramWidget(viewer).figure + return deepcopy(fig) + + +@pytest.mark.mpl_image_compare +def test_feature_histogram_points(make_napari_viewer): np.random.seed(0) n_points = 1000 random_points = np.random.random((n_points, 3)) * 10 feature1 = np.random.random(n_points) - feature2 = np.random.normal(size=n_points) viewer = make_napari_viewer() viewer.add_points( random_points, - properties={"feature1": feature1, "feature2": feature2}, + properties={"feature1": feature1}, name="points1", ) - viewer.add_points( - random_points, - properties={"feature1": feature1, "feature2": feature2}, - name="points2", - ) widget = FeaturesHistogramWidget(viewer) viewer.window.add_dock_widget(widget) diff --git a/src/napari_matplotlib/tests/test_layer_changes.py b/src/napari_matplotlib/tests/test_layer_changes.py index bdd6c600..15958c07 100644 --- a/src/napari_matplotlib/tests/test_layer_changes.py +++ b/src/napari_matplotlib/tests/test_layer_changes.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import Any, Dict, Tuple, Type +from typing import Any import numpy as np import numpy.typing as npt @@ -61,8 +61,8 @@ def test_change_features_layer( def assert_features_plot_changes( viewer: Viewer, - widget_cls: Type[NapariMPLWidget], - data: Tuple[npt.NDArray[np.generic], Dict[str, Any]], + widget_cls: type[NapariMPLWidget], + data: tuple[npt.NDArray[np.generic], dict[str, Any]], ) -> None: """ When the selected layer is changed, make sure the plot generated diff --git a/src/napari_matplotlib/tests/test_theme.py b/src/napari_matplotlib/tests/test_theme.py index 2a340c89..1042d3f3 100644 --- a/src/napari_matplotlib/tests/test_theme.py +++ b/src/napari_matplotlib/tests/test_theme.py @@ -187,6 +187,6 @@ def test_custom_stylesheet(make_napari_viewer, image_data): assert widget.figure.patch.get_facecolor() == to_rgba("#fdf6e3") for gridline in ax.get_xgridlines() + ax.get_ygridlines(): assert gridline.get_visible() is True - assert gridline.get_color() == "#fdf6e3" + assert gridline.get_color() == "#b0b0b0" finally: os.remove(style_sheet_path) diff --git a/src/napari_matplotlib/util.py b/src/napari_matplotlib/util.py index 2aa15ddd..7d72c9e2 100644 --- a/src/napari_matplotlib/util.py +++ b/src/napari_matplotlib/util.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple, Union +from typing import Optional, Union from warnings import warn import napari.qt @@ -76,7 +76,7 @@ def _helper_text(self) -> Optional[str]: return helper_text -def _has_id(nodes: List[tinycss2.ast.Node], id_name: str) -> bool: +def _has_id(nodes: list[tinycss2.ast.Node], id_name: str) -> bool: """ Is `id_name` in IdentTokens in the list of CSS `nodes`? """ @@ -86,7 +86,7 @@ def _has_id(nodes: List[tinycss2.ast.Node], id_name: str) -> bool: def _get_dimension( - nodes: List[tinycss2.ast.Node], id_name: str + nodes: list[tinycss2.ast.Node], id_name: str ) -> Union[int, None]: """ Get the value of the DimensionToken for the IdentToken `id_name`. @@ -108,7 +108,7 @@ def _get_dimension( def from_napari_css_get_size_of( - qt_element_name: str, fallback: Tuple[int, int] + qt_element_name: str, fallback: tuple[int, int] ) -> QSize: """ Get the size of `qt_element_name` from napari's current stylesheet. diff --git a/tox.ini b/tox.ini index 298887e1..4ec0c702 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,14 @@ [tox] -envlist = py{38,39,310} +envlist = py{39,310,311} isolated_build = true [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [testenv] extras = testing -allowlist_externals = - cp - ls commands = - cp -R {toxinidir}/src/napari_matplotlib/tests/baseline {envdir}/baseline - ls {toxinidir}/src/napari_matplotlib/tests/baseline python -m pytest --mpl --mpl-generate-summary=html --mpl-results-path={toxinidir}/reports -v --color=yes --cov=napari_matplotlib --cov-report=xml
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: