from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, Final, List, Optional, Union
from flexlate.template_data import TemplateData
from pyappconf import AppConfig, BaseConfig, ConfigFormats
from pydantic import BaseModel, Field
from flexlate_dev.dict_merge import merge_dicts_preferring_non_none
from flexlate_dev.exc import (
NoSuchCommandException,
NoSuchDataException,
NoSuchRunConfigurationException,
)
from flexlate_dev.external_command_type import ExternalCLICommandType
from flexlate_dev.ignore import IgnoreSpecification
from flexlate_dev.user_command import UserCommand
from flexlate_dev.user_runner import (
CommandContext,
RunConfiguration,
UserRootRunConfiguration,
UserRunConfiguration,
)
DEFAULT_PROJECT_NAME: Final[str] = "project"
SCHEMA_URL: Final[
str
] = "https://nickderobertis.github.io/flexlate-dev/_static/config-schema.json"
[docs]class DataConfiguration(BaseModel):
data: TemplateData = Field(
default_factory=dict,
description="Key-value pairs of data to be used in the template",
)
folder_name: Optional[str] = Field(
default=None,
description="Name of the folder to be created for the project, "
"if it is a template type that does not specify a name",
)
ignore: List[str] = Field(
default_factory=list,
description="List of files or folders to ignore when creating the project. "
"Full git wildmatch syntax (like .gitignore) is supported including negations",
)
@property
def use_folder_name(self) -> str:
return self.folder_name or DEFAULT_PROJECT_NAME
[docs]class UserDataConfiguration(DataConfiguration):
extends: Optional[str] = Field(
default=None, description="Name of the data configuration to extend"
)
[docs]@dataclass
class FullRunConfiguration:
config: RunConfiguration
data: Optional[DataConfiguration]
_ignore_spec: IgnoreSpecification = field(init=False)
def __post_init__(self):
self._ignore_spec = IgnoreSpecification(ignore_list=self._use_ignore)
[docs] def to_jinja_data(self, context: CommandContext) -> Dict[str, Any]:
return dict(
config=self.config.dict(),
data=self.data.dict() if self.data else {},
context=context.dict(),
)
@property
def _use_ignore(self) -> List[str]:
return self.data.ignore if self.data is not None else []
[docs] def ignore_matches(self, to_match: Union[str, Path]) -> bool:
return self._ignore_spec.file_is_ignored(to_match)
[docs]def create_default_run_configs() -> Dict[str, UserRootRunConfiguration]:
default_publish = UserRootRunConfiguration(
pre_check=[
'if [ -n "$(find . -prune -empty 2>/dev/null)" ]; '
"then gh repo clone {{ data.folder_name }} .; "
"else git pull origin master; "
"fi"
],
post_init=[
"gh repo create --public --source=.",
"git push origin master",
"git push --all origin",
],
post_update=["fxt merge", "git push --all origin"],
)
return dict(
default=UserRootRunConfiguration(publish=default_publish),
)
[docs]class FlexlateDevConfig(BaseConfig):
"""
Flexlate Dev configuration.
"""
data: Dict[str, UserDataConfiguration] = Field(
default_factory=dict, description="Data configurations by name"
)
commands: List[UserCommand] = Field(
default_factory=list,
description="Commands that can be used across multiple configurations",
)
run_configs: Dict[str, UserRootRunConfiguration] = Field(
default_factory=create_default_run_configs,
description="Root run configurations by name",
)
_settings = AppConfig(
app_name="flexlate-dev",
default_format=ConfigFormats.YAML,
config_name="flexlate-dev",
schema_url=SCHEMA_URL,
)
[docs] def save(self, serializer_kwargs: Optional[Dict[str, Any]] = None, **kwargs):
all_kwargs = dict(exclude_none=True, **kwargs)
return super().save(serializer_kwargs=serializer_kwargs, **all_kwargs)
[docs] def get_full_run_config(
self, command: ExternalCLICommandType, name: Optional[str] = None
) -> FullRunConfiguration:
user_run_config = self.get_run_config(command, name)
if user_run_config.data_name is None:
data_config = self.get_default_data()
else:
data_config = self.get_data_config(user_run_config.data_name)
return FullRunConfiguration(config=user_run_config, data=data_config)
[docs] def get_run_config(
self, command: ExternalCLICommandType, name: Optional[str] = None
) -> UserRunConfiguration:
name = name or "default"
user_root_run_config = self.run_configs.get(name)
if not user_root_run_config:
raise NoSuchRunConfigurationException(name)
user_run_config = user_root_run_config.get_run_config(command)
if not user_run_config.extends:
# No extends, so return the config as-is
return user_run_config
# Create a new config by extending the referenced config
extends_config = self.get_run_config(command, user_run_config.extends)
return UserRunConfiguration(
**merge_dicts_preferring_non_none(
extends_config.dict(), user_run_config.dict()
)
)
[docs] def get_run_config_names(self, always_include_default: bool = False) -> List[str]:
names = list(self.run_configs.keys())
if not always_include_default and len(names) > 1:
names.remove("default")
return names
[docs] def get_default_data(self) -> Optional[DataConfiguration]:
try:
return self.get_data_config("default")
except NoSuchDataException:
return None
[docs] def get_data_config(self, name: str) -> DataConfiguration:
user_data_config = self.data.get(name)
if not user_data_config:
raise NoSuchDataException(name)
if not user_data_config.extends:
# No extends, so return the config as-is
return user_data_config
# Create a new config by extending the referenced config
extends_config = self.get_data_config(user_data_config.extends)
extended_data = {**extends_config.data, **user_data_config.data}
folder_name = user_data_config.folder_name or extends_config.folder_name
if extends_config.ignore is None:
extended_ignore = user_data_config.ignore
elif user_data_config.ignore is None:
extended_ignore = extends_config.ignore
else:
# Both specified ignores, extend the list
extended_ignore = [*extends_config.ignore, *user_data_config.ignore]
return DataConfiguration(
data=extended_data, folder_name=folder_name, ignore=extended_ignore
)
[docs] def save_data_for_run_config(
self, run_config: FullRunConfiguration, data: TemplateData
):
data_name = run_config.config.data_name or "default"
data_config = self.data.get(data_name) or UserDataConfiguration()
new_data_config = data_config.copy(update=dict(data=data))
run_config.config.data_name = data_name # set to default if was previously None
self.data[data_name] = new_data_config
self.save()
[docs] def get_global_command_by_id(self, id: str) -> UserCommand:
for command in self.commands:
if command.id == id:
return command
raise NoSuchCommandException(id)
[docs]def load_config(config_path: Optional[Path]) -> FlexlateDevConfig:
if config_path is None:
for possible_name in ["flexlate-dev.yaml", "flexlate-dev.yml"]:
path = Path(possible_name)
if path.exists():
return FlexlateDevConfig.load(path)
elif config_path.exists():
return FlexlateDevConfig.load(config_path)
else:
# Passed config path, but does not exist, might be trying to save new config
return FlexlateDevConfig.load_or_create(config_path.resolve())
# Could not find any config, create a blank one at the default location
return FlexlateDevConfig.load_or_create(Path("flexlate-dev.yaml").resolve())