feat: use jinja
This commit is contained in:
@@ -8,7 +8,8 @@ disable=
|
||||
relative-beyond-top-level,
|
||||
subprocess-run-check,
|
||||
bare-except,
|
||||
fixme
|
||||
fixme,
|
||||
unspecified-encoding
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=120
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
__all__ = (
|
||||
"CommandError",
|
||||
"CommandLogger",
|
||||
"CommandExecutor",
|
||||
)
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
@@ -4,13 +4,6 @@ from typing import Iterable, NamedTuple
|
||||
|
||||
from .common.command import CommandExecutor
|
||||
|
||||
__all__ = (
|
||||
"TaskKey",
|
||||
"Entry",
|
||||
"EntryDetector",
|
||||
"EntryResolver",
|
||||
)
|
||||
|
||||
logger = logging.getLogger("entry")
|
||||
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ from typing import Iterable
|
||||
|
||||
from .common.command import CommandError
|
||||
from .entry import Entry, EntryResolver
|
||||
from .preset.entry import PresetEntryDetector
|
||||
|
||||
__all__ = ("ProjectManager",)
|
||||
from .preset.detector import PresetEntryDetector
|
||||
|
||||
logger = logging.getLogger("project")
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
DEFAULT_IGNORE = ["node_modules", ".venv", ".git", "tests"]
|
||||
# TODO: from .gitignore?
|
||||
DEFAULT_IGNORE = ["node_modules", ".venv", ".git", "tests", "__pycache__"]
|
||||
_logger = logger
|
||||
|
||||
def __init__(self, basepath: Path):
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .core import PIPE_REGISTRY
|
||||
|
||||
PIPE_REGISTRY.register_module("project_manager.pipe.type")
|
||||
@@ -1,87 +0,0 @@
|
||||
import importlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
__all__ = ("Pipe", "PipeRegistry", "PipeParser", "PipeRunner", "PIPE_REGISTRY")
|
||||
|
||||
I = TypeVar("I")
|
||||
O = TypeVar("O")
|
||||
|
||||
|
||||
class Pipe(Generic[I, O]):
|
||||
type: str
|
||||
|
||||
def apply(self, value: I, basepath: Path) -> O | None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PipeRegistry:
|
||||
def __init__(self):
|
||||
self._pipes = {}
|
||||
|
||||
def register(self, pipe_type: type[Pipe]):
|
||||
self._pipes[pipe_type.type] = pipe_type
|
||||
|
||||
def register_module(self, module_name: str):
|
||||
module = importlib.import_module(module_name)
|
||||
modulepath = Path(module.__file__).parent
|
||||
# pylint:disable=too-many-nested-blocks
|
||||
for item in modulepath.iterdir():
|
||||
if item.suffix == ".py" and not item.name.startswith("__"):
|
||||
sub_module = importlib.import_module(f"{module_name}.{item.stem}")
|
||||
for v in sub_module.__dict__.values():
|
||||
if isinstance(v, type):
|
||||
for baseclass in v.__bases__:
|
||||
if baseclass == Pipe:
|
||||
self.register(v)
|
||||
|
||||
def get(self, pipe_type: str) -> type[Pipe]:
|
||||
return self._pipes[pipe_type]
|
||||
|
||||
|
||||
class PipeParser:
|
||||
_PIPE_PATTERN = re.compile("^(\\w+)\\((.*)\\)")
|
||||
|
||||
def parse(self, pipeline: str) -> list[Pipe]:
|
||||
chain = [item.strip() for item in pipeline.split("|")]
|
||||
result = []
|
||||
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_type = PIPE_REGISTRY.get(pipe_name)
|
||||
result.append(pipe_type(*pipe_args))
|
||||
return result
|
||||
|
||||
|
||||
class PipeRunner:
|
||||
def __init__(self, basepath: Path, parser: PipeParser | None = None):
|
||||
self._basepath = basepath
|
||||
self._parser = parser or PipeParser()
|
||||
|
||||
def run(self, pipes: list[Pipe]) -> Any:
|
||||
result = None
|
||||
for pipe in pipes:
|
||||
result = pipe.apply(result, self._basepath)
|
||||
if result is None:
|
||||
break
|
||||
return result
|
||||
|
||||
def resolve_value(self, pipeline: str) -> Any:
|
||||
pipe = self._parser.parse(pipeline)
|
||||
return self.run(pipe)
|
||||
|
||||
def check_conditions(self, conditions: list[str] | None) -> bool:
|
||||
if conditions:
|
||||
for condition in conditions:
|
||||
if not self.resolve_value(condition):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
PIPE_REGISTRY = PipeRegistry()
|
||||
@@ -1,16 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..core import Pipe
|
||||
|
||||
__all__ = ("EqualPipe",)
|
||||
|
||||
|
||||
class EqualPipe(Pipe[Any, bool]):
|
||||
type = "equal"
|
||||
|
||||
def __init__(self, value: Any):
|
||||
self._value = value
|
||||
|
||||
def apply(self, value: Any, basepath: Path) -> bool:
|
||||
return self._value == value
|
||||
@@ -1,54 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
from typing import Any
|
||||
|
||||
import dotenv
|
||||
|
||||
from ..core import Pipe
|
||||
|
||||
__all__ = (
|
||||
"FileData",
|
||||
"FilePipe",
|
||||
)
|
||||
|
||||
|
||||
class FileData:
|
||||
|
||||
def __init__(self, filepath: Path, loader: str | None = None):
|
||||
self._filepath = filepath
|
||||
self._loader = loader
|
||||
self._data = None
|
||||
|
||||
def _load_data(self):
|
||||
if self._filepath.suffix == ".toml":
|
||||
return tomllib.loads(self._filepath.read_text())
|
||||
elif self._filepath.suffix == ".json":
|
||||
return json.loads(self._filepath.read_text())
|
||||
elif self._filepath.suffix == ".env" or self._filepath.name == ".env":
|
||||
return dotenv.dotenv_values(self._filepath)
|
||||
return self._filepath.read_text()
|
||||
|
||||
def get(self, key: str):
|
||||
if self._data is None and self._filepath.exists():
|
||||
self._data = self._load_data()
|
||||
if self._data:
|
||||
return self._data.get(key)
|
||||
return None
|
||||
|
||||
def __bool__(self):
|
||||
return self._filepath.exists()
|
||||
|
||||
|
||||
class FilePipe(Pipe[None, Any]):
|
||||
type = "file"
|
||||
|
||||
def __init__(self, filename: str | None, loader: str | None = None):
|
||||
self._filename = filename
|
||||
self._loader = loader
|
||||
|
||||
def apply(self, value: str | None, basepath: Path) -> Any | None:
|
||||
filepath = Path(value or self._filename) # TODO: raise none filepath exception
|
||||
if not filepath.is_absolute():
|
||||
filepath = basepath / filepath
|
||||
return FileData(filepath, self._loader)
|
||||
@@ -1,32 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..core import Pipe
|
||||
|
||||
__all__ = ("GetPipe", "KeyPipe")
|
||||
|
||||
|
||||
class GetPipe(Pipe[dict, Any]):
|
||||
type = "get"
|
||||
|
||||
def __init__(self, keypath: str):
|
||||
self._keypath = keypath
|
||||
|
||||
def apply(self, value: dict, basepath: Path) -> Any | None:
|
||||
path = self._keypath.split(".")
|
||||
result = value.get(path[0])
|
||||
for key in path[1:]:
|
||||
if result:
|
||||
result = result.get(key)
|
||||
return result
|
||||
|
||||
|
||||
class KeyPipe(Pipe[dict, Any]):
|
||||
type = "key"
|
||||
|
||||
def __init__(self, keyindex: str | None):
|
||||
self._keyindex = int(keyindex) if keyindex else 0
|
||||
|
||||
def apply(self, value: dict, basepath: Path) -> Any | None:
|
||||
keys = list(value.keys())
|
||||
return keys[self._keyindex]
|
||||
@@ -1,13 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from ..core import Pipe
|
||||
|
||||
__all__ = ("NotPipe",)
|
||||
|
||||
|
||||
class NotPipe(Pipe[Any, bool]):
|
||||
type = "not"
|
||||
|
||||
def apply(self, value: Any, basepath: Path) -> bool:
|
||||
return not value
|
||||
@@ -1,20 +0,0 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from ..core import Pipe
|
||||
|
||||
__all__ = ("SearchPipe",)
|
||||
|
||||
|
||||
class SearchPipe(Pipe[str, str]):
|
||||
type = "search"
|
||||
|
||||
def __init__(self, patter: str, position: str = "1"):
|
||||
self._patter = patter
|
||||
self._position = int(position)
|
||||
|
||||
def apply(self, value: str, basepath: Path) -> str | None:
|
||||
matched = re.compile(self._patter).search(value)
|
||||
if matched:
|
||||
return matched.group(self._position)
|
||||
return None
|
||||
@@ -1,16 +1,36 @@
|
||||
[presets.docker]
|
||||
conditions = ["file(Dockerfile)"]
|
||||
conditions = ["{{ 'Dockerfile' | file }}"]
|
||||
variables.docker_name = "{{ 'pyproject.toml' | read | get('project.name') or 'package.json' | read | get('name') }}"
|
||||
variables.docker_version = "{{ 'pyproject.toml' | read | get('project.version') or 'package.json' | read | get('version') }}"
|
||||
variables.docker_repo = "{{ '.env' | read | get('DOCKER_REPO') }}"
|
||||
variables.docker_group = "{{ '.env' | read | get('DOCKER_GROUP') }}"
|
||||
variables.docker_image = "{{ docker_group }}/{{ docker_name }}"
|
||||
variables.docker_tag = "{{ '.env' | read | get('DOCKER_TAG') or 'latest' }}"
|
||||
|
||||
# build
|
||||
[presets.docker.tasks.build.default]
|
||||
variables.docker_target = "file(.env) | get(DOCKER_PROJECTS)"
|
||||
variables.docker_name = "file(pyproject.toml) | get(project.name)"
|
||||
variables.docker_group = "file(.env) | get(DOCKER_GROUP)"
|
||||
commands = ["docker build -t {{ docker_image }} . -f Dockerfile"]
|
||||
|
||||
# publish version
|
||||
[presets.docker.tasks.publish.version]
|
||||
conditions = ["{{ docker_version }}"]
|
||||
commands = [
|
||||
# "docker build --target {docker_target} -t {docker_group}/{docker_name} . -f Dockerfile",
|
||||
"docker build -t {docker_group}/{docker_name} . -f Dockerfile",
|
||||
"docker tag {{ docker_image }} {{ docker_repo }}/{{ docker_image }}:{{ docker_version }}",
|
||||
"docker push {{ docker_repo }}/{{ docker_image }}:{{ docker_version }}",
|
||||
]
|
||||
|
||||
# publish
|
||||
[presets.docker.tasks.publish.default]
|
||||
commands = ["docker publish"]
|
||||
# publish tag
|
||||
[presets.docker.tasks.publish.tag]
|
||||
conditions = ["{{ docker_tag }}"]
|
||||
commands = [
|
||||
"docker tag {{ docker_image }} {{ docker_repo }}/{{ docker_image }}:{{ docker_tag }}",
|
||||
"docker push {{ docker_repo }}/{{ docker_image }}:{{ docker_tag }}",
|
||||
]
|
||||
|
||||
# save
|
||||
[presets.docker.tasks.save.default]
|
||||
variables.docker_output = "./dist"
|
||||
commands = [
|
||||
"mkdir -p {{ docker_output }}",
|
||||
"docker save --output {{ docker_output }}/{{ docker_name }}-{{ docker_version }}.tar {{ docker_image }}",
|
||||
]
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
[presets.nodejs]
|
||||
conditions = ["file(package.json)"]
|
||||
conditions = ["{{ 'package.json' | file }}"]
|
||||
|
||||
[presets.nodejs.tasks.setup.npm]
|
||||
conditions = ["file(package-lock.json)"]
|
||||
conditions = ["{{ 'package-lock.json' | file }}"]
|
||||
commands = ["npm install-clean"]
|
||||
|
||||
[presets.nodejs.tasks.setup.npm-legacy]
|
||||
conditions = ["file(package-lock.json) | not"]
|
||||
conditions = ["{{ not ('package-lock.json' | file) }}"]
|
||||
commands = ["npm install"]
|
||||
|
||||
[presets.nodejs.tasks.test.jest]
|
||||
conditions = ["file(node_modules/jest)"]
|
||||
conditions = ["{{ 'node_modules/jest' | file }}"]
|
||||
commands = ["npx jest"]
|
||||
|
||||
[presets.nodejs.tasks.build.npm]
|
||||
conditions = ["file(package.json) | get(scripts.build)"]
|
||||
conditions = ["{{ 'package.json' | read | get('scripts.build') }}"]
|
||||
commands = ["npm run build"]
|
||||
|
||||
[presets.nodejs.tasks.publish.npm]
|
||||
conditions = ["file(package.json) | get(scripts.publish)"]
|
||||
conditions = ["{{ 'package.json' | read | get('scripts.publish') }}"]
|
||||
commands = ["npm run publish"]
|
||||
|
||||
[presets.nodejs.tasks.clean.node_modules]
|
||||
conditions = ["file(node_modules)"]
|
||||
conditions = ["{{ 'node_modules' | file }}"]
|
||||
commands = ["rm -rf node_modules"]
|
||||
|
||||
@@ -1,61 +1,55 @@
|
||||
[presets.python]
|
||||
conditions = ["file(pyproject.toml)"]
|
||||
conditions = ["{{ 'pyproject.toml' | file }}"]
|
||||
variables.python_poetry = "{{ 'pyproject.toml' | read | get('build-system.build-backend') == 'poetry.core.masonry.api' }}"
|
||||
|
||||
# setup
|
||||
[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(project.requires-python) | search((\\d+.\\d+))"
|
||||
commands = [
|
||||
"poetry env use {python_version}",
|
||||
"poetry install",
|
||||
]
|
||||
conditions = ["{{ python_poetry }}"]
|
||||
variables.python_version = "{{ 'pyproject.toml' | read | get('project.requires-python') | search('(\\d+.\\d+)') }}"
|
||||
commands = ["poetry env use {{ python_version }}", "poetry install"]
|
||||
|
||||
# build
|
||||
[presets.python.tasks.build.poetry]
|
||||
conditions = [
|
||||
"file(pyproject.toml) | get(build-system.build-backend) | equal(poetry.core.masonry.api)",
|
||||
]
|
||||
conditions = ["{{ python_poetry }}"]
|
||||
commands = ["poetry build"]
|
||||
|
||||
# publish
|
||||
[presets.python.tasks.publish.poetry]
|
||||
conditions = [
|
||||
"file(pyproject.toml) | get(build-system.build-backend) | equal(poetry.core.masonry.api)",
|
||||
"file(poetry.toml) | get(repositories)",
|
||||
"{{ python_poetry }}",
|
||||
"{{ 'poetry.toml' | read | get('repositories') }}",
|
||||
]
|
||||
variables.python_repository = "file(poetry.toml) | get(repositories) | key(0)"
|
||||
commands = ["poetry publish -r {python_repository}"]
|
||||
variables.python_repository = "{{ 'poetry.toml' | read | get('repositories') | key(0) }}"
|
||||
commands = ["poetry publish -r {{ python_repository }}"]
|
||||
|
||||
# test
|
||||
[presets.python.tasks.test.pytest]
|
||||
conditions = ["file(.venv/bin/pytest)"]
|
||||
conditions = ["{{ '.venv/bin/pytest' | file }}"]
|
||||
commands = [".venv/bin/python -m pytest"]
|
||||
|
||||
# lint
|
||||
[presets.python.tasks.lint.pylint]
|
||||
conditions = ["file(.venv/bin/pylint)"]
|
||||
conditions = ["{{ '.venv/bin/pylint' | file }}"]
|
||||
commands = [".venv/bin/pylint ./"]
|
||||
|
||||
[presets.python.tasks.lint.isort]
|
||||
conditions = ["file(.venv/bin/isort)"]
|
||||
conditions = ["{{ '.venv/bin/isort' | file }}"]
|
||||
commands = [".venv/bin/isort --profile black --check-only ./"]
|
||||
|
||||
[presets.python.tasks.lint.black]
|
||||
conditions = ["file(.venv/bin/black)"]
|
||||
conditions = ["{{ '.venv/bin/black' | file }}"]
|
||||
commands = [".venv/bin/black --check --diff ./"]
|
||||
|
||||
# format
|
||||
[presets.python.tasks.format.isort]
|
||||
conditions = ["file(.venv/bin/isort)"]
|
||||
conditions = ["{{ '.venv/bin/isort' | file }}"]
|
||||
commands = [".venv/bin/isort --profile black ./"]
|
||||
|
||||
[presets.python.tasks.format.black]
|
||||
conditions = ["file(.venv/bin/black)"]
|
||||
conditions = ["{{ '.venv/bin/isort' | file }}"]
|
||||
commands = [".venv/bin/black ./"]
|
||||
|
||||
# clean
|
||||
[presets.python.tasks.clean.venv]
|
||||
conditions = ["file(.venv)"]
|
||||
conditions = ["{{ '.venv' | file }}"]
|
||||
commands = ["rm -rf .venv"]
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import json
|
||||
import re
|
||||
import tomllib
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import dotenv
|
||||
from jinja2 import Environment
|
||||
|
||||
from ..entry import Entry, EntryDetector, TaskKey
|
||||
from .loader import PresetLoader, PresetLoaderConfig
|
||||
from .model import Variables
|
||||
|
||||
|
||||
def _build_env(basepath: Path) -> Environment:
|
||||
env = Environment()
|
||||
|
||||
def do_file(filename: str) -> Path | None:
|
||||
result = Path(filename)
|
||||
if not result.is_absolute():
|
||||
result = basepath / result
|
||||
if result.exists():
|
||||
return result
|
||||
return None
|
||||
|
||||
def do_read(filename: str) -> dict | None:
|
||||
path = do_file(filename)
|
||||
if path:
|
||||
if path.suffix == ".toml":
|
||||
return tomllib.loads(path.read_text())
|
||||
if path.suffix == ".json":
|
||||
return json.loads(path.read_text())
|
||||
if path.suffix == ".env" or path.name == ".env":
|
||||
return dotenv.dotenv_values(path)
|
||||
return None
|
||||
|
||||
def do_get(data: dict | None, keypath: str) -> str | None:
|
||||
if not data:
|
||||
return None
|
||||
path = keypath.split(".")
|
||||
result = data.get(path[0])
|
||||
for key in path[1:]:
|
||||
if result:
|
||||
result = result.get(key)
|
||||
return result
|
||||
|
||||
def do_search(data: str | None, pattern: str, position: int = 1) -> str | None:
|
||||
if not data:
|
||||
return None
|
||||
matched = re.compile(pattern).search(data)
|
||||
if matched:
|
||||
return matched.group(position)
|
||||
return None
|
||||
|
||||
def do_key(data: dict | None, keyindex: int = 0) -> str | None:
|
||||
if not data:
|
||||
return None
|
||||
keys = list(data.keys())
|
||||
return keys[keyindex]
|
||||
|
||||
env.filters["file"] = do_file
|
||||
env.filters["read"] = do_read
|
||||
env.filters["get"] = do_get
|
||||
env.filters["search"] = do_search
|
||||
env.filters["key"] = do_key
|
||||
return env
|
||||
|
||||
|
||||
class PathProcessor:
|
||||
def __init__(self, basepath: Path):
|
||||
self._basepath = basepath
|
||||
self._env = _build_env(basepath)
|
||||
|
||||
def update_variables(self, templates: Variables, variables: Variables):
|
||||
for k, v in templates.items():
|
||||
variables[k] = self._env.from_string(v).render(**variables)
|
||||
|
||||
def fill_template(self, template: str, variables: Variables) -> str:
|
||||
return self._env.from_string(template).render(**variables)
|
||||
|
||||
def check_conditions(self, templates: list[str], variables: Variables) -> bool:
|
||||
for template in templates:
|
||||
value = self.fill_template(template, variables)
|
||||
if value.lower() in ["false", "null", "none", "0", ""]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class PresetEntryDetector(EntryDetector):
|
||||
|
||||
def __init__(self, preset_loader_config: PresetLoaderConfig | None = None):
|
||||
self._preset_loader_config = preset_loader_config
|
||||
|
||||
def detect_entry(self, basepath: Path) -> Entry | None:
|
||||
processor = PathProcessor(basepath)
|
||||
preset = PresetLoader(basepath, self._preset_loader_config).load()
|
||||
if preset.skip:
|
||||
return None
|
||||
tasks = defaultdict(list)
|
||||
for tag, preset in preset.presets.items():
|
||||
preset_variables: Variables = {"entry_path": basepath}
|
||||
if preset.variables:
|
||||
processor.update_variables(preset.variables, preset_variables)
|
||||
if preset.conditions and not processor.check_conditions(
|
||||
preset.conditions, preset_variables
|
||||
):
|
||||
continue
|
||||
for task_name, task_pack in preset.tasks.items():
|
||||
for task in task_pack.values():
|
||||
if not task.conditions or processor.check_conditions(
|
||||
task.conditions, preset_variables
|
||||
):
|
||||
variables = preset_variables.copy()
|
||||
if task.variables:
|
||||
processor.update_variables(task.variables, variables)
|
||||
tasks[TaskKey(task_name, tag)] += [
|
||||
processor.fill_template(command, variables)
|
||||
for command in task.commands
|
||||
]
|
||||
if tasks:
|
||||
return Entry(tasks, basepath)
|
||||
return None
|
||||
@@ -1,45 +0,0 @@
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from ..entry import Entry, EntryDetector, TaskKey
|
||||
from ..pipe.core import PipeParser, PipeRunner
|
||||
from .resolve import PresetResolver, PresetResolverConfig
|
||||
|
||||
__all__ = ("PresetEntryDetector",)
|
||||
|
||||
|
||||
class PresetEntryDetector(EntryDetector):
|
||||
_PIPE_PARSER = PipeParser()
|
||||
|
||||
def __init__(self, preset_resolver_config: PresetResolverConfig | None = None):
|
||||
self._pipeparser = PipeParser()
|
||||
self._preset_resolver_config = preset_resolver_config
|
||||
|
||||
def detect_entry(self, basepath: Path) -> Entry | None:
|
||||
preset = PresetResolver(basepath, self._preset_resolver_config).resolve_preset()
|
||||
if preset.skip:
|
||||
return None
|
||||
piperunner = PipeRunner(basepath)
|
||||
tasks = defaultdict(list)
|
||||
for tag, preset in preset.presets.items():
|
||||
if not piperunner.check_conditions(preset.conditions):
|
||||
continue
|
||||
for task_name, task_pack in preset.tasks.items():
|
||||
for task in task_pack.values():
|
||||
if piperunner.check_conditions(task.conditions):
|
||||
variables = {
|
||||
"entry_path": basepath,
|
||||
}
|
||||
if task.variables:
|
||||
variables.update(
|
||||
{
|
||||
k: piperunner.resolve_value(v)
|
||||
for k, v in task.variables.items()
|
||||
}
|
||||
)
|
||||
tasks[TaskKey(task_name, tag)] += [
|
||||
command.format(**variables) for command in task.commands
|
||||
]
|
||||
if tasks:
|
||||
return Entry(tasks, basepath)
|
||||
return None
|
||||
@@ -1,33 +1,27 @@
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
import tomli
|
||||
|
||||
from ..util import merge_dicts
|
||||
from .model import Preset
|
||||
|
||||
__all__ = (
|
||||
"PresetResolverConfig",
|
||||
"PresetResolver",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetResolverConfig:
|
||||
class PresetLoaderConfig:
|
||||
default: bool = True
|
||||
user: bool = True
|
||||
project: bool = True
|
||||
|
||||
|
||||
class PresetResolver:
|
||||
class PresetLoader:
|
||||
_source_cache: dict[Path, dict] = {}
|
||||
|
||||
def __init__(self, basepath: Path, config: PresetResolverConfig | None = None):
|
||||
def __init__(self, basepath: Path, config: PresetLoaderConfig | None = None):
|
||||
self._basepath = basepath
|
||||
self._config = config or PresetResolverConfig()
|
||||
self._config = config or PresetLoaderConfig()
|
||||
|
||||
def iter_sources(self) -> Iterable[Path]:
|
||||
def _iter_sources(self) -> Iterable[Path]:
|
||||
if self._config.default:
|
||||
yield from (Path(__file__).parent / "default").iterdir()
|
||||
if self._config.user:
|
||||
@@ -44,11 +38,11 @@ class PresetResolver:
|
||||
break
|
||||
projectpath = projectpath.parent
|
||||
|
||||
def resolve_preset(self) -> Preset:
|
||||
def load(self) -> Preset:
|
||||
result_preset_data = {}
|
||||
for filepath in self.iter_sources():
|
||||
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)
|
||||
self._source_cache[filepath] = tomllib.load(fd)
|
||||
merge_dicts(result_preset_data, self._source_cache[filepath])
|
||||
return Preset(**result_preset_data)
|
||||
@@ -1,20 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
__all__ = (
|
||||
"TaskPreset",
|
||||
"EntryPreset",
|
||||
"Preset",
|
||||
)
|
||||
Variables = dict[str, str]
|
||||
|
||||
|
||||
class TaskPreset(BaseModel):
|
||||
conditions: list[str] | None = None
|
||||
variables: dict[str, str] | None = None
|
||||
variables: Variables | None = None
|
||||
commands: list[str]
|
||||
|
||||
|
||||
class EntryPreset(BaseModel):
|
||||
conditions: list[str] | None = None
|
||||
variables: Variables | None = None
|
||||
tasks: dict[str, dict[str, TaskPreset]]
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
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:
|
||||
|
||||
+12
-10
@@ -5,21 +5,21 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from project_manager.entry import Entry, EntryDetector
|
||||
from project_manager.preset.entry import PresetEntryDetector
|
||||
from project_manager.preset.resolve import PresetResolverConfig
|
||||
from project_manager.preset.detector import PresetEntryDetector
|
||||
from project_manager.preset.loader import PresetLoaderConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def preset_resolver_config():
|
||||
return PresetResolverConfig(
|
||||
def preset_loader_config():
|
||||
return PresetLoaderConfig(
|
||||
user=False,
|
||||
project=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entry_detector(preset_resolver_config: PresetResolverConfig):
|
||||
return PresetEntryDetector(preset_resolver_config)
|
||||
def entry_detector(preset_loader_config: PresetLoaderConfig):
|
||||
return PresetEntryDetector(preset_loader_config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -31,18 +31,20 @@ def entry(entry_detector: EntryDetector, entry_path: Path, tmp_path: Path):
|
||||
@pytest.mark.parametrize("entry_path", [Path(__file__).parent / "python"])
|
||||
def test_python_entry(entry: Entry | None, entry_detector: EntryDetector):
|
||||
assert entry is not None
|
||||
assert {"setup", "build"} == entry._tasks.keys()
|
||||
assert {"setup", "build"} == {k.name for k in entry._tasks.keys()}
|
||||
entry.run_task("setup")
|
||||
entry = entry_detector.detect_entry(entry._basepath)
|
||||
assert entry is not None
|
||||
assert {"setup", "build", "lint", "format", "test", "clean"} == entry._tasks.keys()
|
||||
assert {"setup", "build", "lint", "format", "test", "clean"} == {
|
||||
k.name for k in entry._tasks.keys()
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entry_path", [Path(__file__).parent / "nodejs"])
|
||||
def test_nodejs_entry(entry: Entry | None, entry_detector: EntryDetector):
|
||||
assert entry is not None
|
||||
assert {"setup", "build"} == entry._tasks.keys()
|
||||
assert {"setup", "build"} == {k.name for k in entry._tasks.keys()}
|
||||
entry.run_task("setup")
|
||||
entry = entry_detector.detect_entry(entry._basepath)
|
||||
assert entry is not None
|
||||
assert {"setup", "build", "test", "clean"} == entry._tasks.keys()
|
||||
assert {"setup", "build", "test", "clean"} == {k.name for k in entry._tasks.keys()}
|
||||
|
||||
Reference in New Issue
Block a user