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
9 changes: 9 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
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
92 changes: 92 additions & 0 deletions scripts/make_cmapnames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Generate a `Literal[...]` type annotation for named colormaps."""

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

import cmap

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.pyi")

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 get_cmap_names():
"""Get the names of colormap items."""
visited = set()
catalog = cmap.Catalog()
catalog.disable_warn_on_alias()
for item in catalog.values():
for name in [item.qualified_name, item.name]:
for rev in ["", "_r"]:
full = name + rev
if full in visited:
continue
yield full
visited.add(full)


def generate_script():
"""Produce the expected script."""
names_iter = get_cmap_names()
names_fmt = "".join(" " * 4 + f'"{n}",\n' for n in names_iter)

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())
3 changes: 3 additions & 0 deletions src/cmap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def namespaced_keys(self) -> set[str]:
def resolve(self, name: str) -> str:
"""Return the fully qualified, normalized name of a colormap or alias."""

def disable_warn_on_alias(self) -> str:
"""Disable warnings when a colormap is loaded which clashes with another."""

else:
from ._catalog import Catalog, CatalogItem

Expand Down
18 changes: 13 additions & 5 deletions src/cmap/_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ def __init__(
# _rev_aliases maps fully qualified names to a list of aliases
self._rev_aliases: dict[str, list[str]] = {}

self._warn_on_alias: bool = True

# sort record files. Put matplotlib-related names first so that un-namespaced
# names resolve to the matplotlib colormaps by default.
MPL_PRIORTY_NAMESPACES = ("matplotlib", "bids", "matlab", "gnuplot")
Expand All @@ -263,6 +265,10 @@ def _sorter(x: Path) -> tuple[int, Path]:
normed_name
)

def disable_warn_on_alias(self) -> None:
"""Disable warnings when a colormap is loaded which is an alias of another."""
self._warn_on_alias = False

def unique_keys(
self,
prefer_short_names: bool = True,
Expand Down Expand Up @@ -369,11 +375,13 @@ def _load(self, normed_key: str) -> CatalogItem:
item = cast("UnloadedCatalogAlias", item)
namespaced = item["alias"]
if conflicts := item.get("conflicts"):
logger.warning(
f"WARNING: The name {normed_key!r} is an alias for {namespaced!r}, "
f"but is also available as: {', '.join(conflicts)!r}.\nTo "
"silence this warning, use a fully namespaced name.",
)
if self._warn_on_alias:
logger.warning(
f"WARNING: The name {normed_key!r} "
f"is an alias for {namespaced!r}, "
f"but is also available as: {', '.join(conflicts)!r}.\n"
"To silence this warning, use a fully namespaced name.",
)
return self[namespaced]

_item = cast("UnloadedCatalogItem", item.copy())
Expand Down
8 changes: 5 additions & 3 deletions src/cmap/_colormap.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from typing_extensions import TypeAlias, TypedDict, TypeGuard

from ._catalog import CatalogItem
from ._colormapname import ColormapName

LutCacheKey = tuple[int, float, bool]
Interpolation = Literal["linear", "nearest"]
Expand All @@ -58,13 +59,14 @@ 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",
dict[float, ColorLike],
"ColorStops",
LutCallable,
str,
]
"""Data types that can be passed to the [cmap.Colormap][] constructor."""

Expand Down Expand Up @@ -491,7 +493,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 +1082,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