From 81bf685fec09672cae670cf56cddb2244709a3ee Mon Sep 17 00:00:00 2001 From: shmyga Date: Sun, 19 Mar 2023 21:06:43 +0300 Subject: [PATCH] feat: update presets --- project_manager/default_preset.toml | 51 +++++++++++------ project_manager/manager.py | 6 +- project_manager/preset/entry.py | 19 +++++-- project_manager/preset/model.py | 11 ++-- project_manager/preset/util.py | 34 +---------- project_manager/preset/value.py | 88 +++++++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 59 deletions(-) create mode 100644 project_manager/preset/value.py diff --git a/project_manager/default_preset.toml b/project_manager/default_preset.toml index fd1d01b..11fc131 100644 --- a/project_manager/default_preset.toml +++ b/project_manager/default_preset.toml @@ -1,35 +1,54 @@ [presets.python] filename = "pyproject.toml" -loader = "tomli:load" [presets.python.tasks.setup.poetry] -conditions = { "build-system.build-backend" = "poetry.core.masonry.api" } -commands = ["poetry env use 3.8", "poetry install"] +conditions = [ + "file(pyproject.toml) | get(build-system.build-backend) | equal(poetry.core.masonry.api)", +] +variables.python_version = "file(pyproject.toml) | get(tool.poetry.dependencies.python) | search((\\d.\\d))" +commands = ["poetry env use {python_version}", "poetry install"] [presets.python.tasks.test.pytest] -conditions = { "tool.poetry.group.dev.dependencies.pytest" = "*" } +conditions = [ + "file(pyproject.toml) | get(tool.poetry.group.dev.dependencies.pytest)", +] commands = ["poetry run pytest -p no:warnings"] -[presets.python.tasks.lint.black] -conditions = { "tool.poetry.group.dev.dependencies.black" = "*" } -commands = ["poetry run black --check --diff ./"] - -[presets.python.tasks.format.black] -conditions = { "tool.poetry.group.dev.dependencies.black" = "*" } -commands = ["poetry run black ./"] - [presets.python.tasks.lint.isort] -conditions = { "tool.poetry.group.dev.dependencies.isort" = "*" } +conditions = [ + "file(pyproject.toml) | get(tool.poetry.group.dev.dependencies.isort)", +] commands = ["poetry run isort --profile black --check-only ./"] [presets.python.tasks.format.isort] -conditions = { "tool.poetry.group.dev.dependencies.isort" = "*" } +conditions = [ + "file(pyproject.toml) | get(tool.poetry.group.dev.dependencies.isort)", +] commands = ["poetry run isort ./"] +[presets.python.tasks.lint.black] +conditions = [ + "file(pyproject.toml) | get(tool.poetry.group.dev.dependencies.black)", +] +commands = ["poetry run black --check --diff ./"] + +[presets.python.tasks.format.black] +conditions = [ + "file(pyproject.toml) | get(tool.poetry.group.dev.dependencies.black)", +] +commands = ["poetry run black ./"] + [presets.nodejs] filename = "package.json" -loader = "json:load" [presets.nodejs.tasks.setup.npm] -conditions = {} +conditions = ["file(package-lock.json)"] commands = ["npm install-clean"] + +[presets.nodejs.tasks.setup.npm-legacy] +conditions = ["file(package-lock.json) | not"] +commands = ["npm install"] + +[presets.nodejs.tasks.test.jest] +conditions = ["file(package.json) | get(devDependencies.jest)"] +commands = ["npx jest"] diff --git a/project_manager/manager.py b/project_manager/manager.py index 3d8bc03..e63af96 100644 --- a/project_manager/manager.py +++ b/project_manager/manager.py @@ -13,13 +13,15 @@ logger = logging.getLogger("project") class ProjectManager: + DEFAULT_PRESET = Path(__file__).parent / "default_preset.toml" + DEFAULT_IGNORE = ["node_modules", ".venv", ".git"] _logger = logger def __init__(self, basepath: Path): self._basepaht = basepath self._resolver = EntryResolver( - PresetEntryDetector(Path(__file__).parent / "default_preset.toml"), - ["node_modules", ".venv", ".git"], + PresetEntryDetector(self.DEFAULT_PRESET), + self.DEFAULT_IGNORE, ) def _iter_entries(self) -> Iterable[Entry]: diff --git a/project_manager/preset/entry.py b/project_manager/preset/entry.py index 0b8935b..14b5e5e 100644 --- a/project_manager/preset/entry.py +++ b/project_manager/preset/entry.py @@ -4,6 +4,7 @@ from typing import Optional from ..entry import Entry, EntryDetector from .util import PresetUtil +from .value import ValueResolver __all__ = ("PresetEntryDetector",) @@ -17,12 +18,20 @@ class PresetEntryDetector(EntryDetector): for preset_name, preset in self._preset.presets.items(): filename = basepath / preset.filename if filename.exists(): - loader = PresetUtil.resolve_loader(preset.loader) - with open(filename, "rb") as fd: - data = loader(fd) + value_resolver = ValueResolver(basepath) for task_name, task_pack in preset.tasks.items(): for task_item_name, task in task_pack.items(): - if PresetUtil.check_conditions(data, task.conditions): - tasks[task_name] += task.commands + if value_resolver.check_conditions(task.conditions): + variables = ( + { + k: value_resolver.resolve_value(v) + for k, v in task.variables.items() + } + if task.variables + else {} + ) + tasks[task_name] += [ + command.format(**variables) for command in task.commands + ] if tasks: return Entry(tasks, basepath) diff --git a/project_manager/preset/model.py b/project_manager/preset/model.py index d9e7f19..7b35019 100644 --- a/project_manager/preset/model.py +++ b/project_manager/preset/model.py @@ -1,18 +1,21 @@ -from typing import Dict, List +from typing import Dict, List, Optional from pydantic import BaseModel -__all__ = ("TaskPreset", "FilePreset", "Preset") +__all__ = ("PipeStr", "TaskPreset", "FilePreset", "Preset") + + +PipeStr = str class TaskPreset(BaseModel): - conditions: Dict[str, str] + conditions: List[PipeStr] + variables: Optional[Dict[str, PipeStr]] commands: List[str] class FilePreset(BaseModel): filename: str - loader: str tasks: Dict[str, Dict[str, TaskPreset]] diff --git a/project_manager/preset/util.py b/project_manager/preset/util.py index c56851a..63b5ee0 100644 --- a/project_manager/preset/util.py +++ b/project_manager/preset/util.py @@ -1,43 +1,13 @@ -__all__ = ("PresetUtil",) - -import importlib from pathlib import Path -from typing import Any, Dict import tomli from .model import Preset +__all__ = ("PresetUtil",) + class PresetUtil: - @classmethod - def resolve_loader(cls, loader: str) -> Any: - module_name, item_name = loader.split(":") - module = importlib.import_module(module_name) - return getattr(module, item_name) - - @classmethod - def get_key_path_value(cls, key_path: str, data: dict): - path = key_path.split(".") - value = data.get(path[0]) - for key in path[1:]: - if value: - value = value.get(key) - return value - - @classmethod - def match_condition_value(cls, value: Any, pattern: str) -> bool: - if pattern == "*": - return bool(value) - return value == pattern - - @classmethod - def check_conditions(cls, data: dict, conditions: Dict[str, str]) -> bool: - for key, value in conditions.items(): - if not cls.match_condition_value(cls.get_key_path_value(key, data), value): - return False - return True - @classmethod def resolve_preset(cls, filename: Path) -> Preset: with open(filename, "rb") as fd: diff --git a/project_manager/preset/value.py b/project_manager/preset/value.py new file mode 100644 index 0000000..df36ac7 --- /dev/null +++ b/project_manager/preset/value.py @@ -0,0 +1,88 @@ +import importlib +import re +from pathlib import Path +from typing import Any, List, Optional + +from .model import PipeStr + +__all__ = ("ValueResolver",) + + +class ValueResolver: + PIPE_PATTERN = re.compile("^(\\w+)\\((.*)\\)") + FILE_LOADERS = { + ".toml": "tomli:load", + ".json": "json:load", + } + + def __init__(self, basepath: Path): + self._basepath = basepath + + def _reflect(self, value: str) -> Any: + if ":" in value: + module_name, item_name = value.split(":") + else: + module_name, item_name = "builtins", value + module = importlib.import_module(module_name) + return getattr(module, item_name) + + def pipe_file(self, pipe, filename: str, loader: Optional[str] = None): + filepath = Path(filename) + if not filepath.is_absolute(): + filepath = self._basepath / filepath + if loader is None: + loader = self.FILE_LOADERS.get(Path(filename).suffix) + if not filepath.exists(): + return None + with open(filepath, "rb") as fd: + if loader: + loader_fun = self._reflect(loader) + data = loader_fun(fd) + else: + data = fd.read() + return data + + def pipe_get(self, pipe, keypath: str): + path = keypath.split(".") + value = pipe.get(path[0]) + for key in path[1:]: + if value: + value = value.get(key) + return value + + def pipe_search(self, pipe, pattern: str, position_arg: str = "1"): + matched = re.compile(pattern).search(pipe) + position = int(position_arg) + if matched: + return matched.group(position) + + def pipe_equal(self, pipe, value: Any): + return pipe == value + + def pipe_convert(self, pipe, converter: str): + converter_fun = self._reflect(converter) + return converter_fun(pipe) + + def pipe_not(self, pipe): + return not pipe + + def resolve_value(self, operator: str) -> Any: + chain = [item.strip() for item in operator.split("|")] + pipe = None + for part in chain: + matched = self.PIPE_PATTERN.match(part) + if matched: + pipe_name = matched.group(1) + pipe_args = [item.strip() for item in matched.group(2).split(",")] + else: + pipe_name = part.strip() + pipe_args = [] + pipe_func = getattr(self, f"pipe_{pipe_name}") + pipe = pipe_func(pipe, *pipe_args) + return pipe + + def check_conditions(self, conditions: List[PipeStr]) -> bool: + for condition in conditions: + if not self.resolve_value(condition): + return False + return True