Skip to content

Commit

Permalink
feat: add colorcet aliases (#11)
Browse files Browse the repository at this point in the history
* feat: add colorcet aliases

* refactor: change alias structure

* fix: fix mock in test

* fix: add changelog to check-manifest

* test: add test
  • Loading branch information
tlambert03 authored May 6, 2023
1 parent d8b84c7 commit 239a122
Show file tree
Hide file tree
Showing 7 changed files with 394 additions and 12 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The `cmap.Colormap` object has convenience methods that allow it to be used with
See [documentation](https://cmap-docs.readthedocs.io/en/latest/colormaps/#usage-with-external-visualization-libraries)
for details.

If you would like to see support added for a particular library, please open an issue or PR.
If you would like to see support added for a particular library, please open an issue or PR.

## Alternatives

Expand Down
16 changes: 16 additions & 0 deletions docs/_gen_cmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
from cmap import Colormap, _catalog
from cmap._util import report

# TODO: convert to jinja
TEMPLATE = """# {name}
{aliases}
{info}
| category | license | authors | source |
Expand Down Expand Up @@ -87,6 +90,11 @@ def build_catalog(catalog: _catalog.Catalog) -> None:
license_: str = info.license
except KeyError as e:
raise KeyError(f"Missing info for {name}: {e}") from e

if info.qualified_name.lower() != name.lower():
# skip aliases
continue

source = info.source
source = f"[{source}]({source})" if source.startswith("http") else f"`{source}`"
authors = ", ".join(info.authors)
Expand All @@ -101,6 +109,9 @@ def build_catalog(catalog: _catalog.Catalog) -> None:
if k in INCLUDE_DATA
}

_aliases = [x for x in info.aliases if x != info.name]
aliases = _make_aliases_md(_aliases) if _aliases else ""

# write the actual markdown file
with mkdocs_gen_files.open(f"catalog/{category}/{name.lower()}.md", "w") as f:
f.write(
Expand All @@ -110,10 +121,15 @@ def build_catalog(catalog: _catalog.Catalog) -> None:
license=license_,
authors=authors,
source=source,
aliases=aliases,
info=info.info,
data=json.dumps({name: cmap_data}, separators=(",", ":")),
)
)


def _make_aliases_md(aliases: list[str]) -> str:
return "**Aliases**: " + ", ".join(f"`{a}`" for a in aliases)


build_catalog(_catalog.catalog)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ source = "vcs"

# https://hatch.pypa.io/latest/config/build/#file-selection
[tool.hatch.build.targets.sdist]
include = ["/src", "/tests"]
include = ["/src", "/tests", "CHANGELOG.md"]


# https://github.com/charliermarsh/ruff
Expand Down
65 changes: 56 additions & 9 deletions src/cmap/_catalog.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""Catalog of available colormaps.
This module contains the logic that indexes all of the "record.json" files found
in the data directory.
TODO: this needs to be cleaned up, and documented better.
"""
from __future__ import annotations

import json
import warnings
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, Literal, Mapping, cast
Expand All @@ -23,13 +30,16 @@ class CatalogItem(TypedDict):
tags: NotRequired[list[str]]
interpolation: NotRequired[bool]
info: NotRequired[str]
aliases: NotRequired[list[str]]

class CatalogAlias(TypedDict):
alias: str
conflicts: NotRequired[list[str]]

CatalogDict: TypeAlias = dict[str, CatalogItem]

logger = logging.getLogger("cmap")


def _norm_name(name: str) -> str:
return name.lower().replace(" ", "_").replace("-", "_")
Expand All @@ -47,6 +57,11 @@ class LoadedCatalogItem:
authors: list[str] = field(default_factory=list)
interpolation: bool | Interpolation = "linear"
tags: list[str] = field(default_factory=list)
aliases: list[str] = field(default_factory=list)

@property
def qualified_name(self) -> str:
return f"{self.namespace}:{self.name}"


CATALOG: dict[str, CatalogItem | CatalogAlias] = {}
Expand All @@ -62,30 +77,62 @@ def _populate_catalog() -> None:
for r in sorted(Path(cmap.data.__file__).parent.rglob("record.json")):
with open(r) as f:
data = json.load(f)
namespace = data["namespace"]
for name, v in data["colormaps"].items():
v = cast("CatalogItem | CatalogAlias", v)
namespaced = f"{data['namespace']}:{name}"
namespaced = f"{namespace}:{name}"

# if the key "alias" exists, this is a CatalogAlias.
# We just add it to the catalog under both the namespaced name
# and the short name. The Catalog._load method will handle the resolution
# of the alias.
if "alias" in v:
v = cast("CatalogAlias", v)
if ":" not in v["alias"]: # pragma: no cover
raise ValueError(f"{namespaced!r} alias is not namespaced")
CATALOG[namespaced] = v
CATALOG[name] = v # FIXME
continue

# otherwise we have a CatalogItem
v = cast("CatalogItem", v)

# here we add any global keys to the colormap that are not already there.
for k in ("license", "namespace", "source", "authors", "category"):
if k in data:
v.setdefault(k, data[k])

# add the fully namespaced colormap to the catalog
CATALOG[namespaced] = v

# if the short name is not already in the catalog, add it as a pointer
# to the fully namespaced colormap.
if name not in CATALOG:
CATALOG[name] = {"alias": namespaced, "conflicts": []}
else:
cast("CatalogAlias", CATALOG[name])["conflicts"].append(namespaced)
# if the short name is already in the catalog, we have a conflict.
# add the fully namespaced name to the conflicts list.
entry = cast("CatalogAlias", CATALOG[name])
entry.setdefault("conflicts", []).append(namespaced)

# lastly, the `aliases` key of a colormap refers to aliases within the
# namespace. These are keys that *must* be accessed using the fullly
# namespaced name (with a colon). We add these to the catalog as well
# so that they can be
for alias in v.get("aliases", []):
if ":" in alias: # pragma: no cover
raise ValueError(
f"internal alias {alias!r} in namespace {namespace} "
"should not have colon."
)
CATALOG[f"{namespace}:{alias}"] = {"alias": namespaced}


_populate_catalog()
_CATALOG_LOWER = {_norm_name(k): v for k, v in CATALOG.items()}
_ALIASES: dict[str, list[str]] = {}
for k, v in _CATALOG_LOWER.items():
if alias := v.get("alias"):
_ALIASES.setdefault(_norm_name(alias), []).append(k) # type: ignore


class Catalog(Mapping[str, "LoadedCatalogItem"]):
Expand Down Expand Up @@ -119,11 +166,10 @@ def _load(self, key: str) -> LoadedCatalogItem:
item = cast("CatalogAlias", item)
namespaced = item["alias"]
if conflicts := item.get("conflicts"):
warnings.warn(
f"The name {key!r} is an alias for {namespaced!r}, but is also "
f"available as: {', '.join(conflicts)!r}. To silence this "
"warning, use a fully namespaced name.",
stacklevel=2,
logger.warning(
f"WARNING: The name {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.",
)
return self[namespaced]

Expand All @@ -136,6 +182,7 @@ def _load(self, key: str) -> LoadedCatalogItem:
# well tested on internal data though
mod = __import__(module, fromlist=[attr])
_item["data"] = getattr(mod, attr)
_item["aliases"] = _ALIASES.get(key, [])
return LoadedCatalogItem(name=key.split(":", 1)[-1], **_item)


Expand Down
2 changes: 1 addition & 1 deletion src/cmap/_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def rich_print_colormap(cm: Colormap, width: int | None = None) -> None:
if cm.interpolation == "nearest":
width = len(cm.color_stops)
else:
width or (console.width - 12)
width = width or (console.width - 12)
for color in cm.iter_colors(width):
color_cell += Text(" ", style=Style(bgcolor=color.hex[:7]))
console.print(color_cell)
Loading

0 comments on commit 239a122

Please sign in to comment.