Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data classes #101

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
use pkgutil to load package resources
  • Loading branch information
fabi1cazenave committed Feb 8, 2024
commit a4b8554b86d46d258edb0d6e0398e87a748f997f
20 changes: 14 additions & 6 deletions kalamine/cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#!/usr/bin/env python3
import json
import pkgutil
from contextlib import contextmanager
from importlib import metadata
from pathlib import Path
from typing import Iterator, List, Literal, Union

import click
import tomli

from .layout import KeyboardLayout
from .layout import KeyboardLayout, load_layout
from .server import keyboard_server


@@ -113,7 +115,7 @@ def make(
"""Convert TOML/YAML descriptions into OS-specific keyboard drivers."""

for input_file in layout_descriptors:
layout = KeyboardLayout(input_file, angle_mod)
layout = KeyboardLayout(load_layout(input_file), angle_mod)

# default: build all in the `dist` subdirectory
if out == "all":
@@ -187,15 +189,19 @@ def make(
@click.option("--1dk/--no-1dk", "odk", default=False, help="Set a custom dead key.")
def create(output_file: Path, geometry: str, altgr: bool, odk: bool) -> None:
"""Create a new TOML layout description."""
base_dir_path = Path(__file__).resolve(strict=True).parent.parent
# base_dir_path = Path(__file__).resolve(strict=True).parent.parent

def get_layout(name: str) -> KeyboardLayout:
"""Return a layout of type NAME with constrained geometry."""
layout = KeyboardLayout(base_dir_path / "layouts" / f"{name}.toml")

descriptor = pkgutil.get_data(__package__, f"../layouts/{name}.toml")
layout = KeyboardLayout(tomli.loads(descriptor.decode("utf-8")))
layout.geometry = geometry
return layout

def keymap(layout_name, layout_layer, layer_name=""):
"""Return a multiline keymap ASCII art for the specified layout."""

layer = "\n"
layer += f"\n{layer_name or layout_layer} = '''"
layer += "\n"
@@ -216,8 +222,10 @@ def keymap(layout_name, layout_layer, layer_name=""):
content += keymap("ansi", "base")

# append user guide sections
with (base_dir_path / "docs" / "README.md").open() as f:
sections = "".join(f.readlines()).split("\n\n\n")
doc = pkgutil.get_data(__package__, "../docs/README.md").decode("utf-8")
sections = doc.split("\n\n\n")
# with (base_dir_path / "docs" / "README.md").open() as f:
# sections = "".join(f.readlines()).split("\n\n\n")
for topic in sections[1:]:
content += "\n\n"
content += "\n# "
4 changes: 2 additions & 2 deletions kalamine/cli_xkb.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@

import click

from .layout import KeyboardLayout
from .layout import KeyboardLayout, load_layout
from .xkb_manager import WAYLAND, Index, XKBManager


@@ -35,7 +35,7 @@ def apply(filepath: Path, angle_mod: bool) -> None:
"You appear to be running Wayland, which does not support this operation."
)

layout = KeyboardLayout(filepath, angle_mod)
layout = KeyboardLayout(load_layout(filepath), angle_mod)
with tempfile.NamedTemporaryFile(
mode="w+", suffix=".xkb", encoding="utf-8"
) as temp_file:
113 changes: 75 additions & 38 deletions kalamine/layout.py
Original file line number Diff line number Diff line change
@@ -100,20 +100,65 @@ def load_tpl(layout: "KeyboardLayout", ext: str) -> str:
return out


def load_descriptor(file_path: Path) -> Dict:
if file_path.suffix in [".yaml", ".yml"]:
with file_path.open(encoding="utf-8") as file:
return yaml.load(file, Loader=yaml.SafeLoader)

with file_path.open(mode="rb") as dfile:
return tomli.load(dfile)
def load_layout(layout_path: Path) -> Dict:
"""Load the TOML/YAML layout description data (and its ancessor, if any)."""

def load_descriptor(file_path: Path) -> Dict:
if file_path.suffix in [".yaml", ".yml"]:
with file_path.open(encoding="utf-8") as file:
return yaml.load(file, Loader=yaml.SafeLoader)

with file_path.open(mode="rb") as dfile:
return tomli.load(dfile)

try:
cfg = load_descriptor(layout_path)
if "name" not in cfg:
cfg["name"] = layout_path.stem
if "extends" in cfg:
parent_path = filepath.parent / cfg["extends"]
ext = load_descriptor(parent_path)
ext.update(cfg)
cfg = ext
return cfg

except Exception as exc:
click.echo("File could not be parsed.", err=True)
click.echo(f"Error: {exc}.", err=True)
sys.exit(1)


###
# Constants
#


# fmt: off
@dataclass
class MetaDescr:
name: str = "custom"
name8: str = "custom"
variant: str = "custom"
fileName: str = "custom"
locale: str = "us"
geometry: str = "ISO"
description: str = ""
author: str = "nobody"
license: str = ""
version: str = "0.0.1"
lastChange: str = datetime.date.today().isoformat()


@dataclass
class SpacebarDescr:
shift: str = " "
altgr: str = " "
altgt_shift: str = " "
odk: str = "'"
odk_shift: str = "'"
# fmt: on


CONFIG = {
"author": "nobody",
"license": "WTFPL - Do What The Fuck You Want Public License",
@@ -150,9 +195,9 @@ def from_dict(cls: Type[T], src: Dict) -> T:
)


geometry_data = load_data("geometry.yaml")

GEOMETRY = {key: GeometryDescr.from_dict(val) for key, val in geometry_data.items()}
GEOMETRY = {
key: GeometryDescr.from_dict(val) for key, val in load_data("geometry").items()
}


###
@@ -163,42 +208,32 @@ def from_dict(cls: Type[T], src: Dict) -> T:
class KeyboardLayout:
"""Lafayette-style keyboard layout: base + 1dk + altgr layers."""

def __init__(self, filepath: Path, angle_mod: bool = False) -> None:
# self.meta = {key: MetaDescr.from_dict(val) for key, val in geometry_data.items()}

def __init__(self, layout_data: Dict, angle_mod: bool = False) -> None:
"""Import a keyboard layout to instanciate the object."""

# initialize a blank layout
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 = Dict[str, str] = {} # default parameters, hardcoded
self.meta = CONFIG.copy() # default parameters, hardcoded
self.has_altgr = False
self.has_1dk = False

# load the YAML data (and its ancessor, if any)
try:
cfg = load_descriptor(filepath)
if "extends" in cfg:
path = filepath.parent / cfg["extends"]
ext = load_descriptor(path)
ext.update(cfg)
cfg = ext
except Exception as exc:
click.echo("File could not be parsed.", err=True)
click.echo(f"Error: {exc}.", err=True)
sys.exit(1)

# metadata: self.meta
for k in cfg:
for k in layout_data:
if (
k != "base"
and k != "full"
and k != "altgr"
and not isinstance(cfg[k], dict)
and not isinstance(layout_data[k], dict)
):
self.meta[k] = cfg[k]
filename = filepath.stem
self.meta["name"] = cfg["name"] if "name" in cfg else filename
self.meta["name8"] = cfg["name8"] if "name8" in cfg else self.meta["name"][0:8]
self.meta[k] = layout_data[k]
self.meta["name8"] = (
layout_data["name8"] if "name8" in layout_data else self.meta["name"][0:8]
)
self.meta["fileName"] = self.meta["name8"].lower()
self.meta["lastChange"] = datetime.date.today().isoformat()

@@ -216,24 +251,26 @@ def __init__(self, filepath: Path, angle_mod: bool = False) -> None:
"Warning: geometry does not support angle-mod; ignoring the --angle-mod argument"
)

