feat: add preset resolver

This commit is contained in:
2023-03-21 20:19:27 +03:00
parent 81c704576a
commit 265c80f082
12 changed files with 183 additions and 126 deletions
-54
View File
@@ -1,54 +0,0 @@
[presets.python]
filename = "pyproject.toml"
[presets.python.tasks.setup.poetry]
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 = [
"file(pyproject.toml) | get(tool.poetry.group.dev.dependencies.pytest)",
]
commands = ["poetry run pytest -p no:warnings"]
[presets.python.tasks.lint.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 = [
"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"
[presets.nodejs.tasks.setup.npm]
conditions = ["file(package-lock.json)"]
commands = ["npm install-clean --legacy-peer-deps"]
[presets.nodejs.tasks.setup.npm-legacy]
conditions = ["file(package-lock.json) | not"]
commands = ["npm install --legacy-peer-deps"]
[presets.nodejs.tasks.test.jest]
conditions = ["file(package.json) | get(devDependencies.jest)"]
commands = ["npx jest"]
-1
View File
@@ -14,7 +14,6 @@ def run():
"task",
nargs="?",
default="show",
choices=("show", "setup", "lint", "format", "test"),
)
parser.add_argument("-p", "--path", default=os.getcwd())
parser.add_argument("-v", "--verbose", action="count", default=1)
+5 -4
View File
@@ -73,10 +73,11 @@ class PipeRunner:
pipe = self._parser.parse(pipeline)
return self.run(pipe)
def check_conditions(self, conditions: List[str]) -> bool:
for condition in conditions:
if not self.resolve_value(condition):
return False
def check_conditions(self, conditions: Optional[List[str]]) -> bool:
if conditions:
for condition in conditions:
if not self.resolve_value(condition):
return False
return True
+34 -15
View File
@@ -1,37 +1,56 @@
from pathlib import Path
from typing import Any, Optional
from ...util import reflect
from ..core import Pipe
from ..util import reflect
__all__ = ("FilePipe",)
__all__ = (
"FileData",
"FilePipe",
)
class FilePipe(Pipe[None, Any]):
type = "file"
class FileData:
_LOADERS = {
".toml": "tomli:load",
".json": "json:load",
}
def __init__(self, filename: Optional[str], loader: Optional[str] = None):
self._filename = filename
def __init__(self, filepath: Path, loader: Optional[str] = None):
self._filepath = filepath
self._loader = loader
self._data = None
def _resolve_loader(self, filepath: Path):
loader = self._loader or self._LOADERS.get(filepath.suffix)
return reflect(loader) if loader else None
def _load_data(self):
loader = self._resolve_loader(self._filepath)
with open(self._filepath, "rb") as fd:
if loader:
self._data = loader(fd)
else:
self._data = fd.read()
def get(self, key: str):
if self._data is None and self._filepath.exists():
self._load_data()
return self._data.get(key)
def __bool__(self):
return self._filepath.exists()
class FilePipe(Pipe[None, Any]):
type = "file"
def __init__(self, filename: Optional[str], loader: Optional[str] = None):
self._filename = filename
self._loader = loader
def apply(self, value: Optional[str], basepath: Path) -> Optional[Any]:
filepath = Path(value or self._filename) # TODO: raise none filepath exception
if not filepath.is_absolute():
filepath = basepath / filepath
if not filepath.exists():
return None
loader = self._resolve_loader(filepath)
with open(filepath, "rb") as fd:
if loader:
result = loader(fd)
else:
result = fd.read()
return result
return FileData(filepath, self._loader)
-13
View File
@@ -1,13 +0,0 @@
import importlib
from typing import Any
__all__ = ("reflect",)
def reflect(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)
@@ -0,0 +1,18 @@
[presets.nodejs]
conditions = ["file(package.json)"]
[presets.nodejs.tasks.setup.npm]
conditions = ["file(package-lock.json)"]
commands = ["npm install-clean --legacy-peer-deps"]
[presets.nodejs.tasks.setup.npm-legacy]
conditions = ["file(package-lock.json) | not"]
commands = ["npm install --legacy-peer-deps"]
[presets.nodejs.tasks.test.jest]
conditions = ["file(node_modules/jest)"]
commands = ["npx jest"]
[presets.nodejs.tasks.clean.node_modules]
conditions = ["file(node_modules)"]
commands = ["rm -rf node_modules"]
@@ -0,0 +1,33 @@
[presets.python]
conditions = ["file(pyproject.toml)"]
[presets.python.tasks.setup.poetry]
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 = ["file(.venv/bin/pytest)"]
commands = [".venv/bin/pytest -p no:warnings"]
[presets.python.tasks.lint.isort]
conditions = ["file(.venv/bin/isort)"]
commands = [".venv/bin/isort --profile black --check-only ./"]
[presets.python.tasks.format.isort]
conditions = ["file(.venv/bin/isort)"]
commands = [".venv/bin/isort ./"]
[presets.python.tasks.lint.black]
conditions = ["file(.venv/bin/black)"]
commands = [".venv/bin/black --check --diff ./"]
[presets.python.tasks.format.black]
conditions = ["file(.venv/bin/black)"]
commands = [".venv/bin/black ./"]
[presets.python.tasks.clean.venv]
conditions = ["file(.venv)"]
commands = ["rm -rf .venv"]
+19 -19
View File
@@ -5,7 +5,7 @@ from typing import Optional
from ..entry import Entry, EntryDetector
from ..pipe.core import PipeParser, PipeRunner
from .util import PresetUtil
from .resolve import PresetResolver
__all__ = ("PresetEntryDetector",)
@@ -14,28 +14,28 @@ class PresetEntryDetector(EntryDetector):
_PIPE_PARSER = PipeParser()
def __init__(self, filename: Path):
self._preset = PresetUtil.resolve_preset(filename)
self._pipeparser = PipeParser()
def detect_entry(self, basepath: Path) -> Optional["Entry"]:
preset = PresetResolver(basepath).resolve_preset()
piperunner = PipeRunner(basepath)
tasks = defaultdict(list)
for preset_name, preset in self._preset.presets.items():
filename = basepath / preset.filename
if filename.exists():
for task_name, task_pack in preset.tasks.items():
for task_item_name, task in task_pack.items():
if piperunner.check_conditions(task.conditions):
variables = (
{
k: piperunner.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
]
for preset_name, preset in preset.presets.items():
if not piperunner.check_conditions(preset.conditions):
continue
for task_name, task_pack in preset.tasks.items():
for task_item_name, task in task_pack.items():
if piperunner.check_conditions(task.conditions):
variables = (
{
k: piperunner.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)
+5 -5
View File
@@ -2,19 +2,19 @@ from typing import Dict, List, Optional
from pydantic import BaseModel
__all__ = ("TaskPreset", "FilePreset", "Preset")
__all__ = ("TaskPreset", "EntryPreset", "Preset")
class TaskPreset(BaseModel):
conditions: List[str]
conditions: Optional[List[str]]
variables: Optional[Dict[str, str]]
commands: List[str]
class FilePreset(BaseModel):
filename: str
class EntryPreset(BaseModel):
conditions: Optional[List[str]]
tasks: Dict[str, Dict[str, TaskPreset]]
class Preset(BaseModel):
presets: Dict[str, FilePreset]
presets: Dict[str, EntryPreset]
+41
View File
@@ -0,0 +1,41 @@
__all__ = ("PresetResolver",)
from pathlib import Path
from typing import Dict, Iterable
import tomli
from ..util import merge_dicts
from .model import Preset
class PresetResolver:
_source_cache: Dict[Path, dict] = {}
def __init__(self, basepath: Path):
self._basepath = basepath
def iter_sources(self) -> Iterable[Path]:
for filepath in (Path(__file__).parent / "default").iterdir():
yield filepath
user_preset = Path.home() / ".preset.toml"
if user_preset.exists():
yield user_preset
projectpath = self._basepath
while projectpath.exists():
project_preset = projectpath / ".preset.toml"
if project_preset.exists():
yield project_preset
if projectpath.parent == projectpath:
break
projectpath = projectpath.parent
def resolve_preset(self) -> Preset:
result_preset_data = {}
for filepath in self.iter_sources():
if filepath not in self._source_cache:
with open(filepath, "rb") as fd:
self._source_cache[filepath] = tomli.load(fd)
merge_dicts(result_preset_data, self._source_cache[filepath])
return Preset(**result_preset_data)
-15
View File
@@ -1,15 +0,0 @@
from pathlib import Path
import tomli
from .model import Preset
__all__ = ("PresetUtil",)
class PresetUtil:
@classmethod
def resolve_preset(cls, filename: Path) -> Preset:
with open(filename, "rb") as fd:
preset_data = tomli.load(fd)
return Preset(**preset_data)
+28
View File
@@ -0,0 +1,28 @@
import importlib
__all__ = (
"reflect",
"merge_dicts",
)
def reflect(value: str):
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 merge_dicts(target, enhancer):
for key, val in enhancer.items():
if key not in target:
target[key] = val
continue
if isinstance(val, dict):
merge_dicts(target[key], val)
else:
target[key] = val
return target