Use dataclasses for the model
This commit is contained in:
parent
39ee66364d
commit
a472abb90f
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
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.project.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 = {
|
||||
"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:
|
||||
raise RuntimeError(f"Missing $schema in {path}")
|
||||
|
||||
if manifest["$schema"] in UNSUPORTED_MANIFEST:
|
||||
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:
|
||||
raise RuntimeError(
|
||||
f"Unsupported manifest schema {manifest['$schema']} in {path}")
|
||||
f"Unsupported manifest schema {manifest['$schema']} in {path}"
|
||||
)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
VERSION = (0, 6, 0, "dev")
|
||||
VERSION_STR = f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if VERSION[3] else ''}"
|
||||
MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
VERSION_STR = (
|
||||
f"{VERSION[0]}.{VERSION[1]}.{VERSION[2]}{'-' + VERSION[3] if VERSION[3] else ''}"
|
||||
)
|
||||
ARGV0 = os.path.basename(sys.argv[0])
|
||||
PROJECT_CK_DIR = ".cutekit"
|
||||
GLOBAL_CK_DIR = os.path.join(os.path.expanduser("~"), ".cutekit")
|
||||
|
@ -16,5 +18,4 @@ TARGETS_DIR = os.path.join(META_DIR, "targets")
|
|||
DEFAULT_REPO_TEMPLATES = "cute-engineering/cutekit-templates"
|
||||
DESCRIPTION = "A build system and package manager for low-level software development"
|
||||
PROJECT_LOG_FILE = os.path.join(PROJECT_CK_DIR, "cutekit.log")
|
||||
GLOBAL_LOG_FILE = os.path.join(
|
||||
os.path.expanduser("~"), ".cutekit", "cutekit.log")
|
||||
GLOBAL_LOG_FILE = os.path.join(os.path.expanduser("~"), ".cutekit", "cutekit.log")
|
||||
|
|
|
@ -46,7 +46,7 @@ class ComponentInstance:
|
|||
return self.manifest.id
|
||||
|
||||
def isLib(self):
|
||||
return self.manifest.type == model.Type.LIB
|
||||
return self.manifest.type == model.Kind.LIB
|
||||
|
||||
def objdir(self) -> str:
|
||||
return os.path.join(self.context.builddir(), f"{self.manifest.id}/obj")
|
||||
|
@ -88,7 +88,7 @@ class ComponentInstance:
|
|||
def cinclude(self) -> str:
|
||||
if "cpp-root-include" in self.manifest.props:
|
||||
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)
|
||||
else:
|
||||
return ""
|
||||
|
@ -131,7 +131,7 @@ class Context(IContext):
|
|||
|
||||
def hashid(self) -> str:
|
||||
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]
|
||||
|
||||
def builddir(self) -> str:
|
||||
|
@ -154,14 +154,19 @@ def loadAllTargets() -> list[model.Target]:
|
|||
ret = []
|
||||
for entry in paths:
|
||||
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
|
||||
|
||||
|
||||
def loadProject(path: str) -> model.Project:
|
||||
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:
|
||||
|
@ -175,7 +180,12 @@ def loadAllComponents() -> list[model.Component]:
|
|||
files = shell.find(const.SRC_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(
|
||||
|
@ -242,7 +252,7 @@ def resolveDeps(
|
|||
|
||||
enabled, unresolvedReason, resolved = resolveInner(componentSpec)
|
||||
|
||||
return enabled, unresolvedReason, resolved
|
||||
return enabled, unresolvedReason, utils.uniq(resolved)
|
||||
|
||||
|
||||
def instanciate(
|
||||
|
@ -250,7 +260,10 @@ def instanciate(
|
|||
) -> Optional[ComponentInstance]:
|
||||
manifest = next(filter(lambda c: c.id == componentSpec, components))
|
||||
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"))
|
||||
|
||||
|
@ -299,9 +312,7 @@ def contextFor(targetSpec: str, props: model.Props = {}) -> Context:
|
|||
for toolSpec in target.tools:
|
||||
tool = target.tools[toolSpec]
|
||||
|
||||
tools[toolSpec] = model.Tool(
|
||||
strict=False, cmd=tool.cmd, args=tool.args, files=tool.files
|
||||
)
|
||||
tools[toolSpec] = model.Tool(cmd=tool.cmd, args=tool.args, files=tool.files)
|
||||
|
||||
tools[toolSpec].args += rules.rules[toolSpec].args
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Any, cast, Callable, Final
|
||||
from . import shell, compat
|
||||
|
@ -9,8 +10,8 @@ Builtin = Callable[..., Json]
|
|||
|
||||
BUILTINS: Final[dict[str, Builtin]] = {
|
||||
"uname": lambda arg, ctx: getattr(shell.uname(), arg).lower(),
|
||||
"include": lambda arg, ctx: evalRead(arg, compatibilityCheck=False),
|
||||
"evalRead": lambda arg, ctx: evalRead(arg, compatibilityCheck=False),
|
||||
"include": lambda arg, ctx: evalRead(arg),
|
||||
"evalRead": lambda arg, ctx: evalRead(arg),
|
||||
"join": lambda lhs, rhs, ctx: cast(
|
||||
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):
|
||||
result = {}
|
||||
for k in cast(dict[str, Json], jexpr):
|
||||
|
@ -49,7 +50,7 @@ def eval(jexpr: Json, filePath: str) -> Json:
|
|||
return jexpr
|
||||
|
||||
|
||||
def read(path: str) -> Json:
|
||||
def read(path: Path) -> Json:
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
@ -57,8 +58,6 @@ def read(path: str) -> Json:
|
|||
raise RuntimeError(f"Failed to read {path}")
|
||||
|
||||
|
||||
def evalRead(path: str, compatibilityCheck: bool = True) -> Json:
|
||||
def evalRead(path: Path) -> Json:
|
||||
data = read(path)
|
||||
if compatibilityCheck:
|
||||
compat.ensureSupportedManifest(data, path)
|
||||
return eval(data, path)
|
||||
|
|
284
cutekit/model.py
284
cutekit/model.py
|
@ -1,18 +1,21 @@
|
|||
import os
|
||||
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__)
|
||||
|
||||
Props = dict[str, Any]
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
class Kind(Enum):
|
||||
UNKNOWN = "unknown"
|
||||
PROJECT = "project"
|
||||
TARGET = "target"
|
||||
|
@ -20,115 +23,46 @@ class Type(Enum):
|
|||
EXE = "exe"
|
||||
|
||||
|
||||
class Manifest:
|
||||
id: str = ""
|
||||
type: Type = Type.UNKNOWN
|
||||
path: str = ""
|
||||
@dataclass
|
||||
class Manifest(DataClassJsonMixin):
|
||||
id: str
|
||||
type: Kind = field(default=Kind.UNKNOWN)
|
||||
path: str = field(default="")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
json: jexpr.Json = None,
|
||||
path: str = "",
|
||||
strict: bool = True,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if json is not None:
|
||||
if "id" not in json:
|
||||
raise RuntimeError("Missing id")
|
||||
@staticmethod
|
||||
def parse(path: Path, data: dict[str, Any]) -> "Manifest":
|
||||
compat.ensureSupportedManifest(data, path)
|
||||
kind = Kind(data["type"])
|
||||
del data["$schema"]
|
||||
obj = KINDS[kind].from_dict(data)
|
||||
obj.path = str(path)
|
||||
return obj
|
||||
|
||||
self.id = json["id"]
|
||||
|
||||
if "type" not in json and strict:
|
||||
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})"
|
||||
@staticmethod
|
||||
def load(path: Path) -> "Manifest":
|
||||
return Manifest.parse(path, jexpr.evalRead(path))
|
||||
|
||||
def dirname(self) -> str:
|
||||
return os.path.dirname(self.path)
|
||||
|
||||
|
||||
class Extern:
|
||||
git: str = ""
|
||||
tag: str = ""
|
||||
|
||||
def __init__(self, json: jexpr.Json = None, strict: bool = True, **kwargs: Any):
|
||||
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})"
|
||||
def ensureType(self, t: Type[utils.T]) -> utils.T:
|
||||
if not isinstance(self, t):
|
||||
raise RuntimeError(
|
||||
f"{self.path} should be a {type.__name__} manifest but is a {self.__class__.__name__} manifest"
|
||||
)
|
||||
return cast(utils.T, self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Extern(DataClassJsonMixin):
|
||||
git: str
|
||||
tag: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Project(Manifest):
|
||||
description: str = ""
|
||||
extern: dict[str, Extern] = {}
|
||||
|
||||
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})"
|
||||
description: str = field(default="(No description)")
|
||||
extern: dict[str, Extern] = field(default_factory=dict)
|
||||
|
||||
@staticmethod
|
||||
def root() -> str | None:
|
||||
|
@ -149,80 +83,21 @@ class Project(Manifest):
|
|||
os.chdir(path)
|
||||
|
||||
|
||||
class Tool:
|
||||
cmd: str = ""
|
||||
args: list[str] = []
|
||||
files: list[str] = []
|
||||
|
||||
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})"
|
||||
@dataclass
|
||||
class Tool(DataClassJsonMixin):
|
||||
cmd: str = field(default="")
|
||||
args: list[str] = field(default_factory=list)
|
||||
files: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
Tools = dict[str, Tool]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Target(Manifest):
|
||||
props: Props
|
||||
tools: Tools
|
||||
routing: dict[str, str]
|
||||
|
||||
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})"
|
||||
props: Props = field(default_factory=dict)
|
||||
tools: Tools = field(default_factory=dict)
|
||||
routing: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def route(self, componentSpec: str):
|
||||
return (
|
||||
|
@ -250,54 +125,15 @@ class Target(Manifest):
|
|||
return defines
|
||||
|
||||
|
||||
@dataclass
|
||||
class Component(Manifest):
|
||||
decription: str = "(No description)"
|
||||
props: Props = {}
|
||||
tools: Tools = {}
|
||||
enableIf: dict[str, list[Any]] = {}
|
||||
requires: list[str] = []
|
||||
provides: list[str] = []
|
||||
subdirs: list[str] = []
|
||||
|
||||
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})"
|
||||
decription: str = field(default="(No description)")
|
||||
props: Props = field(default_factory=dict)
|
||||
tools: Tools = field(default_factory=dict)
|
||||
enableIf: dict[str, list[Any]] = field(default_factory=dict)
|
||||
requires: list[str] = field(default_factory=list)
|
||||
provides: list[str] = field(default_factory=list)
|
||||
subdirs: list[str] = field(default_factory=list)
|
||||
|
||||
def isEnabled(self, target: Target) -> tuple[bool, str]:
|
||||
for k, v in self.enableIf.items():
|
||||
|
@ -316,3 +152,11 @@ class Component(Manifest):
|
|||
)
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
KINDS: dict[Kind, Type[Manifest]] = {
|
||||
Kind.PROJECT: Project,
|
||||
Kind.TARGET: Target,
|
||||
Kind.LIB: Component,
|
||||
Kind.EXE: Component,
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ from typing import Any, TypeVar, cast, Optional, Union
|
|||
import json
|
||||
import hashlib
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def uniq(l: list[str]) -> list[str]:
|
||||
result: list[str] = []
|
||||
def uniq(l: list[T]) -> list[T]:
|
||||
result: list[T] = []
|
||||
for i in l:
|
||||
if i in result:
|
||||
result.remove(i)
|
||||
|
@ -15,7 +15,9 @@ def uniq(l: list[str]) -> list[str]:
|
|||
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 = {}
|
||||
if len(keys) == 0:
|
||||
toHash = obj
|
||||
|
@ -28,7 +30,7 @@ def hash(obj: Any, keys: list[str] = [], cls: Optional[type[json.JSONEncoder]] =
|
|||
|
||||
|
||||
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:]
|
||||
return s
|
||||
|
||||
|
@ -60,4 +62,6 @@ def asList(i: Optional[Union[T, list[T]]]) -> list[T]:
|
|||
|
||||
|
||||
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
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue