Skip to content

Commit

Permalink
eat(settings): add user settings support with defaults values (fix #…
Browse files Browse the repository at this point in the history
  • Loading branch information
noirbizarre committed Jan 27, 2025
1 parent 57439e5 commit 0ac0f8f
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 16 deletions.
4 changes: 4 additions & 0 deletions copier/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,7 @@ class DirtyLocalWarning(UserWarning, CopierWarning):

class ShallowCloneWarning(UserWarning, CopierWarning):
"""The template repository is a shallow clone."""


class MissingSettingsWarning(UserWarning, CopierWarning):
"""Settings path has been defined but file is missing."""
24 changes: 12 additions & 12 deletions copier/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
YieldTagInFileError,
)
from .jinja_ext import YieldEnvironment, YieldExtension
from .settings import Settings
from .subproject import Subproject
from .template import Task, Template
from .tools import (
Expand All @@ -58,13 +59,7 @@
scantree,
set_git_alternates,
)
from .types import (
MISSING,
AnyByStrDict,
JSONSerializable,
RelativePath,
StrOrPath,
)
from .types import MISSING, AnyByStrDict, JSONSerializable, RelativePath, StrOrPath
from .user_data import DEFAULT_DATA, AnswersMap, Question
from .vcs import get_git

Expand Down Expand Up @@ -192,6 +187,7 @@ class Worker:
answers_file: RelativePath | None = None
vcs_ref: str | None = None
data: AnyByStrDict = field(default_factory=dict)
settings: Settings = field(default_factory=Settings.from_file)
exclude: Sequence[str] = ()
use_prereleases: bool = False
skip_if_exists: Sequence[str] = ()
Expand Down Expand Up @@ -467,6 +463,7 @@ def _ask(self) -> None: # noqa: C901
question = Question(
answers=result,
jinja_env=self.jinja_env,
settings=self.settings,
var_name=var_name,
**details,
)
Expand Down Expand Up @@ -998,11 +995,14 @@ def _apply_update(self) -> None: # noqa: C901
)
subproject_subdir = self.subproject.local_abspath.relative_to(subproject_top)

with TemporaryDirectory(
prefix=f"{__name__}.old_copy.",
) as old_copy, TemporaryDirectory(
prefix=f"{__name__}.new_copy.",
) as new_copy:
with (
TemporaryDirectory(
prefix=f"{__name__}.old_copy.",
) as old_copy,
TemporaryDirectory(
prefix=f"{__name__}.new_copy.",
) as new_copy,
):
# Copy old template into a temporary destination
with replace(
self,
Expand Down
39 changes: 39 additions & 0 deletions copier/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""User settings models and helper functions."""
from __future__ import annotations

import os
import warnings
from pathlib import Path
from typing import Any

import yaml
from platformdirs import user_config_path
from pydantic import BaseModel, Field

from .errors import MissingSettingsWarning

ENV_VAR = "COPIER_SETTINGS_PATH"


class Settings(BaseModel):
"""User settings model."""

defaults: dict[str, Any] = Field(default_factory=dict)

@classmethod
def from_file(cls, settings_path: Path | None = None) -> Settings:
"""Load settings from a file."""
env_path = os.getenv(ENV_VAR)
if settings_path is None:
if env_path:
settings_path = Path(env_path)
else:
settings_path = user_config_path("copier") / "settings.yml"
if settings_path.is_file():
data = yaml.safe_load(settings_path.read_text())
return cls.model_validate(data)
elif env_path:
warnings.warn(
f"Settings file not found at {env_path}", MissingSettingsWarning
)
return cls()
7 changes: 6 additions & 1 deletion copier/user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from pygments.lexers.data import JsonLexer, YamlLexer
from questionary.prompts.common import Choice

from copier.settings import Settings

from .errors import InvalidTypeError, UserMessageError
from .tools import cast_to_bool, cast_to_str, force_str_end
from .types import MISSING, AnyByStrDict, MissingType, OptStrOrPath, StrOrPath
Expand Down Expand Up @@ -178,6 +180,7 @@ class Question:
var_name: str
answers: AnswersMap
jinja_env: SandboxedEnvironment
settings: Settings = field(default_factory=Settings)
choices: Sequence[Any] | dict[Any, Any] | str = field(default_factory=list)
multiselect: bool = False
default: Any = MISSING
Expand Down Expand Up @@ -246,7 +249,9 @@ def get_default(self) -> Any:
except KeyError:
if self.default is MISSING:
return MISSING
result = self.render_value(self.default)
result = self.render_value(
self.settings.defaults.get(self.var_name, self.default)
)
result = self.cast_answer(result)
return result

Expand Down
1 change: 1 addition & 0 deletions docs/reference/settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: copier.settings
39 changes: 39 additions & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Settings

Copier settings are stored in `<CONFIG_ROOT>/settings.yml` where `<CONFIG_ROOT>` is the
standard configuration directory for your platform:

- `$XDG_CONFIG_HOME/copier` (`~/.config/copier ` in most cases) on Linux as defined by
[XDG Base Directory Specifications](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
- `~/Library/Application Support/copier` on macOS as defined by
[Apple File System Basics](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)
- `%USERPROFILE%\AppData\Local\copier` on Windows as defined in
[Known folders](https://docs.microsoft.com/en-us/windows/win32/shell/known-folders)

This location can be overridden by setting the `COPIER_SETTINGS_PATH` environment
variable.

## User defaults

Users may define some reusable default variables in the `defaults` section of the
configuration file.

```yaml title="<CONFIG_ROOT>/settings.yml"
defaults:
user_name: "John Doe"
user_email: [email protected]
```
This user data will replace the default value of fields of the same name.
### Well-known variables
To ensure templates efficiently reuse user-defined variables, we invite template authors
to use the following well-known variables:
| Variable name | Type | Description |
| ------------- | ----- | ---------------------- |
| `user_name` | `str` | User's full name |
| `user_email` | `str` | User's email address |
| `github_user` | `str` | User's GitHub username |
| `gitlab_user` | `str` | User's GitLab username |
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ nav:
- Configuring a template: "configuring.md"
- Generating a project: "generating.md"
- Updating a project: "updating.md"
- Settings: "settings.md"
- Reference:
- cli.py: "reference/cli.md"
- errors.py: "reference/errors.md"
- jinja_ext.py: "reference/jinja_ext.md"
- main.py: "reference/main.md"
- settings.py: "reference/settings.md"
- subproject.py: "reference/subproject.md"
- template.py: "reference/template.md"
- tools.py: "reference/tools.md"
Expand Down
Loading

0 comments on commit 0ac0f8f

Please sign in to comment.