feat: add preset resolver
This commit is contained in:
@@ -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"]
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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,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):
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user