if "full" in cfg:
full = text_to_lines(cfg["full"])
if "full" in layout_data:
full = text_to_lines(layout_data["full"])
self._parse_template(full, rows, Layer.BASE)
self._parse_template(full, rows, Layer.ALTGR)
self.has_altgr = True
else:
base = text_to_lines(cfg["base"])
base = text_to_lines(layout_data["base"])
self._parse_template(base, rows, Layer.BASE)
self._parse_template(base, rows, Layer.ODK)
if "altgr" in cfg:
if "altgr" in layout_data:
self.has_altgr = True
self._parse_template(text_to_lines(cfg["altgr"]), rows, Layer.ALTGR)
self._parse_template(
text_to_lines(layout_data["altgr"]), rows, Layer.ALTGR
)

# space bar
spc = SPACEBAR.copy()
if "spacebar" in cfg:
for k in cfg["spacebar"]:
spc[k] = cfg["spacebar"][k]
if "spacebar" in layout_data:
for k in layout_data["spacebar"]:
spc[k] = layout_data["spacebar"][k]
self.layers[Layer.BASE]["spce"] = " "
self.layers[Layer.SHIFT]["spce"] = spc["shift"]
if True or self.has_1dk: # XXX self.has_1dk is not defined yet
5 changes: 2 additions & 3 deletions kalamine/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import json
import os
import threading
import webbrowser
from http.server import HTTPServer, SimpleHTTPRequestHandler
@@ -9,11 +8,11 @@
import click
from livereload import Server # type: ignore

