Use dataclasses for the model

This commit is contained in:
Sleepy Monax 2023-11-11 19:05:41 +01:00
parent b1415cce16
commit 8f4d19c98e
6 changed files with 112 additions and 250 deletions

View file

@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any from typing import Any
@ -5,10 +6,11 @@ SUPPORTED_MANIFEST = [
"https://schemas.cute.engineering/stable/cutekit.manifest.component.v1", "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1",
"https://schemas.cute.engineering/stable/cutekit.manifest.project.v1", "https://schemas.cute.engineering/stable/cutekit.manifest.project.v1",
"https://schemas.cute.engineering/stable/cutekit.manifest.target.v1", "https://schemas.cute.engineering/stable/cutekit.manifest.target.v1",
] ]
OSDK_MANIFEST_NOT_SUPPORTED = "OSDK manifests are not supported by CuteKit. Please use CuteKit manifest instead" OSDK_MANIFEST_NOT_SUPPORTED = (
"OSDK manifests are not supported by CuteKit. Please use CuteKit manifest instead"
)
UNSUPORTED_MANIFEST = { UNSUPORTED_MANIFEST = {
"https://schemas.cute.engineering/stable/osdk.manifest.component.v1": OSDK_MANIFEST_NOT_SUPPORTED, "https://schemas.cute.engineering/stable/osdk.manifest.component.v1": OSDK_MANIFEST_NOT_SUPPORTED,
@ -20,14 +22,16 @@ UNSUPORTED_MANIFEST = {
} }
def ensureSupportedManifest(manifest: Any, path: str): def ensureSupportedManifest(manifest: Any, path: Path):
if "$schema" not in manifest: if "$schema" not in manifest:
raise RuntimeError(f"Missing $schema in {path}") raise RuntimeError(f"Missing $schema in {path}")
if manifest["$schema"] in UNSUPORTED_MANIFEST: if manifest["$schema"] in UNSUPORTED_MANIFEST:
raise RuntimeError( raise RuntimeError(
f"Unsupported manifest schema {manifest['$schema']} in {path}: {UNSUPORTED_MANIFEST[manifest['$schema']]}") f"Unsupported manifest schema {manifest['$schema']} in {path}: {UNSUPORTED_MANIFEST[manifest['$schema']]}"
)
if manifest["$schema"] not in SUPPORTED_MANIFEST: if manifest["$schema"] not in SUPPORTED_MANIFEST:
raise RuntimeError( raise RuntimeError(
f"Unsupported manifest schema {manifest['$schema']} in {path}") f"Unsupported manifest schema {manifest['$schema']} in {path}"
)

View file

@ -1,7 +1,7 @@
import os import os
import sys import sys
VERSION = (0, 5, 4) VERSION = (0, 6, 0, "dev")
VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}" VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if len(VERSION) >= 4 else ''}"
MODULE_DIR = os.path.dirname(os.path.realpath(__file__)) MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
ARGV0 = os.path.basename(sys.argv[0]) ARGV0 = os.path.basename(sys.argv[0])

View file

