Skip to content

Commit 8a8a214

Browse files
Add type hints (#146)
* Update colormap.py * Create test_mypy.yml * add Mypy requirement * Update pyproject.toml * Update element.py * utilities.py wip * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update branca/utilities.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Restore Div and MacroElement * Update test_mypy.yml * Update colormap.py * Update element.py * Update utilities.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * error after rebase * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Revert "[pre-commit.ci] auto fixes from pre-commit.com hooks" This reverts commit e21b6d4. * another rebase artefact :/ --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 8ed9839 commit 8a8a214

File tree

6 files changed

+291
-171
lines changed

6 files changed

+291
-171
lines changed

.github/workflows/test_mypy.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Mypy type hint checks
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
run:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup Micromamba env
17+
uses: mamba-org/setup-micromamba@v1
18+
with:
19+
environment-name: TEST
20+
create-args: >-
21+
python=3
22+
--file requirements.txt
23+
--file requirements-dev.txt
24+
25+
- name: Install branca from source
26+
shell: bash -l {0}
27+
run: |
28+
python -m pip install -e . --no-deps --force-reinstall
29+
30+
- name: Mypy test
31+
shell: bash -l {0}
32+
run: |
33+
mypy branca

branca/colormap.py

Lines changed: 101 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,51 +9,67 @@
99
import json
1010
import math
1111
import os
12+
from typing import Dict, List, Optional, Sequence, Tuple, Union
1213

1314
from jinja2 import Template
1415

1516
from branca.element import ENV, Figure, JavascriptLink, MacroElement
1617
from branca.utilities import legend_scaler
1718

18-
rootpath = os.path.abspath(os.path.dirname(__file__))
19+
rootpath: str = os.path.abspath(os.path.dirname(__file__))
1920

2021
with open(os.path.join(rootpath, "_cnames.json")) as f:
21-
_cnames = json.loads(f.read())
22+
_cnames: Dict[str, str] = json.loads(f.read())
2223

2324
with open(os.path.join(rootpath, "_schemes.json")) as f:
24-
_schemes = json.loads(f.read())
25+
_schemes: Dict[str, List[str]] = json.loads(f.read())
2526

2627

27-
def _is_hex(x):
28+
TypeRGBInts = Tuple[int, int, int]
29+
TypeRGBFloats = Tuple[float, float, float]
30+
TypeRGBAInts = Tuple[int, int, int, int]
31+
TypeRGBAFloats = Tuple[float, float, float, float]
32+
TypeAnyColorType = Union[TypeRGBInts, TypeRGBFloats, TypeRGBAInts, TypeRGBAFloats, str]
33+
34+
35+
def _is_hex(x: str) -> bool:
2836
return x.startswith("#") and len(x) == 7
2937

3038

31-
def _parse_hex(color_code):
39+
def _parse_hex(color_code: str) -> TypeRGBAFloats:
3240
return (
33-
int(color_code[1:3], 16),
34-
int(color_code[3:5], 16),
35-
int(color_code[5:7], 16),
41+
_color_int_to_float(int(color_code[1:3], 16)),
42+
_color_int_to_float(int(color_code[3:5], 16)),
43+
_color_int_to_float(int(color_code[5:7], 16)),
44+
1.0,
3645
)
3746

3847

39-
def _parse_color(x):
48+
def _color_int_to_float(x: int) -> float:
49+
"""Convert an integer between 0 and 255 to a float between 0. and 1.0"""
50+
return x / 255.0
51+
52+
53+
def _color_float_to_int(x: float) -> int:
54+
"""Convert a float between 0. and 1.0 to an integer between 0 and 255"""
55+
return int(x * 255.9999)
56+
57+
58+
def _parse_color(x: Union[tuple, list, str]) -> TypeRGBAFloats:
4059
if isinstance(x, (tuple, list)):
41-
color_tuple = tuple(x)[:4]
42-
elif isinstance(x, (str, bytes)) and _is_hex(x):
43-
color_tuple = _parse_hex(x)
44-
elif isinstance(x, (str, bytes)):
60+
return tuple(tuple(x) + (1.0,))[:4] # type: ignore
61+
elif isinstance(x, str) and _is_hex(x):
62+
return _parse_hex(x)
63+
elif isinstance(x, str):
4564
cname = _cnames.get(x.lower(), None)
4665
if cname is None:
4766
raise ValueError(f"Unknown color {cname!r}.")
48-
color_tuple = _parse_hex(cname)
67+
return _parse_hex(cname)
4968
else:
5069
raise ValueError(f"Unrecognized color code {x!r}")
51-
if max(color_tuple) > 1.0:
52-
color_tuple = tuple(u / 255.0 for u in color_tuple)
53-
return tuple(map(float, (color_tuple + (1.0,))[:4]))
5470

5571

56-
def _base(x):
72+
def _base(x: float) -> float:
5773
if x > 0:
5874
base = pow(10, math.floor(math.log10(x)))
5975
return round(x / base) * base
@@ -78,15 +94,15 @@ class ColorMap(MacroElement):
7894
Maximum number of legend tick labels
7995
"""
8096

81-
_template = ENV.get_template("color_scale.js")
97+
_template: Template = ENV.get_template("color_scale.js")
8298

8399
def __init__(
84100
self,
85-
vmin=0.0,
86-
vmax=1.0,
87-
caption="",
88-
text_color="black",
89-
max_labels=10,
101+
vmin: float = 0.0,
102+
vmax: float = 1.0,
103+
caption: str = "",
104+
text_color: str = "black",
105+
max_labels: int = 10,
90106
):
91107
super().__init__()
92108
self._name = "ColorMap"
@@ -95,9 +111,9 @@ def __init__(
95111
self.vmax = vmax
96112
self.caption = caption
97113
self.text_color = text_color
98-
self.index = [vmin, vmax]
114+
self.index: List[float] = [vmin, vmax]
99115
self.max_labels = max_labels
100-
self.tick_labels = None
116+
self.tick_labels: Optional[Sequence[Union[float, str]]] = None
101117

102118
self.width = 450
103119
self.height = 40
@@ -127,7 +143,7 @@ def render(self, **kwargs):
127143
name="d3",
128144
) # noqa
129145

130-
def rgba_floats_tuple(self, x):
146+
def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
131147
"""
132148
This class has to be implemented for each class inheriting from
133149
Colormap. This has to be a function of the form float ->
@@ -137,37 +153,37 @@ def rgba_floats_tuple(self, x):
137153
"""
138154
raise NotImplementedError
139155

140-
def rgba_bytes_tuple(self, x):
156+
def rgba_bytes_tuple(self, x: float) -> TypeRGBAInts:
141157
"""Provides the color corresponding to value `x` in the
142158
form of a tuple (R,G,B,A) with int values between 0 and 255.
143159
"""
144-
return tuple(int(u * 255.9999) for u in self.rgba_floats_tuple(x))
160+
return tuple(_color_float_to_int(u) for u in self.rgba_floats_tuple(x)) # type: ignore
145161

146-
def rgb_bytes_tuple(self, x):
162+
def rgb_bytes_tuple(self, x: float) -> TypeRGBInts:
147163
"""Provides the color corresponding to value `x` in the
148164
form of a tuple (R,G,B) with int values between 0 and 255.
149165
"""
150166
return self.rgba_bytes_tuple(x)[:3]
151167

152-
def rgb_hex_str(self, x):
168+
def rgb_hex_str(self, x: float) -> str:
153169
"""Provides the color corresponding to value `x` in the
154170
form of a string of hexadecimal values "#RRGGBB".
155171
"""
156172
return "#%02x%02x%02x" % self.rgb_bytes_tuple(x)
157173

158-
def rgba_hex_str(self, x):
174+
def rgba_hex_str(self, x: float) -> str:
159175
"""Provides the color corresponding to value `x` in the
160176
form of a string of hexadecimal values "#RRGGBBAA".
161177
"""
162178
return "#%02x%02x%02x%02x" % self.rgba_bytes_tuple(x)
163179

164-
def __call__(self, x):
180+
def __call__(self, x: float) -> str:
165181
"""Provides the color corresponding to value `x` in the
166182
form of a string of hexadecimal values "#RRGGBBAA".
167183
"""
168184
return self.rgba_hex_str(x)
169185

170-
def _repr_html_(self):
186+
def _repr_html_(self) -> str:
171187
"""Display the colormap in a Jupyter Notebook.
172188
173189
Does not support all the class arguments.
@@ -264,14 +280,14 @@ class LinearColormap(ColorMap):
264280

265281
def __init__(
266282
self,
267-
colors,
268-
index=None,
269-
vmin=0.0,
270-
vmax=1.0,
271-
caption="",
272-
text_color="black",
273-
max_labels=10,
274-
tick_labels=None,
283+
colors: Sequence[TypeAnyColorType],
284+
index: Optional[Sequence[float]] = None,
285+
vmin: float = 0.0,
286+
vmax: float = 1.0,
287+
caption: str = "",
288+
text_color: str = "black",
289+
max_labels: int = 10,
290+
tick_labels: Optional[Sequence[float]] = None,
275291
):
276292
super().__init__(
277293
vmin=vmin,
@@ -280,7 +296,7 @@ def __init__(
280296
text_color=text_color,
281297
max_labels=max_labels,
282298
)
283-
self.tick_labels = tick_labels
299+
self.tick_labels: Optional[Sequence[float]] = tick_labels
284300

285301
n = len(colors)
286302
if n < 2:
@@ -289,9 +305,9 @@ def __init__(
289305
self.index = [vmin + (vmax - vmin) * i * 1.0 / (n - 1) for i in range(n)]
290306
else:
291307
self.index = list(index)
292-
self.colors = [_parse_color(x) for x in colors]
308+
self.colors: List[TypeRGBAFloats] = [_parse_color(x) for x in colors]
293309

294-
def rgba_floats_tuple(self, x):
310+
def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
295311
"""Provides the color corresponding to value `x` in the
296312
form of a tuple (R,G,B,A) with float values between 0. and 1.
297313
"""
@@ -308,20 +324,20 @@ def rgba_floats_tuple(self, x):
308324
else:
309325
raise ValueError("Thresholds are not sorted.")
310326

311-
return tuple(
327+
return tuple( # type: ignore
312328
(1.0 - p) * self.colors[i - 1][j] + p * self.colors[i][j] for j in range(4)
313329
)
314330

315331
def to_step(
316332
self,
317-
n=None,
318-
index=None,
319-
data=None,
320-
method=None,
321-
quantiles=None,
322-
round_method=None,
323-
max_labels=10,
324-
):
333+
n: Optional[int] = None,
334+
index: Optional[Sequence[float]] = None,
335+
data: Optional[Sequence[float]] = None,
336+
method: str = "linear",
337+
quantiles: Optional[Sequence[float]] = None,
338+
round_method: Optional[str] = None,
339+
max_labels: int = 10,
340+
) -> "StepColormap":
325341
"""Splits the LinearColormap into a StepColormap.
326342
327343
Parameters
@@ -382,11 +398,7 @@ def to_step(
382398
max_ = max(data)
383399
min_ = min(data)
384400
scaled_cm = self.scale(vmin=min_, vmax=max_)
385-
method = (
386-
"quantiles"
387-
if quantiles is not None
388-
else method if method is not None else "linear"
389-
)
401+
method = "quantiles" if quantiles is not None else method
390402
if method.lower().startswith("lin"):
391403
if n is None:
392404
raise ValueError(msg)
@@ -454,7 +466,12 @@ def to_step(
454466
tick_labels=self.tick_labels,
455467
)
456468

457-
def scale(self, vmin=0.0, vmax=1.0, max_labels=10):
469+
def scale(
470+
self,
471+
vmin: float = 0.0,
472+
vmax: float = 1.0,
473+
max_labels: int = 10,
474+
) -> "LinearColormap":
458475
"""Transforms the colorscale so that the minimal and maximal values
459476
fit the given parameters.
460477
"""
@@ -510,14 +527,14 @@ class StepColormap(ColorMap):
510527

511528
def __init__(
512529
self,
513-
colors,
514-
index=None,
515-
vmin=0.0,
516-
vmax=1.0,
517-
caption="",
518-
text_color="black",
519-
max_labels=10,
520-
tick_labels=None,
530+
colors: Sequence[TypeAnyColorType],
531+
index: Optional[Sequence[float]] = None,
532+
vmin: float = 0.0,
533+
vmax: float = 1.0,
534+
caption: str = "",
535+
text_color: str = "black",
536+
max_labels: int = 10,
537+
tick_labels: Optional[Sequence[float]] = None,
521538
):
522539
super().__init__(
523540
vmin=vmin,
@@ -535,9 +552,9 @@ def __init__(
535552
self.index = [vmin + (vmax - vmin) * i * 1.0 / n for i in range(n + 1)]
536553
else:
537554
self.index = list(index)
538-
self.colors = [_parse_color(x) for x in colors]
555+
self.colors: List[TypeRGBAFloats] = [_parse_color(x) for x in colors]
539556

540-
def rgba_floats_tuple(self, x):
557+
def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
541558
"""
542559
Provides the color corresponding to value `x` in the
543560
form of a tuple (R,G,B,A) with float values between 0. and 1.
@@ -549,9 +566,13 @@ def rgba_floats_tuple(self, x):
549566
return self.colors[-1]
550567

551568
i = len([u for u in self.index if u <= x]) # 0 < i < n.
552-
return tuple(self.colors[i - 1])
569+
return self.colors[i - 1]
553570

554-
def to_linear(self, index=None, max_labels=10):
571+
def to_linear(
572+
self,
573+
index: Optional[Sequence[float]] = None,
574+
max_labels: int = 10,
575+
) -> LinearColormap:
555576
"""
556577
Transforms the StepColormap into a LinearColormap.
557578
@@ -584,7 +605,12 @@ def to_linear(self, index=None, max_labels=10):
584605
max_labels=max_labels,
585606
)
586607

587-
def scale(self, vmin=0.0, vmax=1.0, max_labels=10):
608+
def scale(
609+
self,
610+
vmin: float = 0.0,
611+
vmax: float = 1.0,
612+
max_labels: int = 10,
613+
) -> "StepColormap":
588614
"""Transforms the colorscale so that the minimal and maximal values
589615
fit the given parameters.
590616
"""
@@ -611,7 +637,7 @@ def __init__(self):
611637
for key, val in _schemes.items():
612638
setattr(self, key, LinearColormap(val))
613639

614-
def _repr_html_(self):
640+
def _repr_html_(self) -> str:
615641
return Template(
616642
"""
617643
<table>
@@ -634,7 +660,7 @@ def __init__(self):
634660
for key, val in _schemes.items():
635661
setattr(self, key, StepColormap(val))
636662

637-
def _repr_html_(self):
663+
def _repr_html_(self) -> str:
638664
return Template(
639665
"""
640666
<table>

0 commit comments

Comments
 (0)