diff --git a/copier/errors.py b/copier/errors.py index d74bd41fd..f1bbf8a86 100644 --- a/copier/errors.py +++ b/copier/errors.py @@ -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.""" diff --git a/copier/main.py b/copier/main.py index f2b1db195..426314a84 100644 --- a/copier/main.py +++ b/copier/main.py @@ -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 ( @@ -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 @@ -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] = () @@ -245,7 +241,7 @@ def _cleanup(self) -> None: def _check_unsafe(self, mode: Literal["copy", "update"]) -> None: """Check whether a template uses unsafe features.""" - if self.unsafe: + if self.unsafe or self.settings.is_trusted(self.template.url): return features: set[str] = set() if self.template.jinja_extensions: @@ -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, ) @@ -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, diff --git a/copier/settings.py b/copier/settings.py new file mode 100644 index 000000000..f17191db7 --- /dev/null +++ b/copier/settings.py @@ -0,0 +1,61 @@ +"""User setttings models and helper functions.""" + +from __future__ import annotations + +import os +import warnings +from os.path import expanduser +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, description="Default values for questions" + ) + trust: set[str] = Field( + default_factory=set, description="List of trusted repositories or prefixes" + ) + + @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() + + def is_trusted(self, repository: str) -> bool: + """Check if a repository is trusted.""" + return any( + repository.startswith(self.normalize(trusted)) + if trusted.endswith("/") + else repository == self.normalize(trusted) + for trusted in self.trust + ) + + def normalize(self, url: str) -> str: + """Normalize an URL using user settings.""" + if url.startswith("~"): # Only expand on str to avoid messing with URLs + url = expanduser(url) + return url diff --git a/copier/user_data.py b/copier/user_data.py index 6672128e0..3cb870a10 100644 --- a/copier/user_data.py +++ b/copier/user_data.py @@ -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 @@ -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 @@ -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 diff --git a/docs/configuring.md b/docs/configuring.md index 2da6f4776..c79d2a55b 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -1588,6 +1588,10 @@ switch `--UNSAFE` or `--trust`. Not supported in `copier.yml`. +!!! tip + + See the [`trust` setting][trusted-locations] to mark some repositories as always trusted. + ### `use_prereleases` - Format: `bool` diff --git a/docs/reference/settings.md b/docs/reference/settings.md new file mode 100644 index 000000000..2bdf0d303 --- /dev/null +++ b/docs/reference/settings.md @@ -0,0 +1 @@ +::: copier.settings diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 000000000..d559d2d4b --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,56 @@ +# Settings + +Copier settings are stored in `/settings.yml` where `` 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="/settings.yml" +defaults: + user_name: "John Doe" + user_email: john.doe@acme.com +``` + +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 | + +## Trusted locations + +Users may define trusted locations in the `trust` setting. It should be a list of Copier +template repositories, or repositories prefix. + +```yaml +trust: + - https://github.com/your_account/your_template.git + - https://github.com/your_account/ + - ~/templates/ +``` + +!!! warning "Security considerations" + + Locations ending with `/` will be matched as prefixes, trusting all templates starting with that path. + Locations not ending with `/` will be matched exactly. diff --git a/mkdocs.yml b/mkdocs.yml index 4eeed4709..2d3761ccc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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" diff --git a/poetry.lock b/poetry.lock index 10afb5145..a562f2e70 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -17,6 +18,7 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, @@ -31,6 +33,7 @@ version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, @@ -42,6 +45,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -53,6 +57,7 @@ version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["docs"] files = [ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, @@ -167,6 +172,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -181,6 +187,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "docs"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -192,6 +199,7 @@ version = "7.6.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, @@ -269,6 +277,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -280,6 +289,7 @@ version = "1.23.0" description = "Dynamic version generation" optional = false python-versions = ">=3.5" +groups = ["main", "build"] files = [ {file = "dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041"}, {file = "dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4"}, @@ -294,6 +304,8 @@ version = "0.2.2" description = "Like `typing._eval_type`, but lets older Python versions use newer typing features." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.10\"" files = [ {file = "eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a"}, {file = "eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1"}, @@ -308,6 +320,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -322,6 +336,7 @@ version = "2.1.1" description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, @@ -336,6 +351,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -352,6 +368,7 @@ version = "2.0" description = "A fancy and practical functional tools" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0"}, {file = "funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb"}, @@ -363,6 +380,7 @@ version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, @@ -380,6 +398,7 @@ version = "1.5.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "griffe-1.5.1-py3-none-any.whl", hash = "sha256:ad6a7980f8c424c9102160aafa3bcdf799df0e75f7829d75af9ee5aef656f860"}, {file = "griffe-1.5.1.tar.gz", hash = "sha256:72964f93e08c553257706d6cd2c42d1c172213feb48b2be386f243380b405d4b"}, @@ -394,6 +413,7 @@ version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, @@ -408,6 +428,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -422,6 +443,8 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["docs"] +markers = "python_version < \"3.10\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -445,6 +468,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -456,6 +480,7 @@ version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main", "build", "docs"] files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, @@ -473,6 +498,7 @@ version = "1.3.2" description = "A port of Ansible's jinja2 filters without requiring ansible core." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b"}, {file = "jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34"}, @@ -491,6 +517,7 @@ version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, @@ -509,6 +536,7 @@ version = "1.10.0" description = "Utilities to execute code blocks in Markdown files." optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "markdown_exec-1.10.0-py3-none-any.whl", hash = "sha256:dea4e8b78a3fe7d8e664088ebaccbd4de51b65c45b9e0db9509a9bb4fce33192"}, {file = "markdown_exec-1.10.0.tar.gz", hash = "sha256:d1fa017995ef337ec59e7ce49fbf3e051145a62c3124ae687c17e987f1392cd0"}, @@ -526,6 +554,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main", "build", "docs"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -596,6 +625,7 @@ version = "1.3.4" description = "A deep merge function for 🐍." optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, @@ -607,6 +637,7 @@ version = "1.6.1" description = "Project documentation with Markdown." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, @@ -638,6 +669,7 @@ version = "1.2.0" description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"}, {file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"}, @@ -654,6 +686,7 @@ version = "0.2.0" description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, @@ -671,6 +704,7 @@ version = "9.5.50" description = "Documentation that simply works" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_material-9.5.50-py3-none-any.whl", hash = "sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385"}, {file = "mkdocs_material-9.5.50.tar.gz", hash = "sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825"}, @@ -700,6 +734,7 @@ version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, @@ -711,6 +746,7 @@ version = "0.27.0" description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"}, {file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"}, @@ -740,6 +776,7 @@ version = "1.12.2" description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a"}, {file = "mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3"}, @@ -756,6 +793,7 @@ version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -815,6 +853,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -826,6 +865,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -837,6 +877,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "build", "dev", "docs"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -848,6 +889,7 @@ version = "0.5.7" description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, @@ -863,6 +905,7 @@ version = "0.2.1" description = "Bring colors to your terminal." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, @@ -874,6 +917,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -885,6 +929,7 @@ version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, @@ -899,6 +944,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -915,6 +961,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -930,6 +977,7 @@ version = "1.9.0" description = "Plumbum: shell combinators library" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5"}, {file = "plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219"}, @@ -950,6 +998,7 @@ version = "0.32.1" description = "A task runner that works well with poetry." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "poethepoet-0.32.1-py3-none-any.whl", hash = "sha256:d1e0a52a2f677870fac17dfb26bfe4910242756ac821443ef31f90ad26227c2d"}, {file = "poethepoet-0.32.1.tar.gz", hash = "sha256:471e1a025812dcd3d2997e30989681be5ab0a49232ee5fba94859629671c9584"}, @@ -969,6 +1018,8 @@ version = "1.7.0" description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" optional = false python-versions = "<4.0,>=3.7" +groups = ["build"] +markers = "python_version < \"4\"" files = [ {file = "poetry_dynamic_versioning-1.7.0-py3-none-any.whl", hash = "sha256:90db889a43a6da3af43d9609908bd1940c80379509cf92511c9468420a4f1d2f"}, {file = "poetry_dynamic_versioning-1.7.0.tar.gz", hash = "sha256:fe9f65e1b408d8b99ea28e5b1edde8f06ca5ff58026aa5d7376ec74f1342a104"}, @@ -988,6 +1039,7 @@ version = "4.1.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, @@ -1006,6 +1058,7 @@ version = "3.0.36" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.6.2" +groups = ["main"] files = [ {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, @@ -1020,6 +1073,7 @@ version = "0.7.0" description = "Run a subprocess in a pseudo terminal" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -1031,6 +1085,7 @@ version = "2.10.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"}, {file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"}, @@ -1051,6 +1106,7 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -1163,6 +1219,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1177,6 +1234,7 @@ version = "10.11.2" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "pymdown_extensions-10.11.2-py3-none-any.whl", hash = "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf"}, {file = "pymdown_extensions-10.11.2.tar.gz", hash = "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049"}, @@ -1195,6 +1253,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1217,6 +1276,7 @@ version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, @@ -1235,6 +1295,7 @@ version = "0.7.0" description = "Provide a Git config sandbox for testing" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "pytest_gitconfig-0.7.0-py3-none-any.whl", hash = "sha256:7be768b98399817262aff65a1a695a4a441c889e6bd260643ea7beb46619c9d7"}, {file = "pytest_gitconfig-0.7.0.tar.gz", hash = "sha256:7d8a49747c09da0416704e911d4eccecbae11a28f997cdeba77aab9ab4975b1f"}, @@ -1249,6 +1310,7 @@ version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, @@ -1269,6 +1331,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["docs"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1283,6 +1346,8 @@ version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\"" files = [ {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, @@ -1310,6 +1375,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1372,6 +1438,7 @@ version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, @@ -1386,6 +1453,7 @@ version = "2.1.0" description = "Python library to build pretty command line user prompts ⭐️" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec"}, {file = "questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587"}, @@ -1400,6 +1468,7 @@ version = "2024.9.11" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, @@ -1503,6 +1572,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1524,6 +1594,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["docs"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1535,6 +1606,8 @@ version = "2.0.2" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, @@ -1546,6 +1619,8 @@ version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["build"] +markers = "python_version < \"4\"" files = [ {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, @@ -1557,6 +1632,7 @@ version = "0.1.3" description = "Typing stubs for backports" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "types-backports-0.1.3.tar.gz", hash = "sha256:f4b7206c073df88d6200891e3d27506185fd60cda66fb289737b2fa92c0010cf"}, {file = "types_backports-0.1.3-py2.py3-none-any.whl", hash = "sha256:dafcd61848081503e738a7768872d1dd6c018401b4d2a1cfb608ea87ec9864b9"}, @@ -1568,6 +1644,7 @@ version = "0.4.15.20240311" description = "Typing stubs for colorama" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-colorama-0.4.15.20240311.tar.gz", hash = "sha256:a28e7f98d17d2b14fb9565d32388e419f4108f557a7d939a66319969b2b99c7a"}, {file = "types_colorama-0.4.15.20240311-py3-none-any.whl", hash = "sha256:6391de60ddc0db3f147e31ecb230006a6823e81e380862ffca1e4695c13a0b8e"}, @@ -1579,6 +1656,7 @@ version = "0.21.0.20241005" description = "Typing stubs for docutils" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-docutils-0.21.0.20241005.tar.gz", hash = "sha256:48f804a2b50da3a1b1681c4ca1b6184416a6e4129e302d15c44e9d97c59b3365"}, {file = "types_docutils-0.21.0.20241005-py3-none-any.whl", hash = "sha256:4d9021422f2f3fca8b0726fb8949395f66a06c0d951479eb3b1387d75b134430"}, @@ -1590,6 +1668,7 @@ version = "6.1.0.20241221" description = "Typing stubs for psutil" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types_psutil-6.1.0.20241221-py3-none-any.whl", hash = "sha256:8498dbe13285a9ba7d4b2fa934c569cc380efc74e3dacdb34ae16d2cdf389ec3"}, {file = "types_psutil-6.1.0.20241221.tar.gz", hash = "sha256:600f5a36bd5e0eb8887f0e3f3ff2cf154d90690ad8123c8a707bba4ab94d3185"}, @@ -1601,6 +1680,7 @@ version = "2.19.0.20250107" description = "Typing stubs for Pygments" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types_Pygments-2.19.0.20250107-py3-none-any.whl", hash = "sha256:34a555ed327f249daed18c6309e6e62770cdb8b9c321029ba7fd852d10b16f10"}, {file = "types_pygments-2.19.0.20250107.tar.gz", hash = "sha256:94de72c7f09b956c518f566e056812c698272a7a03a9cd81f0065576c6bd3219"}, @@ -1616,6 +1696,7 @@ version = "6.0.12.20241230" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, @@ -1627,6 +1708,7 @@ version = "75.2.0.20241025" description = "Typing stubs for setuptools" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-setuptools-75.2.0.20241025.tar.gz", hash = "sha256:2949913a518d5285ce00a3b7d88961c80a6e72ffb8f3da0a3f5650ea533bd45e"}, {file = "types_setuptools-75.2.0.20241025-py3-none-any.whl", hash = "sha256:6721ac0f1a620321e2ccd87a9a747c4a383dc381f78d894ce37f2455b45fcf1c"}, @@ -1638,10 +1720,12 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +markers = {docs = "python_version < \"3.10\""} [[package]] name = "urllib3" @@ -1649,6 +1733,7 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, @@ -1666,6 +1751,7 @@ version = "20.27.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"}, {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"}, @@ -1686,6 +1772,7 @@ version = "5.0.3" description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea"}, {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb"}, @@ -1728,6 +1815,7 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -1739,6 +1827,8 @@ version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" +groups = ["docs"] +markers = "python_version < \"3.10\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, @@ -1753,6 +1843,6 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", type = ["pytest-mypy"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.9" -content-hash = "888edb00b45adbcefa12e5e242365976d18e2f7356d522b873e973debbb59517" +content-hash = "3c9bb33fa0a43bd04125dbd879cef233ac4e3582431b3e06817f77bc9da84920" diff --git a/pyproject.toml b/pyproject.toml index b21f28c1c..a082b29ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ pygments = ">=2.7.1" pyyaml = ">=5.3.1" questionary = ">=1.8.1" eval-type-backport = { version = ">=0.1.3,<0.3.0", python = "<3.10" } +platformdirs = ">=4.3.6" [tool.poetry.group.dev] optional = true diff --git a/tests/conftest.py b/tests/conftest.py index c5d5ba834..2d3d3e7b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,9 @@ import platform import sys +from pathlib import Path from typing import Any, Iterator +from unittest.mock import patch import pytest from coverage.tracer import CTracer @@ -75,3 +77,18 @@ def gitconfig(gitconfig: GitConfig) -> Iterator[GitConfig]: """ with local.env(GIT_CONFIG_GLOBAL=str(gitconfig)): yield gitconfig + + +@pytest.fixture +def config_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: + config_path = tmp_path / "config" + monkeypatch.delenv("COPIER_SETTINGS_PATH", raising=False) + with patch("copier.settings.user_config_path", return_value=config_path): + yield config_path + + +@pytest.fixture +def settings_path(config_path: Path) -> Path: + config_path.mkdir() + settings_path = config_path / "settings.yml" + return settings_path diff --git a/tests/test_config.py b/tests/test_config.py index 1c13dd146..03b66b3b5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from pathlib import Path from textwrap import dedent from typing import Any, Callable @@ -91,6 +92,59 @@ def test_read_data( ) +def test_settings_defaults_precedence( + tmp_path_factory: pytest.TempPathFactory, settings_path: Path +) -> None: + src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) + build_file_tree( + { + (src / "copier.yml"): ( + f"""\ + # This is a comment + _envops: {BRACKET_ENVOPS_JSON} + a_string: lorem ipsum + a_number: 12345 + a_boolean: true + a_list: + - one + - two + - three + """ + ), + (src / "user_data.txt.jinja"): ( + """\ + A string: [[ a_string ]] + A number: [[ a_number ]] + A boolean: [[ a_boolean ]] + A list: [[ ", ".join(a_list) ]] + """ + ), + } + ) + settings_path.write_text( + dedent( + """\ + defaults: + a_string: whatever + a_number: 42 + a_boolean: false + a_list: + - one + - two + """ + ) + ) + copier.run_copy(str(src), dst, defaults=True, overwrite=True) + assert (dst / "user_data.txt").read_text() == dedent( + """\ + A string: whatever + A number: 42 + A boolean: False + A list: one, two + """ + ) + + def test_invalid_yaml(capsys: pytest.CaptureFixture[str]) -> None: conf_path = Path("tests", "demo_invalid", "copier.yml") with pytest.raises(InvalidConfigFileError): @@ -445,6 +499,7 @@ def test_user_defaults( "user_defaults_updated", "data_initial", "data_updated", + "settings_defaults", "expected_initial", "expected_updated", ), @@ -460,6 +515,53 @@ def test_user_defaults( }, {}, {}, + {}, + dedent( + """\ + A string: foo + """ + ), + dedent( + """\ + A string: foo + """ + ), + ), + # Settings defaults takes precedence over initial defaults. + # The output should remain unchanged following the update operation. + ( + {}, + {}, + {}, + {}, + { + "a_string": "bar", + }, + dedent( + """\ + A string: bar + """ + ), + dedent( + """\ + A string: bar + """ + ), + ), + # User provided defaults takes precedence over initial defaults and settings defaults. + # The output should remain unchanged following the update operation. + ( + { + "a_string": "foo", + }, + { + "a_string": "foobar", + }, + {}, + {}, + { + "a_string": "bar", + }, dedent( """\ A string: foo @@ -484,6 +586,9 @@ def test_user_defaults( "a_string": "yosemite", }, {}, + { + "a_string": "bar", + }, dedent( """\ A string: yosemite @@ -510,6 +615,9 @@ def test_user_defaults( { "a_string": "red rocks", }, + { + "a_string": "bar", + }, dedent( """\ A string: yosemite @@ -529,8 +637,10 @@ def test_user_defaults_updated( user_defaults_updated: AnyByStrDict, data_initial: AnyByStrDict, data_updated: AnyByStrDict, + settings_defaults: AnyByStrDict, expected_initial: str, expected_updated: str, + settings_path: Path, ) -> None: src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) with local.cwd(src): @@ -557,6 +667,7 @@ def test_user_defaults_updated( } ) git_init() + settings_path.write_text(f"defaults: {json.dumps(settings_defaults)}") copier.run_copy( str(src), diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 000000000..9cef2218d --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from copier.errors import MissingSettingsWarning +from copier.settings import Settings + + +def test_default_settings() -> None: + settings = Settings() + + assert settings.defaults == {} + assert settings.trust == set() + + +def test_settings_from_default_location(settings_path: Path) -> None: + settings_path.write_text("defaults:\n foo: bar") + + settings = Settings.from_file() + + assert settings.defaults == {"foo": "bar"} + + +@pytest.mark.usefixtures("config_path") +def test_settings_from_default_location_dont_exists() -> None: + settings = Settings.from_file() + + assert settings.defaults == {} + + +def test_settings_from_env_location( + settings_path: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + settings_path.write_text("defaults:\n foo: bar") + + settings_from_env_path = tmp_path / "settings.yml" + settings_from_env_path.write_text("defaults:\n foo: baz") + + monkeypatch.setenv("COPIER_SETTINGS_PATH", str(settings_from_env_path)) + + settings = Settings.from_file() + + assert settings.defaults == {"foo": "baz"} + + +def test_settings_from_param( + settings_path: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + settings_path.write_text("defaults:\n foo: bar") + + settings_from_env_path = tmp_path / "settings.yml" + settings_from_env_path.write_text("defaults:\n foo: baz") + + monkeypatch.setenv("COPIER_SETTINGS_PATH", str(settings_from_env_path)) + + file_path = tmp_path / "file.yml" + file_path.write_text("defaults:\n from: file") + + settings = Settings.from_file(file_path) + + assert settings.defaults == {"from": "file"} + + +def test_settings_defined_but_missing( + settings_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("COPIER_SETTINGS_PATH", str(settings_path)) + + with pytest.warns(MissingSettingsWarning): + Settings.from_file() + + +@pytest.mark.parametrize( + ("repository", "trust", "is_trusted"), + [ + ("https://github.com/user/repo.git", set(), False), + ( + "https://github.com/user/repo.git", + {"https://github.com/user/repo.git"}, + True, + ), + ("https://github.com/user/repo", {"https://github.com/user/repo.git"}, False), + ("https://github.com/user/repo.git", {"https://github.com/user/"}, True), + ("https://github.com/user/repo.git", {"https://github.com/user/repo"}, False), + ("https://github.com/user/repo.git", {"https://github.com/user"}, False), + ("https://github.com/user/repo.git", {"https://github.com/"}, True), + ("https://github.com/user/repo.git", {"https://github.com"}, False), + (f"{Path.home()}/template", set(), False), + (f"{Path.home()}/template", {f"{Path.home()}/template"}, True), + (f"{Path.home()}/template", {"~/template"}, True), + (f"{Path.home()}/path/to/template", {"~/path/to/template"}, True), + (f"{Path.home()}/path/to/template", {"~/path/to/"}, True), + (f"{Path.home()}/path/to/template", {"~/path/to"}, False), + ], +) +def test_is_trusted(repository: str, trust: set[str], is_trusted: bool) -> None: + settings = Settings(trust=trust) + + assert settings.is_trusted(repository) == is_trusted diff --git a/tests/test_unsafe.py b/tests/test_unsafe.py index 25ff7f6fe..e194a19a5 100644 --- a/tests/test_unsafe.py +++ b/tests/test_unsafe.py @@ -1,6 +1,7 @@ from __future__ import annotations from contextlib import nullcontext as does_not_raise +from pathlib import Path from typing import ContextManager import pytest @@ -90,20 +91,26 @@ def test_copy( @pytest.mark.parametrize("unsafe", [False, True]) +@pytest.mark.parametrize("trusted_from_settings", [False, True]) def test_copy_cli( tmp_path_factory: pytest.TempPathFactory, capsys: pytest.CaptureFixture[str], unsafe: bool, + trusted_from_settings: bool, + settings_path: Path, ) -> None: src, dst = map(tmp_path_factory.mktemp, ["src", "dst"]) build_file_tree( {(src / "copier.yaml"): yaml.safe_dump({"_tasks": ["touch task.txt"]})} ) + if trusted_from_settings: + settings_path.write_text(f"trust: ['{src}']") + _, retcode = CopierApp.run( ["copier", "copy", *(["--UNSAFE"] if unsafe else []), str(src), str(dst)], exit=False, ) - if unsafe: + if unsafe or trusted_from_settings: assert retcode == 0 else: assert retcode == 4 @@ -322,10 +329,13 @@ def test_update( @pytest.mark.parametrize("unsafe", [False, "--trust", "--UNSAFE"]) +@pytest.mark.parametrize("trusted_from_settings", [False, True]) def test_update_cli( tmp_path_factory: pytest.TempPathFactory, capsys: pytest.CaptureFixture[str], unsafe: bool | str, + trusted_from_settings: bool, + settings_path: Path, ) -> None: src, dst = map(tmp_path_factory.mktemp, ["src", "dst"]) unsafe_args = [unsafe] if unsafe else [] @@ -342,6 +352,9 @@ def test_update_cli( git("commit", "-m1") git("tag", "v1") + if trusted_from_settings: + settings_path.write_text(f"trust: ['{src}']") + _, retcode = CopierApp.run( ["copier", "copy", str(src), str(dst)] + unsafe_args, exit=False, @@ -374,7 +387,7 @@ def test_update_cli( + unsafe_args, exit=False, ) - if unsafe: + if unsafe or trusted_from_settings: assert retcode == 0 else: assert retcode == 4