diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73f481ad..26347ccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - # Run static checks only for Ubuntu and Python 3.9 + # Run static checks only for Ubuntu and the lowest support version of Python static-checks: name: Static checks runs-on: ubuntu-latest @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-python with: - python-version: "3.9" + python-version: "3.10" requirements: tox # Instead of running all checks in one task (i.e., `tox -m static`), # we'll instead run them one-by-one, so that it's easier to @@ -47,11 +47,11 @@ jobs: strategy: matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" + #- "3.14" os: - ubuntu-latest # There shouldn't be any behavior differences between OSes, diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e2f53c3..53303284 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: fetch-depth: 0 - uses: ./.github/actions/setup-python with: - python-version: "3.9" + python-version: "3.10" requirements: tox - name: Build source (sdist) and binary (wheel) distributions run: tox -e build-dists @@ -112,7 +112,7 @@ jobs: fetch-depth: 0 - uses: ./.github/actions/setup-python with: - python-version: "3.9" + python-version: "3.10" requirements: tox - name: Generate release notes run: tox -e release-notes diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37979ef0..1d794156 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.9 + python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c128eeaa..4b428e03 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,7 +5,7 @@ version: 2 build: os: ubuntu-lts-latest tools: - python: "3.9" + python: "3.10" apt_packages: # chromium-browser is required by Kaleido to generate Plotly figures via fig.write_image() - chromium-browser diff --git a/Makefile b/Makefile index 30f74d1b..e13afd42 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # >>> Variables # ============================================================== -BASE_PYTHON ?= python3.9 +BASE_PYTHON ?= python3.10 VENV_PATH := .venv VENV_BIN := $(VENV_PATH)/bin diff --git a/docs/conf.py b/docs/conf.py index 78f0d198..d525f428 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,9 @@ from __future__ import annotations +import ast import importlib.metadata +import inspect +import pkgutil import sys from contextlib import contextmanager from datetime import datetime @@ -10,6 +13,8 @@ from typing_extensions import Any +import ridgeplot as ridgeplot_pkg + try: from ridgeplot_examples import ALL_EXAMPLES except ImportError: @@ -271,47 +276,49 @@ # Type aliases -_TYPE_ALIASES_FULLY_QUALIFIED = { - # ------- ._color.css_colors ------------------- - "ridgeplot._color.css_colors.CssNamedColor", - # ------- ._color.interpolation ---------------- - "ridgeplot._color.interpolation.ColorscaleInterpolants", - "ridgeplot._color.interpolation.SolidColormode", - # ------- ._kde -------------------------------- - "ridgeplot._kde.KDEPoints", - "ridgeplot._kde.KDEBandwidth", - # ------- ._missing ---------------------------- - "ridgeplot._missing.MISSING", - "ridgeplot._missing.MissingType", - # ------- ._types ------------------------------ - "ridgeplot._types.Color", - "ridgeplot._types.ColorScale", - "ridgeplot._types.NormalisationOption", - "ridgeplot._types.CollectionL1", - "ridgeplot._types.CollectionL2", - "ridgeplot._types.CollectionL3", - "ridgeplot._types.Float", - "ridgeplot._types.Int", - "ridgeplot._types.Numeric", - "ridgeplot._types.NumericT", - "ridgeplot._types.XYCoordinate", - "ridgeplot._types.DensityTrace", - "ridgeplot._types.DensitiesRow", - "ridgeplot._types.Densities", - "ridgeplot._types.ShallowDensities", - "ridgeplot._types.SamplesTrace", - "ridgeplot._types.SamplesRow", - "ridgeplot._types.Samples", - "ridgeplot._types.ShallowSamples", - "ridgeplot._types.TraceType", - "ridgeplot._types.TraceTypesArray", - "ridgeplot._types.ShallowTraceTypesArray", - "ridgeplot._types.LabelsArray", - "ridgeplot._types.ShallowLabelsArray", - "ridgeplot._types.SampleWeights", - "ridgeplot._types.SampleWeightsArray", - "ridgeplot._types.ShallowSampleWeightsArray", -} +def _get_module_type_aliases(module_name: str) -> set[str]: + module = import_module(module_name) + source = inspect.getsource(module) + tree = ast.parse(source) + type_aliases = { + f"{module_name}.{node.target.id}" + for node in ast.walk(tree) + # Handle annotated assignments: x: TypeAlias = ... + if ( + isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and isinstance(node.annotation, ast.Name) + and node.annotation.id == "TypeAlias" + ) + } + return type_aliases + + +def _get_package_type_aliases(package_name: str) -> set[str]: + """Recursively extract TypeAlias definitions from a package.""" + package = import_module(package_name) + prefix = f"{package.__name__}." + type_aliases = set() + for _, modname, is_pkg in pkgutil.walk_packages(package.__path__, prefix): + if is_pkg: + type_aliases.update(_get_package_type_aliases(modname)) + else: + type_aliases.update(_get_module_type_aliases(modname)) + return type_aliases + + +_TYPE_ALIASES_FULLY_QUALIFIED = ( + # Automatically extract all TypeAlias + # definitions from the `ridgeplot` package + _get_package_type_aliases(ridgeplot_pkg.__name__) + | { + # `MISSING` is a special case that is + # widely referenced in type annotations + "ridgeplot._missing.MISSING", + # Same thing with the `NumericT` TypeVar + "ridgeplot._types.NumericT", + } +) for fq in _TYPE_ALIASES_FULLY_QUALIFIED: module_name, _, type_name = fq.rpartition(".") try: diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 1c2e686e..880a67cb 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -26,7 +26,7 @@ Here are our guidelines for setting a **working** development environment. Most :::{admonition} Prerequisites :class: important -To follow along with this guide, you will need to have [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), and [uv](https://docs.astral.sh/uv/) installed on your system. We recommend using [uv](https://docs.astral.sh/uv/guides/install-python/) to also manage your Python installations. For this project you will need Python 3.9 or higher. +To follow along with this guide, you will need to have [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), and [uv](https://docs.astral.sh/uv/) installed on your system. We recommend using [uv](https://docs.astral.sh/uv/guides/install-python/) to also manage your Python installations. For this project you will need Python 3.10 or higher. ::: First, you will need to [clone](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#step-2-create-a-local-clone-of-your-fork) this repository. For this, make sure you have a [GitHub account](https://github.com/join), fork ridgeplot by clicking the [Fork](https://github.com/tpvasconcelos/ridgeplot/fork) button, and clone the main repository locally (_e.g.,_ using SSH) @@ -55,10 +55,10 @@ The following command will: make init ``` -The default and **recommended** base Python is `python3.9`. However, if you encounter any issues or don't have this specific version installed on your system, you can change by it exporting the `BASE_PYTHON` environment variable to a valid executable you do have installed. Please note that we no longer support any Python versions lower than 3.9. For example, to use `python3.13`, you should run: +The default and **recommended** base Python is `python3.10`. However, if you encounter any issues or don't have this specific version installed on your system, you can change by it exporting the `BASE_PYTHON` environment variable to a valid executable you do have installed. Please note that we no longer support any Python versions lower than 3.10. For example, to use `python3.14`, you should run: ```shell -BASE_PYTHON=python3.13 make init +BASE_PYTHON=python3.14 make init ``` If for whatever reason you don't have a working internet connection (✈️, ⛵, 🏕️, 🚀, etc.), you can try exposing the `OFFLINE=1` environment variable. This will only work if compatible packages have already been cached by `uv` on your system. diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md index d69e4dff..e4444331 100644 --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -5,11 +5,12 @@ This document outlines the list of changes to ridgeplot between each release. Fo Unreleased changes ------------------ +- Dropped support for Python 3.9, in accordance with the official Python support policy[^1] ({gh-pr}`345`) - Bump project classification from Pre-Alpha to Alpha ({gh-pr}`336`) -- Bump actions/github-script from 7 to 8 ({gh-pr}`338`) ### CI/CD +- Bump actions/github-script from 7 to 8 ({gh-pr}`338`) - pre-commit autoupdate ({gh-pr}`340`) --- diff --git a/misc/brand/logo.ipynb b/misc/brand/logo.ipynb index c358551a..4b15512d 100644 --- a/misc/brand/logo.ipynb +++ b/misc/brand/logo.ipynb @@ -46,7 +46,7 @@ }, "outputs": [], "source": [ - "from typing import TYPE_CHECKING, Union\n", + "from typing import TYPE_CHECKING\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", @@ -70,7 +70,7 @@ }, "outputs": [], "source": [ - "ColorsRow = Union[str, list[str]]\n", + "ColorsRow = str | list[str]\n", "Colors = tuple[ColorsRow, ColorsRow, ColorsRow, ColorsRow]\n", "\n", "\n", diff --git a/mypy.ini b/mypy.ini index 7a369126..a4a8c65a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 3.9 +python_version = 3.10 # Import discovery --- files = src/ridgeplot, tests, docs, cicd_utils diff --git a/pyproject.toml b/pyproject.toml index 6a414f01..17204a18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,11 +22,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + #"Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development", "Topic :: Scientific/Engineering", @@ -37,13 +37,12 @@ classifiers = [ keywords = [ "ridgeline", "ridgeplot", "joyplot", "ggridges", "ridges", "ridge", "plot", "plotting", "distplot", "plotly", "data-visualization", "visualization", "data-science", "statistics", "ggplot" ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "numpy>=1", "plotly>=5.20", # The `fillgradient` option was added in 5.20 "statsmodels>=0.12,!=0.14.2", # See GH197 for details "typing-extensions", - 'importlib-resources; python_version<"3.10"', ] [project.urls] diff --git a/pyrightconfig.json b/pyrightconfig.json index 9f6acd4c..2b65b73d 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -15,7 +15,7 @@ "extraPaths": [ "cicd_utils" ], - "pythonVersion": "3.9", + "pythonVersion": "3.10", "pythonPlatform": "All", "typeCheckingMode": "strict", // stricter settings diff --git a/requirements/typing.txt b/requirements/typing.txt index ae068bb4..eb8e2e12 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -7,8 +7,9 @@ types-pytz types-PyYAML types-requests types-tqdm -plotly-stubs; python_version >= "3.10" pandas-stubs +# These seem to bring more problems than they solve for now... 👇 +# plotly-stubs # pyright also needs to inherit other environment dependencies in # order to correctly infer types for code in tests, docs, etc. diff --git a/ruff.toml b/ruff.toml index a04aa35f..1c0fd6c4 100644 --- a/ruff.toml +++ b/ruff.toml @@ -60,7 +60,6 @@ ignore = [ # pandas-vet (PD) "PD015", # Use `.merge` method instead of `pd.merge` function. They have equivalent functionality. - "PD901", # `df` is a bad variable name. Be kinder to your future self. # Convention (PLC) "PLC0415", # `import` should be at the top-level of a file diff --git a/src/ridgeplot/_color/css_colors.py b/src/ridgeplot/_color/css_colors.py index b29fec33..0cac3f2e 100644 --- a/src/ridgeplot/_color/css_colors.py +++ b/src/ridgeplot/_color/css_colors.py @@ -2,11 +2,13 @@ from __future__ import annotations +from typing import TypeAlias + from typing_extensions import Literal # Taken from https://www.w3.org/TR/css-color-3/#svg-color -CssNamedColor = Literal[ +CssNamedColor: TypeAlias = Literal[ "aliceblue", "antiquewhite", "aqua", diff --git a/src/ridgeplot/_color/interpolation.py b/src/ridgeplot/_color/interpolation.py index 3505af49..1249f86a 100644 --- a/src/ridgeplot/_color/interpolation.py +++ b/src/ridgeplot/_color/interpolation.py @@ -3,14 +3,13 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeAlias from typing_extensions import Literal, Protocol from ridgeplot._color.utils import apply_alpha, round_color, to_rgb, unpack_rgb from ridgeplot._types import CollectionL2, ColorScale from ridgeplot._utils import get_xy_extrema, normalise_min_max -from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: from collections.abc import Generator @@ -110,7 +109,7 @@ def slice_colorscale( # ============================================================== -ColorscaleInterpolants = CollectionL2[float] +ColorscaleInterpolants: TypeAlias = CollectionL2[float] """A :data:`ColorscaleInterpolants` contains the interpolants for a :data:`ColorScale`. Example @@ -151,7 +150,7 @@ def __call__(self, ctx: InterpolationContext) -> ColorscaleInterpolants: ... def _mul(a: tuple[Numeric, ...], b: tuple[Numeric, ...]) -> tuple[Numeric, ...]: """Multiply two tuples element-wise.""" - return tuple(a_i * b_i for a_i, b_i in zip_strict(a, b)) + return tuple(a_i * b_i for a_i, b_i in zip(a, b, strict=True)) def _interpolate_row_index(ctx: InterpolationContext) -> ColorscaleInterpolants: @@ -215,7 +214,7 @@ def _interpolate_mean_means(ctx: InterpolationContext) -> ColorscaleInterpolants ] -SolidColormode = Literal[ +SolidColormode: TypeAlias = Literal[ "row-index", "trace-index", "trace-index-row-wise", diff --git a/src/ridgeplot/_color/utils.py b/src/ridgeplot/_color/utils.py index 42494960..d20bcdb1 100644 --- a/src/ridgeplot/_color/utils.py +++ b/src/ridgeplot/_color/utils.py @@ -12,7 +12,6 @@ if TYPE_CHECKING: from collections.abc import Collection - from typing import Union from ridgeplot._types import Color @@ -54,7 +53,7 @@ def unpack_rgb(rgb: str) -> tuple[float, float, float, float] | tuple[float, flo prefix = rgb.split("(")[0] + "(" values_str = map(str.strip, rgb.removeprefix(prefix).removesuffix(")").split(",")) values_num = tuple(int(v) if v.isdecimal() else float(v) for v in values_str) - return cast("Union[tuple[float, float, float, float], tuple[float, float, float]]", values_num) + return cast("tuple[float, float, float, float] | tuple[float, float, float]", values_num) def apply_alpha(color: Color, alpha: float) -> str: diff --git a/src/ridgeplot/_figure_factory.py b/src/ridgeplot/_figure_factory.py index 06ddf4fe..d5e48334 100644 --- a/src/ridgeplot/_figure_factory.py +++ b/src/ridgeplot/_figure_factory.py @@ -35,7 +35,6 @@ normalise_row_attrs, ordered_dedup, ) -from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: from collections.abc import Collection @@ -188,12 +187,12 @@ def create_ridgeplot( fig = go.Figure() ith_trace = 0 for ith_row, (row_traces, row_trace_types, row_trace_labels, row_colors) in enumerate( - zip_strict(densities, trace_types, trace_labels, solid_colors) + zip(densities, trace_types, trace_labels, solid_colors, strict=True) ): y_base = float(-ith_row * y_max * spacing) tickvals.append(y_base) - for trace, trace_type, label, color in zip_strict( - row_traces, row_trace_types, row_trace_labels, row_colors + for trace, trace_type, label, color in zip( + row_traces, row_trace_types, row_trace_labels, row_colors, strict=True ): trace_drawer = get_trace_cls(trace_type)( trace=trace, diff --git a/src/ridgeplot/_hist.py b/src/ridgeplot/_hist.py index 31a87ae7..160deac2 100644 --- a/src/ridgeplot/_hist.py +++ b/src/ridgeplot/_hist.py @@ -7,7 +7,6 @@ import numpy as np from ridgeplot._kde import normalize_sample_weights -from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: from ridgeplot._types import ( @@ -48,7 +47,7 @@ def bin_samples( return [ [ bin_trace_samples(trace_samples, nbins=nbins, weights=weights) - for trace_samples, weights in zip_strict(samples_row, weights_row) + for trace_samples, weights in zip(samples_row, weights_row, strict=True) ] - for samples_row, weights_row in zip_strict(samples, normalised_weights) + for samples_row, weights_row in zip(samples, normalised_weights, strict=True) ] diff --git a/src/ridgeplot/_kde.py b/src/ridgeplot/_kde.py index edf8840d..d9110648 100644 --- a/src/ridgeplot/_kde.py +++ b/src/ridgeplot/_kde.py @@ -4,7 +4,7 @@ from collections.abc import Callable, Collection from functools import partial -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, TypeAlias, cast import numpy as np import numpy.typing as npt @@ -23,16 +23,15 @@ nest_shallow_collection, ) from ridgeplot._utils import normalise_row_attrs -from ridgeplot._vendor.more_itertools import zip_strict if TYPE_CHECKING: from ridgeplot._types import Densities, Samples, SamplesTrace -KDEPoints = Union[int, CollectionL1[Numeric]] +KDEPoints: TypeAlias = int | CollectionL1[Numeric] """The :paramref:`ridgeplot.ridgeplot.kde_points` parameter.""" -KDEBandwidth = Union[str, float, Callable[[CollectionL1[Numeric], StatsmodelsKernel], float]] +KDEBandwidth: TypeAlias = str | float | Callable[[CollectionL1[Numeric], StatsmodelsKernel], float] """The :paramref:`ridgeplot.ridgeplot.bandwidth` parameter.""" @@ -204,7 +203,7 @@ def estimate_densities( return [ [ kde(samples_trace, weights=weights) - for samples_trace, weights in zip_strict(samples_row, weights_row) + for samples_trace, weights in zip(samples_row, weights_row, strict=True) ] - for samples_row, weights_row in zip_strict(samples, normalised_weights) + for samples_row, weights_row in zip(samples, normalised_weights, strict=True) ] diff --git a/src/ridgeplot/_obj/traces/base.py b/src/ridgeplot/_obj/traces/base.py index 502b73ef..12f56942 100644 --- a/src/ridgeplot/_obj/traces/base.py +++ b/src/ridgeplot/_obj/traces/base.py @@ -8,8 +8,6 @@ from typing_extensions import Literal -from ridgeplot._vendor.more_itertools import zip_strict - if TYPE_CHECKING: from plotly import graph_objects as go @@ -68,7 +66,7 @@ def __init__( line_width: float | None, ): super().__init__() - self.x, self.y = zip_strict(*trace) + self.x, self.y = zip(*trace, strict=True) self.label = label self.solid_color = solid_color self.zorder = zorder diff --git a/src/ridgeplot/_types.py b/src/ridgeplot/_types.py index a9933305..ab628c01 100644 --- a/src/ridgeplot/_types.py +++ b/src/ridgeplot/_types.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Collection -from typing import Optional, Union +from typing import TypeAlias import numpy as np from typing_extensions import Any, Literal, TypeIs, TypeVar @@ -37,11 +37,11 @@ # --- Miscellaneous types # ======================================================== -Color = Union[str, tuple[float, float, float]] +Color: TypeAlias = str | tuple[float, float, float] """A color can be represented by a tuple of ``(r, g, b)`` values or any valid CSS color string - including hex, rgb/a, hsl/a, hsv/a, and named CSS colors.""" -ColorScale = Collection[tuple[float, Color]] +ColorScale: TypeAlias = Collection[tuple[float, Color]] """The canonical form for a color scale is represented by a list of tuples of two elements: @@ -65,7 +65,7 @@ ] """ -NormalisationOption = Literal["probability", "percent"] +NormalisationOption: TypeAlias = Literal["probability", "percent"] """A :data:`~typing.Literal` type that represents the normalisation options available for the ridgeplot. See :paramref:`ridgeplot.ridgeplot.norm` for more details.""" @@ -74,7 +74,7 @@ # --- Base nested Collection types (ragged arrays) # ======================================================== -CollectionL1 = Collection[_T] +CollectionL1: TypeAlias = Collection[_T] """A :data:`~typing.TypeAlias` for a 1-level-deep :class:`~collections.abc.Collection`. Example @@ -83,7 +83,7 @@ >>> c1 = [1, 2, 3] """ -CollectionL2 = Collection[Collection[_T]] +CollectionL2: TypeAlias = Collection[Collection[_T]] """A :data:`~typing.TypeAlias` for a 2-level-deep :class:`~collections.abc.Collection`. Example @@ -92,7 +92,7 @@ >>> c2 = [[1, 2, 3], [4, 5, 6]] """ -CollectionL3 = Collection[Collection[Collection[_T]]] +CollectionL3: TypeAlias = Collection[Collection[Collection[_T]]] """A :data:`~typing.TypeAlias` for a 3-level-deep :class:`~collections.abc.Collection`. Example @@ -108,13 +108,13 @@ # --- Numeric types # ======================================================== -Float = Union[float, np.floating[Any]] +Float: TypeAlias = float | np.floating[Any] """A :data:`~typing.TypeAlias` for float types.""" -Int = Union[int, np.integer[Any]] +Int: TypeAlias = int | np.integer[Any] """A :data:`~typing.TypeAlias` for a int types.""" -Numeric = Union[Int, Float] +Numeric: TypeAlias = Int | Float """A :data:`~typing.TypeAlias` for *numeric* types.""" NumericT = TypeVar("NumericT", bound=Numeric) @@ -150,7 +150,7 @@ def _is_numeric(obj: Any) -> TypeIs[Numeric]: # --- `Densities` array # ======================================================== -XYCoordinate = tuple[Numeric, Numeric] +XYCoordinate: TypeAlias = tuple[Numeric, Numeric] """A 2D :math:`(x, y)` coordinate, represented as a :class:`~tuple` of two :data:`Numeric` values. @@ -160,7 +160,7 @@ def _is_numeric(obj: Any) -> TypeIs[Numeric]: >>> xy_coord = (1, 2) """ -DensityTrace = CollectionL1[XYCoordinate] +DensityTrace: TypeAlias = CollectionL1[XYCoordinate] r"""A 2D line/trace represented as a collection of :math:`(x, y)` coordinates (i.e. :data:`XYCoordinate`\s). @@ -189,7 +189,7 @@ def _is_numeric(obj: Any) -> TypeIs[Numeric]: """ -DensitiesRow = CollectionL1[DensityTrace] +DensitiesRow: TypeAlias = CollectionL1[DensityTrace] r"""A :data:`DensitiesRow` represents a set of :data:`DensityTrace`\s that are to be plotted on a given row of a ridgeplot. @@ -221,7 +221,7 @@ def _is_numeric(obj: Any) -> TypeIs[Numeric]: .. image:: /_static/img/api/types/densities_row.webp """ -Densities = CollectionL1[DensitiesRow] +Densities: TypeAlias = CollectionL1[DensitiesRow] r"""The :data:`Densities` type represents the entire collection of traces that are to be plotted on a ridgeplot. @@ -266,7 +266,7 @@ def _is_numeric(obj: Any) -> TypeIs[Numeric]: """ -ShallowDensities = CollectionL1[DensityTrace] +ShallowDensities: TypeAlias = CollectionL1[DensityTrace] """Shallow type for :data:`Densities` where each row of the ridgeplot contains only a single trace. @@ -346,7 +346,7 @@ def is_shallow_densities(obj: Any) -> TypeIs[ShallowDensities]: # --- `Samples` array # ======================================================== -SamplesTrace = CollectionL1[Numeric] +SamplesTrace: TypeAlias = CollectionL1[Numeric] """A :data:`SamplesTrace` is a collection of numeric values representing a set of samples from which a :data:`DensityTrace` can be estimated via KDE. @@ -368,7 +368,7 @@ def is_shallow_densities(obj: Any) -> TypeIs[ShallowDensities]: .. image:: /_static/img/api/types/samples_trace.webp """ -SamplesRow = CollectionL1[SamplesTrace] +SamplesRow: TypeAlias = CollectionL1[SamplesTrace] r"""A :data:`SamplesRow` represents a set of :data:`SamplesTrace`\s that are to be plotted on a given row of a ridgeplot. @@ -396,7 +396,7 @@ def is_shallow_densities(obj: Any) -> TypeIs[ShallowDensities]: .. image:: /_static/img/api/types/samples_row.webp """ -Samples = CollectionL1[SamplesRow] +Samples: TypeAlias = CollectionL1[SamplesRow] r"""The :data:`Samples` type represents the entire collection of samples that are to be plotted on a ridgeplot. @@ -437,7 +437,7 @@ def is_shallow_densities(obj: Any) -> TypeIs[ShallowDensities]: .. image:: /_static/img/api/types/samples.webp """ -ShallowSamples = CollectionL1[SamplesTrace] +ShallowSamples: TypeAlias = CollectionL1[SamplesTrace] """Shallow type for :data:`Samples` where each row of the ridgeplot contains only a single trace. @@ -507,11 +507,11 @@ def is_shallow_samples(obj: Any) -> TypeIs[ShallowSamples]: # Trace types --- -TraceType = Literal["area", "bar"] +TraceType: TypeAlias = Literal["area", "bar"] """The type of trace to draw in a ridgeplot. See :paramref:`ridgeplot.ridgeplot.trace_type` for more information.""" -TraceTypesArray = CollectionL2[TraceType] +TraceTypesArray: TypeAlias = CollectionL2[TraceType] """A :data:`TraceTypesArray` represents the types of traces in a ridgeplot. Example @@ -522,7 +522,7 @@ def is_shallow_samples(obj: Any) -> TypeIs[ShallowSamples]: ... ] """ -ShallowTraceTypesArray = CollectionL1[TraceType] +ShallowTraceTypesArray: TypeAlias = CollectionL1[TraceType] """Shallow type for :data:`TraceTypesArray`. Example @@ -582,7 +582,7 @@ def is_trace_types_array(obj: Any) -> TypeIs[TraceTypesArray]: # Labels --- -LabelsArray = CollectionL2[str] +LabelsArray: TypeAlias = CollectionL2[str] """A :data:`LabelsArray` represents the labels of traces in a ridgeplot. Example @@ -594,7 +594,7 @@ def is_trace_types_array(obj: Any) -> TypeIs[TraceTypesArray]: ... ] """ -ShallowLabelsArray = CollectionL1[str] +ShallowLabelsArray: TypeAlias = CollectionL1[str] """Shallow type for :data:`LabelsArray`. Example @@ -605,15 +605,15 @@ def is_trace_types_array(obj: Any) -> TypeIs[TraceTypesArray]: # Sample weights --- -SampleWeights = Optional[CollectionL1[Numeric]] +SampleWeights: TypeAlias = CollectionL1[Numeric] | None """An array of KDE weights corresponding to each sample.""" -SampleWeightsArray = CollectionL2[SampleWeights] +SampleWeightsArray: TypeAlias = CollectionL2[SampleWeights] """A :data:`SampleWeightsArray` represents the weights of the datapoints in a :data:`Samples` array. The shape of the :data:`SampleWeightsArray` array should match the shape of the corresponding :data:`Samples` array.""" -ShallowSampleWeightsArray = CollectionL1[SampleWeights] +ShallowSampleWeightsArray: TypeAlias = CollectionL1[SampleWeights] """Shallow type for :data:`SampleWeightsArray`.""" # ======================================================== diff --git a/src/ridgeplot/_vendor/_typeis.py b/src/ridgeplot/_vendor/_typeis.py index be597da4..d0085ade 100644 --- a/src/ridgeplot/_vendor/_typeis.py +++ b/src/ridgeplot/_vendor/_typeis.py @@ -32,7 +32,7 @@ class TypeGuardFunc(Protocol[_T]): def __call__(self, obj: Any) -> TypeIs[_T]: ... -_typeguard_registry = {} +_typeguard_registry: dict[type, TypeGuardFunc[Any]] = {} def typeis(obj: Any, tp: _T) -> TypeIs[_T]: diff --git a/src/ridgeplot/_vendor/more_itertools.py b/src/ridgeplot/_vendor/more_itertools.py deleted file mode 100644 index 3f84251b..00000000 --- a/src/ridgeplot/_vendor/more_itertools.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2012 Erik Rose -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -# of the Software, and to permit persons to whom the Software is furnished to do -# so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# ============================================================================== -# -# This module contains an adapted version of more-itertools's implementation of -# the `zip_equal` function. The original source code can be found at: -# https://github.com/more-itertools/more-itertools/ -# -# This vendorized version is used to avoid adding more-itertools as dependency -# to the project just for this single function. -# -# Once support for Python 3.9 is dropped, this module can be removed and the -# `zip_equal` function can be replaced with the built-in `zip` function, which -# (since Python 3.10) supports the `strict` parameter to raise an exception if -# the iterables have different lengths. -# -# The original license is included above. -# - -from __future__ import annotations - -import functools -import sys -from itertools import zip_longest -from typing import TYPE_CHECKING - -from typing_extensions import Any - -if TYPE_CHECKING: - from collections.abc import Iterable - -_marker = object() - - -class UnequalIterablesError(ValueError): - def __init__(self, details: tuple[int, int, int] | None = None) -> None: - msg = "Iterables have different lengths" - if details is not None: - msg += ": index 0 has length {}; index {} has length {}".format(*details) - - super().__init__(msg) - - -def _zip_equal_generator(iterables: Iterable[Any]) -> Iterable[tuple[Any, ...]]: - for combo in zip_longest(*iterables, fillvalue=_marker): - for val in combo: - if val is _marker: - raise UnequalIterablesError - yield combo - - -def _zip_equal(*iterables: Iterable[Any]) -> Iterable[tuple[Any, ...]]: - # Check whether the iterables are all the same size. - iterlist = [list(it) for it in iterables] - try: - first_size = len(iterlist[0]) - for i, it in enumerate(iterlist[1:], 1): - size = len(it) - if size != first_size: - raise UnequalIterablesError(details=(first_size, i, size)) - # All sizes are equal, we can use the built-in zip. - return zip(*iterlist) - # If any one of the iterables didn't have a length, start reading - # them until one runs out. - except TypeError: - return _zip_equal_generator(iterables) - - -if sys.version_info >= (3, 10): - zip_strict = functools.partial(zip, strict=True) -else: - zip_strict = _zip_equal diff --git a/src/ridgeplot/datasets/__init__.py b/src/ridgeplot/datasets/__init__.py index d86b311f..e34f55a8 100644 --- a/src/ridgeplot/datasets/__init__.py +++ b/src/ridgeplot/datasets/__init__.py @@ -2,14 +2,9 @@ from __future__ import annotations -import sys +from importlib.resources import as_file, files from typing import TYPE_CHECKING -if sys.version_info >= (3, 10): - from importlib.resources import as_file, files -else: - from importlib_resources import as_file, files - if TYPE_CHECKING: from typing_extensions import Literal diff --git a/tests/unit/color/test_interpolation.py b/tests/unit/color/test_interpolation.py index 4509b6a7..ea5c0039 100644 --- a/tests/unit/color/test_interpolation.py +++ b/tests/unit/color/test_interpolation.py @@ -47,9 +47,12 @@ def test_interpolate_color_p_not_in_scale(viridis_colorscale: ColorScale) -> Non @pytest.mark.parametrize("p", [-10.0, -1.3, 1.9, 100.0]) -def test_interpolate_color_fails_for_p_out_of_bounds(p: float) -> None: +def test_interpolate_color_fails_for_p_out_of_bounds( + p: float, + viridis_colorscale: ColorScale, +) -> None: with pytest.raises(ValueError, match="should be a float value between 0 and 1"): - interpolate_color(colorscale=..., p=p) + interpolate_color(colorscale=viridis_colorscale, p=p) # ============================================================== diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 787f9dbb..610e8827 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -7,7 +7,6 @@ if TYPE_CHECKING: from collections.abc import Collection - from typing import Union from ridgeplot._types import Color, ColorScale @@ -52,7 +51,7 @@ def viridis_colorscale() -> ColorScale: def valid_colorscale( request: pytest.FixtureRequest, ) -> tuple[ColorScale | Collection[Color] | str, ColorScale]: - return cast("tuple[Union[ColorScale, Collection[Color], str], ColorScale]", request.param) + return cast("tuple[ColorScale | Collection[Color] | str, ColorScale]", request.param) INVALID_COLOR_SCALES = [ @@ -70,4 +69,4 @@ def valid_colorscale( @pytest.fixture(scope="session", params=INVALID_COLOR_SCALES) def invalid_colorscale(request: pytest.FixtureRequest) -> ColorScale | Collection[Color] | str: - return cast("Union[ColorScale, Collection[Color], str]", request.param) + return cast("ColorScale | Collection[Color] | str", request.param) diff --git a/tests/unit/test_figure_factory.py b/tests/unit/test_figure_factory.py index 6ae0aa35..15d46db5 100644 --- a/tests/unit/test_figure_factory.py +++ b/tests/unit/test_figure_factory.py @@ -25,14 +25,14 @@ def test_densities_must_be_4d(self, densities: Densities) -> None: with pytest.raises(ValueError, match="Expected a 4D array of densities"): create_ridgeplot( densities=densities, - trace_types=..., - row_labels=..., - colorscale=..., - opacity=..., - colormode=..., - trace_labels=..., - line_color=..., - line_width=..., - spacing=..., - xpad=..., + trace_types=..., # type: ignore[reportArgumentType] + row_labels=..., # type: ignore[reportArgumentType] + colorscale=..., # type: ignore[reportArgumentType] + opacity=..., # type: ignore[reportArgumentType] + colormode=..., # type: ignore[reportArgumentType] + trace_labels=..., # type: ignore[reportArgumentType] + line_color=..., # type: ignore[reportArgumentType] + line_width=..., # type: ignore[reportArgumentType] + spacing=..., # type: ignore[reportArgumentType] + xpad=..., # type: ignore[reportArgumentType] )