@ -46,7 +46,7 @@ class ComponentInstance:
return self.manifest.id return self.manifest.id
def isLib(self): def isLib(self):
return self.manifest.type == model.Type.LIB return self.manifest.type == model.Kind.LIB
def objdir(self) -> str: def objdir(self) -> str:
return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj") return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj")
@ -88,7 +88,7 @@ class ComponentInstance:
def cinclude(self) -> str: def cinclude(self) -> str:
if "cpp-root-include" in self.manifest.props: if "cpp-root-include" in self.manifest.props:
return self.manifest.dirname() return self.manifest.dirname()
elif self.manifest.type == model.Type.LIB: elif self.manifest.type == model.Kind.LIB:
return str(Path(self.manifest.dirname()).parent) return str(Path(self.manifest.dirname()).parent)
else: else:
return "" return ""
@ -131,7 +131,7 @@ class Context(IContext):
def hashid(self) -> str: def hashid(self) -> str:
return utils.hash( return utils.hash(
(self.target.props, [self.tools[t].toJson() for t in self.tools]) (self.target.props, [self.tools[t].to_dict() for t in self.tools])
)[0:8] )[0:8]
def builddir(self) -> str: def builddir(self) -> str:
@ -154,14 +154,19 @@ def loadAllTargets() -> list[model.Target]:
ret = [] ret = []
for entry in paths: for entry in paths:
files = shell.find(entry, ["*.json"]) files = shell.find(entry, ["*.json"])
ret += list(map(lambda path: model.Target(jexpr.evalRead(path), path), files)) ret += list(
map(
lambda path: model.Manifest.load(Path(path)).ensureType(model.Target),
files,
)
)
return ret return ret
def loadProject(path: str) -> model.Project: def loadProject(path: str) -> model.Project:
path = os.path.join(path, "project.json") path = os.path.join(path, "project.json")
return model.Project(jexpr.evalRead(path), path) return model.Manifest.load(Path(path)).ensureType(model.Project)
def loadTarget(id: str) -> model.Target: def loadTarget(id: str) -> model.Target:
@ -175,7 +180,12 @@ def loadAllComponents() -> list[model.Component]:
files = shell.find(const.SRC_DIR, ["manifest.json"]) files = shell.find(const.SRC_DIR, ["manifest.json"])
files += shell.find(const.EXTERN_DIR, ["manifest.json"]) files += shell.find(const.EXTERN_DIR, ["manifest.json"])
return list(map(lambda path: model.Component(jexpr.evalRead(path), path), files)) return list(
map(
lambda path: model.Manifest.load(Path(path)).ensureType(model.Component),
files,
)
)
def filterDisabled( def filterDisabled(
@ -242,7 +252,7 @@ def resolveDeps(
enabled, unresolvedReason, resolved = resolveInner(componentSpec) enabled, unresolvedReason, resolved = resolveInner(componentSpec)
return enabled, unresolvedReason, resolved return enabled, unresolvedReason, utils.uniq(resolved)
def instanciate( def instanciate(
@ -250,7 +260,10 @@ def instanciate(
) -> Optional[ComponentInstance]: ) -> Optional[ComponentInstance]:
manifest = next(filter(lambda c: c.id == componentSpec, components)) manifest = next(filter(lambda c: c.id == componentSpec, components))
wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values()))) wildcards = set(chain(*map(lambda rule: rule.fileIn, rules.rules.values())))
sources = shell.find(manifest.subdirs, list(wildcards), recusive=False) dirs = [manifest.dirname()] + list(
map(lambda d: os.path.join(manifest.dirname(), d), manifest.subdirs)
)
sources = shell.find(dirs, list(wildcards), recusive=False)
res = shell.find(os.path.join(manifest.dirname(), "res")) res = shell.find(os.path.join(manifest.dirname(), "res"))
@ -299,9 +312,7 @@ def contextFor(targetSpec: str, props: model.Props = {}) -> Context:
for toolSpec in target.tools: for toolSpec in target.tools:
tool = target.tools[toolSpec] tool = target.tools[toolSpec]
tools[toolSpec] = model.Tool( tools[toolSpec] = model.Tool(cmd=tool.cmd, args=tool.args, files=tool.files)
strict=False, cmd=tool.cmd, args=tool.args, files=tool.files
)
tools[toolSpec].args += rules.rules[toolSpec].args tools[toolSpec].args += rules.rules[toolSpec].args

View file

@ -1,5 +1,6 @@
import os import os
import json import json
from pathlib import Path
from typing import Any, cast, Callable, Final from typing import Any, cast, Callable, Final
from . import shell, compat from . import shell, compat
@ -9,8 +10,8 @@ Builtin = Callable[..., Json]
BUILTINS: Final[dict[str, Builtin]] = { BUILTINS: Final[dict[str, Builtin]] = {
"uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(), "uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(),
"include": lambda arg, ctx: evalRead(arg, compatibilityCheck=False), "include": lambda arg, ctx: evalRead(arg),
"evalRead": lambda arg, ctx: evalRead(arg, compatibilityCheck=False), "evalRead": lambda arg, ctx: evalRead(arg),
"join": lambda lhs, rhs, ctx: cast( "join": lambda lhs, rhs, ctx: cast(
Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs Json, {**lhs, **rhs} if isinstance(lhs, dict) else lhs + rhs
), ),
@ -27,7 +28,7 @@ BUILTINS: Final[dict[str, Builtin]] = {
} }
def eval(jexpr: Json, filePath: str) -> Json: def eval(jexpr: Json, filePath: Path) -> Json:
if isinstance(jexpr, dict): if isinstance(jexpr, dict):
result = {} result = {}
for k in cast(dict[str, Json], jexpr): for k in cast(dict[str, Json], jexpr):
@ -49,7 +50,7 @@ def eval(jexpr: Json, filePath: str) -> Json:
return jexpr return jexpr
def read(path: str) -> Json: def read(path: Path) -> Json:
try: try:
with open(path, "r") as f: with open(path, "r") as f:
return json.load(f) return json.load(f)
@ -57,8 +58,6 @@ def read(path: str) -> Json:
raise RuntimeError(f"Failed to read {path}") raise RuntimeError(f"Failed to read {path}")
def evalRead(path: str, compatibilityCheck: bool = True) -> Json: def evalRead(path: Path) -> Json:
data = read(path) data = read(path)
if compatibilityCheck:
compat.ensureSupportedManifest(data, path)
return eval(data, path) return eval(data, path)

View file

@ -1,18 +1,21 @@
import os import os
import logging import logging
from enum import Enum
from typing import Any
from pathlib import Path
from . import jexpr
from enum import Enum
from typing import Any, Type, cast
from pathlib import Path
from dataclasses_json import DataClassJsonMixin, config
from dataclasses import dataclass, field
from . import jexpr, compat, utils
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
Props = dict[str, Any] Props = dict[str, Any]
class Type(Enum): class Kind(Enum):
UNKNOWN = "unknown" UNKNOWN = "unknown"
PROJECT = "project" PROJECT = "project"
TARGET = "target" TARGET = "target"
@ -20,115 +23,46 @@ class Type(Enum):
EXE = "exe" EXE = "exe"
class Manifest: @dataclass
id: str = "" class Manifest(DataClassJsonMixin):
type: Type = Type.UNKNOWN id: str
path: str = "" type: Kind = field(default=Kind.UNKNOWN)
path: str = field(default="")
def __init__( @staticmethod
self, def parse(path: Path, data: dict[str, Any]) -> "Manifest":
json: jexpr.Json = None, compat.ensureSupportedManifest(data, path)
path: str = "", kind = Kind(data["type"])
strict: bool = True, del data["$schema"]
**kwargs: Any, obj = KINDS[kind].from_dict(data)
): obj.path = str(path)
if json is not None: return obj
if "id" not in json:
raise RuntimeError("Missing id")
self.id = json["id"] @staticmethod
def load(path: Path) -> "Manifest":
if "type" not in json and strict: return Manifest.parse(path, jexpr.evalRead(path))
raise RuntimeError("Missing type")
self.type = Type(json["type"])
self.path = path
elif strict:
raise RuntimeError("Missing json")
for key in kwargs:
setattr(self, key, kwargs[key])
def toJson(self) -> jexpr.Json:
return {"id": self.id, "type": self.type.value, "path": self.path}
def __str__(self):
return f"Manifest(id={self.id}, type={self.type}, path={self.path})"
def __repr__(self):
return f"Manifest({id})"
def dirname(self) -> str: def dirname(self) -> str:
return os.path.dirname(self.path) return os.path.dirname(self.path)
def ensureType(self, t: Type[utils.T]) -> utils.T:
class Extern: if not isinstance(self, t):
git: str = "" raise RuntimeError(
tag: str = "" f"{self.path} should be a {type.__name__} manifest but is a {self.__class__.__name__} manifest"
)
def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any): return cast(utils.T, self)
if json is not None:
if "git" not in json and strict:
raise RuntimeError("Missing git")
self.git = json["git"]
if "tag" not in json and strict:
raise RuntimeError("Missing tag")
self.tag = json["tag"]
elif strict:
raise RuntimeError("Missing json")
for key in kwargs:
setattr(self, key, kwargs[key])
def toJson(self) -> jexpr.Json:
return {"git": self.git, "tag": self.tag}
def __str__(self):
return f"Extern(git={self.git}, tag={self.tag})"
def __repr__(self):
return f"Extern({self.git})"
@dataclass
class Extern(DataClassJsonMixin):
git: str
tag: str
@dataclass
class Project(Manifest): class Project(Manifest):
description: str = "" description: str = field(default="(No description)")
extern: dict[str, Extern] = {} extern: dict[str, Extern] = field(default_factory=dict)
def __init__(
self,
json: jexpr.Json = None,
path: str = "",
strict: bool = True,
**kwargs: Any,
):
if json is not None:
if "description" not in json and strict:
raise RuntimeError("Missing description")
self.description = json["description"]
self.extern = {k: Extern(v) for k, v in json.get("extern", {}).items()}
elif strict:
raise RuntimeError("Missing json")
super().__init__(json, path, strict, **kwargs)
def toJson(self) -> jexpr.Json:
return {
**super().toJson(),
"description": self.description,
"extern": {k: v.toJson() for k, v in self.extern.items()},
}
def __str__(self):
return f"ProjectManifest(id={self.id}, type={self.type}, path={self.path}, description={self.description}, extern={self.extern})"
def __repr__(self):
return f"ProjectManifest({self.id})"
@staticmethod @staticmethod
def root() -> str | None: def root() -> str | None:
@ -149,80 +83,21 @@ class Project(Manifest):
os.chdir(path) os.chdir(path)
class Tool: @dataclass
cmd: str = "" class Tool(DataClassJsonMixin):
args: list[str] = [] cmd: str = field(default="")
files: list[str] = [] args: list[str] = field(default_factory=list)
files: list[str] = field(default_factory=list)
def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any):
if json is not None:
if "cmd" not in json and strict:
raise RuntimeError("Missing cmd")
self.cmd = json.get("cmd", self.cmd)
if "args" not in json and strict:
raise RuntimeError("Missing args")
self.args = json.get("args", [])
self.files = json.get("files", [])
elif strict:
raise RuntimeError("Missing json")
for key in kwargs:
setattr(self, key, kwargs[key])
def toJson(self) -> jexpr.Json:
return {"cmd": self.cmd, "args": self.args, "files": self.files}
def __str__(self):
return f"Tool(cmd={self.cmd}, args={self.args}, files={self.files})"
def __repr__(self):
return f"Tool({self.cmd})"
Tools = dict[str, Tool] Tools = dict[str, Tool]
@dataclass
class Target(Manifest): class Target(Manifest):
props: Props props: Props = field(default_factory=dict)
tools: Tools tools: Tools = field(default_factory=dict)
routing: dict[str, str] routing: dict[str, str] = field(default_factory=dict)
def __init__(
self,
json: jexpr.Json = None,
path: str = "",
strict: bool = True,
**kwargs: Any,
):
if json is not None:
if "props" not in json and strict:
raise RuntimeError("Missing props")
self.props = json["props"]
if "tools" not in json and strict:
raise RuntimeError("Missing tools")
self.tools = {k: Tool(v) for k, v in json["tools"].items()}
self.routing = json.get("routing", {})
super().__init__(json, path, strict, **kwargs)
def toJson(self) -> jexpr.Json:
return {
**super().toJson(),
"props": self.props,
"tools": {k: v.toJson() for k, v in self.tools.items()},
"routing": self.routing,
}
def __repr__(self):
return f"TargetManifest({self.id})"
def route(self, componentSpec: str): def route(self, componentSpec: str):
return ( return (
@ -250,54 +125,15 @@ class Target(Manifest):
return defines return defines
@dataclass
class Component(Manifest): class Component(Manifest):
decription: str = "(No description)" decription: str = field(default="(No description)")
props: Props = {} props: Props = field(default_factory=dict)
tools: Tools = {} tools: Tools = field(default_factory=dict)
enableIf: dict[str, list[Any]] = {} enableIf: dict[str, list[Any]] = field(default_factory=dict)
requires: list[str] = [] requires: list[str] = field(default_factory=list)
provides: list[str] = [] provides: list[str] = field(default_factory=list)
subdirs: list[str] = [] subdirs: list[str] = field(default_factory=list)
def __init__(
self,
json: jexpr.Json = None,
path: str = "",
strict: bool = True,
**kwargs: Any,
):
if json is not None:
self.decription = json.get("description", self.decription)
self.props = json.get("props", self.props)
self.tools = {
k: Tool(v, strict=False) for k, v in json.get("tools", {}).items()
}
self.enableIf = json.get("enableIf", self.enableIf)
self.requires = json.get("requires", self.requires)
self.provides = json.get("provides", self.provides)
self.subdirs = list(
map(
lambda x: os.path.join(os.path.dirname(path), x),
json.get("subdirs", [""]),
)
)
super().__init__(json, path, strict, **kwargs)
def toJson(self) -> jexpr.Json:
return {
**super().toJson(),
"description": self.decription,
"props": self.props,
"tools": {k: v.toJson() for k, v in self.tools.items()},
"enableIf": self.enableIf,
"requires": self.requires,
"provides": self.provides,
"subdirs": self.subdirs,
}
def __repr__(self):
return f"ComponentManifest({self.id})"
def isEnabled(self, target: Target) -> tuple[bool, str]: def isEnabled(self, target: Target) -> tuple[bool, str]:
for k, v in self.enableIf.items(): for k, v in self.enableIf.items():
@ -316,3 +152,11 @@ class Component(Manifest):
) )
return True, "" return True, ""
KINDS: dict[Kind, Type[Manifest]] = {
Kind.PROJECT: Project,
Kind.TARGET: Target,
Kind.LIB: Component,
Kind.EXE: Component,
}

