Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"

- name: Check ColormapName annotation is up to date
run: uv run --no-dev python scripts/make_cmapnames.py --check

- name: Run min test
run: uv run --no-dev --group test_min pytest -v

Expand Down
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ ci:
exclude: ^LICENSE

repos:
- repo: local
hooks:
- id: make_cmapnames
name: make_cmapnames
entry: python scripts/make_cmapnames.py --exit-code
language: python
files: src/cmap/data/[a-zA-Z0-9_]+/record.json
pass_filenames: false

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
Expand Down Expand Up @@ -35,3 +44,5 @@ repos:
files: "^src/"
additional_dependencies:
- numpy>=2.2
stages:
- pre-push
4 changes: 4 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Once you have picked a namespace:
]
```

1. Run `scripts/make_cmapnames.py` to ensure that the `ColormapLike` type alias
includes your contribution.
`pre-commit` will update it for you, but will fail if changes were made.

!!!tip
It may be helpful to look at existing folders and files in the
[`cmap/data` directory](https://github.com/pyapp-kit/cmap/tree/main/src/cmap/data)
Expand Down
88 changes: 88 additions & 0 deletions scripts/make_cmapnames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Generate a `Literal[...]` type annotation for named colormaps."""

import json
import sys
from argparse import ArgumentParser
from difflib import unified_diff
from pathlib import Path

scripts_dir = Path(__file__).resolve().parent
module_dir = scripts_dir.parent.joinpath("src", "cmap")
data_dir = module_dir.joinpath("data")
out_file = module_dir.joinpath("_colormapname.py")

SCRIPT_TEMPLATE = '''
"""Type annotation for literal colormap names.

Auto-generated by make_cmapnames.py : DO NOT EDIT.
"""

from typing import Literal, TypeAlias

ColormapName: TypeAlias = Literal[
{}]
'''.lstrip()


def generate_script():
"""Produce the expected script."""
names = []

for rel_path in sorted(data_dir.glob("*/record.json")):
p = data_dir.joinpath(rel_path)
prefix_name = rel_path.parent.name
with p.open() as f:
d = json.load(f)

for subname in d.get("colormaps", []):
for prefix in ["", f"{prefix_name}:"]:
for suffix in ["", "_r"]:
names.append(f"{prefix}{subname.lower()}{suffix}")

names_fmt = "".join(" " * 4 + f'"{n}",\n' for n in names)

return SCRIPT_TEMPLATE.format(names_fmt)


def main(args=None):
"""Main function for this script."""
parser = ArgumentParser()
parser.add_argument(
"--check",
action="store_true",
help=("do not write results, just print the diff and exit code 1 if non-empty"),
)
parser.add_argument(
"--exit-code",
action="store_true",
help=("write the results, but return exit code 1 if there are changes to make"),
)

parsed = parser.parse_args(args)

new = generate_script()
old = out_file.read_text() if out_file.is_file() else ""

if new == old:
return 0

if parsed.check:
sys.stdout.writelines(
unified_diff(
old.splitlines(keepends=True),
new.splitlines(keepends=True),
fromfile="existing",
tofile="new",
)
)
return 1

out_file.write_text(new)
if parsed.exit_code:
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
7 changes: 4 additions & 3 deletions src/cmap/_colormap.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from . import _external
from ._catalog import Catalog
from ._color import Color, ColorLike
from ._colormapname import ColormapName

if TYPE_CHECKING:
from collections.abc import Iterator
Expand Down Expand Up @@ -58,7 +59,7 @@ class ColormapDict(TypedDict):

# All of the things that we can pass to the constructor of Colormap
ColormapLike: TypeAlias = Union[
str, # colormap name, w/ optional "_r" suffix
ColormapName,
Iterable[Union[ColorLike, ColorStopLike]],
"NDArray",
"MPLSegmentData",
Expand Down Expand Up @@ -491,7 +492,7 @@ def iter_colors(self, N: Iterable[float] | int | None = None) -> Iterator[Color]
N = self.num_colors
nums = np.linspace(0, 1, N) if isinstance(N, int) else np.asarray(N)
for c in self(nums, N=len(nums)):
yield Color(c)
yield Color(c) # type: ignore

def reversed(self, name: str | None = None) -> Colormap:
"""Return a new Colormap, with reversed colors.
Expand Down Expand Up @@ -1080,7 +1081,7 @@ def to_css(
stops = tuple(np.linspace(0, 1, max_stops))
colors = tuple(Color(c) for c in self.to_lut(max_stops))
else:
stops, colors = self.stops, self.colors
stops, colors = self.stops, self.colors # type: ignore
if not colors:
return ""
out = f"background: {colors[0].hex if as_hex else colors[0].rgba_string};\n"
Expand Down
Loading
Loading