from .layout import KeyboardLayout
from .layout import KeyboardLayout, load_layout


def keyboard_server(file_path: Path) -> None:
kb_layout = KeyboardLayout(file_path)
kb_layout = KeyboardLayout(load_layout(file_path))

host_name = "localhost"
webserver_port = 1664
4 changes: 2 additions & 2 deletions kalamine/template.py
Original file line number Diff line number Diff line change
@@ -16,8 +16,8 @@
# Helpers
#

KEY_CODES = load_data("key_codes.yaml")
XKB_KEY_SYM = load_data("key_sym.yaml")
KEY_CODES = load_data("key_codes")
XKB_KEY_SYM = load_data("key_sym")


def hex_ord(char: str) -> str:
7 changes: 4 additions & 3 deletions kalamine/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import pkgutil
from dataclasses import dataclass
from enum import IntEnum
from pathlib import Path
@@ -28,8 +29,8 @@ def text_to_lines(text: str) -> List[str]:


def load_data(filename: str) -> Dict:
filepath = Path(__file__).parent / "data" / filename
return yaml.load(filepath.open(encoding="utf-8"), Loader=yaml.SafeLoader)
descriptor = pkgutil.get_data(__package__, f"data/{filename}.yaml")
return yaml.safe_load(descriptor.decode("utf-8"))


class Layer(IntEnum):
@@ -65,7 +66,7 @@ class DeadKeyDescr:
alt_self: str


DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys.yaml")]
DEAD_KEYS = [DeadKeyDescr(**data) for data in load_data("dead_keys")]

ODK_ID = "**" # must match the value in dead_keys.yaml
LAYER_KEYS = [
6 changes: 3 additions & 3 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from kalamine import KeyboardLayout

from .util import get_layout_path
from .util import get_layout_dict


def load_layout(filename, angle_mod=False):
return KeyboardLayout(get_layout_path() / (filename + ".toml"), angle_mod)
def load_layout(filename: str, angle_mod: bool = False) -> KeyboardLayout:
return KeyboardLayout(get_layout_dict(filename), angle_mod)


def test_ansi():
8 changes: 4 additions & 4 deletions tests/test_serializer_ahk.py
Original file line number Diff line number Diff line change
@@ -3,14 +3,14 @@
from kalamine import KeyboardLayout
from kalamine.template import ahk_keymap, ahk_shortcuts

from .util import get_layout_path
from .util import get_layout_dict


def load_layout(filename) -> KeyboardLayout:
return KeyboardLayout(get_layout_path() / (filename + ".toml"))
def load_layout(filename: str) -> KeyboardLayout:
return KeyboardLayout(get_layout_dict(filename))


def split(multiline_str):
def split(multiline_str: str):
return dedent(multiline_str).lstrip().splitlines()


8 changes: 4 additions & 4 deletions tests/test_serializer_keylayout.py
Original file line number Diff line number Diff line change
@@ -3,14 +3,14 @@
from kalamine import KeyboardLayout
from kalamine.template import osx_actions, osx_keymap, osx_terminators

from .util import get_layout_path
from .util import get_layout_dict


def load_layout(filename):
return KeyboardLayout(get_layout_path() / (filename + ".toml"))
def load_layout(filename: str) -> KeyboardLayout:
return KeyboardLayout(get_layout_dict(filename))


def split(multiline_str):
def split(multiline_str: str):
return dedent(multiline_str).lstrip().rstrip().splitlines()


8 changes: 3 additions & 5 deletions tests/test_serializer_klc.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import os
from enum import Enum
from textwrap import dedent

from kalamine import KeyboardLayout
from kalamine.template import klc_deadkeys, klc_dk_index, klc_keymap

from .util import get_layout_path
from .util import get_layout_dict


def split(multiline_str):
def split(multiline_str: str):
return dedent(multiline_str).lstrip().rstrip().splitlines()


LAYOUTS = {}
for filename in ["ansi", "intl", "prog"]:
LAYOUTS[filename] = KeyboardLayout(get_layout_path() / (filename + ".toml"))
LAYOUTS[filename] = KeyboardLayout(get_layout_dict(filename))


def test_ansi_keymap():
Loading