From 4f67b9812cc48f6df3b164454a43ed763cb57186 Mon Sep 17 00:00:00 2001 From: Patrick Massot Date: Sat, 3 Feb 2024 15:16:33 -0500 Subject: [PATCH] Type fixes (#84) --- kalamine/layout.py | 95 +++++++++++++++++++++++++++----------------- kalamine/template.py | 32 +++++++-------- kalamine/utils.py | 34 +++++++++++++++- 3 files changed, 106 insertions(+), 55 deletions(-) diff --git a/kalamine/layout.py b/kalamine/layout.py index 00efca9..5ff3b88 100644 --- a/kalamine/layout.py +++ b/kalamine/layout.py @@ -4,7 +4,8 @@ import re import sys from pathlib import Path -from typing import Dict, List, Union +from typing import Dict, List, Union, Optional, TypeVar, Type, Set +from dataclasses import dataclass import click import tomli @@ -100,8 +101,8 @@ def load_descriptor(file_path: Path) -> Dict: with file_path.open(encoding="utf-8") as file: return yaml.load(file, Loader=yaml.SafeLoader) - with file_path.open(mode="rb") as file: - return tomli.load(file) + with file_path.open(mode="rb") as dfile: + return tomli.load(dfile) ### @@ -123,7 +124,27 @@ def load_descriptor(file_path: Path) -> Dict: "1dk_shift": "'", } -GEOMETRY = load_data("geometry.yaml") +@dataclass +class RowDescr: + offset: int + keys: List[str] + +T = TypeVar('T', bound='GeometryDescr') + +@dataclass +class GeometryDescr: + template: str + rows: List[RowDescr] + + @classmethod + def from_dict(cls: Type[T], src: Dict) -> T: + return cls(template=src['template'], + rows = [RowDescr(**row) for row in src['rows']]) + +geometry_data = load_data( "geometry.yaml") + +GEOMETRY = {key: GeometryDescr.from_dict(val) + for key, val in geometry_data.items()} ### @@ -138,9 +159,9 @@ def __init__(self, filepath: Path) -> None: """Import a keyboard layout to instanciate the object.""" # initialize a blank layout - self.layers = [{}, {}, {}, {}, {}, {}] - self.dk_set = set() - self.dead_keys = {} # dictionary subset of DEAD_KEYS + self.layers: Dict[Layer, Dict[str, str]] = {layer: {} for layer in Layer} + self.dk_set: Set[str] = set() + self.dead_keys: Dict[str, Dict[str, str]] = {} # dictionary subset of DEAD_KEYS self.meta = CONFIG.copy() # default parameters, hardcoded self.has_altgr = False self.has_1dk = False @@ -174,7 +195,7 @@ def __init__(self, filepath: Path) -> None: self.meta["lastChange"] = datetime.date.today().isoformat() # keyboard layers: self.layers & self.dead_keys - rows = GEOMETRY[self.meta["geometry"]]["rows"] + rows = GEOMETRY[self.meta["geometry"]].rows if "full" in cfg: full = text_to_lines(cfg["full"]) self._parse_template(full, rows, Layer.BASE) @@ -227,13 +248,13 @@ def layout_has_char(char: str) -> bool: self.dead_keys = {} for dk in DEAD_KEYS: - id = dk["char"] + id = dk.char if id not in self.dk_set: continue self.dead_keys[id] = {} deadkey = self.dead_keys[id] - deadkey[id] = dk["alt_self"] + deadkey[id] = dk.alt_self if id == ODK_ID: self.has_1dk = True @@ -242,29 +263,30 @@ def layout_has_char(char: str) -> bool: continue for i in [Layer.ODK_SHIFT, Layer.ODK]: if key_name in self.layers[i]: - deadkey[self.layers[i - Layer.ODK][key_name]] = self.layers[ + deadkey[self.layers[i.necromance()][key_name]] = self.layers[ i ][key_name] for space in all_spaces: deadkey[space] = spc["1dk"] else: - base = dk["base"] - alt = dk["alt"] + base = dk.base + alt = dk.alt for i in range(len(base)): if layout_has_char(base[i]): deadkey[base[i]] = alt[i] for space in all_spaces: - deadkey[space] = dk["alt_space"] + deadkey[space] = dk.alt_space - def _parse_template(self, template: str, rows: List[str], layer_number: Layer): + def _parse_template(self, template: List[str], + rows: List[RowDescr], layer_number: Layer) -> None: """Extract a keyboard layer from a template.""" j = 0 col_offset = 0 if layer_number == Layer.BASE else 2 for row in rows: - i = row["offset"] + col_offset - keys = row["keys"] + i = row.offset + col_offset + keys = row.keys base = list(template[2 + j * 3]) shift = list(template[1 + j * 3]) @@ -287,13 +309,13 @@ def _parse_template(self, template: str, rows: List[str], layer_number: Layer): # shift_key = base_key.upper() if base_key != " ": - self.layers[layer_number + 0][key] = base_key + self.layers[layer_number][key] = base_key if shift_key != " ": - self.layers[layer_number + 1][key] = shift_key + self.layers[layer_number.next()][key] = shift_key for dk in DEAD_KEYS: - if base_key == dk["char"] or shift_key == dk["char"]: - self.dk_set.add(dk["char"]) + if base_key == dk.char or shift_key == dk.char: + self.dk_set.add(dk.char) i += 6 j += 1 @@ -303,8 +325,8 @@ def _parse_template(self, template: str, rows: List[str], layer_number: Layer): # def _fill_template( - self, template: str, rows: List[str], layer_number: Layer - ) -> str: + self, template: List[str], rows: List[RowDescr], layer_number: Layer + ) -> List[str]: """Fill a template with a keyboard layer.""" if layer_number == Layer.BASE: @@ -316,8 +338,8 @@ def _fill_template( j = 0 for row in rows: - i = row["offset"] + col_offset - keys = row["keys"] + i = row.offset + col_offset + keys = row.keys base = list(template[2 + j * 3]) shift = list(template[1 + j * 3]) @@ -328,8 +350,8 @@ def _fill_template( base_key = self.layers[layer_number][key] shift_key = " " - if key in self.layers[layer_number + 1]: - shift_key = self.layers[layer_number + 1][key] + if key in self.layers[layer_number.next()]: + shift_key = self.layers[layer_number.next()][key] dead_base = len(base_key) == 2 and base_key[0] == "*" dead_shift = len(shift_key) == 2 and shift_key[0] == "*" @@ -359,13 +381,12 @@ def _fill_template( return template - def _get_geometry(self, layers: Union[List[Layer], None] = None) -> str: + def _get_geometry(self, layers: Optional[List[Layer]] = None) -> List[str]: """`geometry` view of the requested layers.""" - if layers is None: - layers = [Layer.BASE] + layers = layers or [Layer.BASE] - rows = GEOMETRY[self.geometry]["rows"] - template = GEOMETRY[self.geometry]["template"].split("\n")[:-1] + rows = GEOMETRY[self.geometry].rows + template = GEOMETRY[self.geometry].template.split("\n")[:-1] for i in layers: template = self._fill_template(template, rows, i) return template @@ -384,17 +405,17 @@ def geometry(self, value: str) -> None: self.meta["geometry"] = shape @property - def base(self) -> str: + def base(self) -> List[str]: """Base + 1dk layers.""" - return self._get_geometry([0, Layer.ODK]) + return self._get_geometry([Layer.BASE, Layer.ODK]) @property - def full(self) -> str: + def full(self) -> List[str]: """Base + AltGr layers.""" - return self._get_geometry([0, Layer.ALTGR]) + return self._get_geometry([Layer.BASE, Layer.ALTGR]) @property - def altgr(self) -> str: + def altgr(self) -> List[str]: """AltGr layer only.""" return self._get_geometry([Layer.ALTGR]) diff --git a/kalamine/template.py b/kalamine/template.py index e2d7362..4a53077 100644 --- a/kalamine/template.py +++ b/kalamine/template.py @@ -9,7 +9,7 @@ DK_INDEX = {} for dk in DEAD_KEYS: - DK_INDEX[dk["char"]] = dk + DK_INDEX[dk.char] = dk ### @@ -49,7 +49,7 @@ def xkb_keymap(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: odk_symbol = "ISO_Level5_Latch" if eight_level else "ISO_Level3_Latch" max_length = 16 # `ISO_Level3_Latch` should be the longest symbol name - output = [] + output: List[str] = [] for key_name in LAYER_KEYS: if key_name.startswith("-"): # separator if output: @@ -59,13 +59,13 @@ def xkb_keymap(layout: "KeyboardLayout", xkbcomp: bool = False) -> List[str]: symbols = [] description = " //" - for layer in layout.layers: + for layer in layout.layers.values(): if key_name in layer: keysym = layer[key_name] desc = keysym # dead key? if keysym in DK_INDEX: - name = DK_INDEX[keysym]["name"] + name = DK_INDEX[keysym].name desc = layout.dead_keys[keysym][keysym] symbol = odk_symbol if keysym == ODK_ID else f"dead_{name}" # regular key: use a keysym if possible, utf-8 otherwise @@ -296,7 +296,7 @@ def klc_deadkeys(layout: "KeyboardLayout") -> List[str]: continue dk = layout.dead_keys[k] - output.append(f"// DEADKEY: {DK_INDEX[k]['name'].upper()} //" + "{{{") + output.append(f"// DEADKEY: {DK_INDEX[k].name.upper()} //" + "{{{") output.append(f"DEADKEY\t{hex_ord(dk[' '])}") for base, alt in dk.items(): @@ -328,7 +328,7 @@ def klc_dk_index(layout: "KeyboardLayout") -> List[str]: if k not in layout.dead_keys: continue dk = layout.dead_keys[k] - output.append(f"{hex_ord(dk[' '])}\t\"{DK_INDEX[k]['name'].upper()}\"") + output.append(f"{hex_ord(dk[' '])}\t\"{DK_INDEX[k].name.upper()}\"") return output @@ -338,7 +338,7 @@ def klc_dk_index(layout: "KeyboardLayout") -> List[str]: # -def osx_keymap(layout: "KeyboardLayout") -> List[str]: +def osx_keymap(layout: "KeyboardLayout") -> List[List[str]]: """macOS layout, main part.""" ret_str = [] @@ -356,7 +356,7 @@ def has_dead_keys(letter): return True return False - output = [] + output: List[str] = [] for key_name in LAYER_KEYS: if key_name in ["ae13", "ab11"]: # ABNT / JIS keys continue # these two keys are not supported yet @@ -373,7 +373,7 @@ def has_dead_keys(letter): if key_name in layer: key = layer[key_name] if key in layout.dead_keys: - symbol = f"dead_{DK_INDEX[key]['name']}" + symbol = f"dead_{DK_INDEX[key].name}" final_key = False else: symbol = xml_proof(key.upper() if caps else key) @@ -398,7 +398,7 @@ def osx_actions(layout: "KeyboardLayout") -> List[str]: def when(state, action): state_attr = f'state="{state}"'.ljust(18) if action in layout.dead_keys: - action_attr = f"next=\"{DK_INDEX[action]['name']}\"" + action_attr = f"next=\"{DK_INDEX[action].name}\"" elif action.startswith("dead_"): action_attr = f'next="{action[5:]}"' else: @@ -414,12 +414,12 @@ def append_actions(symbol, actions): # dead key definitions for key in layout.dead_keys: - name = DK_INDEX[key]["name"] + name = DK_INDEX[key].name term = layout.dead_keys[key][key] ret_actions.append(f'') ret_actions.append(f' ') if name == "1dk" and term in layout.dead_keys: - nested_dk = DK_INDEX[term]["name"] + nested_dk = DK_INDEX[term].name ret_actions.append(f' ') ret_actions.append("") continue @@ -436,7 +436,7 @@ def append_actions(symbol, actions): continue key = layout.layers[i][key_name] - if i and key == layout.layers[0][key_name]: + if i and key == layout.layers[Layer.BASE][key_name]: continue if key in layout.dead_keys: continue @@ -445,7 +445,7 @@ def append_actions(symbol, actions): for k in DK_INDEX: if k in layout.dead_keys: if key in layout.dead_keys[k]: - actions.append((DK_INDEX[k]["name"], layout.dead_keys[k][key])) + actions.append((DK_INDEX[k].name, layout.dead_keys[k][key])) if actions: append_actions(xml_proof(key), actions) @@ -453,7 +453,7 @@ def append_actions(symbol, actions): actions = [] for k in DK_INDEX: if k in layout.dead_keys: - actions.append((DK_INDEX[k]["name"], layout.dead_keys[k][" "])) + actions.append((DK_INDEX[k].name, layout.dead_keys[k][" "])) append_actions(" ", actions) # space append_actions(" ", actions) # no-break space append_actions(" ", actions) # fine no-break space @@ -469,7 +469,7 @@ def osx_terminators(layout: "KeyboardLayout") -> List[str]: if key not in layout.dead_keys: continue dk = layout.dead_keys[key] - name = DK_INDEX[key]["name"] + name = DK_INDEX[key].name term = dk[key] if name == "1dk" and term in layout.dead_keys: term = dk[" "] diff --git a/kalamine/utils.py b/kalamine/utils.py index f2b5ca3..92dc976 100644 --- a/kalamine/utils.py +++ b/kalamine/utils.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from dataclasses import dataclass import os from enum import IntEnum from pathlib import Path @@ -7,7 +8,13 @@ import yaml -def lines_to_text(lines: List[str], indent: str = ""): +def lines_to_text(lines: List[str], indent: str = "") -> str: + """ + From a list lines of string, produce a string concatenating the elements + of lines indented by prepending indent and followed by a new line. + Example: lines_to_text(["one", "two", "three"], " ") returns + ' one\n two\n three' + """ out = "" for line in lines: if len(line): @@ -17,6 +24,7 @@ def lines_to_text(lines: List[str], indent: str = ""): def text_to_lines(text: str) -> List[str]: + """Split given text into lines""" return text.split("\n") @@ -35,8 +43,30 @@ class Layer(IntEnum): ALTGR = 4 ALTGR_SHIFT = 5 + def next(self) -> 'Layer': + """The next layer in the layer ordering.""" + return Layer(int(self)+1) + + def necromance(self) -> 'Layer': + """Remove the effect of the dead key if any.""" + if self == Layer.ODK: + return Layer.BASE + elif self == Layer.ODK_SHIFT: + return Layer.SHIFT + return self + +@dataclass +class DeadKeyDescr: + char: str + name: str + base: str + alt: str + alt_space: str + alt_self: str + + +DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data( "dead_keys.yaml")] -DEAD_KEYS = load_data("dead_keys.yaml") ODK_ID = "**" # must match the value in dead_keys.yaml LAYER_KEYS = [ "- Digits",