View file

@ -3,11 +3,11 @@ from typing import Any, TypeVar, cast, Optional, Union
import json import json
import hashlib import hashlib
T = TypeVar('T') T = TypeVar("T")
def uniq(l: list[str]) -> list[str]: def uniq(l: list[T]) -> list[T]:
result: list[str] = [] result: list[T] = []
for i in l: for i in l:
if i in result: if i in result:
result.remove(i) result.remove(i)
@ -15,7 +15,9 @@ def uniq(l: list[str]) -> list[str]:
return result return result
def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None) -> str: def hash(
obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] = None
) -> str:
toHash = {} toHash = {}
if len(keys) == 0: if len(keys) == 0:
toHash = obj toHash = obj
@ -28,7 +30,7 @@ def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] =
def camelCase(s: str) -> str: def camelCase(s: str) -> str:
s = ''.join(x for x in s.title() if x != '_' and x != '-') s = "".join(x for x in s.title() if x != "_" and x != "-")
s = s[0].lower() + s[1:] s = s[0].lower() + s[1:]
return s return s
@ -60,4 +62,6 @@ def asList(i: Optional[Union[T, list[T]]]) -> list[T]:
def isNewer(path1: str, path2: str) -> bool: def isNewer(path1: str, path2: str) -> bool:
return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime(path2) return not os.path.exists(path2) or os.path.getmtime(path1) > os.path.getmtime(
path2
)