1
+ """Catalog of available colormaps.
2
+
3
+ This module contains the logic that indexes all of the "record.json" files found
4
+ in the data directory.
5
+
6
+ TODO: this needs to be cleaned up, and documented better.
7
+ """
1
8
from __future__ import annotations
2
9
3
10
import json
4
- import warnings
11
+ import logging
5
12
from dataclasses import dataclass , field
6
13
from pathlib import Path
7
14
from typing import TYPE_CHECKING , Iterator , Literal , Mapping , cast
@@ -23,13 +30,16 @@ class CatalogItem(TypedDict):
23
30
tags : NotRequired [list [str ]]
24
31
interpolation : NotRequired [bool ]
25
32
info : NotRequired [str ]
33
+ aliases : NotRequired [list [str ]]
26
34
27
35
class CatalogAlias (TypedDict ):
28
36
alias : str
29
37
conflicts : NotRequired [list [str ]]
30
38
31
39
CatalogDict : TypeAlias = dict [str , CatalogItem ]
32
40
41
+ logger = logging .getLogger ("cmap" )
42
+
33
43
34
44
def _norm_name (name : str ) -> str :
35
45
return name .lower ().replace (" " , "_" ).replace ("-" , "_" )
@@ -47,6 +57,11 @@ class LoadedCatalogItem:
47
57
authors : list [str ] = field (default_factory = list )
48
58
interpolation : bool | Interpolation = "linear"
49
59
tags : list [str ] = field (default_factory = list )
60
+ aliases : list [str ] = field (default_factory = list )
61
+
62
+ @property
63
+ def qualified_name (self ) -> str :
64
+ return f"{ self .namespace } :{ self .name } "
50
65
51
66
52
67
CATALOG : dict [str , CatalogItem | CatalogAlias ] = {}
@@ -62,30 +77,62 @@ def _populate_catalog() -> None:
62
77
for r in sorted (Path (cmap .data .__file__ ).parent .rglob ("record.json" )):
63
78
with open (r ) as f :
64
79
data = json .load (f )
80
+ namespace = data ["namespace" ]
65
81
for name , v in data ["colormaps" ].items ():
66
- v = cast ("CatalogItem | CatalogAlias" , v )
67
- namespaced = f"{ data ['namespace' ]} :{ name } "
82
+ namespaced = f"{ namespace } :{ name } "
83
+
84
+ # if the key "alias" exists, this is a CatalogAlias.
85
+ # We just add it to the catalog under both the namespaced name
86
+ # and the short name. The Catalog._load method will handle the resolution
87
+ # of the alias.
68
88
if "alias" in v :
89
+ v = cast ("CatalogAlias" , v )
69
90
if ":" not in v ["alias" ]: # pragma: no cover
70
91
raise ValueError (f"{ namespaced !r} alias is not namespaced" )
71
92
CATALOG [namespaced ] = v
72
93
CATALOG [name ] = v # FIXME
73
94
continue
74
95
96
+ # otherwise we have a CatalogItem
97
+ v = cast ("CatalogItem" , v )
98
+
99
+ # here we add any global keys to the colormap that are not already there.
75
100
for k in ("license" , "namespace" , "source" , "authors" , "category" ):
76
101
if k in data :
77
102
v .setdefault (k , data [k ])
78
103
104
+ # add the fully namespaced colormap to the catalog
79
105
CATALOG [namespaced ] = v
80
106
107
+ # if the short name is not already in the catalog, add it as a pointer
108
+ # to the fully namespaced colormap.
81
109
if name not in CATALOG :
82
110
CATALOG [name ] = {"alias" : namespaced , "conflicts" : []}
83
111
else :
84
- cast ("CatalogAlias" , CATALOG [name ])["conflicts" ].append (namespaced )
112
+ # if the short name is already in the catalog, we have a conflict.
113
+ # add the fully namespaced name to the conflicts list.
114
+ entry = cast ("CatalogAlias" , CATALOG [name ])
115
+ entry .setdefault ("conflicts" , []).append (namespaced )
116
+
117
+ # lastly, the `aliases` key of a colormap refers to aliases within the
118
+ # namespace. These are keys that *must* be accessed using the fullly
119
+ # namespaced name (with a colon). We add these to the catalog as well
120
+ # so that they can be
121
+ for alias in v .get ("aliases" , []):
122
+ if ":" in alias : # pragma: no cover
123
+ raise ValueError (
124
+ f"internal alias { alias !r} in namespace { namespace } "
125
+ "should not have colon."
126
+ )
127
+ CATALOG [f"{ namespace } :{ alias } " ] = {"alias" : namespaced }
85
128
86
129
87
130
_populate_catalog ()
88
131
_CATALOG_LOWER = {_norm_name (k ): v for k , v in CATALOG .items ()}
132
+ _ALIASES : dict [str , list [str ]] = {}
133
+ for k , v in _CATALOG_LOWER .items ():
134
+ if alias := v .get ("alias" ):
135
+ _ALIASES .setdefault (_norm_name (alias ), []).append (k ) # type: ignore
89
136
90
137
91
138
class Catalog (Mapping [str , "LoadedCatalogItem" ]):
@@ -119,11 +166,10 @@ def _load(self, key: str) -> LoadedCatalogItem:
119
166
item = cast ("CatalogAlias" , item )
120
167
namespaced = item ["alias" ]
121
168
if conflicts := item .get ("conflicts" ):
122
- warnings .warn (
123
- f"The name { key !r} is an alias for { namespaced !r} , but is also "
124
- f"available as: { ', ' .join (conflicts )!r} . To silence this "
125
- "warning, use a fully namespaced name." ,
126
- stacklevel = 2 ,
169
+ logger .warning (
170
+ f"WARNING: The name { key !r} is an alias for { namespaced !r} , "
171
+ f"but is also available as: { ', ' .join (conflicts )!r} .\n To "
172
+ "silence this warning, use a fully namespaced name." ,
127
173
)
128
174
return self [namespaced ]
129
175
@@ -136,6 +182,7 @@ def _load(self, key: str) -> LoadedCatalogItem:
136
182
# well tested on internal data though
137
183
mod = __import__ (module , fromlist = [attr ])
138
184
_item ["data" ] = getattr (mod , attr )
185
+ _item ["aliases" ] = _ALIASES .get (key , [])
139
186
return LoadedCatalogItem (name = key .split (":" , 1 )[- 1 ], ** _item )
140
187
141
188
0 commit comments