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", "task",
nargs="?", nargs="?",
default="show", default="show",
choices=("show", "setup", "lint", "format", "test"),
) )
parser.add_argument("-p", "--path", default=os.getcwd()) parser.add_argument("-p", "--path", default=os.getcwd())
parser.add_argument("-v", "--verbose", action="count", default=1) parser.add_argument("-v", "--verbose", action="count", default=1)
+2 -1
View File
@@ -73,7 +73,8 @@ class PipeRunner:
pipe = self._parser.parse(pipeline) pipe = self._parser.parse(pipeline)
return self.run(pipe) return self.run(pipe)
def check_conditions(self, conditions: List[str]) -> bool: def check_conditions(self, conditions: Optional[List[str]]) -> bool:
if conditions:
for condition in conditions: for condition in conditions:
if not self.resolve_value(condition): if not self.resolve_value(condition):
return False return False
+34 -15
View File
@@ -1,37 +1,56 @@
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
from ...util import reflect
from ..core import Pipe from ..core import Pipe
from ..util import reflect
__all__ = ("FilePipe",) __all__ = (
"FileData",
"FilePipe",
)
class FilePipe(Pipe[None, Any]): class FileData:
type = "file"
_LOADERS = { _LOADERS = {
".toml": "tomli:load", ".toml": "tomli:load",
".json": "json:load", ".json": "json:load",
} }
def __init__(self, filename: Optional[str], loader: Optional[str] = None): def __init__(self, filepath: Path, loader: Optional[str] = None):
self._filename = filename self._filepath = filepath
self._loader = loader self._loader = loader
self._data = None
def _resolve_loader(self, filepath: Path): def _resolve_loader(self, filepath: Path):
loader = self._loader or self._LOADERS.get(filepath.suffix) loader = self._loader or self._LOADERS.get(filepath.suffix)
return reflect(loader) if loader else None 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]: def apply(self, value: Optional[str], basepath: Path) -> Optional[Any]:
filepath = Path(value or self._filename) # TODO: raise none filepath exception filepath = Path(value or self._filename) # TODO: raise none filepath exception
if not filepath.is_absolute(): if not filepath.is_absolute():
filepath = basepath / filepath filepath = basepath / filepath
if not filepath.exists(): return FileData(filepath, self._loader)
return None
loader = self._resolve_loader(filepath)
with open(filepath, "rb") as fd:
if loader:
result = loader(fd)
else:
result = fd.read()
return result
-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"]
+5 -5
View File
@@ -5,7 +5,7 @@ from typing import Optional
from ..entry import Entry, EntryDetector from ..entry import Entry, EntryDetector
from ..pipe.core import PipeParser, PipeRunner from ..pipe.core import PipeParser, PipeRunner
from .util import PresetUtil from .resolve import PresetResolver
__all__ = ("PresetEntryDetector",) __all__ = ("PresetEntryDetector",)
@@ -14,15 +14,15 @@ class PresetEntryDetector(EntryDetector):
_PIPE_PARSER = PipeParser() _PIPE_PARSER = PipeParser()
def __init__(self, filename: Path): def __init__(self, filename: Path):
self._preset = PresetUtil.resolve_preset(filename)
self._pipeparser = PipeParser() self._pipeparser = PipeParser()
def detect_entry(self, basepath: Path) -> Optional["Entry"]: def detect_entry(self, basepath: Path) -> Optional["Entry"]:
preset = PresetResolver(basepath).resolve_preset()
piperunner = PipeRunner(basepath) piperunner = PipeRunner(basepath)
tasks = defaultdict(list) tasks = defaultdict(list)
for preset_name, preset in self._preset.presets.items(): for preset_name, preset in preset.presets.items():
filename = basepath / preset.filename if not piperunner.check_conditions(preset.conditions):
if filename.exists(): continue
for task_name, task_pack in preset.tasks.items(): for task_name, task_pack in preset.tasks.items():
for task_item_name, task in task_pack.items(): for task_item_name, task in task_pack.items():
if piperunner.check_conditions(task.conditions): if piperunner.check_conditions(task.conditions):
+5 -5
View File
@@ -2,19 +2,19 @@ from typing import Dict, List, Optional
from pydantic import BaseModel from pydantic import BaseModel
__all__ = ("TaskPreset", "FilePreset", "Preset") __all__ = ("TaskPreset", "EntryPreset", "Preset")
class TaskPreset(BaseModel): class TaskPreset(BaseModel):
conditions: List[str] conditions: Optional[List[str]]
variables: Optional[Dict[str, str]] variables: Optional[Dict[str, str]]
commands: List[str] commands: List[str]
class FilePreset(BaseModel): class EntryPreset(BaseModel):
filename: str conditions: Optional[List[str]]
tasks: Dict[str, Dict[str, TaskPreset]] tasks: Dict[str, Dict[str, TaskPreset]]
class Preset(BaseModel): 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