From fa7bea1ab3a6578410430e531cb739616e119e52 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Sat, 31 Aug 2024 03:00:17 -0400 Subject: [PATCH] =?UTF-8?q?Rewrite=20from=20Python=20=F0=9F=90=8D=20to=20R?= =?UTF-8?q?ust=20=F0=9F=A6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 31 +- .gitignore | 9 +- .rustfmt.toml | 2 + Cargo.lock | 366 +++++++++++ Cargo.toml | 10 + Makefile | 10 - README.md | 25 +- createnv/__init__.py | 9 - createnv/cli.py | 32 - createnv/config.py | 84 --- createnv/echo.py | 0 createnv/generator.py | 50 -- createnv/parser.py | 194 ------ poetry.lock | 877 --------------------------- pyproject.toml | 36 -- setup.cfg | 19 - src/main.rs | 101 +++ src/model.rs | 281 +++++++++ src/parser.rs | 289 +++++++++ tests/.env.sample | 21 - tests/__init__.py | 0 tests/test_auto_config_class.py | 7 - tests/test_block_class.py | 32 - tests/test_cli_module.py | 28 - tests/test_config_class.py | 38 -- tests/test_echo_method.py | 0 tests/test_generator_class.py | 119 ---- tests/test_group_class.py | 69 --- tests/test_line_class.py | 21 - tests/test_parser_class.py | 162 ----- tests/test_parser_error_exception.py | 18 - tests/test_random_config_class.py | 26 - 32 files changed, 1077 insertions(+), 1889 deletions(-) create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 Makefile delete mode 100644 createnv/__init__.py delete mode 100644 createnv/cli.py delete mode 100644 createnv/config.py delete mode 100644 createnv/echo.py delete mode 100644 createnv/generator.py delete mode 100644 createnv/parser.py delete mode 100644 poetry.lock delete mode 100644 pyproject.toml delete mode 100644 setup.cfg create mode 100644 src/main.rs create mode 100644 src/model.rs create mode 100644 src/parser.rs delete mode 100644 tests/.env.sample delete mode 100644 tests/__init__.py delete mode 100644 tests/test_auto_config_class.py delete mode 100644 tests/test_block_class.py delete mode 100644 tests/test_cli_module.py delete mode 100644 tests/test_config_class.py delete mode 100644 tests/test_echo_method.py delete mode 100644 tests/test_generator_class.py delete mode 100644 tests/test_group_class.py delete mode 100644 tests/test_line_class.py delete mode 100644 tests/test_parser_class.py delete mode 100644 tests/test_parser_error_exception.py delete mode 100644 tests/test_random_config_class.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb56edc..1a8141e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,19 +1,22 @@ name: Tests on: [push, pull_request] jobs: - build: + fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Install dependencies - uses: abatilo/actions-poetry@v1.5.0 - with: - python_version: 3.9 - poetry_version: 1.1.12 - args: install - - name: Run tests - uses: abatilo/actions-poetry@v1.5.0 - with: - python_version: 3.9 - poetry_version: 1.1.12 - args: run pytest + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: rustup component add rustfmt + - run: cargo +nightly fmt --check + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo clippy + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test diff --git a/.gitignore b/.gitignore index 1497e66..b7f7b51 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,2 @@ -*.pyc -.coverage .env -.mypy_cache/ -.tox/ -__pycache__/ -createnv.egg-info/ -dist/ -htmlcov/ +target/ diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..6abf97b --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +reorder_imports = true +group_imports = "StdExternalCrate" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..080272d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,366 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "createnv" +version = "0.0.3" +dependencies = [ + "anyhow", + "clap", + "rand", + "regex", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3394497 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "createnv" +version = "0.0.3" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +clap = "4.5.16" +rand = "0.8.5" +regex = "1.10.6" diff --git a/Makefile b/Makefile deleted file mode 100644 index 2bea9bd..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -clean: - @rm -rf .coverage - @rm -rf .mypy_cache - @rm -rf .pytest_cache - @rm -rf .ropeproject - @rm -rf createnv.egg-info - @rm -rf dist - @rm -rf htmlcov - @find . -iname "*.pyc" | xargs rm - @find . -iname "__pycache__" | xargs rm -rf diff --git a/README.md b/README.md index 10a41ee..ac9f662 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,4 @@ -# Createnv - -[![GitHub Actions: Tests](https://github.com/cuducos/createnv/workflows/Tests/badge.svg)]() -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/createnv)](https://pypi.org/project/createnv/) -[![PyPI](https://img.shields.io/pypi/v/createnv)](https://pypi.org/project/createnv/) - +# Createnv [![Tests](https://github.com/cuducos/createnv/actions/workflows/tests.yml/badge.svg)](https://github.com/cuducos/createnv/actions/workflows/tests.yml) A simple CLI to create `.env` files. ## Motivation @@ -22,10 +17,10 @@ You can now experiment by yourself, or try more advanced `.env.sample` such as t ## Install -Createnv requires [Python](https://python.org) 3.7 or newer: +Createnv requires [Rust's `cargo`](https://www.rust-lang.org/tools/install): ```console -$ pip install createnv +$ cargo install --path . ``` ## Usage @@ -38,13 +33,13 @@ $ createnv ### Options -| Option | Default | Description | +| Option | Description | Default | |---|---|---| -| `--target` | `.env` | File to write the result | -| `--source` | `.env.sample` | File to use as a sample | -| `--overwrite` and `--no-overwrite` | `--no-overwrite` | Whether to ask before overwriting files -| `--use-default` or `--no-use-default` | `--no-use-default` | Whether to ask for input on fields that have a default value set | -| `--chars-for-random-string` | All ASCII letters, numbers and a few extra characters (`!@#$%^&*(-_=+)`) | Characters used to create random strings | +| `--target` | File to write the result | `.env` | +| `--source` | File to use as a sample | `.env.sample` | +| `--chars-for-random-string` | Characters used to create random strings | All ASCII letters, numbers and a few extra characters (`!@#$%^&*(-_=+)`) | +| `--overwrite` | Do not ask before overwriting files | | +| `--use-default` | Do not ask for input on fields that have a default value | | ## Format of sample files @@ -106,7 +101,7 @@ Now it's a complete variable with a name (_NAME_), a default value (_Cuducos_), If you want to have a variable with a random value, you can set its default value to `` and Createnv will take care of it. Optionally you can specify how long this variable should be with `:int`. -You can use the [`--chars-for-random-string` option](#options) to specify which characters to be used in the random value. +You can use the `--chars-for-random-string` option to specify which characters to be used in the random value. ##### Example diff --git a/createnv/__init__.py b/createnv/__init__.py deleted file mode 100644 index c5a0a0a..0000000 --- a/createnv/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from functools import partial - -import typer - - -echo = typer.echo -error = partial(typer.secho, fg="red") -success = partial(typer.secho, fg="green") -warning = partial(typer.secho, fg="yellow") diff --git a/createnv/cli.py b/createnv/cli.py deleted file mode 100644 index b6a7fdc..0000000 --- a/createnv/cli.py +++ /dev/null @@ -1,32 +0,0 @@ -from pathlib import Path -from string import ascii_letters, digits - -from typer import run - -from createnv.generator import Generator -from createnv.parser import Parser - - -RANDOM_CHARS = f"{ascii_letters}{digits}!@#$%^&*(-_=+)" - - -def main( - target: str = ".env", - source: str = ".env.sample", - overwrite: bool = False, - use_default: bool = False, - chars_for_random_string: str = RANDOM_CHARS, -): - """Creates a .env file with the environment variables following a sample - .env.sample file. These defaults and other options can be changed. Check - them with the --help option.""" - Generator( - Path(target), - Parser(Path(source), chars_for_random_string), - overwrite, - use_default, - )() - - -def cli(): - run(main) diff --git a/createnv/config.py b/createnv/config.py deleted file mode 100644 index c7e522c..0000000 --- a/createnv/config.py +++ /dev/null @@ -1,84 +0,0 @@ -from dataclasses import dataclass -from random import choice, randint -from typing import Iterable, Mapping, Optional, Union - -from typer import prompt - - -@dataclass -class Config: - name: str - human_name: Optional[str] = None - default: Optional[str] = None - - def __str__(self) -> str: - return self.human_name or self.name - - def __call__(self, use_default: bool = False) -> str: - if self.default and use_default: - return self.default - return prompt(str(self), default=self.default) - - -@dataclass -class RandomConfig: - name: str - allowed_chars: str - human_name: Optional[str] = None - length: Optional[int] = None - - def __str__(self) -> str: - return self.human_name or self.name - - @property - def default(self) -> str: - length = self.length or randint(64, 128) - chars = (choice(self.allowed_chars) for _ in range(length)) - return "".join(chars) - - def __call__(self, use_default: bool = False) -> str: - if use_default: - return self.default - - return prompt(str(self), default=self.default) - - -@dataclass -class AutoConfig: - """Config generated within a Group, using other Config values. The `value` - format method is called used `arguments`, so it expects the curly-braces - syntax with ordered arguments.""" - - name: str - value: str - - def __call__(self, settings: Mapping[str, str]) -> Mapping[str, str]: - value = self.value.format(**settings) - return {self.name: value} - - -@dataclass -class Group: - title: str - configs: Iterable[Union[Config, RandomConfig]] - description: Optional[str] = None - auto_config: Optional[AutoConfig] = None - - def __str__(self): - contents = ("", self.title, f"({self.description})") - return "\n".join(contents if self.description else contents[:-1]) - - def should_echo(self, use_default: bool = False) -> bool: - if not use_default: - return True - - return not all(c.default for c in self.configs) - - def __call__(self, use_default: bool = False) -> Mapping[str, str]: - settings = {c.name: c(use_default) for c in self.configs} - - if self.auto_config: - auto_settings = self.auto_config(settings) - settings.update(auto_settings) - - return settings diff --git a/createnv/echo.py b/createnv/echo.py deleted file mode 100644 index e69de29..0000000 diff --git a/createnv/generator.py b/createnv/generator.py deleted file mode 100644 index 677292e..0000000 --- a/createnv/generator.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable, Iterator - -from typer import confirm - -from createnv import echo, error, success, warning -from createnv.config import Group -from createnv.parser import Parser, ParserError - - -@dataclass -class Generator: - path: Path - parser: Parser - overwrite: bool - use_default: bool - - def can_write_to_path(self) -> bool: - if self.overwrite or not self.path.exists(): - return True - - warning(f"There is an existing {self.path.name} file.") - return confirm("Do you want to overwrite it?") - - def contents(self, settings: Iterable[Group]) -> Iterator[str]: - for group in settings: - if group.should_echo(self.use_default): - echo(str(group)) - - values = group(self.use_default) - yield f"# {group.title}" - if group.description: - yield f"# {group.description}" - yield from (f"{key}={value}" for key, value in values.items()) - yield "" - - def __call__(self) -> None: - try: - settings = self.parser() - except ParserError as parser_error: - error(str(parser_error)) - return - - if not self.can_write_to_path(): - return - - contents = "\n".join(self.contents(settings)) - self.path.write_text(contents) - success(f"{self.path.name} created!") diff --git a/createnv/parser.py b/createnv/parser.py deleted file mode 100644 index b6dfdb0..0000000 --- a/createnv/parser.py +++ /dev/null @@ -1,194 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from re import match, search -from typing import Iterable, Iterator, List, Optional, Union - -from createnv.config import AutoConfig, Config, Group, RandomConfig - - -@dataclass -class Line: - number: int - contents: str - - def cleaned(self) -> str: - return self.contents.strip() - - def is_empty(self) -> bool: - return not bool(self.cleaned()) - - def is_comment(self) -> bool: - return bool(self.cleaned().startswith("#")) - - -@dataclass -class Block: - lines: List[Line] = field(default_factory=list) - - def __add__(self, line: Line) -> Block: - self.lines.append(line) - return self - - def __iter__(self) -> Iterator[Line]: - yield from self.lines - - def is_empty(self) -> bool: - return not bool(self.lines) - - -class ParserError(Exception): - def __init__(self, *args, **kwargs): - if isinstance(args[0], Line): - line, text, *new_args = args - return super().__init__(self.message(line, text), *new_args, **kwargs) - return super().__init__(*args, **kwargs) - - @staticmethod - def message(line: Line, text: str) -> str: - message = ( - "", - f"==> Parsing error at line {line.number}:", - f" {text}", - "", - f" The content of the line {line.number} is:", - f" {line.contents}", - "", - ) - return "\n".join(message) - - -@dataclass -class Parser: - source: Path - chars_for_random_string: str - - TITLE: str = r"^# (?P.+)$" - CONFIG: str = r"^(?P<name>[A-Z_0-9]+)=(?P<value>.+)?" - AUTO_CONFIG_VALUE: str = r"{[A-Z_0-0]+}" - RANDOM_VALUE: str = r"<random(:(?P<length>\d+))?>" - INLINE_COMMENT: str = " # " - - def blocks(self) -> Iterator[Block]: - block = Block() - for values in enumerate(self.source.open(), 1): - line = Line(*values) - if line.is_empty(): - if block.is_empty(): - continue - - yield block - block = Block() - - else: - block += line - - if not block.is_empty(): - yield block - - def parse_title(self, line: Line) -> str: - matches = match(self.TITLE, line.cleaned()) - if not matches: - message = ( - f"This is the first line of a block in {self.source}. A block " - "is a group of lines separated from others by one (or more) " - "empty line(s). The first line of a block is expected to be a " - "title, that is to say, to start with `# `, the remaining " - "text is considered the title of this block. This lines " - "does not match this pattern." - ) - raise ParserError(line, message) - - return matches.group("title") - - def parse_config(self, line: Line) -> Union[Config, AutoConfig, RandomConfig]: - matches = match(self.CONFIG, line.cleaned()) - if not matches: - message = ( - "This line was expected to be a config variable. The format " - "should be a name using capital ASCII letters, digits or " - "underscore, followed by an equal sign. This line does not " - "match this expected pattern." - ) - raise ParserError(line, message) - - name, value, human = matches.group("name"), matches.group("value"), None - if value is not None and self.INLINE_COMMENT in value: - value, human = value.rsplit(self.INLINE_COMMENT, maxsplit=1) - - if search(self.AUTO_CONFIG_VALUE, value or ""): - return AutoConfig(name, value) - - random_match = match(self.RANDOM_VALUE, value or "") - if random_match: - length = random_match.group("length") - return RandomConfig( - name=name, - allowed_chars=self.chars_for_random_string, - human_name=human or None, - length=int(length) if length else None, - ) - - return Config(name, human or None, value or None) - - def parse_description_or_config( - self, line: Line - ) -> Union[str, Config, AutoConfig, RandomConfig]: - method = self.parse_title if line.is_comment() else self.parse_config - try: - result = method(line) - except ParserError: - message = ( - f"This is the second line of a block in {self.source}. A " - "block is a group of lines separated from others by one (or " - "more) empty line(s). The second line of a block is expected " - "to be a description of that block or a config variable. The " - "description line should start `# `, and the remaining text " - "is considered the description of this block. A config " - "variable line should start with a name in uppercase, no " - "spaces, followed by an equal sign. This lines does not match " - "this expected patterns." - ) - raise ParserError(line, message) - - return result - - def parse(self, block: Block) -> Group: - _title, _description_or_config, *_configs = block - - title: str = self.parse_title(_title) - configs: List[Union[Config, RandomConfig]] = [] - description: Optional[str] = None - auto_config: Optional[AutoConfig] = None - - parsed = self.parse_description_or_config(_description_or_config) - if isinstance(parsed, str): - description = parsed - elif isinstance(parsed, AutoConfig): - auto_config = parsed - else: - configs.append(parsed) - - for line in _configs: - parsed = self.parse_config(line) - if isinstance(parsed, (Config, RandomConfig)): - configs.append(parsed) - elif isinstance(parsed, AutoConfig) and not auto_config: - auto_config = parsed - - return Group( - title=title, - configs=configs, - description=description, - auto_config=auto_config, - ) - - def __call__(self) -> Iterable[Group]: - if not self.source.exists(): - raise ParserError(f"{self.source} does not exist.") - - if not self.source.is_file(): - raise ParserError(f"{self.source} is not a file.") - - return tuple(self.parse(block) for block in self.blocks()) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index bbadd01..0000000 --- a/poetry.lock +++ /dev/null @@ -1,877 +0,0 @@ -[[package]] -name = "appnope" -version = "0.1.2" -description = "Disable App Nap on macOS >= 10.9" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "21.4.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] - -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "black" -version = "22.1.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "click" -version = "8.0.4" -description = "Composable command line interface toolkit" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "coverage" -version = "6.3.2" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "filelock" -version = "3.6.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] - -[[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" - -[[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "ipdb" -version = "0.13.9" -description = "IPython-enabled pdb" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -decorator = {version = "*", markers = "python_version > \"3.6\""} -ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""} -toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} - -[[package]] -name = "ipython" -version = "7.32.0" -description = "IPython: Productive Interactive Computing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" -pygments = "*" -traitlets = ">=4.2" - -[package.extras] -all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] -doc = ["Sphinx (>=1.3)"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["notebook", "ipywidgets"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] - -[[package]] -name = "jedi" -version = "0.18.1" -description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -parso = ">=0.8.0,<0.9.0" - -[package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "matplotlib-inline" -version = "0.1.3" -description = "Inline Matplotlib backend for Jupyter" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "more-itertools" -version = "8.12.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "mypy" -version = "0.931" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "parso" -version = "0.8.3" -description = "A Python Parser" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] - -[[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "pexpect" -version = "4.8.0" -description = "Pexpect allows easy control of interactive console applications." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "platformdirs" -version = "2.5.1" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] - -[[package]] -name = "pluggy" -version = "0.13.1" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.28" -description = "Library for building powerful interactive command lines in Python" -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pygments" -version = "2.11.2" -description = "Pygments is a syntax highlighting package written in Python." -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "5.4.3" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = ">=4.0.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" - -[package.extras] -checkqa-mypy = ["mypy (==v0.761)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] - -[[package]] -name = "pytest-black" -version = "0.3.12" -description = "A pytest plugin to enable format checking with black" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -black = {version = "*", markers = "python_version >= \"3.6\""} -pytest = ">=3.5.0" -toml = "*" - -[[package]] -name = "pytest-cov" -version = "2.12.1" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -coverage = ">=5.2.1" -pytest = ">=4.6" -toml = "*" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-flake8" -version = "1.0.7" -description = "pytest plugin to check FLAKE8 requirements" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -flake8 = ">=3.5" -pytest = ">=3.5" - -[[package]] -name = "pytest-mock" -version = "2.0.0" -description = "Thin-wrapper around the mock package for easier use with py.test" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pytest = ">=2.7" - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -name = "pytest-mypy" -version = "0.5.0" -description = "Mypy static type checker plugin for Pytest" -category = "dev" -optional = false -python-versions = "~=3.4" - -[package.dependencies] -filelock = ">=3.0" -mypy = [ - {version = ">=0.500", markers = "python_version >= \"3.5\" and python_version < \"3.8\""}, - {version = ">=0.700", markers = "python_version >= \"3.8\""}, -] -pytest = {version = ">=3.5", markers = "python_version >= \"3.5\""} - -[[package]] -name = "shellingham" -version = "1.4.0" -description = "Tool to Detect Surrounding Shell" -category = "main" -optional = false -python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.6" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "traitlets" -version = "5.1.1" -description = "Traitlets Python configuration system" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "typed-ast" -version = "1.5.2" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "typer" -version = "0.4.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -click = ">=7.1.1,<9.0.0" -colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} -shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} - -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"] -test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)"] - -[[package]] -name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "zipp" -version = "3.7.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "69434ae97f4f0df68c5b00cd11ea54eb775c4299036ddf30fa4e03a6fb255f3c" - -[metadata.files] -appnope = [ - {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, - {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -backcall = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] -black = [ - {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, - {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, - {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, - {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, - {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, - {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, - {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, - {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, - {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, - {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, - {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, - {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, - {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, - {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, - {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, - {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, - {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, - {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, - {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, - {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, - {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, -] -click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, - {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, - {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, - {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, - {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, - {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, - {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, - {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, - {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, - {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, - {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, - {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, - {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, - {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, - {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, - {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, - {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, - {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, - {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, - {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, - {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, - {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, - {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, -] -decorator = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] -filelock = [ - {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, - {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -ipdb = [ - {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, -] -ipython = [ - {file = "ipython-7.32.0-py3-none-any.whl", hash = "sha256:86df2cf291c6c70b5be6a7b608650420e89180c8ec74f376a34e2dc15c3400e7"}, - {file = "ipython-7.32.0.tar.gz", hash = "sha256:468abefc45c15419e3c8e8c0a6a5c115b2127bafa34d7c641b1d443658793909"}, -] -jedi = [ - {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, - {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, -] -matplotlib-inline = [ - {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, - {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -more-itertools = [ - {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, - {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, -] -mypy = [ - {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, - {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, - {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, - {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, - {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, - {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, - {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, - {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, - {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, - {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, - {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, - {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, - {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, - {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, - {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, - {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, - {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, - {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, - {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, - {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -parso = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -pexpect = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, -] -pickleshare = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] -platformdirs = [ - {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, - {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"}, - {file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"}, -] -ptyprocess = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pygments = [ - {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, - {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, -] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, -] -pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, -] -pytest-black = [ - {file = "pytest-black-0.3.12.tar.gz", hash = "sha256:1d339b004f764d6cd0f06e690f6dd748df3d62e6fe1a692d6a5500ac2c5b75a5"}, -] -pytest-cov = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, -] -pytest-flake8 = [ - {file = "pytest-flake8-1.0.7.tar.gz", hash = "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b"}, - {file = "pytest_flake8-1.0.7-py2.py3-none-any.whl", hash = "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1"}, -] -pytest-mock = [ - {file = "pytest-mock-2.0.0.tar.gz", hash = "sha256:b35eb281e93aafed138db25c8772b95d3756108b601947f89af503f8c629413f"}, - {file = "pytest_mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:cb67402d87d5f53c579263d37971a164743dc33c159dfb4fb4a86f37c5552307"}, -] -pytest-mypy = [ - {file = "pytest-mypy-0.5.0.tar.gz", hash = "sha256:14c746bd0db5e36618f2fda0ba61ddeb5dc52129ab3923a70f592f934c8887db"}, - {file = "pytest_mypy-0.5.0-py3-none-any.whl", hash = "sha256:6d47b786e460c5101423fec8462e17ac1b6e9c497b1052e790b2e4850a8b3796"}, -] -shellingham = [ - {file = "shellingham-1.4.0-py2.py3-none-any.whl", hash = "sha256:536b67a0697f2e4af32ab176c00a50ac2899c5a05e0d8e2dadac8e58888283f9"}, - {file = "shellingham-1.4.0.tar.gz", hash = "sha256:4855c2458d6904829bd34c299f11fdeed7cfefbf8a2c522e4caea6cd76b3171e"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -traitlets = [ - {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, - {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, -] -typed-ast = [ - {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, - {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, - {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, - {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, - {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, - {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, - {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, - {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, - {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, - {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, - {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, - {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, - {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, - {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, - {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, - {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, - {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, -] -typer = [ - {file = "typer-0.4.0-py3-none-any.whl", hash = "sha256:d81169725140423d072df464cad1ff25ee154ef381aaf5b8225352ea187ca338"}, - {file = "typer-0.4.0.tar.gz", hash = "sha256:63c3aeab0549750ffe40da79a1b524f60e08a2cbc3126c520ebf2eeaf507f5dd"}, -] -typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, -] diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index a3fedd7..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -[tool.poetry] -name = "createnv" -version = "0.0.2" -description = "CLI to create .env files with environment variables." -authors = ["Eduardo Cuducos <cuducos@users.noreply.github.com>"] -license = "BSD-3-Clause" -readme = "README.md" -repository = "https://github.com/cuducos/createnv" -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Natural Language :: English", - "Topic :: Software Development", - "Topic :: Utilities", -] - -[tool.poetry.scripts] -createnv = "createnv.cli:cli" - -[tool.poetry.dependencies] -python = "^3.7" -typer = {extras = ["all"], version = "^0.4.0"} - -[tool.poetry.dev-dependencies] -ipdb = "^0.13.2" -ipython = "^7.13.0" -pytest = "^5.4.1" -pytest-black = "^0.3.8" -pytest-cov = "^2.8.1" -pytest-flake8 = "^1.0.4" -pytest-mock = "^2.0.0" -pytest-mypy = "^0.5.0" - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0dd8089..0000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[tool:pytest] -addopts = --black --mypy --flake8 --cov=createnv --cov-report term - -[flake8] -max-line-length = 88 - -[testenv] -deps = - pytest - pytest-black - pytest-cov - pytest-mock - pytest-mypy -commands = pytest - -[coverage:report] -exclude_lines = - pragma: no cover - if __name__ == .__main__.: diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6c17abf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,101 @@ +use std::{ + fs::{metadata, File}, + io::{stdin, stdout, Write}, + process::exit, +}; + +use anyhow::Result; +use clap::{Arg, ArgAction, Command}; +use parser::Parser; + +mod model; +mod parser; + +const DEFAULT_ENV_SAMPLE: &str = ".env.sample"; +const DEFAULT_ENV: &str = ".env"; +const DEFAULT_RANDOM_CHARS: &str = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"; + +fn should_write_to(path: &str) -> Result<bool> { + if metadata(path).is_ok() { + print!( + "{} already exists. Do you want to overwrite it? (y/n) ", + path + ); + stdout().flush()?; + let mut input = String::new(); + stdin().read_line(&mut input)?; + let input = input.trim(); + match input.to_lowercase().as_str() { + "y" | "yes" => { + return Ok(true); + // Perform the overwrite operation here + } + "n" | "no" => { + return Ok(false); + } + _ => return should_write_to(path), + } + } + Ok(true) +} + +fn main() -> Result<()> { + let matches = Command::new(env!("CARGO_PKG_NAME")) + .version(env!("CARGO_PKG_VERSION")) + .arg( + clap::Arg::new("target") + .long("target") + .short('t') + .default_value(DEFAULT_ENV) + .help("File to write the result"), + ) + .arg( + Arg::new("source") + .long("source") + .short('s') + .default_value(DEFAULT_ENV_SAMPLE) + .help("File to use as a sample"), + ) + .arg( + Arg::new("overwrite") + .long("overwrite") + .short('o') + .action(ArgAction::SetTrue) + .help("Overwrites target file without asking for user input"), + ) + .arg( + Arg::new("use-default") + .long("use-default") + .short('u') + .action(ArgAction::SetTrue) + .help("Use default values without asking for user input"), + ) + .arg( + Arg::new("chars-for-random-string") + .long("chars-for-random-string") + .short('c') + .default_value(DEFAULT_RANDOM_CHARS) + .help("Characters used to create random strings"), + ) + .get_matches(); + + let target = matches.get_one::<String>("target").unwrap(); + let overwrite = matches.get_one::<bool>("overwrite").unwrap(); + if !overwrite && !should_write_to(target)? { + exit(0); + } + + let source = matches.get_one::<String>("source").unwrap(); + let use_default = matches.get_one::<bool>("use-default").unwrap(); + let chars = matches + .get_one::<String>("chars-for-random-string") + .unwrap(); + + let mut parser = Parser::new(source.as_str(), chars, use_default)?; + parser.parse(&mut stdin().lock())?; + + let mut output = File::create(target)?; + output.write_all(parser.to_string().as_bytes())?; + Ok(()) +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..ed5e0c4 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,281 @@ +use std::fmt; +use std::{ + collections::HashMap, + fmt::Display, + io::{stdout, BufRead, Write}, +}; + +use anyhow::Result; + +#[derive(Clone)] +pub struct Comment { + contents: String, +} + +impl Comment { + pub fn new(contents: &str) -> Self { + Self { + contents: contents.to_string(), + } + } +} + +impl fmt::Display for Comment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "# {}", self.contents) + } +} + +trait Variable { + fn value(&self) -> String; +} + +#[derive(Clone, Debug)] +pub struct SimpleVariable { + pub name: String, + pub default: Option<String>, + pub help: Option<String>, + input: Option<String>, +} + +impl SimpleVariable { + pub fn new(name: &str, default: Option<&str>, help: Option<&str>) -> Self { + Self { + name: name.to_string(), + default: default.map(|s| s.to_string()), + help: help.map(|s| s.to_string()), + input: None, + } + } + + fn resolve<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + match (&self.help, &self.default) { + (Some(h), Some(d)) => print!("{} [{}]: ", h, d), + (Some(h), None) => print!("{}: ", h), + (None, Some(d)) => print!("{} [{}]: ", self.name, d), + (None, None) => print!("{}: ", self.name), + }; + + stdout().flush()?; + let mut input = "".to_string(); + terminal.read_line(&mut input)?; + + let value = input.trim(); + if value.is_empty() && self.default.is_none() { + return self.resolve(terminal); + } + if !value.is_empty() { + self.input = Some(value.to_string()); + } + Ok(()) + } +} + +impl Display for SimpleVariable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}={}", self.name, self.value()) + } +} + +impl Variable for SimpleVariable { + fn value(&self) -> String { + if let Some(input) = &self.input { + return input.clone(); + } + if let Some(default) = &self.default { + return default.clone(); + } + panic!( + "Tryinyg to read the value of a {} before resolving it", + self.name + ); + } +} + +#[derive(Clone, Debug)] +pub struct AutoGeneratedVariable { + pub name: String, + pattern: String, + context: HashMap<String, String>, +} + +impl AutoGeneratedVariable { + pub fn new(name: &str, pattern: &str) -> Self { + Self { + name: name.to_string(), + pattern: pattern.to_string(), + context: HashMap::new(), + } + } + + fn load_context(&mut self, ctx: &HashMap<String, String>) { + for (k, v) in ctx.iter() { + self.context.insert(k.to_string(), v.to_string()); + } + } +} +impl Display for AutoGeneratedVariable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}={}", self.name, self.value()) + } +} + +impl Variable for AutoGeneratedVariable { + fn value(&self) -> String { + let mut value: String = self.pattern.clone(); + for (k, v) in self.context.iter() { + let key = format!("{{{}}}", *k); + value = value.replace(&key, v); + } + value + } +} + +#[derive(Clone, Debug)] +pub enum VariableType { + Input(SimpleVariable), + AutoGenerated(AutoGeneratedVariable), +} + +#[derive(Clone)] +pub struct Block { + pub title: Comment, + pub description: Option<Comment>, + pub variables: Vec<VariableType>, +} + +impl Block { + pub fn new(title: Comment, description: Option<Comment>) -> Self { + Self { + title, + description, + variables: vec![], + } + } + + fn has_auto_input_variables(&self) -> bool { + self.variables + .iter() + .any(|v| matches!(v, VariableType::Input(_))) + } + + fn has_auto_generated_variables(&self) -> bool { + self.variables + .iter() + .any(|v| matches!(v, VariableType::AutoGenerated(_))) + } + + pub fn resolve<T: BufRead>(&mut self, terminal: &mut T, use_default: bool) -> Result<()> { + if self.has_auto_input_variables() { + println!( + "{}", + self.title.to_string().strip_prefix("# ").unwrap_or("") + ); + if let Some(desc) = &self.description { + println!("{}", desc.to_string().strip_prefix("# ").unwrap_or("")); + } + } + for variable in &mut self.variables { + if let VariableType::Input(var) = variable { + if var.input.is_none() { + if use_default && var.default.is_some() { + var.input = var.default.clone(); + } else { + var.resolve(terminal)?; + } + } + } + } + if !self.has_auto_generated_variables() { + return Ok(()); + } + let mut context = HashMap::new(); + for var in &self.variables { + if let VariableType::Input(v) = var { + context.insert(v.name.clone(), v.value()); + } + } + for variable in &mut self.variables { + if let VariableType::AutoGenerated(var) = variable { + var.load_context(&context); + } + } + Ok(()) + } +} + +impl Display for Block { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.title)?; + if let Some(desc) = &self.description { + writeln!(f, "{}", desc)?; + } + for variable in &self.variables { + let content = match variable { + VariableType::Input(var) => var.to_string(), + VariableType::AutoGenerated(var) => var.to_string(), + }; + writeln!(f, "{}", content)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + + #[test] + fn test_title() { + let line = Comment::new("Forty-two"); + assert_eq!(line.to_string(), "# Forty-two") + } + + #[test] + fn test_variable() { + let mut var = SimpleVariable::new("ANSWER", None, None); + var.resolve(&mut Cursor::new("42")).unwrap(); + assert_eq!(format!("{}", var), "ANSWER=42") + } + + #[test] + fn test_empty_variable_with_default_value() { + let var = SimpleVariable::new("ANSWER", Some("42"), None); + assert_eq!(format!("{}", var), "ANSWER=42"); + } + + #[test] + fn test_variable_with_default_value_and_input() { + let mut var = SimpleVariable::new("ANSWER", Some("42"), None); + var.resolve(&mut Cursor::new("forty two")).unwrap(); + assert_eq!(format!("{}", var), "ANSWER=forty two"); + } + + #[test] + fn test_auto_generated_variable() { + let mut var = AutoGeneratedVariable::new("ANSWER", "{FIRST} {SECOND}"); + let mut ctx = HashMap::new(); + ctx.insert("FIRST".to_string(), "Forty".to_string()); + ctx.insert("SECOND".to_string(), "two".to_string()); + var.load_context(&ctx); + assert_eq!(format!("{}", var), "ANSWER=Forty two"); + } + + #[test] + fn test_block_with_description() { + let title = Comment::new("42"); + let description = Some(Comment::new("Forty-two")); + let mut variable1 = SimpleVariable::new("ANSWER", None, None); + variable1.resolve(&mut Cursor::new("42")).unwrap(); + let variable2 = SimpleVariable::new("AS_TEXT", Some("forty two"), None); + let mut block = Block::new(title, description); + block.variables.push(VariableType::Input(variable1)); + block.variables.push(VariableType::Input(variable2)); + assert_eq!( + block.to_string(), + "# 42\n# Forty-two\nANSWER=42\nAS_TEXT=forty two\n" + ) + } +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..8af5b25 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,289 @@ +use std::{ + fmt::Display, + fs::File, + io::{BufRead, BufReader}, +}; + +use anyhow::Result; +use rand::{thread_rng, Rng}; +use regex::Regex; + +use crate::model::{AutoGeneratedVariable, Block, Comment, SimpleVariable, VariableType}; + +const NAME_PATTERN: &str = r"^[A-Z0-9_]+$"; +const RANDOM_VARIABLE_PATTERN: &str = r"\<random(:(?P<size>\d*))?\>"; +const AUTO_GENERATED_PATTERN: &str = r"\{[A-Z0-9_]+\}"; + +const HELP_TITLE: &str = "This is the first line of a block. A block is a \ + group of lines separated from others by one (or more) empty line(s). \ + The first line of a block is expected to be a title, that is to say, to \ + start with `# `, the remaining text is considered the title of this block. \ + This line does not match this pattern."; +const HELP_DESCRIPTION: &str = "This is the second line of a block. A block is \ + a group of lines separated from others by one (or more) empty line(s). The \ + second line of a block is expected to be a description of that block or a \ + variable line. The description line should start `# `, and the remaining \ + text is considered the description of this block. A config variable line \ + should start with a name in uppercase, no spaces, followed by an equal \ + sign. No spaces before the equal sign. This lines does not match this \ + expected patterns."; +const HELP_VARIABLE: &str = "This line was expected to be a variable line. The \ + format should be a name using capital ASCII letters, digits or underscore, \ + followed by an equal sign. No spaces before the equal sign. This line does \ + not match this expected pattern."; + +enum Expecting { + Title, + DescriptionOrVariables, + Variables, +} + +impl Display for Expecting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Expecting::Title => write!(f, "expecting a block title"), + Expecting::DescriptionOrVariables => { + write!(f, "expecting a block description or a variable line") + } + Expecting::Variables => write!(f, "expecting a variable line"), + } + } +} + +pub struct Parser { + path: String, + random_chars: String, + use_default: bool, + name_pattern: Regex, + random_pattern: Regex, + auto_generated_pattern: Regex, + state: Expecting, + buffer: Option<Block>, + pub blocks: Vec<Block>, +} + +impl Parser { + pub fn new(path: &str, random_chars: &str, use_default: &bool) -> Result<Self> { + Ok(Self { + path: path.to_string(), + random_chars: random_chars.to_string(), + use_default: *use_default, + name_pattern: Regex::new(NAME_PATTERN)?, + random_pattern: Regex::new(RANDOM_VARIABLE_PATTERN)?, + auto_generated_pattern: Regex::new(AUTO_GENERATED_PATTERN)?, + state: Expecting::Title, + buffer: None, + blocks: vec![], + }) + } + + fn parse_random_variable( + &self, + name: &str, + description: Option<&str>, + value: &str, + ) -> Result<SimpleVariable> { + if let Some(matches) = self.random_pattern.captures(value) { + let mut rng = thread_rng(); + let length = matches + .name("size") + .map(|m| m.as_str().parse::<usize>()) + .transpose()? + .unwrap_or(rng.gen_range(64..=128)); + let max_chars_idx = self.random_chars.chars().count(); + let mut value: String = String::from(""); + for _ in 0..length { + let pos = rng.gen_range(0..max_chars_idx); + value.push(self.random_chars.chars().nth(pos).unwrap()) + } + Ok(SimpleVariable::new(name, Some(value.as_str()), description)) + } else { + Err(anyhow::anyhow!("Invalid random variable: {}", value)) + } + } + + fn parse_auto_generated_variable( + &self, + name: &str, + value: &str, + ) -> Result<AutoGeneratedVariable> { + if self.auto_generated_pattern.find(value).is_some() { + return Ok(AutoGeneratedVariable::new(name, value)); + } + Err(anyhow::anyhow!( + "Invalid auto-generated variable: {}", + value + )) + } + + fn parse_variable(&self, pos: usize, line: &str) -> Result<VariableType> { + let (name, rest) = line.split_once('=').ok_or(anyhow::anyhow!( + "Invalid variable line on line {}: {}\nHint: {}", + pos, + line, + HELP_VARIABLE + ))?; + if !self.name_pattern.is_match(name) { + return Err(anyhow::anyhow!( + "Invalid variable name on line {}: {}\nHint :{}", + pos, + name, + HELP_VARIABLE + )); + } + let (mut default, description) = match rest.split_once(" # ") { + Some((default, help)) => (Some(default), Some(help)), + None => (Some(rest), None), + }; + if let Some(val) = default { + if val.is_empty() { + default = None; + } else { + if let Ok(v) = self.parse_random_variable(name, description, val) { + return Ok(VariableType::Input(v)); + } + if let Ok(v) = self.parse_auto_generated_variable(name, val) { + return Ok(VariableType::AutoGenerated(v)); + } + } + } + let variable = SimpleVariable::new(name, default, description); + Ok(VariableType::Input(variable)) + } + + fn flush<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + if let Some(block) = self.buffer.as_mut() { + block.resolve(terminal, self.use_default)?; + self.blocks.push(block.clone()); + self.buffer = None + } + Ok(()) + } + + pub fn parse<T: BufRead>(&mut self, terminal: &mut T) -> Result<()> { + let reader = BufReader::new(File::open(&self.path)?); + let mut cursor: usize = 0; + for (idx, line) in reader.lines().enumerate() { + cursor = idx + 1; + let cleaned = line?.trim().to_string(); + if cleaned.is_empty() { + self.flush(terminal)?; + self.state = Expecting::Title; + continue; + } + match self.state { + Expecting::Title => { + if let Some(txt) = cleaned.strip_prefix('#') { + self.buffer = Some(Block::new(Comment::new(txt.trim()), None)); + self.state = Expecting::DescriptionOrVariables; + } else { + return Err(anyhow::anyhow!( + "Unexpected title on line {}: {}\nHint: {}", + cursor, + cleaned, + HELP_TITLE + )); + } + } + Expecting::DescriptionOrVariables => { + if let Some(txt) = cleaned.strip_prefix('#') { + if let Some(b) = self.buffer.as_mut() { + b.description = Some(Comment::new(txt.trim())); + } + self.state = Expecting::Variables; + } else { + let variable = self.parse_variable(cursor, &cleaned)?; + if let Some(b) = self.buffer.as_mut() { + b.variables.push(variable); + } + } + } + Expecting::Variables => { + let variable = self.parse_variable(cursor, &cleaned)?; + if let Some(b) = self.buffer.as_mut() { + b.variables.push(variable); + } + } + } + } + let last_block_has_variables = self + .buffer + .as_ref() + .map(|block| !block.variables.is_empty()) + .unwrap_or(false); + if !last_block_has_variables { + let help = match self.state { + Expecting::Title => HELP_TITLE, + Expecting::DescriptionOrVariables => HELP_DESCRIPTION, + Expecting::Variables => HELP_VARIABLE, + }; + return Err(anyhow::anyhow!( + "Unexpected EOF while {} at line {}: the last block has no variables\nHint: {}", + self.state, + cursor, + help + )); + } + self.flush(terminal)?; + Ok(()) + } +} + +impl Display for Parser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for block in &self.blocks { + if !first { + writeln!(f)?; + } + write!(f, "{}", block)?; + first = false; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{io::Cursor, path::PathBuf}; + + use super::*; + use crate::DEFAULT_RANDOM_CHARS; + + #[test] + fn test_parser() { + let sample = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(".env.sample") + .into_os_string() + .into_string(); + let mut parser = Parser::new(&sample.unwrap(), DEFAULT_RANDOM_CHARS, &false).unwrap(); + parser.parse(&mut Cursor::new("World")).unwrap(); + assert_eq!(parser.blocks.len(), 1); + assert_eq!(parser.blocks[0].variables.len(), 4); + let names: [&str; 4] = ["NAME", "GREETING", "DO_YOU_LIKE_OPEN_SOURCE", "PASSWORD"]; + for (variable, expected) in parser.blocks[0].variables.iter().zip(names) { + let got = match variable { + VariableType::Input(v) => &v.name, + VariableType::AutoGenerated(v) => &v.name, + }; + assert_eq!(got, expected); + } + for (idx, variable) in parser.blocks[0].variables.iter().enumerate() { + if idx != 1 { + assert!( + matches!(variable, VariableType::Input(_)), + "Expected variable number {} to be Input, got {:?}", + idx + 1, + variable + ); + } + if idx == 1 { + assert!( + matches!(variable, VariableType::AutoGenerated(_)), + "Expected variable 2 to be AutoGenerated, got {:?}", + variable + ); + } + } + } +} diff --git a/tests/.env.sample b/tests/.env.sample deleted file mode 100644 index 8855cca..0000000 --- a/tests/.env.sample +++ /dev/null @@ -1,21 +0,0 @@ -# This is the title -# (Here comes details to make the interface more user-friendly) -MY_FIRST_VARIABLE= -MY_SECOND_VARIABLE=42 -MY_THIRD_VARIABLE=42 # My third variable - -# This block has no description -SHOULD_I_DO_THAT=False - -# This block uses the auto-config and the random features -NAME=Cuducos -PERIOD=morning -THIS_IS_NOT_USED_IN_AUTO_CONFIG=ok? -GREETINGS=Good {PERIOD}, {NAME}! -I_HAVE_A_SECRET=<random> -I_HAVE_A_PIN_SECRET=<random:4> - -# Test auto-config as first line -GREETINGS=Good {PERIOD}, {NAME}! -NAME=Cuducos -PERIOD=morning diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_auto_config_class.py b/tests/test_auto_config_class.py deleted file mode 100644 index 371172e..0000000 --- a/tests/test_auto_config_class.py +++ /dev/null @@ -1,7 +0,0 @@ -from createnv.config import AutoConfig - - -def test_call(): - settings = {"NAME": "Cuducos", "PERIOD": "morning"} - config = AutoConfig("GREETING", "Good {PERIOD}, {NAME}!") - assert config(settings) == {"GREETING": "Good morning, Cuducos!"} diff --git a/tests/test_block_class.py b/tests/test_block_class.py deleted file mode 100644 index d1e70d1..0000000 --- a/tests/test_block_class.py +++ /dev/null @@ -1,32 +0,0 @@ -from createnv.parser import Block, Line - - -def test_init(): - block = Block() - assert block.lines == [] - - -def test_add(): - line1, line2 = Line(1, "Hell yeah!"), Line(2, "This is awesome") - block = Block() - block + line1 - block += line2 - assert block.lines == [line1, line2] - - -def test_iter(): - lines = (Line(1, "Hell yeah!"), Line(2, "This is awesome")) - block = Block() - for line in lines: - block += line - assert tuple(block.lines) == lines - - -def test_is_empty(): - block = Block() - assert block.is_empty() - - lines = (Line(1, "Hell yeah!"), Line(2, "This is awesome")) - for line in lines: - block += line - assert not block.is_empty() diff --git a/tests/test_cli_module.py b/tests/test_cli_module.py deleted file mode 100644 index c72a618..0000000 --- a/tests/test_cli_module.py +++ /dev/null @@ -1,28 +0,0 @@ -from pathlib import Path - - -from createnv.cli import RANDOM_CHARS, cli, main - - -def test_main_with_default_values(mocker): - parser = mocker.patch("createnv.cli.Parser") - generator = mocker.patch("createnv.cli.Generator") - main() - parser.assert_called_once_with(Path(".env.sample"), RANDOM_CHARS) - generator.assert_called_once_with(Path(".env"), parser.return_value, False, False) - generator.return_value.assert_called_once_with() - - -def test_main_with_custom_values(mocker): - parser = mocker.patch("createnv.cli.Parser") - generator = mocker.patch("createnv.cli.Generator") - main("env", "sample", True, True, "ab") - parser.assert_called_once_with(Path("sample"), "ab") - generator.assert_called_once_with(Path("env"), parser.return_value, True, True) - generator.return_value.assert_called_once_with() - - -def test_cli(mocker): - run = mocker.patch("createnv.cli.run") - cli() - run.assert_called_once_with(main) diff --git a/tests/test_config_class.py b/tests/test_config_class.py deleted file mode 100644 index 501e5e3..0000000 --- a/tests/test_config_class.py +++ /dev/null @@ -1,38 +0,0 @@ -from createnv.config import Config - - -def test_str(): - human, bot = Config("DEBUG", "Debug mode"), Config("DEBUG") - assert str(human) == "Debug mode" - assert str(bot) == "DEBUG" - - -def test_call_with_default_using_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - config = Config("DEBUG", "Debug mode", "True") - assert config(True) == "True" - prompt.assert_not_called() - - -def test_call_with_default_but_not_using_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = Config("DEBUG", "Debug mode", "True") - assert config() == "42" - prompt.assert_called_once_with(str(config), default="True") - - -def test_call_without_default_trying_to_use_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = Config("DEBUG", "Debug mode") - assert config(True) == "42" - prompt.assert_called_once_with(str(config), default=None) - - -def test_call_without_default_without_trying_to_use_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = Config("DEBUG", "Debug mode") - assert config() == "42" - prompt.assert_called_once_with(str(config), default=None) diff --git a/tests/test_echo_method.py b/tests/test_echo_method.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_generator_class.py b/tests/test_generator_class.py deleted file mode 100644 index e0526ff..0000000 --- a/tests/test_generator_class.py +++ /dev/null @@ -1,119 +0,0 @@ -from pathlib import Path - -from createnv.config import Config, Group -from createnv.generator import Generator -from createnv.parser import ParserError - - -def test_can_write_to_non_existent_path_without_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = False - generator = Generator(Path(".env"), mocker.Mock(), False, False) - assert generator.can_write_to_path() - - -def test_can_write_to_non_existent_path_with_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = False - generator = Generator(Path(".env"), mocker.Mock(), True, False) - assert generator.can_write_to_path() - - -def test_can_write_to_existent_path_with_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = False - generator = Generator(Path(".env"), mocker.Mock(), True, False) - assert generator.can_write_to_path() - - -def test_can_write_to_existent_path_manually_confirming_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = True - confirm = mocker.patch("createnv.generator.confirm") - confirm.return_value = True - warning = mocker.patch("createnv.generator.warning") - generator = Generator(Path(".env"), mocker.Mock(), False, False) - assert generator.can_write_to_path() - warning.assert_called_once_with("There is an existing .env file.") - confirm.called_once_with("Do you want to overwrite it?") - - -def test_can_write_to_existent_path_without_manually_confirming_overwrite(mocker): - exists = mocker.patch.object(Path, "exists") - exists.return_value = True - confirm = mocker.patch("createnv.generator.confirm") - confirm.return_value = False - warning = mocker.patch("createnv.generator.warning") - generator = Generator(Path(".env"), mocker.Mock(), False, False) - assert not generator.can_write_to_path() - warning.assert_called_once_with("There is an existing .env file.") - confirm.assert_called_once_with("Do you want to overwrite it?") - - -def test_contents(mocker): - group = mocker.patch.object(Group, "__call__") - group.side_effect = ({"NAME": "Cuducos", "PERIOD": "morning"}, {"DEBUG": "True"}) - should_echo = mocker.patch.object(Group, "should_echo") - should_echo.side_effect = (True, False) - echo = mocker.patch("createnv.generator.echo") - generator = Generator(Path(".env"), mocker.Mock(), False, False) - settings = ( - Group("Greeting", (Config("NAME"), Config("Period")), "Say hi!"), - Group("Environment", (Config("DEBUG"))), - ) - assert tuple(generator.contents(settings)) == ( - "# Greeting", - "# Say hi!", - "NAME=Cuducos", - "PERIOD=morning", - "", - "# Environment", - "DEBUG=True", - "", - ) - echo.assert_called_once_with("\nGreeting\n(Say hi!)") - - -def test_call_prints_errors_from_parser(mocker): - error = mocker.patch("createnv.generator.error") - write_text = mocker.patch.object(Path, "write_text") - success = mocker.patch("createnv.generator.success") - parser = mocker.Mock() - parser.side_effect = ParserError("oops") - generator = Generator(Path(".env"), parser, False, False) - generator() - parser.assert_called_once_with() - error.assert_called_once_with("oops") - write_text.assert_not_called() - success.assert_not_called() - - -def test_call_stops_when_it_cannot_write_to_target(mocker): - error = mocker.patch("createnv.generator.error") - write_text = mocker.patch.object(Path, "write_text") - success = mocker.patch("createnv.generator.success") - can_write_to_path = mocker.patch.object(Generator, "can_write_to_path") - parser = mocker.Mock() - can_write_to_path.return_value = False - generator = Generator(Path(".env"), parser, False, False) - generator() - parser.assert_called_once_with() - error.assert_not_called() - write_text.assert_not_called() - success.assert_not_called() - - -def test_call(mocker): - error = mocker.patch("createnv.generator.error") - write_text = mocker.patch.object(Path, "write_text") - success = mocker.patch("createnv.generator.success") - can_write_to_path = mocker.patch.object(Generator, "can_write_to_path") - can_write_to_path.return_value = True - contents = mocker.patch.object(Generator, "contents") - contents.return_value = ("Hell yeah!", "This is awesome") - parser = mocker.Mock() - generator = Generator(Path(".env"), parser, False, False) - generator() - error.assert_not_called() - write_text.assert_called_once_with("Hell yeah!\nThis is awesome") - success.assert_called_once_with(".env created!") diff --git a/tests/test_group_class.py b/tests/test_group_class.py deleted file mode 100644 index 27b8d18..0000000 --- a/tests/test_group_class.py +++ /dev/null @@ -1,69 +0,0 @@ -from unittest.mock import call - -from createnv.config import AutoConfig, Config, Group - - -def test_complete_str(): - group = Group("Hell yeah!", (Config("DEBUG"),), "This is awesome!") - assert str(group) == "\nHell yeah!\n(This is awesome!)" - - -def test_str_without_description(): - group = Group("Hell yeah!", (Config("DEBUG"),)) - assert str(group) == "\nHell yeah!" - - -def test_should_echo_without_default_but_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG"),)) - assert group.should_echo(True) - - -def test_should_echo_without_default_and_not_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG"),)) - assert group.should_echo() - - -def test_should_echo_with_default_and_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG", default="42"),)) - assert not group.should_echo(True) - - -def test_should_echo_with_default_and_not_trying_to_use_default(): - group = Group("Hell yeah!", (Config("DEBUG", default="42"),)) - assert group.should_echo() - - -def test_call_without_auto_config_without_use_default(mocker): - config = mocker.patch.object(Config, "__call__") - config.side_effect = ("True", "localhost") - group = Group("Hell yeah!", (Config("DEBUG"), Config("ALLOWED_HOSTS"))) - assert group() == {"DEBUG": "True", "ALLOWED_HOSTS": "localhost"} - assert config.call_count == 2 - config.assert_has_calls((call(False), call(False))) - - -def test_call_without_auto_config_with_use_default(mocker): - config = mocker.patch.object(Config, "__call__") - config.side_effect = ("True", "localhost") - group = Group("Hell yeah!", (Config("DEBUG"), Config("ALLOWED_HOSTS"))) - assert group(True) == {"DEBUG": "True", "ALLOWED_HOSTS": "localhost"} - assert config.call_count == 2 - config.assert_has_calls((call(True), call(True))) - - -def test_call_with_auto_config(mocker): - config = mocker.patch.object(Config, "__call__") - config.side_effect = ("morning", "Cuducos") - auto_config = mocker.patch.object(AutoConfig, "__call__") - auto_config.return_value = {"GREETING": "Good morning, Cuducos!"} - group = Group( - "Hell yeah!", - (Config("PERIOD"), Config("NAME")), - "This is pretty cool.", - AutoConfig("GREETING", "Good {PERIOD}, {NAME}!"), - ) - assert group() == { - "NAME": "Cuducos", - "PERIOD": "morning", - "GREETING": "Good morning, Cuducos!", - } diff --git a/tests/test_line_class.py b/tests/test_line_class.py deleted file mode 100644 index e9d08d2..0000000 --- a/tests/test_line_class.py +++ /dev/null @@ -1,21 +0,0 @@ -from createnv.parser import Line - - -def test_cleaned(): - assert Line(42, " \tyay\n ").cleaned() == "yay" - - -def test_is_empty(): - assert Line(42, "").is_empty() - assert Line(42, " ").is_empty() - assert Line(42, "\t").is_empty() - assert Line(42, "\n\t").is_empty() - assert not Line(42, " \tyay\n ").is_empty() - - -def test_is_comment(): - assert Line(42, "# Hell yeah!").is_comment() - assert Line(42, "\t# Hell yeah!").is_comment() - assert Line(42, " # Hell yeah!").is_comment() - assert not Line(42, "").is_comment() - assert not Line(42, "NAME=CUDUCOS").is_comment() diff --git a/tests/test_parser_class.py b/tests/test_parser_class.py deleted file mode 100644 index a734e6b..0000000 --- a/tests/test_parser_class.py +++ /dev/null @@ -1,162 +0,0 @@ -from pathlib import Path - -import pytest # type: ignore - -from createnv.cli import RANDOM_CHARS -from createnv.config import AutoConfig, Config, Group, RandomConfig -from createnv.parser import Block, Line, Parser, ParserError - - -def test_block(mocker): - path_open = mocker.patch.object(Path, "open") - path_open.return_value = ( - "\t", - "# Title", - "# Description", - "VARIABLE=", - "", - "# Another title", - "ANOTHER_VARIABLE=", - "", - ) - expected = ( - Block([Line(2, "# Title"), Line(3, "# Description"), Line(4, "VARIABLE=")]), - Block([Line(6, "# Another title"), Line(7, "ANOTHER_VARIABLE=")]), - ) - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - assert tuple(parser.blocks()) == expected - - -def test_parse_title(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - assert parser.parse_title(Line(2, "# Title")) == "Title" - with pytest.raises(ParserError): - parser.parse_title(Line(2, "VARIABLE=")) - - -def test_parse_config_with_config_lines(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - values = ( - (Line(4, "VARIABLE="), Config("VARIABLE")), - (Line(4, "VARIABLE= # Variable"), Config("VARIABLE", "Variable")), - (Line(4, "VARIABLE=42"), Config("VARIABLE", default="42")), - (Line(4, "VARIABLE=42 # Variable"), Config("VARIABLE", "Variable", "42")), - ) - for line, expected in values: - assert parser.parse_config(line) == expected - - -def test_parse_config_with_auto_config_line(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - line = Line(4, "VARIABLE=Hello, {NAME}!") - assert parser.parse_config(line) == AutoConfig("VARIABLE", "Hello, {NAME}!") - - -def test_parse_config_with_random_config_lines(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - lines = (Line(4, "VARIABLE=<random>"), Line(4, "VARIABLE=<random:42> # Variable")) - expected = ( - RandomConfig("VARIABLE", parser.chars_for_random_string), - RandomConfig("VARIABLE", parser.chars_for_random_string, "Variable", length=42), - ) - for line, expected in zip(lines, expected): - assert parser.parse_config(line) == expected - - -def test_parse_config_with_invalid_line(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser.parse_config(Line(2, "# Title")) - - -def test_parse_description_or_config_with_description(mocker): - description = Line(3, "# Here comes a description") - parse_title = mocker.patch.object(Parser, "parse_title") - parse_config = mocker.patch.object(Parser, "parse_config") - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - parser.parse_description_or_config(description) - parse_title.assert_called_once_with(description) - parse_config.asseert_not_called() - - -def test_parse_description_or_config_with_config(mocker): - config = Line(4, "VARIABLE=") - parse_title = mocker.patch.object(Parser, "parse_title") - parse_config = mocker.patch.object(Parser, "parse_config") - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - parser.parse_description_or_config(config) - parse_title.assert_not_called() - parse_config.assert_called_once_with(config) - - -def test_parse_description_or_config_with_invalid_line(): - nothing = Line(1, "\t") - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser.parse_description_or_config(nothing) - - -def test_call_raises_error_without_source(): - fixture = Path() / "tests" / ".env.sample.that.does.not.exist" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser() - - -def test_call_raises_error_if_source_is_a_directory(): - fixture = Path() / "tests" - parser = Parser(fixture, RANDOM_CHARS) - with pytest.raises(ParserError): - parser() - - -def test_parser(): - fixture = Path() / "tests" / ".env.sample" - parser = Parser(fixture, RANDOM_CHARS) - expected = ( - Group( - title="This is the title", - description="(Here comes details to make the interface more user-friendly)", - configs=[ - Config("MY_FIRST_VARIABLE"), - Config("MY_SECOND_VARIABLE", default="42"), - Config( - "MY_THIRD_VARIABLE", human_name="My third variable", default="42" - ), - ], - ), - Group( - title="This block has no description", - configs=[Config("SHOULD_I_DO_THAT", default="False")], - ), - Group( - title="This block uses the auto-config and the random features", - configs=[ - Config("NAME", default="Cuducos"), - Config("PERIOD", default="morning"), - Config("THIS_IS_NOT_USED_IN_AUTO_CONFIG", default="ok?"), - RandomConfig("I_HAVE_A_SECRET", RANDOM_CHARS), - RandomConfig("I_HAVE_A_PIN_SECRET", RANDOM_CHARS, length=4), - ], - auto_config=AutoConfig("GREETINGS", "Good {PERIOD}, {NAME}!"), - ), - Group( - title="Test auto-config as first line", - configs=[ - Config("NAME", default="Cuducos"), - Config("PERIOD", default="morning"), - ], - auto_config=AutoConfig("GREETINGS", "Good {PERIOD}, {NAME}!"), - ), - ) - result = parser() - assert result == expected diff --git a/tests/test_parser_error_exception.py b/tests/test_parser_error_exception.py deleted file mode 100644 index 21e04ac..0000000 --- a/tests/test_parser_error_exception.py +++ /dev/null @@ -1,18 +0,0 @@ -from createnv.parser import Line, ParserError - - -def test_regular_call(): - exception = ParserError("oops") - assert str(exception) == "oops" - - -def test_call_with_line(): - exception = ParserError(Line(42, "yay"), "oops") - assert str(exception) == ( - "\n" - "==> Parsing error at line 42:\n" - " oops\n" - "\n" - " The content of the line 42 is:\n" - " yay\n" - ) diff --git a/tests/test_random_config_class.py b/tests/test_random_config_class.py deleted file mode 100644 index cbaf4ac..0000000 --- a/tests/test_random_config_class.py +++ /dev/null @@ -1,26 +0,0 @@ -from createnv.config import RandomConfig - - -def test_str(): - assert str(RandomConfig("SECRET_KEY", "ab")) == "SECRET_KEY" - assert str(RandomConfig("SECRET_KEY", "ab", "Secret key")) == "Secret key" - - -def test_default(): - config = RandomConfig("SECRET_KEY", "ab", "Secret key", 2) - assert config.default in {"aa", "ab", "ba", "bb"} - - -def test_call_without_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - prompt.return_value = "42" - config = RandomConfig("SECRET_KEY", "a", "Secret key", 3) - assert config() == "42" - prompt.assert_called_once_with("Secret key", default="aaa") - - -def test_call_with_default(mocker): - prompt = mocker.patch("createnv.config.prompt") - config = RandomConfig("SECRET_KEY", "a", "Secret key", 3) - assert config(True) == "aaa" - prompt.assert_not_called()