diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faecce34b..b981c2997 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,14 +42,11 @@ jobs: - name: Enable msys binaries if: ${{ runner.os == 'Windows' }} run: | - echo "::add-path::C:\msys64\usr\bin" + echo "C:\msys64\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append rm C:\msys64\usr\bin\bash.exe - run: git config --global user.name copier-ci - shell: bash - run: git config --global user.email copier@copier - shell: bash - run: git config --global core.autocrlf input - shell: bash - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -58,8 +55,8 @@ jobs: - name: generate cache key PY shell: bash run: - echo "::set-env name=PY::$((python -VV; pip freeze) | sha256sum | cut -d' ' - -f1)" + echo "PY=$((python -VV; pip freeze) | sha256sum | cut -d' ' -f1)" >> + $GITHUB_ENV - uses: actions/cache@v2.1.0 with: path: | @@ -69,22 +66,11 @@ jobs: cache|${{ runner.os }}|${{ env.PY }}|${{ hashFiles('pyproject.toml') }}|${{ hashFiles('poetry.lock') }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Install dependencies - shell: bash run: | - python -m pip install poetry poetry-dynamic-versioning poethepoet - poetry run python -m pip install pip -U - poetry install - - name: Run pre-commit - shell: bash - run: poe lint --color=always - # FIXME Make pre-commit pass reliably on Windows, and remove this - continue-on-error: ${{ runner.os == 'Windows' }} - - name: Run mypy - shell: bash - run: poe types + python -m pip install poetry + poetry install -vvv - name: Run pytest - shell: bash - run: poe test -ra . + run: poetry run poe test -ra . publish: if: github.event_name == 'release' @@ -98,8 +84,8 @@ jobs: python-version: 3.8 - name: generate cache key PY run: - echo "::set-env name=PY::$((python -VV; pip freeze) | sha256sum | cut -d' ' - -f1)" + echo "PY=$((python -VV; pip freeze) | sha256sum | cut -d' ' -f1)" >> + $GITHUB_ENV - uses: actions/cache@v2.1.0 with: path: | diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8d78caee4..5eeba1424 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,8 +19,8 @@ jobs: - run: python -m pip install poetry poetry-dynamic-versioning - name: generate cache key PY run: - echo "::set-env name=PY::$((python -VV; pip freeze) | sha256sum | cut -d' ' - -f1)" + echo "PY=$((python -VV; pip freeze) | sha256sum | cut -d' ' -f1)" >> + $GITHUB_ENV - uses: actions/cache@v2.1.0 with: path: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b68bf0315..964aeee73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,12 @@ repos: # hooks running from local virtual environment - repo: local hooks: + - id: autoflake + name: autoflake + entry: poetry run autoflake + language: system + types: [python] + args: ["-i", "--remove-all-unused-imports", "--ignore-init-module-imports"] - id: black name: black entry: poetry run black diff --git a/copier/config/factory.py b/copier/config/factory.py index a0ee730c6..4e952a85c 100644 --- a/copier/config/factory.py +++ b/copier/config/factory.py @@ -132,6 +132,7 @@ def make_config( {k: v for k, v in questions_data.items() if k in data}, {}, data, + init_args["data_from_template_defaults"], False, init_args["envops"], ), @@ -141,6 +142,7 @@ def make_config( questions_data, init_args["data_from_answers_file"], init_args["data_from_init"], + init_args["data_from_template_defaults"], not force, init_args["envops"], ) diff --git a/copier/config/objects.py b/copier/config/objects.py index 999e3d6d1..89bf2a160 100644 --- a/copier/config/objects.py +++ b/copier/config/objects.py @@ -1,5 +1,4 @@ """Pydantic models, exceptions and default values.""" - import datetime from collections import ChainMap from copy import deepcopy @@ -8,7 +7,7 @@ from pathlib import Path from typing import Any, ChainMap as t_ChainMap, Sequence, Tuple, Union -from pydantic import BaseModel, Extra, StrictBool, validator +from pydantic import BaseModel, Extra, Field, StrictBool, validator from ..types import AnyByStrDict, OptStr, PathSeq, StrOrPathSeq, StrSeq @@ -35,8 +34,6 @@ class UserMessageError(Exception): """Exit the program giving a message to the user.""" - pass - class NoSrcPathError(UserMessageError): pass @@ -91,10 +88,10 @@ class ConfigData(BaseModel): migrations: Sequence[Migrations] = () secret_questions: StrSeq = () answers_file: Path = Path(".copier-answers.yml") - data_from_init: AnyByStrDict = {} - data_from_asking_user: AnyByStrDict = {} - data_from_answers_file: AnyByStrDict = {} - data_from_template_defaults: AnyByStrDict = {} + data_from_init: AnyByStrDict = Field(default_factory=dict) + data_from_asking_user: AnyByStrDict = Field(default_factory=dict) + data_from_answers_file: AnyByStrDict = Field(default_factory=dict) + data_from_template_defaults: AnyByStrDict = Field(default_factory=dict) # Private _data_mutable: AnyByStrDict diff --git a/copier/config/user_data.py b/copier/config/user_data.py index 887dbf214..a50e70436 100644 --- a/copier/config/user_data.py +++ b/copier/config/user_data.py @@ -3,20 +3,23 @@ import json import re from collections import ChainMap -from functools import partial +from contextlib import suppress from pathlib import Path -from typing import Any, Callable, Dict +from typing import Any, Callable, ChainMap as t_ChainMap, Dict, Iterable, List, Union import yaml from iteration_utilities import deepflatten from jinja2 import UndefinedError from jinja2.sandbox import SandboxedEnvironment -from plumbum.cli.terminal import ask, choose, prompt -from plumbum.colors import bold, info, italics +from prompt_toolkit.lexers import PygmentsLexer +from pydantic import BaseModel, Field, validator +from pygments.lexers.data import JsonLexer, YamlLexer +from questionary import prompt +from questionary.prompts.common import Choice from yamlinclude import YamlIncludeConstructor -from ..tools import get_jinja_env, printf_exception -from ..types import AnyByStrDict, Choices, OptStrOrPath, PathSeq, StrOrPath +from ..tools import cast_str_to_bool, force_str_end, get_jinja_env, printf_exception +from ..types import AnyByStrDict, OptStrOrPath, PathSeq, StrOrPath from .objects import DEFAULT_DATA, EnvOps, UserMessageError __all__ = ("load_config_data", "query_user_data") @@ -44,6 +47,343 @@ class InvalidTypeError(TypeError): pass +class Question(BaseModel): + """One question asked to the user. + + All attributes are init kwargs. + + Attributes: + choices: + Selections available for the user if the question requires them. + Can be templated. + + default: + Default value presented to the user to make it easier to respond. + Can be templated. + + help_text: + Additional text printed to the user, explaining the purpose of + this question. Can be templated. + + multiline: + Indicates if the question should allow multiline input. Defaults + to `True` for JSON and YAML questions, and to `False` otherwise. + Only meaningful for str-based questions. Can be templated. + + placeholder: + Text that appears if there's nothing written in the input field, + but disappears as soon as the user writes anything. Can be templated. + + questionary: + Reference to the [Questionary][] object where this [Question][] is + attached. + + secret: + Indicates if the question should be removed from the answers file. + If the question type is str, it will hide user input on the screen + by displaying asterisks: `****`. + + type_name: + The type of question. Affects the rendering, validation and filtering. + Can be templated. + + var_name: + Question name in the answers dict. + + when: + Condition that, if `False`, skips the question. Can be templated. + If it is a boolean, it is used directly. If it is a str, it is + converted to boolean using a parser similar to YAML, but only for + boolean values. + """ + + choices: Union[Dict[Any, Any], List[Any]] = Field(default_factory=list) + default: Any = None + help_text: str = "" + multiline: Union[str, bool] = False + placeholder: str = "" + questionary: "Questionary" + secret: bool = False + type_name: str = "" + var_name: str + when: Union[str, bool] = True + + class Config: + arbitrary_types_allowed = True + + def __init__(self, **kwargs): + # Transform arguments that are named like python keywords + to_rename = (("help", "help_text"), ("type", "type_name")) + for from_, to in to_rename: + with suppress(KeyError): + kwargs.setdefault(to, kwargs.pop(from_)) + # Infer type from default if missing + super().__init__(**kwargs) + self.questionary.questions.append(self) + + def __repr__(self): + return f"Question({self.var_name})" + + @validator("var_name") + def _check_var_name(cls, v): + if v in DEFAULT_DATA: + raise ValueError("Invalid question name") + return v + + @validator("type_name", always=True) + def _check_type_name(cls, v, values): + if v == "": + default_type_name = type(values.get("default")).__name__ + v = default_type_name if default_type_name in CAST_STR_TO_NATIVE else "yaml" + return v + + def _iter_choices(self) -> Iterable[Choice]: + """Iterates choices in a format that the questionary lib likes.""" + choices = self.choices + if isinstance(self.choices, dict): + choices = list(self.choices.items()) + for choice in choices: + # If a choice is a dict, it can be used raw + if isinstance(choice, dict): + name = choice["name"] + value = choice["value"] + # ... or a value pair + elif isinstance(choice, (tuple, list)): + name, value = choice + # However, a choice can also be a single value... + else: + name = value = choice + # The name must always be a str + name = str(name) + # The value can be templated + value = self.render_value(value) + yield Choice(name, value) + + def get_default(self, for_rendering: bool) -> Any: + """Get the default value for this question. + + Parameters: + for_rendering: + Indicates if the result should be returned casted for normal + usage, or casted for rendering. + """ + cast_fn = self.get_cast_fn() + try: + result = self.questionary.answers_forced[self.var_name] + except KeyError: + try: + result = self.questionary.answers_last[self.var_name] + except KeyError: + result = self.render_value(self.default) + result = cast_answer_type(result, cast_fn) + if not for_rendering or self.type_name == "bool": + return result + if result is None: + return "" + else: + return str(result) + + def get_choices(self) -> List[Choice]: + """Obtain choices rendered and properly formatted.""" + return list(self._iter_choices()) + + def filter_answer(self, answer) -> Any: + """Cast the answer to the desired type.""" + if answer == self.get_default(for_rendering=True): + return self.get_default(for_rendering=False) + return cast_answer_type(answer, self.get_cast_fn()) + + def get_message(self) -> str: + """Get the message that will be printed to the user.""" + message = "" + if self.help_text: + rendered_help = self.render_value(self.help_text) + message = force_str_end(rendered_help) + message += f"{self.var_name}? Format: {self.type_name}" + return message + + def get_placeholder(self) -> str: + """Render and obtain the placeholder.""" + return self.render_value(self.placeholder) + + def get_questionary_structure(self): + """Get the question in a format that the questionary lib understands.""" + lexer = None + result = { + "default": self.get_default(for_rendering=True), + "filter": self.filter_answer, + "message": self.get_message(), + "mouse_support": True, + "name": self.var_name, + "qmark": "🕵️" if self.secret else "🎤", + "when": self.get_when, + } + questionary_type = "input" + if self.type_name == "bool": + questionary_type = "confirm" + if self.choices: + questionary_type = "select" + result["choices"] = self.get_choices() + # The default value must be a choice object from the list + for choice in result["choices"]: + if result["default"] == choice.value: + result["default"] = choice + break + else: + result.pop("default") # Default is not selectable + if questionary_type == "input": + if self.secret: + questionary_type = "password" + elif self.type_name == "yaml": + lexer = PygmentsLexer(YamlLexer) + elif self.type_name == "json": + lexer = PygmentsLexer(JsonLexer) + if lexer: + result["lexer"] = lexer + result["multiline"] = self.get_multiline() + placeholder = self.get_placeholder() + if placeholder: + result["placeholder"] = placeholder + result["validate"] = self.validate_answer + result.update({"type": questionary_type}) + return result + + def get_cast_fn(self) -> Callable: + """Obtain function to cast user answer to desired type.""" + type_name = self.render_value(self.type_name) + if type_name not in CAST_STR_TO_NATIVE: + raise InvalidTypeError("Invalid question type") + return CAST_STR_TO_NATIVE.get(type_name, parse_yaml_string) + + def get_multiline(self) -> bool: + """Get the value for multiline.""" + multiline = self.render_value(self.multiline) + multiline = cast_answer_type(multiline, cast_str_to_bool) + return bool(multiline) + + def validate_answer(self, answer) -> bool: + """Validate user answer.""" + cast_fn = self.get_cast_fn() + try: + cast_fn(answer) + return True + except Exception: + return False + + def get_when(self, answers) -> bool: + """Get skip condition for question.""" + if ( + # Skip on --force + not self.questionary.ask_user + # Skip on --data=this_question=some_answer + or self.var_name in self.questionary.answers_forced + ): + return False + when = self.when + when = self.render_value(when) + when = cast_answer_type(when, cast_str_to_bool) + return bool(when) + + def render_value(self, value: Any) -> str: + """Render a single templated value using Jinja. + + If the value cannot be used as a template, it will be returned as is. + """ + try: + template = self.questionary.env.from_string(value) + except TypeError: + # value was not a string + return value + try: + return template.render( + **self.questionary.get_best_answers(), **DEFAULT_DATA + ) + except UndefinedError as error: + raise UserMessageError(str(error)) from error + + +class Questionary(BaseModel): + """An object holding all [Question][] items and user answers. + + All attributes are also init kwargs. + + Attributes: + answers_default: + Default answers as specified in the template. + + answers_forced: + Answers forced by the user, either by an API call like + `data={'some_question': 'forced_answer'}` or by a CLI call like + `--data=some_question=forced_answer`. + + answers_last: + Answers obtained from the `.copier-answers.yml` file. + + answers_user: + Dict containing user answers for the current questionary. It should + be empty always. + + ask_user: + Indicates if the questionary should be asked, or just forced. + + env: + The Jinja environment for rendering. + + questions: + A list containing all [Question][] objects for this [Questionary][]. + """ + + answers_default: AnyByStrDict = Field(default_factory=dict) + answers_forced: AnyByStrDict = Field(default_factory=dict) + answers_last: AnyByStrDict = Field(default_factory=dict) + answers_user: AnyByStrDict = Field(default_factory=dict) + ask_user: bool = True + env: SandboxedEnvironment + questions: List[Question] = Field(default_factory=list) + + class Config: + arbitrary_types_allowed = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def get_best_answers(self) -> t_ChainMap[str, Any]: + """Get dict-like object with the best answers for each question.""" + return ChainMap( + self.answers_user, + self.answers_last, + self.answers_forced, + self.answers_default, + ) + + def get_answers(self) -> AnyByStrDict: + """Obtain answers for all questions. + + It produces a TUI for querying the user if `ask_user` is true. Otherwise, + it gets answers from other sources. + """ + previous_answers = self.get_best_answers() + if self.ask_user: + self.answers_user = prompt( + (question.get_questionary_structure() for question in self.questions), + answers=previous_answers, + ) + # HACK https://github.com/tmbo/questionary/issues/74 + if not self.answers_user and self.questions: + raise KeyboardInterrupt + else: + # Avoid prompting to not requiring a TTy when --force + for question in self.questions: + new_answer = question.get_default(for_rendering=False) + previous_answer = previous_answers.get(question.var_name) + if new_answer != previous_answer: + self.answers_user[question.var_name] = new_answer + return self.answers_user + + +Question.update_forward_refs() + + def load_yaml_data(conf_path: Path, quiet: bool = False) -> AnyByStrDict: """Load the `copier.yml` file. @@ -136,102 +476,32 @@ def cast_answer_type(answer: Any, type_fn: Callable) -> Any: return answer -def render_value(value: Any, env: SandboxedEnvironment, context: AnyByStrDict) -> str: - """Render a single templated value using Jinja. - - If the value cannot be used as a template, it will be returned as is. - """ - try: - template = env.from_string(value) - except TypeError: - # value was not a string - return value - try: - return template.render(**context) - except UndefinedError as error: - raise UserMessageError(str(error)) from error - - -def render_choices( - choices: Choices, env: SandboxedEnvironment, context: AnyByStrDict -) -> Choices: - """Render a list or dictionary of templated choices using Jinja.""" - render = partial(render_value, env=env, context=context) - if isinstance(choices, dict): - choices = {render(k): render(v) for k, v in choices.items()} - elif isinstance(choices, list): - for i, choice in enumerate(choices): - if isinstance(choice, (tuple, list)) and len(choice) == 2: - choices[i] = (render(choice[0]), render(choice[1])) - else: - choices[i] = render(choice) - return choices +CAST_STR_TO_NATIVE: Dict[str, Callable] = { + "bool": cast_str_to_bool, + "float": float, + "int": int, + "json": json.loads, + "str": str, + "yaml": parse_yaml_string, +} def query_user_data( questions_data: AnyByStrDict, last_answers_data: AnyByStrDict, forced_answers_data: AnyByStrDict, + default_answers_data: AnyByStrDict, ask_user: bool, envops: EnvOps, ) -> AnyByStrDict: """Query the user for questions given in the config file.""" - type_maps: Dict[str, Callable] = { - "bool": bool, - "float": float, - "int": int, - "json": json.loads, - "str": str, - "yaml": parse_yaml_string, - } - env = get_jinja_env(envops=envops) - result: AnyByStrDict = {} - defaults: AnyByStrDict = {} - _render_value = partial( - render_value, - env=env, - context=ChainMap(result, forced_answers_data, defaults, DEFAULT_DATA), - ) - _render_choices = partial( - render_choices, - env=env, - context=ChainMap(result, forced_answers_data, defaults, DEFAULT_DATA), + questionary = Questionary( + answers_forced=forced_answers_data, + answers_last=last_answers_data, + answers_default=default_answers_data, + ask_user=ask_user, + env=get_jinja_env(envops=envops), ) - for question, details in questions_data.items(): - # Get question type; by default let YAML decide it - type_name = _render_value(details.get("type", "yaml")) - try: - type_fn = type_maps[type_name] - except KeyError: - raise InvalidTypeError() - # Get default answer - ask_this = ask_user - default = cast_answer_type(_render_value(details.get("default")), type_fn) - defaults[question] = default - try: - # Use forced answer - answer = forced_answers_data[question] - ask_this = False - except KeyError: - # Get default answer - answer = last_answers_data.get(question, default) - if ask_this: - # Generate message to ask the user - emoji = "🕵️" if details.get("secret", False) else "🎤" - message = f"\n{bold | question}? Format: {type_name}\n{emoji} " - if details.get("help"): - message = ( - f"\n{info & italics | _render_value(details['help'])}{message}" - ) - # Use the right method to ask - if type_fn is bool: - answer = ask(message, answer) - elif details.get("choices"): - choices = _render_choices(details["choices"]) - answer = choose(message, choices, answer) - else: - answer = prompt(message, type_fn, answer) - if answer != details.get("default", default): - result[question] = cast_answer_type(answer, type_fn) - return result + Question(var_name=question, questionary=questionary, **details) + return questionary.get_answers() diff --git a/copier/tools.py b/copier/tools.py index fefa99f28..5797f112a 100644 --- a/copier/tools.py +++ b/copier/tools.py @@ -85,6 +85,27 @@ def required(value: T, **kwargs: Any) -> T: return value +def cast_str_to_bool(value: Any) -> bool: + """Parse a string to bool, YAML-like. + + Params: + value: + Anything to be casted to a bool. Usually a `str`. + """ + try: + lower = value.lower() + except AttributeError: + return bool(value) + result = None + if lower in {"1", "y", "yes", "t", "true", "on"}: + result = True + elif lower in {"", "0", "n", "no", "f", "false", "off"}: + result = False + if result is None: + raise ValueError("Invalid bool value") + return result + + def make_folder(folder: Path) -> None: if not folder.exists(): try: @@ -110,7 +131,7 @@ def to_nice_yaml(data: Any, **kwargs) -> str: def get_jinja_env( - envops: EnvOps, + envops: "EnvOps", filters: Optional[Filters] = None, paths: Optional[LoaderPaths] = None, **kwargs: Any, @@ -131,8 +152,8 @@ def get_jinja_env( class Renderer: """The Jinja template renderer.""" - def __init__(self, conf: ConfigData) -> None: - envops: EnvOps = conf.envops + def __init__(self, conf: "ConfigData") -> None: + envops: "EnvOps" = conf.envops paths = [str(conf.src_path)] + list(map(str, conf.extra_paths or [])) self.env = get_jinja_env(envops=envops, paths=paths) self.conf = conf @@ -172,6 +193,18 @@ def normalize_str(text: StrOrPath, form: str = "NFD") -> str: return unicodedata.normalize(form, str(text)) +def force_str_end(original_str: str, end: str = "\n") -> str: + """Make sure a `original_str` ends with `end`. + + Params: + original_str: String that you want to ensure ending. + end: String that must exist at the end of `original_str` + """ + if not original_str.endswith(end): + return original_str + end + return original_str + + def create_path_filter(patterns: StrOrPathSeq) -> CheckPathFunc: """Returns a function that matches a path against given patterns.""" patterns = [normalize_str(p) for p in patterns] @@ -183,7 +216,7 @@ def match(path: StrOrPath) -> bool: return match -def get_migration_tasks(conf: ConfigData, stage: str) -> List[Dict]: +def get_migration_tasks(conf: "ConfigData", stage: str) -> List[Dict]: """Get migration objects that match current version spec. Versions are compared using PEP 440. diff --git a/poetry.lock b/poetry.lock index db395f8f2..f67f46769 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,82 +1,74 @@ [[package]] -category = "dev" -description = "apipkg: namespace control and lazy-import mechanism" name = "apipkg" +version = "1.5" +description = "apipkg: namespace control and lazy-import mechanism" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.5" [[package]] -category = "dev" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." name = "appdirs" -optional = false -python-versions = "*" version = "1.4.4" - -[[package]] +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" -description = "Disable App Nap on OS X 10.9" -marker = "python_version >= \"3.4\" and sys_platform == \"darwin\"" -name = "appnope" optional = false python-versions = "*" -version = "0.1.0" [[package]] -category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" 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.*" -version = "1.4.0" [[package]] -category = "dev" -description = "Classes Without Boilerplate" name = "attrs" +version = "20.2.0" +description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.2.0" [package.extras] -dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] [[package]] +name = "autoflake" +version = "1.4" +description = "Removes unused imports and unused variables" category = "dev" -description = "Specifications for callback functions passed in to an API" -marker = "python_version >= \"3.4\"" -name = "backcall" optional = false python-versions = "*" -version = "0.2.0" + +[package.dependencies] +pyflakes = ">=1.1.0" [[package]] -category = "main" -description = "Screen-scraping library" name = "beautifulsoup4" +version = "4.9.3" +description = "Screen-scraping library" +category = "main" optional = false python-versions = "*" -version = "4.9.1" [package.dependencies] -soupsieve = [">1.2", "<2.0"] +soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} [package.extras] html5lib = ["html5lib"] lxml = ["lxml"] [[package]] -category = "dev" -description = "The uncompromising code formatter." name = "black" +version = "19.10b0" +description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.6" -version = "19.10b0" [package.dependencies] appdirs = "*" @@ -91,89 +83,87 @@ typed-ast = ">=1.4.0" d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] -category = "main" -description = "Python package for providing Mozilla's CA Bundle." name = "certifi" +version = "2020.6.20" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = "*" -version = "2020.6.20" [[package]] -category = "dev" -description = "Validate configuration and produce human readable error messages." name = "cfgv" +version = "3.2.0" +description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false -python-versions = ">=3.6" -version = "3.0.0" +python-versions = ">=3.6.1" [[package]] -category = "main" -description = "Universal encoding detector for Python 2 and 3" name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "main" optional = false python-versions = "*" -version = "3.0.4" [[package]] -category = "main" -description = "Composable command line interface toolkit" name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" [[package]] -category = "main" -description = "Cross-platform colored terminal text." name = "colorama" +version = "0.4.3" +description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" [[package]] -category = "dev" -description = "Code coverage measurement for Python" name = "coverage" +version = "5.3" +description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.3" [package.extras] toml = ["toml"] [[package]] -category = "main" -description = "A backport of the dataclasses module for Python 3.6" -marker = "python_version < \"3.7\"" name = "dataclasses" +version = "0.7" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" optional = false -python-versions = "*" -version = "0.6" +python-versions = ">=3.6, <3.7" [[package]] +name = "distlib" +version = "0.3.1" +description = "Distribution utilities" category = "dev" -description = "Decorators for Humans" -marker = "python_version >= \"3.4\"" -name = "decorator" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.4.2" +python-versions = "*" [[package]] -category = "main" -description = "EditorConfig File Locator and Interpreter for Python" name = "editorconfig" +version = "0.12.2" +description = "EditorConfig File Locator and Interpreter for Python" +category = "main" optional = false python-versions = "*" -version = "0.12.2" [[package]] -category = "dev" -description = "execnet: rapid multi-Python deployment" name = "execnet" +version = "1.7.1" +description = "execnet: rapid multi-Python deployment" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.7.1" [package.dependencies] apipkg = ">=1.4" @@ -182,107 +172,105 @@ apipkg = ">=1.4" testing = ["pre-commit"] [[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." category = "dev" -description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = "*" + +[[package]] name = "flake8" +version = "3.8.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.3" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.6.0a1,<2.7.0" pyflakes = ">=2.2.0,<2.3.0" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - [[package]] -category = "dev" -description = "A flake8 extension that checks for blind except: statements" name = "flake8-blind-except" +version = "0.1.1" +description = "A flake8 extension that checks for blind except: statements" +category = "dev" optional = false python-versions = "*" -version = "0.1.1" - -[package.dependencies] -setuptools = "*" [[package]] -category = "dev" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." name = "flake8-bugbear" +version = "20.1.4" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" optional = false python-versions = ">=3.6" -version = "20.1.4" [package.dependencies] attrs = ">=19.2.0" flake8 = ">=3.0.0" [[package]] -category = "dev" -description = "A flake8 plugin to help you write better list/set/dict comprehensions." name = "flake8-comprehensions" +version = "3.2.3" +description = "A flake8 plugin to help you write better list/set/dict comprehensions." +category = "dev" optional = false python-versions = ">=3.5" -version = "3.2.3" [package.dependencies] flake8 = ">=3.0,<3.2.0 || >3.2.0,<4" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] -category = "dev" -description = "ipdb/pdb statement checker plugin for flake8" name = "flake8-debugger" +version = "3.2.1" +description = "ipdb/pdb statement checker plugin for flake8" +category = "dev" optional = false python-versions = "*" -version = "3.2.1" [package.dependencies] flake8 = ">=1.5" pycodestyle = "*" [[package]] -category = "main" -description = "Clean single-source support for Python 3 and 2" name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.18.2" [[package]] -category = "dev" -description = "File identification library for Python" name = "identify" +version = "1.5.5" +description = "File identification library for Python" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "1.5.5" [package.extras] license = ["editdistance"] [[package]] -category = "main" -description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10" [[package]] -category = "main" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" name = "importlib-metadata" +version = "2.0.0" +description = "Read metadata from Python packages" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "2.0.0" [package.dependencies] zipp = ">=0.5" @@ -292,94 +280,34 @@ docs = ["sphinx", "rst.linker"] testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] -category = "dev" -description = "Read resources from Python packages" -marker = "python_version < \"3.7\"" name = "importlib-resources" +version = "3.0.0" +description = "Read resources from Python packages" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "3.0.0" [package.dependencies] -[package.dependencies.zipp] -python = "<3.8" -version = ">=0.4" +zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} [package.extras] docs = ["sphinx", "rst.linker", "jaraco.packaging"] [[package]] -category = "dev" -description = "iniconfig: brain-dead simple config-ini parsing" name = "iniconfig" -optional = false -python-versions = "*" version = "1.0.1" - -[[package]] -category = "dev" -description = "IPython-enabled pdb" -name = "ipdb" -optional = false -python-versions = ">=2.7" -version = "0.13.3" - -[package.dependencies] -setuptools = "*" - -[package.dependencies.ipython] -python = ">=3.4" -version = ">=5.1.0" - -[[package]] -category = "dev" -description = "IPython: Productive Interactive Computing" -marker = "python_version >= \"3.4\"" -name = "ipython" -optional = false -python-versions = ">=3.6" -version = "7.16.1" - -[package.dependencies] -appnope = "*" -backcall = "*" -colorama = "*" -decorator = "*" -jedi = ">=0.10" -pexpect = "*" -pickleshare = "*" -prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" -pygments = "*" -setuptools = ">=18.5" -traitlets = ">=4.2" - -[package.extras] -all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "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.14)"] - -[[package]] +description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" -description = "Vestigial utilities from IPython" -marker = "python_version >= \"3.4\"" -name = "ipython-genutils" optional = false python-versions = "*" -version = "0.2.0" [[package]] -category = "main" -description = "Utilities based on Pythons iterators and generators." name = "iteration-utilities" +version = "0.10.1" +description = "Utilities based on Pythons iterators and generators." +category = "main" optional = false python-versions = ">=3.5" -version = "0.10.1" [package.extras] all = ["pytest", "sphinx", "numpydoc"] @@ -387,28 +315,12 @@ doc = ["sphinx", "numpydoc"] test = ["pytest"] [[package]] -category = "dev" -description = "An autocompletion tool for Python that can be used for text editors." -marker = "python_version >= \"3.4\"" -name = "jedi" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.17.2" - -[package.dependencies] -parso = ">=0.7.0,<0.8.0" - -[package.extras] -qa = ["flake8 (3.7.9)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] - -[[package]] -category = "main" -description = "A very fast and expressive template engine." name = "jinja2" +version = "2.11.2" +description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.11.2" [package.dependencies] MarkupSafe = ">=0.23" @@ -417,146 +329,133 @@ MarkupSafe = ">=0.23" i18n = ["Babel (>=0.8)"] [[package]] -category = "main" -description = "Lightweight pipelining: using Python functions as pipeline jobs." -marker = "python_version > \"2.7\"" name = "joblib" +version = "0.17.0" +description = "Lightweight pipelining: using Python functions as pipeline jobs." +category = "main" optional = false python-versions = ">=3.6" -version = "0.16.0" [[package]] -category = "main" -description = "JavaScript unobfuscator and beautifier." name = "jsbeautifier" +version = "1.13.0" +description = "JavaScript unobfuscator and beautifier." +category = "main" optional = false python-versions = "*" -version = "1.13.0" [package.dependencies] editorconfig = ">=0.12.2" six = ">=1.13.0" [[package]] -category = "main" -description = "Python LiveReload is an awesome tool for web developers" name = "livereload" +version = "2.6.3" +description = "Python LiveReload is an awesome tool for web developers" +category = "main" optional = false python-versions = "*" -version = "2.6.3" [package.dependencies] six = "*" - -[package.dependencies.tornado] -python = ">=2.8" -version = "*" +tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] -category = "main" -description = "A Python implementation of Lunr.js" name = "lunr" +version = "0.5.8" +description = "A Python implementation of Lunr.js" +category = "main" optional = false python-versions = "*" -version = "0.5.8" [package.dependencies] future = ">=0.16.0" +nltk = {version = ">=3.2.5", optional = true, markers = "python_version > \"2.7\" and extra == \"languages\""} six = ">=1.11.0" -[package.dependencies.nltk] -optional = true -python = ">=2.8" -version = ">=3.2.5" - [package.extras] languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] [[package]] -category = "main" -description = "Python implementation of Markdown." name = "markdown" +version = "3.2.2" +description = "Python implementation of Markdown." +category = "main" optional = false python-versions = ">=3.5" -version = "3.2.2" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage", "pyyaml"] [[package]] -category = "main" -description = "Safely add untrusted strings to HTML/XML markup." name = "markupsafe" +version = "1.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.1.1" [[package]] -category = "dev" -description = "McCabe checker, plugin for flake8" name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = "*" -version = "0.6.1" [[package]] -category = "main" -description = "Project documentation with Markdown." name = "mkdocs" +version = "1.1.2" +description = "Project documentation with Markdown." +category = "main" optional = false python-versions = ">=3.5" -version = "1.1.2" [package.dependencies] +click = ">=3.3" Jinja2 = ">=2.10.1" +livereload = ">=2.5.1" +lunr = {version = "0.5.8", extras = ["languages"]} Markdown = ">=3.2.1" PyYAML = ">=3.10" -click = ">=3.3" -livereload = ">=2.5.1" tornado = ">=5.0" -[package.dependencies.lunr] -extras = ["languages"] -version = "0.5.8" - [[package]] -category = "main" -description = "A Material Design theme for MkDocs" name = "mkdocs-material" +version = "5.5.14" +description = "A Material Design theme for MkDocs" +category = "main" optional = false python-versions = "*" -version = "5.5.14" [package.dependencies] -Pygments = ">=2.4" markdown = ">=3.2" mkdocs = ">=1.1" mkdocs-material-extensions = ">=1.0" +Pygments = ">=2.4" pymdown-extensions = ">=7.0" [[package]] -category = "main" -description = "Extension pack for Python Markdown." name = "mkdocs-material-extensions" +version = "1.0.1" +description = "Extension pack for Python Markdown." +category = "main" optional = false python-versions = ">=3.5" -version = "1.0.1" [package.dependencies] mkdocs-material = ">=5.0.0" [[package]] -category = "main" -description = "A MkDocs plugin for including mermaid graphs in markdown sources" name = "mkdocs-mermaid2-plugin" +version = "0.5.0" +description = "A MkDocs plugin for including mermaid graphs in markdown sources" +category = "main" optional = false python-versions = ">=3.5" -version = "0.5.0" [package.dependencies] beautifulsoup4 = ">=4.6.3" @@ -566,40 +465,31 @@ mkdocs-material = "*" pymdown-extensions = ">=8.0" pyyaml = "*" requests = "*" -setuptools = ">=18.5" [[package]] -category = "main" -description = "Automatic documentation from sources, for MkDocs." name = "mkdocstrings" +version = "0.13.6" +description = "Automatic documentation from sources, for MkDocs." +category = "main" optional = false python-versions = ">=3.6,<4.0" -version = "0.13.4" [package.dependencies] beautifulsoup4 = ">=4.8.2,<5.0.0" mkdocs = ">=1.1,<2.0" pymdown-extensions = ">=6.3,<9.0" -pytkdocs = ">=0.2.0,<0.9.0" +pytkdocs = ">=0.2.0,<0.10.0" [package.extras] tests = ["coverage (>=5.2.1,<6.0.0)", "invoke (>=1.4.1,<2.0.0)", "mkdocs-material (>=5.5.12,<6.0.0)", "mypy (>=0.782,<0.783)", "pytest (>=6.0.1,<7.0.0)", "pytest-cov (>=2.10.1,<3.0.0)", "pytest-randomly (>=3.4.1,<4.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=2.1.0,<3.0.0)"] [[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" -optional = false -python-versions = ">=3.5" -version = "8.5.0" - -[[package]] -category = "dev" -description = "Optional static typing for Python" name = "mypy" +version = "0.782" +description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.782" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" @@ -610,21 +500,20 @@ typing-extensions = ">=3.7.4" dmypy = ["psutil (>=4.0)"] [[package]] -category = "dev" -description = "Experimental type system extensions for programs checked with the mypy typechecker." 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 = "*" -version = "0.4.3" [[package]] -category = "main" -description = "Natural Language Toolkit" -marker = "python_version > \"2.7\"" name = "nltk" +version = "3.5" +description = "Natural Language Toolkit" +category = "main" optional = false python-versions = "*" -version = "3.5" [package.dependencies] click = "*" @@ -641,183 +530,149 @@ tgrep = ["pyparsing"] twitter = ["twython"] [[package]] -category = "dev" -description = "Node.js virtual environment builder" name = "nodeenv" +version = "1.5.0" +description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = "*" -version = "1.5.0" [[package]] -category = "main" -description = "Core utilities for Python packages" name = "packaging" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "20.4" - -[package.dependencies] -pyparsing = ">=2.0.2" -six = "*" - -[[package]] -category = "dev" -description = "A Python Parser" -marker = "python_version >= \"3.4\"" -name = "parso" +description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.7.1" -[package.extras] -testing = ["docopt", "pytest (>=3.0.7)"] +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" [[package]] -category = "dev" -description = "Bring colors to your terminal." name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.2.1" [[package]] -category = "main" -description = "Utility library for gitignore style pattern matching of file paths." name = "pathspec" +version = "0.8.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" [[package]] -category = "dev" -description = "Pexpect allows easy control of interactive console applications." -marker = "python_version >= \"3.4\" and sys_platform != \"win32\"" name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" optional = false python-versions = "*" -version = "4.8.0" [package.dependencies] ptyprocess = ">=0.5" [[package]] -category = "dev" -description = "Tiny 'shelve'-like database with concurrency support" -marker = "python_version >= \"3.4\"" -name = "pickleshare" -optional = false -python-versions = "*" -version = "0.7.5" - -[[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" 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.*" -version = "0.13.1" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] [[package]] -category = "main" -description = "Plumbum: shell combinators library" name = "plumbum" +version = "1.6.9" +description = "Plumbum: shell combinators library" +category = "main" optional = false python-versions = ">=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.6.9" [[package]] -category = "dev" -description = "A task runner that works well with poetry." name = "poethepoet" +version = "0.7.0" +description = "A task runner that works well with poetry." +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "0.7.0" [package.dependencies] pastel = ">=0.2.0,<0.3.0" tomlkit = ">=0.7.0,<0.8.0" [[package]] -category = "dev" -description = "A framework for managing and maintaining multi-language pre-commit hooks." name = "pre-commit" +version = "2.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false -python-versions = ">=3.6" -version = "2.1.1" +python-versions = ">=3.6.1" [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-resources = {version = "*", markers = "python_version < \"3.7\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" toml = "*" -virtualenv = ">=15.2" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - -[package.dependencies.importlib-resources] -python = "<3.7" -version = "*" +virtualenv = ">=20.0.8" [[package]] -category = "dev" -description = "Library for building powerful interactive command lines in Python" -marker = "python_version >= \"3.4\"" name = "prompt-toolkit" +version = "3.0.7" +description = "Library for building powerful interactive command lines in Python" +category = "main" optional = false -python-versions = ">=3.6" -version = "3.0.3" +python-versions = ">=3.6.1" [package.dependencies] wcwidth = "*" [[package]] -category = "dev" -description = "Run a subprocess in a pseudo terminal" -marker = "python_version >= \"3.4\" and sys_platform != \"win32\"" name = "ptyprocess" +version = "0.6.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" optional = false python-versions = "*" -version = "0.6.0" [[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" name = "py" +version = "1.9.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.*" -version = "1.9.0" [[package]] -category = "dev" -description = "Python style guide checker" name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.6.0" [[package]] -category = "main" -description = "Data validation and settings management using python 3.6 type hinting" name = "pydantic" +version = "1.6.1" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" optional = false python-versions = ">=3.6" -version = "1.6.1" [package.dependencies] -[package.dependencies.dataclasses] -python = "<3.7" -version = ">=0.6" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] @@ -825,74 +680,70 @@ email = ["email-validator (>=1.0.3)"] typing_extensions = ["typing-extensions (>=3.7.2)"] [[package]] -category = "dev" -description = "passive checker of Python programs" name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.2.0" [[package]] -category = "main" -description = "Pygments is a syntax highlighting package written in Python." name = "pygments" +version = "2.7.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" optional = false python-versions = ">=3.5" -version = "2.7.1" [[package]] -category = "main" -description = "Extension pack for Python Markdown." name = "pymdown-extensions" +version = "8.0.1" +description = "Extension pack for Python Markdown." +category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "8.0" +python-versions = ">=3.5" [package.dependencies] Markdown = ">=3.2" [[package]] -category = "main" -description = "Python parsing module" name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" [[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" name = "pytest" +version = "6.1.1" +description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.5" -version = "6.0.2" [package.dependencies] -atomicwrites = ">=1.0" +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=17.4.0" -colorama = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" -more-itertools = ">=4.0.0" packaging = "*" pluggy = ">=0.12,<1.0" py = ">=1.8.2" toml = "*" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - [package.extras] checkqa_mypy = ["mypy (0.780)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] -category = "dev" -description = "Pytest plugin for measuring coverage." name = "pytest-cov" +version = "2.10.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.*" -version = "2.10.1" [package.dependencies] coverage = ">=4.4" @@ -902,35 +753,24 @@ pytest = ">=4.6" testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] [[package]] -category = "dev" -description = "run tests in isolated forked subprocesses" name = "pytest-forked" +version = "1.3.0" +description = "run tests in isolated forked subprocesses" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.3.0" [package.dependencies] py = "*" pytest = ">=3.10" [[package]] -category = "dev" -description = "py.test plugin to abort hanging tests" -name = "pytest-timeout" -optional = false -python-versions = "*" -version = "1.4.2" - -[package.dependencies] -pytest = ">=3.6.0" - -[[package]] -category = "dev" -description = "pytest xdist plugin for distributed testing and loop-on-failing modes" name = "pytest-xdist" +version = "2.1.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "dev" optional = false python-versions = ">=3.5" -version = "2.1.0" [package.dependencies] execnet = ">=1.1" @@ -942,31 +782,31 @@ psutil = ["psutil (>=3.0)"] testing = ["filelock"] [[package]] -category = "main" -description = "Load Python objects documentation." name = "pytkdocs" +version = "0.9.0" +description = "Load Python objects documentation." +category = "main" optional = false python-versions = ">=3.6,<4.0" -version = "0.8.0" [package.extras] tests = ["coverage (>=5.2.1,<6.0.0)", "invoke (>=1.4.1,<2.0.0)", "marshmallow (>=3.5.2,<4.0.0)", "mypy (>=0.782,<0.783)", "pydantic (>=1.5.1,<2.0.0)", "pytest (>=6.0.1,<7.0.0)", "pytest-cov (>=2.10.1,<3.0.0)", "pytest-randomly (>=3.4.1,<4.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=2.1.0,<3.0.0)"] [[package]] -category = "main" -description = "YAML parser and emitter for Python" name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.3.1" [[package]] -category = "main" -description = "Extending PyYAML with a custom constructor for including YAML files within YAML files" name = "pyyaml-include" +version = "1.2" +description = "Extending PyYAML with a custom constructor for including YAML files within YAML files" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.2" [package.dependencies] PyYAML = ">=3.12,<4.0.0 || >=5.0.0,<6.0" @@ -976,20 +816,37 @@ all = ["toml"] toml = ["toml"] [[package]] +name = "questionary" +version = "1.6.0" +description = "Python library to build pretty command line user prompts ⭐️" category = "main" -description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.6,<3.9" + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + +[package.source] +type = "git" +url = "https://github.com/Yajo/questionary.git" +reference = "text-custom-lexer" +resolved_reference = "4c54853fcd5d147f2a19ae2091c8dc6f23f0e131" + +[[package]] name = "regex" +version = "2020.10.11" +description = "Alternative regular expression module, to replace re." +category = "main" optional = false python-versions = "*" -version = "2020.7.14" [[package]] -category = "main" -description = "Python HTTP for Humans." name = "requests" +version = "2.24.0" +description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.24.0" [package.dependencies] certifi = ">=2017.4.17" @@ -1002,97 +859,79 @@ security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] [[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" [[package]] -category = "main" -description = "A modern CSS selector implementation for Beautiful Soup." name = "soupsieve" +version = "2.0.1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" optional = false -python-versions = "*" -version = "1.9.6" +python-versions = ">=3.5" [[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" +version = "0.10.1" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = "*" -version = "0.10.1" [[package]] -category = "dev" -description = "Style preserving TOML library" name = "tomlkit" +version = "0.7.0" +description = "Style preserving TOML library" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.7.0" [[package]] -category = "main" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." name = "tornado" +version = "6.0.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "main" optional = false python-versions = ">= 3.5" -version = "6.0.4" [[package]] -category = "main" -description = "Fast, Extensible Progress Meter" -marker = "python_version > \"2.7\"" name = "tqdm" +version = "4.50.0" +description = "Fast, Extensible Progress Meter" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.49.0" [package.extras] dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] [[package]] -category = "dev" -description = "Traitlets Python config system" -marker = "python_version >= \"3.4\"" -name = "traitlets" -optional = false -python-versions = "*" -version = "4.3.3" - -[package.dependencies] -decorator = "*" -ipython-genutils = "*" -six = "*" - -[package.extras] -test = ["pytest", "mock"] - -[[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" +version = "1.4.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = "*" -version = "1.4.1" [[package]] -category = "dev" -description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" optional = false python-versions = "*" -version = "3.7.4.3" [[package]] -category = "main" -description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" +version = "1.25.10" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.10" [package.extras] brotli = ["brotlipy (>=0.6.0)"] @@ -1100,34 +939,40 @@ secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] -category = "dev" -description = "Virtual Python Environment builder" name = "virtualenv" +version = "20.0.33" +description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "16.7.10" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +importlib-metadata = {version = ">=0.12,<3", markers = "python_version < \"3.8\""} +importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} +six = ">=1.9.0,<2" [package.extras] -docs = ["sphinx (>=1.8.0,<2)", "towncrier (>=18.5.0)", "sphinx-rtd-theme (>=0.4.2,<1)"] -testing = ["pytest (>=4.0.0,<5)", "coverage (>=4.5.0,<5)", "pytest-timeout (>=1.3.0,<2)", "six (>=1.10.0,<2)", "pytest-xdist", "pytest-localserver", "pypiserver", "mock", "xonsh"] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] [[package]] -category = "dev" -description = "Measures the displayed width of unicode strings in a terminal" -marker = "python_version >= \"3.4\"" name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" optional = false python-versions = "*" -version = "0.2.5" [[package]] -category = "main" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" name = "zipp" +version = "3.3.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.6" -version = "3.2.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -1137,9 +982,9 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt docs = ["mkdocstrings", "mkdocs-material", "mkdocs-mermaid2-plugin"] [metadata] -content-hash = "70b8a7ab1467b79303b2d87fc14dd5bf4a414bd03837b453dc60afe674feffcd" -lock-version = "1.0" -python-versions = "^3.6" +lock-version = "1.1" +python-versions = ">=3.6.1,<3.9" +content-hash = "0eeafc30d7464cce3ce241cefd03e18531d377020aaf60a66485fd4860f97056" [metadata.files] apipkg = [ @@ -1150,10 +995,6 @@ appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] -appnope = [ - {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, - {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, -] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -1162,14 +1003,13 @@ attrs = [ {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, ] -backcall = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +autoflake = [ + {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, ] beautifulsoup4 = [ - {file = "beautifulsoup4-4.9.1-py2-none-any.whl", hash = "sha256:e718f2342e2e099b640a34ab782407b7b676f47ee272d6739e60b8ea23829f2c"}, - {file = "beautifulsoup4-4.9.1-py3-none-any.whl", hash = "sha256:a6237df3c32ccfaee4fd201c8f5f9d9df619b93121d01353a64a73ce8c6ef9a8"}, - {file = "beautifulsoup4-4.9.1.tar.gz", hash = "sha256:73cc4d115b96f79c7d77c1c7f7a0a8d4c57860d1041df407dd1aae7f07a77fd7"}, + {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, + {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, + {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, ] black = [ {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, @@ -1180,8 +1020,8 @@ certifi = [ {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, ] cfgv = [ - {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, - {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, + {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, + {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -1232,12 +1072,12 @@ coverage = [ {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, ] dataclasses = [ - {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, - {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, ] -decorator = [ - {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, - {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +distlib = [ + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] editorconfig = [ {file = "EditorConfig-0.12.2-py2-none-any.whl", hash = "sha256:60d6f10b87d2572ac1581cb8c9f018163e2b13a9e49588f9fb6dc8c715a1744c"}, @@ -1247,9 +1087,13 @@ execnet = [ {file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"}, {file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"}, ] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] flake8 = [ - {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, - {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, + {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, + {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, ] flake8-blind-except = [ {file = "flake8-blind-except-0.1.1.tar.gz", hash = "sha256:aca3356633825544cec51997260fe31a8f24a1a2795ce8e81696b9916745e599"}, @@ -1289,17 +1133,6 @@ iniconfig = [ {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, ] -ipdb = [ - {file = "ipdb-0.13.3.tar.gz", hash = "sha256:d6f46d261c45a65e65a2f7ec69288a1c511e16206edb2875e7ec6b2f66997e78"}, -] -ipython = [ - {file = "ipython-7.16.1-py3-none-any.whl", hash = "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64"}, - {file = "ipython-7.16.1.tar.gz", hash = "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf"}, -] -ipython-genutils = [ - {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, - {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, -] iteration-utilities = [ {file = "iteration_utilities-0.10.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:958c2aa52795d1100d9caffc3ed6aeda0e23577e3bc5694b3c8b6177c85fa57d"}, {file = "iteration_utilities-0.10.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:209e8a7b224445b66e8114392d7c8be7dc16a5d8d9cbdfca05592c460e037ad0"}, @@ -1324,17 +1157,13 @@ iteration-utilities = [ {file = "iteration_utilities-0.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:8233731c39d614b4939557fa409b57fe136c7570aa54d57bffe0271fe9682424"}, {file = "iteration_utilities-0.10.1.tar.gz", hash = "sha256:536e3e87c5c139c775f9d95bb771c4b366a1d58eb7a39436d4ac839b53742569"}, ] -jedi = [ - {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, - {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, -] jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] joblib = [ - {file = "joblib-0.16.0-py3-none-any.whl", hash = "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49"}, - {file = "joblib-0.16.0.tar.gz", hash = "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6"}, + {file = "joblib-0.17.0-py3-none-any.whl", hash = "sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72"}, + {file = "joblib-0.17.0.tar.gz", hash = "sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5"}, ] jsbeautifier = [ {file = "jsbeautifier-1.13.0.tar.gz", hash = "sha256:f5565fbcd95f79945e124324815e586ae0d2e43df5af82a4400390e6ea789e8b"}, @@ -1406,12 +1235,8 @@ mkdocs-mermaid2-plugin = [ {file = "mkdocs_mermaid2_plugin-0.5.0-py3-none-any.whl", hash = "sha256:1619814fe0437cfe7675a1ead4b148488ee4ca95556186271049aa5334694796"}, ] mkdocstrings = [ - {file = "mkdocstrings-0.13.4-py3-none-any.whl", hash = "sha256:85ea48916f337cfb2f67374765c6f927b80ef44dd228adc22a863bf0ff71222b"}, - {file = "mkdocstrings-0.13.4.tar.gz", hash = "sha256:4fe4800aad44acff5465d7e3ee569cd555307f640cbc8c3933570cdeece3ed2b"}, -] -more-itertools = [ - {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, - {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, + {file = "mkdocstrings-0.13.6-py3-none-any.whl", hash = "sha256:79d2a16b8c86a467bdc84846dfb90552551d2d9fd35578df9f92de13fb3b4537"}, + {file = "mkdocstrings-0.13.6.tar.gz", hash = "sha256:79e5086c79f60d1ae1d4b222f658d348ebdd6302c970cc06ee8394f2839d7c4d"}, ] mypy = [ {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, @@ -1444,10 +1269,6 @@ packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] -parso = [ - {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, - {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, -] pastel = [ {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, @@ -1460,10 +1281,6 @@ 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"}, -] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -1477,12 +1294,12 @@ poethepoet = [ {file = "poethepoet-0.7.0.tar.gz", hash = "sha256:1b2e073c6a97790ecfa5733b597324edb5ae8e27991c0c0274be9924036ffecc"}, ] pre-commit = [ - {file = "pre_commit-2.1.1-py2.py3-none-any.whl", hash = "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6"}, - {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"}, + {file = "pre_commit-2.7.1-py2.py3-none-any.whl", hash = "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a"}, + {file = "pre_commit-2.7.1.tar.gz", hash = "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, - {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, + {file = "prompt_toolkit-3.0.7-py3-none-any.whl", hash = "sha256:83074ee28ad4ba6af190593d4d4c607ff525272a504eb159199b6dd9f950c950"}, + {file = "prompt_toolkit-3.0.7.tar.gz", hash = "sha256:822f4605f28f7d2ba6b0b09a31e25e140871e96364d1d377667b547bb3bf4489"}, ] ptyprocess = [ {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, @@ -1524,16 +1341,16 @@ pygments = [ {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"}, ] pymdown-extensions = [ - {file = "pymdown-extensions-8.0.tar.gz", hash = "sha256:440b0db9823b89f9917482ce3ab3d32ac18e60f2e186770ac37836830d5e7256"}, - {file = "pymdown_extensions-8.0-py2.py3-none-any.whl", hash = "sha256:c3b18b719adc3a441c4edbcf691157998dbd333384a8e2caf0690985a6e586f0"}, + {file = "pymdown-extensions-8.0.1.tar.gz", hash = "sha256:9ba704052d4bdc04a7cd63f7db4ef6add73bafcef22c0cf6b2e3386cf4ece51e"}, + {file = "pymdown_extensions-8.0.1-py2.py3-none-any.whl", hash = "sha256:a3689c04f4cbddacd9d569425c571ae07e2673cc4df63a26cdbf1abc15229137"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.0.2-py3-none-any.whl", hash = "sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40"}, - {file = "pytest-6.0.2.tar.gz", hash = "sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"}, + {file = "pytest-6.1.1-py3-none-any.whl", hash = "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9"}, + {file = "pytest-6.1.1.tar.gz", hash = "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"}, ] pytest-cov = [ {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, @@ -1543,17 +1360,13 @@ pytest-forked = [ {file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"}, {file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"}, ] -pytest-timeout = [ - {file = "pytest-timeout-1.4.2.tar.gz", hash = "sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76"}, - {file = "pytest_timeout-1.4.2-py2.py3-none-any.whl", hash = "sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063"}, -] pytest-xdist = [ {file = "pytest-xdist-2.1.0.tar.gz", hash = "sha256:82d938f1a24186520e2d9d3a64ef7d9ac7ecdf1a0659e095d18e596b8cbd0672"}, {file = "pytest_xdist-2.1.0-py3-none-any.whl", hash = "sha256:7c629016b3bb006b88ac68e2b31551e7becf173c76b977768848e2bbed594d90"}, ] pytkdocs = [ - {file = "pytkdocs-0.8.0-py3-none-any.whl", hash = "sha256:7fdaf52a786e537d8fdeecaf2f4957c459525834ecdf228020b2d86fc1f813a5"}, - {file = "pytkdocs-0.8.0.tar.gz", hash = "sha256:b28d2b6b05930fcab2ed5d9e971c4c0582582b19bd38eadb41b30dffd77346b8"}, + {file = "pytkdocs-0.9.0-py3-none-any.whl", hash = "sha256:12ed87d71b3518301c7b8c12c1a620e4b481a9d2fca1038aea665955000fad7f"}, + {file = "pytkdocs-0.9.0.tar.gz", hash = "sha256:c8c39acb63824f69c3f6f58b3aed6ae55250c35804b76fd0cba09d5c11be13da"}, ] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, @@ -1572,28 +1385,35 @@ pyyaml-include = [ {file = "pyyaml-include-1.2.tar.gz", hash = "sha256:2343c4dad744d3ce907ec50683b29b4383c7c967f142275bdad8ed56d4de9d94"}, {file = "pyyaml_include-1.2-py2.py3-none-any.whl", hash = "sha256:2d4e3843110cf515aaf1568b217b05061f8e9f57cd3b35e8e654c838717796cb"}, ] +questionary = [] regex = [ - {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, - {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, - {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, - {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, - {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, - {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, - {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, - {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, - {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, + {file = "regex-2020.10.11-cp27-cp27m-win32.whl", hash = "sha256:4f5c0fe46fb79a7adf766b365cae56cafbf352c27358fda811e4a1dc8216d0db"}, + {file = "regex-2020.10.11-cp27-cp27m-win_amd64.whl", hash = "sha256:39a5ef30bca911f5a8a3d4476f5713ed4d66e313d9fb6755b32bec8a2e519635"}, + {file = "regex-2020.10.11-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:7c4fc5a8ec91a2254bb459db27dbd9e16bba1dabff638f425d736888d34aaefa"}, + {file = "regex-2020.10.11-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d537e270b3e6bfaea4f49eaf267984bfb3628c86670e9ad2a257358d3b8f0955"}, + {file = "regex-2020.10.11-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a8240df4957a5b0e641998a5d78b3c4ea762c845d8cb8997bf820626826fde9a"}, + {file = "regex-2020.10.11-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4302153abb96859beb2c778cc4662607a34175065fc2f33a21f49eb3fbd1ccd3"}, + {file = "regex-2020.10.11-cp36-cp36m-win32.whl", hash = "sha256:c077c9d04a040dba001cf62b3aff08fd85be86bccf2c51a770c77377662a2d55"}, + {file = "regex-2020.10.11-cp36-cp36m-win_amd64.whl", hash = "sha256:46ab6070b0d2cb85700b8863b3f5504c7f75d8af44289e9562195fe02a8dd72d"}, + {file = "regex-2020.10.11-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:d629d750ebe75a88184db98f759633b0a7772c2e6f4da529f0027b4a402c0e2f"}, + {file = "regex-2020.10.11-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8e7ef296b84d44425760fe813cabd7afbb48c8dd62023018b338bbd9d7d6f2f0"}, + {file = "regex-2020.10.11-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:e490f08897cb44e54bddf5c6e27deca9b58c4076849f32aaa7a0b9f1730f2c20"}, + {file = "regex-2020.10.11-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:850339226aa4fec04916386577674bb9d69abe0048f5d1a99f91b0004bfdcc01"}, + {file = "regex-2020.10.11-cp37-cp37m-win32.whl", hash = "sha256:60c4f64d9a326fe48e8738c3dbc068e1edc41ff7895a9e3723840deec4bc1c28"}, + {file = "regex-2020.10.11-cp37-cp37m-win_amd64.whl", hash = "sha256:8ba3efdd60bfee1aa784dbcea175eb442d059b576934c9d099e381e5a9f48930"}, + {file = "regex-2020.10.11-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2308491b3e6c530a3bb38a8a4bb1dc5fd32cbf1e11ca623f2172ba17a81acef1"}, + {file = "regex-2020.10.11-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b8806649983a1c78874ec7e04393ef076805740f6319e87a56f91f1767960212"}, + {file = "regex-2020.10.11-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a2a31ee8a354fa3036d12804730e1e20d58bc4e250365ead34b9c30bbe9908c3"}, + {file = "regex-2020.10.11-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9d53518eeed12190744d366ec4a3f39b99d7daa705abca95f87dd8b442df4ad"}, + {file = "regex-2020.10.11-cp38-cp38-win32.whl", hash = "sha256:3d5a8d007116021cf65355ada47bf405656c4b3b9a988493d26688275fde1f1c"}, + {file = "regex-2020.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:f579caecbbca291b0fcc7d473664c8c08635da2f9b1567c22ea32311c86ef68c"}, + {file = "regex-2020.10.11-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8c8c42aa5d3ac9a49829c4b28a81bebfa0378996f9e0ca5b5ab8a36870c3e5ee"}, + {file = "regex-2020.10.11-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c529ba90c1775697a65b46c83d47a2d3de70f24d96da5d41d05a761c73b063af"}, + {file = "regex-2020.10.11-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:6cf527ec2f3565248408b61dd36e380d799c2a1047eab04e13a2b0c15dd9c767"}, + {file = "regex-2020.10.11-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:671c51d352cfb146e48baee82b1ee8d6ffe357c292f5e13300cdc5c00867ebfc"}, + {file = "regex-2020.10.11-cp39-cp39-win32.whl", hash = "sha256:a63907332531a499b8cdfd18953febb5a4c525e9e7ca4ac147423b917244b260"}, + {file = "regex-2020.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:1a16afbfadaadc1397353f9b32e19a65dc1d1804c80ad73a14f435348ca017ad"}, + {file = "regex-2020.10.11.tar.gz", hash = "sha256:463e770c48da76a8da82b8d4a48a541f314e0df91cbb6d873a341dbe578efafd"}, ] requests = [ {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, @@ -1604,8 +1424,8 @@ six = [ {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] soupsieve = [ - {file = "soupsieve-1.9.6-py2.py3-none-any.whl", hash = "sha256:feb1e937fa26a69e08436aad4a9037cd7e1d4c7212909502ba30701247ff8abd"}, - {file = "soupsieve-1.9.6.tar.gz", hash = "sha256:7985bacc98c34923a439967c1a602dc4f1e15f923b6fcf02344184f86cc7efaa"}, + {file = "soupsieve-2.0.1-py3-none-any.whl", hash = "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55"}, + {file = "soupsieve-2.0.1.tar.gz", hash = "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"}, ] toml = [ {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, @@ -1627,12 +1447,8 @@ tornado = [ {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] tqdm = [ - {file = "tqdm-4.49.0-py2.py3-none-any.whl", hash = "sha256:8f3c5815e3b5e20bc40463fa6b42a352178859692a68ffaa469706e6d38342a5"}, - {file = "tqdm-4.49.0.tar.gz", hash = "sha256:faf9c671bd3fad5ebaeee366949d969dca2b2be32c872a7092a1e1a9048d105b"}, -] -traitlets = [ - {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, - {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, + {file = "tqdm-4.50.0-py2.py3-none-any.whl", hash = "sha256:2dd75fdb764f673b8187643496fcfbeac38348015b665878e582b152f3391cdb"}, + {file = "tqdm-4.50.0.tar.gz", hash = "sha256:93b7a6a9129fce904f6df4cf3ae7ff431d779be681a95c3344c26f3e6c09abfa"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, @@ -1667,14 +1483,14 @@ urllib3 = [ {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, ] virtualenv = [ - {file = "virtualenv-16.7.10-py2.py3-none-any.whl", hash = "sha256:105893c8dc66b7817691c7371439ec18e3b6c5e323a304b5ed96cdd2e75cc1ec"}, - {file = "virtualenv-16.7.10.tar.gz", hash = "sha256:e88fdcb08b0ecb11da97868f463dd06275923f50d87f4b9c8b2fc0994eec40f4"}, + {file = "virtualenv-20.0.33-py2.py3-none-any.whl", hash = "sha256:35ecdeb58cfc2147bb0706f7cdef69a8f34f1b81b6d49568174e277932908b8f"}, + {file = "virtualenv-20.0.33.tar.gz", hash = "sha256:a5e0d253fe138097c6559c906c528647254f437d1019af9d5a477b09bfa7300f"}, ] 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.2.0-py3-none-any.whl", hash = "sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6"}, - {file = "zipp-3.2.0.tar.gz", hash = "sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"}, + {file = "zipp-3.3.0-py3-none-any.whl", hash = "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066"}, + {file = "zipp-3.3.0.tar.gz", hash = "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b"}, ] diff --git a/pyproject.toml b/pyproject.toml index 686074548..cd5d3f78b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,14 +25,16 @@ copier = "copier.cli:CopierApp.run" "Bug Tracker" = "https://github.com/pykong/copier/issues" [tool.poetry.dependencies] -python = "^3.6" +# HACK https://github.com/tmbo/questionary/pull/71 +python = ">=3.6.1,<3.9" colorama = "^0.4.3" iteration_utilities = "^0.10.1" jinja2 = "^2.11.2" pathspec = "^0.8.0" plumbum = "^1.6.9" pydantic = "^1.5.1" -regex = "^2020.6.8" +# HACK https://github.com/tmbo/questionary/pull/70 +questionary = {git = "https://github.com/Yajo/questionary.git", rev = "text-custom-lexer"} pyyaml = "^5.3.1" pyyaml-include = "^1.2" # packaging is needed when installing from PyPI @@ -51,39 +53,44 @@ flake8-blind-except = "*" flake8-bugbear = "*" flake8-comprehensions = "*" flake8-debugger = "*" -ipdb = "*" mypy = "*" -packaging = "*" +pexpect = "^4.8.0" poethepoet = "^0.7.0" pre-commit = "*" pytest = "*" pytest-cov = "*" pytest-xdist = "*" -pytest-timeout = "^1.4.1" # HACK https://github.com/python-poetry/poetry/issues/2555 # TODO Remove from this section and install with poetry install -E docs when fixed mkdocstrings = "^0.13.1" mkdocs-material = "^5.5.5" +autoflake = "^1.4" mkdocs-mermaid2-plugin = "^0.5.0" -[tool.poe.tasks] -clean.script = "devtasks:clean" -clean.help = "remove build/python artifacts" +[tool.poe.tasks.clean] +script = "devtasks:clean" +help = "remove build/python artifacts" -docs = "mkdocs serve" +[tool.poe.tasks.coverage] +cmd = "pytest --cov-report html --cov copier copier tests" +help = "generate an HTML report of the coverage" -test.cmd = "pytest -n auto --color=yes" -test.help = "run tests" +[tool.poe.tasks.docs] +cmd = "mkdocs serve" +help = "start local docs server" -lint.cmd = "pre-commit run --all-files" -lint.help = "check (and auto-fix) style with pre-commit" +[tool.poe.tasks.lint] +cmd = "pre-commit run --all-files" +help = "check (and auto-fix) style with pre-commit" -types.cmd = "mypy --ignore-missing-imports ." -types.help = "run the type (mypy) checker on the codebase" +[tool.poe.tasks.test] +cmd = "pytest --color=yes" +help = "run tests" -coverage.cmd = "pytest --cov-report html --cov copier copier tests" -coverage.help = "generate an HTML report of the coverage" +[tool.poe.tasks.types] +cmd = "mypy --ignore-missing-imports ." +help = "run the type (mypy) checker on the codebase" [tool.poetry-dynamic-versioning] enable = true @@ -109,5 +116,5 @@ max-complexity = 20 ignore = ",W503,E203,E501,D100,D101,D102,D103,D104,D105,D107," [build-system] -requires = ["poetry>=1.0.3"] -build-backend = "poetry.masonry.api" +requires = ["poetry_core>=1.0.0", "poetry-dynamic-versioning"] +build-backend = "poetry.core.masonry.api" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..45a632865 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -n auto -ra diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..060aea796 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +import platform + +import pytest +from pexpect.popen_spawn import PopenSpawn + + +@pytest.fixture +def spawn(): + """Spawn a copier process TUI to interact with.""" + if platform.system() == "Windows": + # HACK https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1243#issuecomment-706668723 + # FIXME Use pexpect or wexpect somehow to fix this + pytest.xfail("pexpect fails on Windows",) + # Using PopenSpawn, although probably it would be best to use pexpect.spawn + # instead. However, it's working fine and it seems easier to fix in the + # future to work on Windows (where, this way, spawning actually works; it's just + # python-prompt-toolkit that rejects displaying a TUI) + return PopenSpawn diff --git a/tests/demo_complex_questions/copier.yml b/tests/demo_complex_questions/copier.yml index 2d7d56ef2..99ee43b0b 100644 --- a/tests/demo_complex_questions/copier.yml +++ b/tests/demo_complex_questions/copier.yml @@ -11,8 +11,10 @@ your_height: help: What's your height? type: float more_json_info: + multiline: true type: json anything_else: + multiline: true help: Wanna give me any more info? # In choices, the user will always write the choice index (1, 2, 3...) diff --git a/tests/demo_complex_questions/results.txt.tmpl b/tests/demo_complex_questions/results.txt.tmpl index a6b91542a..e50b57e36 100644 --- a/tests/demo_complex_questions/results.txt.tmpl +++ b/tests/demo_complex_questions/results.txt.tmpl @@ -9,4 +9,5 @@ choose_list: [[choose_list|tojson]] choose_tuple: [[choose_tuple|tojson]] choose_dict: [[choose_dict|tojson]] choose_number: [[choose_number|tojson]] +minutes_under_water: [[minutes_under_water|tojson]] optional_value: [[optional_value|tojson]] diff --git a/tests/helpers.py b/tests/helpers.py index 0c9673e42..83f704236 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,10 +1,16 @@ import filecmp import os +import sys import textwrap +from enum import Enum from hashlib import sha1 from pathlib import Path from typing import Dict +from plumbum import local +from prompt_toolkit.input.ansi_escape_sequences import REVERSE_ANSI_SEQUENCES +from prompt_toolkit.keys import Keys + import copier from copier.types import StrOrPath @@ -20,6 +26,28 @@ "description": "A library for rendering projects templates", } +COPIER_CMD = local.get( + # Allow debugging in VSCode + # HACK https://github.com/microsoft/vscode-python/issues/14222 + str(Path(sys.executable).parent / "copier.cmd"), + str(Path(sys.executable).parent / "copier"), + # Poetry installs the executable as copier.cmd in Windows + "copier.cmd", + "copier", +) +COPIER_PATH = str(COPIER_CMD.executable) + + +class Keyboard(str, Enum): + ControlH = REVERSE_ANSI_SEQUENCES[Keys.ControlH] + Enter = "\r" + Esc = REVERSE_ANSI_SEQUENCES[Keys.Escape] + + # Equivalent keystrokes in terminals; see python-prompt-toolkit for + # further explanations + Alt = Esc + Backspace = ControlH + def render(tmp_path, **kwargs): kwargs.setdefault("quiet", True) diff --git a/tests/test_cli.py b/tests/test_cli.py index 049ca0eb4..7817130ce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,10 +1,11 @@ from pathlib import Path import yaml -from plumbum.cmd import copier as copier_cmd from copier.cli import CopierApp +from .helpers import COPIER_CMD + SIMPLE_DEMO_PATH = Path(__file__).parent / "demo_simple" @@ -23,4 +24,4 @@ def test_good_cli_run(tmp_path): def test_help(): - copier_cmd("--help-all") + COPIER_CMD("--help-all") diff --git a/tests/test_complex_questions.py b/tests/test_complex_questions.py index 7794b00c2..464095ee7 100644 --- a/tests/test_complex_questions.py +++ b/tests/test_complex_questions.py @@ -1,11 +1,11 @@ from pathlib import Path from textwrap import dedent -from plumbum.cmd import copier as copier_cmd +import pexpect from copier import copy -from .helpers import PROJECT_TEMPLATE +from .helpers import COPIER_PATH, PROJECT_TEMPLATE, Keyboard SRC = f"{PROJECT_TEMPLATE}_complex_questions" @@ -38,37 +38,86 @@ def test_api(tmp_path): choose_tuple: "second" choose_dict: "third" choose_number: null + minutes_under_water: 10 optional_value: null """ ) -def test_cli(tmp_path): +def test_cli_interactive(tmp_path, spawn): """Test copier correctly processes advanced questions and answers through CLI.""" - ( - copier_cmd["copy", SRC, tmp_path] - << dedent( - # These are the answers; those marked as "wrong" will fail and - # make the prompt function ask again - """\ - y - Guybrush Threpwood - wrong your_age - 22 - wrong your_height - 1.56 - wrong more_json_info - {"objective": "be a pirate"} - {wrong anything_else - ['Want some grog?', I'd love it] - 2 - 3 - 1 - 1 - - """ - ) - )(timeout=5) + invalid = [ + "Invalid value", + "please try again", + ] + tui = spawn([COPIER_PATH, "copy", SRC, str(tmp_path)], timeout=10) + tui.expect_exact(["I need to know it. Do you love me?", "love_me", "Format: bool"]) + tui.send("y") + tui.expect_exact(["Please tell me your name.", "your_name", "Format: str"]) + tui.sendline("Guybrush Threpwood") + q = ["How old are you?", "your_age", "Format: int"] + tui.expect_exact(q) + tui.sendline("wrong your_age") + tui.expect_exact(invalid + q) + tui.send((Keyboard.Alt + Keyboard.Backspace) * 2) + tui.sendline("22") + q = ["What's your height?", "your_height", "Format: float"] + tui.expect_exact(q) + tui.sendline("wrong your_height") + tui.expect_exact(invalid + q) + tui.send((Keyboard.Alt + Keyboard.Backspace) * 2) + tui.sendline("1.56") + q = ["more_json_info", "Format: json"] + tui.expect_exact(q) + tui.sendline('{"objective":') + tui.expect_exact(invalid + q) + tui.sendline('"be a pirate"}') + tui.send(Keyboard.Esc + Keyboard.Enter) + q = ["Wanna give me any more info?", "anything_else", "Format: yaml"] + tui.expect_exact(q) + tui.sendline("- Want some grog?") + tui.expect_exact(invalid + q) + tui.sendline("- I'd love it") + tui.send(Keyboard.Esc + Keyboard.Enter) + tui.expect_exact( + [ + "You see the value of the list items", + "choose_list", + "Format: str", + "first", + "second", + "third", + ] + ) + tui.sendline() + tui.expect_exact( + [ + "You see the 1st tuple item, but I get the 2nd item as a result", + "choose_tuple", + "one", + "two", + "three", + ] + ) + tui.sendline() + tui.expect_exact( + [ + "You see the dict key, but I get the dict value", + "choose_dict", + "one", + "two", + "three", + ] + ) + tui.sendline() + tui.expect_exact(["This must be a number", "choose_number", "-1.1", "0", "1"]) + tui.sendline() + tui.expect_exact(["minutes_under_water", "Format: int", "10"]) + tui.send(Keyboard.Backspace) + tui.sendline() + tui.expect_exact(["optional_value", "Format: yaml"]) + tui.sendline() + tui.expect_exact(pexpect.EOF) results_file = tmp_path / "results.txt" assert results_file.read_text() == dedent( r""" @@ -78,10 +127,11 @@ def test_cli(tmp_path): your_height: 1.56 more_json_info: {"objective": "be a pirate"} anything_else: ["Want some grog?", "I\u0027d love it"] - choose_list: "second" - choose_tuple: "third" - choose_dict: "first" + choose_list: "first" + choose_tuple: "second" + choose_dict: "third" choose_number: -1.1 + minutes_under_water: 1 optional_value: null """ ) @@ -119,40 +169,64 @@ def test_api_str_data(tmp_path): choose_tuple: "second" choose_dict: "third" choose_number: 0.0 + minutes_under_water: 10 optional_value: null """ ) -def test_cli_with_flag_data_and_type_casts(tmp_path: Path): +def test_cli_interatively_with_flag_data_and_type_casts(tmp_path: Path, spawn): """Assert how choices work when copier is invoked with --data interactively.""" - ( - copier_cmd[ + invalid = [ + "Invalid value", + "please try again", + ] + tui = spawn( + [ + COPIER_PATH, "--data=choose_list=second", "--data=choose_dict=first", "--data=choose_tuple=third", "--data=choose_number=1", "copy", SRC, - tmp_path, - ] - << dedent( - # These are the answers; those marked as "wrong" will fail and - # make the prompt function ask again - """\ - y - Guybrush Threpwood - wrong your_age - 22 - wrong your_height - 1.56 - wrong more_json_info - {"objective": "be a pirate"} - {wrong anything_else - ['Want some grog?', I'd love it] - """ - ) - )(timeout=5) + str(tmp_path), + ], + timeout=10, + ) + tui.expect_exact(["I need to know it. Do you love me?", "love_me", "Format: bool"]) + tui.send("y") + tui.expect_exact(["Please tell me your name.", "your_name", "Format: str"]) + tui.sendline("Guybrush Threpwood") + q = ["How old are you?", "your_age", "Format: int"] + tui.expect_exact(q) + tui.sendline("wrong your_age") + tui.expect_exact(invalid + q) + tui.send((Keyboard.Alt + Keyboard.Backspace) * 2) + tui.sendline("22") + q = ["What's your height?", "your_height", "Format: float"] + tui.expect_exact(q) + tui.sendline("wrong your_height") + tui.expect_exact(invalid + q) + tui.send((Keyboard.Alt + Keyboard.Backspace) * 2) + tui.sendline("1.56") + q = ["more_json_info", "Format: json"] + tui.expect_exact(q) + tui.sendline('{"objective":') + tui.expect_exact(invalid + q) + tui.sendline('"be a pirate"}') + tui.send(Keyboard.Esc + Keyboard.Enter) + q = ["Wanna give me any more info?", "anything_else", "Format: yaml"] + tui.expect_exact(q) + tui.sendline("- Want some grog?") + tui.expect_exact(invalid + q) + tui.sendline("- I'd love it") + tui.send(Keyboard.Esc + Keyboard.Enter) + tui.expect_exact(["minutes_under_water", "Format: int", "10"]) + tui.sendline() + tui.expect_exact(["optional_value", "Format: yaml"]) + tui.sendline() + tui.expect_exact(pexpect.EOF) results_file = tmp_path / "results.txt" assert results_file.read_text() == dedent( r""" @@ -166,6 +240,7 @@ def test_cli_with_flag_data_and_type_casts(tmp_path: Path): choose_tuple: "third" choose_dict: "first" choose_number: 1.0 + minutes_under_water: 10 optional_value: null """ ) diff --git a/tests/test_copy.py b/tests/test_copy.py index d1bc77481..95b18d52e 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -88,11 +88,13 @@ def test_include_pattern(tmp_path): assert (tmp_path / ".svn").exists() +@pytest.mark.xfail( + condition=platform.system() == "Darwin", + reason="Mac claims to use UTF-8 filesystem, but behaves differently.", + strict=True, +) def test_exclude_file(tmp_path): print(f"Filesystem encoding is {sys.getfilesystemencoding()}") - if platform.system() == "Darwin": - # FIXME Some generous macOS owner please fix this important test! - pytest.skip("Skipping test that will fail on macOS") # This file name is b"man\xcc\x83ana.txt".decode() render(tmp_path, exclude=["mañana.txt"]) assert not (tmp_path / "doc" / "mañana.txt").exists() diff --git a/tests/test_migrations.py b/tests/test_migrations.py index d425286db..4de056142 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -17,15 +17,18 @@ SRC = Path(f"{PROJECT_TEMPLATE}_migrations").absolute() +# This fails on windows CI because, when the test tries to execute +# `migrations.py`, it doesn't understand that it should be interpreted +# by python.exe. Or maybe it fails because CI is using Git bash instead +# of WSL bash, which happened to work fine in real world tests. +# FIXME Some generous Windows power user please fix this test! +@pytest.mark.xfail( + condition=platform.system() == "Windows", + reason="Windows ignores shebang?", + strict=True, +) def test_migrations_and_tasks(tmp_path: Path): """Check migrations and tasks are run properly.""" - if platform.system() == "Windows": - # This fails on windows CI because, when the test tries to execute - # `migrations.py`, it doesn't understand that it should be interpreted - # by python.exe. Or maybe it fails because CI is using Git bash instead - # of WSL bash, which happened to work fine in real world tests. - # FIXME Some generous Windows power user please fix this test! - pytest.skip("Skipping test that will fail on Windows") # Convert demo_migrations in a git repository with 2 versions git_src, dst = tmp_path / "src", tmp_path / "tmp_path" copytree(SRC, git_src) @@ -181,7 +184,7 @@ def test_prereleases(tmp_path: Path): git("tag", "v2.0.0.alpha1") # Copying with use_prereleases=False copies v1 copy(src_path=str(src), dst_path=dst, force=True) - answers = yaml.load((dst / ".copier-answers.yml").read_text()) + answers = yaml.safe_load((dst / ".copier-answers.yml").read_text()) assert answers["_commit"] == "v1.0.0" assert (dst / "version.txt").read_text() == "v1.0.0" assert not (dst / "v1.9").exists() @@ -204,7 +207,7 @@ def test_prereleases(tmp_path: Path): assert not (dst / "v2.a2").exists() # Update it with prereleases copy(dst_path=dst, force=True, use_prereleases=True) - answers = yaml.load((dst / ".copier-answers.yml").read_text()) + answers = yaml.safe_load((dst / ".copier-answers.yml").read_text()) assert answers["_commit"] == "v2.0.0.alpha1" assert (dst / "version.txt").read_text() == "v2.0.0.alpha1" assert (dst / "v1.9").exists() diff --git a/tests/test_prompt.py b/tests/test_prompt.py index cf41210ad..46ee115c0 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -1,13 +1,12 @@ -from io import StringIO from pathlib import Path +import pexpect import pytest +import yaml from plumbum import local from plumbum.cmd import git -from copier import copy - -from .helpers import build_file_tree +from .helpers import COPIER_PATH, Keyboard, build_file_tree DEFAULT = object() MARIO_TREE = { @@ -32,11 +31,9 @@ } -# @pytest.mark.timeout(5) @pytest.mark.parametrize("name", [DEFAULT, None, "Luigi"]) -def test_copy_default_advertised(tmp_path_factory, monkeypatch, capsys, name): +def test_copy_default_advertised(tmp_path_factory, spawn, name): """Test that the questions for the user are OK""" - monkeypatch.setattr("sys.stdin", StringIO("\n" * 3)) template, subproject = ( tmp_path_factory.mktemp("template"), tmp_path_factory.mktemp("subproject"), @@ -51,32 +48,200 @@ def test_copy_default_advertised(tmp_path_factory, monkeypatch, capsys, name): git("tag", "v2") with local.cwd(subproject): # Copy the v1 template - kwargs = {} + args = [] if name is not DEFAULT: - kwargs["data"] = {"your_name": name} + args.append(f"--data=your_name={name}") else: name = "Mario" # Default in the template - copy(str(template), ".", vcs_ref="v1", **kwargs) - # Check what was captured - captured = capsys.readouterr() - assert "in_love? Format: bool\n🎤? [Y/n] " in captured.out - assert ( - "Secret enemy name\nyour_enemy? Format: str\n🕵️ [Bowser]: " in captured.out + name = str(name) + tui = spawn( + [COPIER_PATH, str(template), ".", "--vcs-ref=v1"] + args, timeout=10 ) - assert ( - f"what_enemy_does? Format: str\n🎤 [Bowser hates {name}]: " in captured.out - ) - # We already sent the name through 'data', so it shouldn't be prompted - assert name == "Mario" or "your_name" not in captured.out + # Check what was captured + tui.expect_exact(["in_love?", "Format: bool", "(Y/n)"]) + tui.sendline() + tui.expect_exact(["Yes"]) + if not args: + tui.expect_exact( + ["If you have a name, tell me now.", "your_name?", "Format: str", name] + ) + tui.sendline() + tui.expect_exact(["Secret enemy name", "your_enemy?", "Format: str", "******"]) + tui.sendline() + tui.expect_exact(["what_enemy_does?", "Format: str", f"Bowser hates {name}"]) + tui.sendline() + tui.expect_exact(pexpect.EOF) assert "_commit: v1" in Path(".copier-answers.yml").read_text() # Update subproject git("init") git("add", ".") + assert "_commit: v1" in Path(".copier-answers.yml").read_text() git("commit", "-m", "v1") - copy(dst_path=".") + tui = spawn([COPIER_PATH], timeout=10) # Check what was captured - captured = capsys.readouterr() - assert ( - f"If you have a name, tell me now.\nyour_name? Format: str\n🎤 [{name}]: " - in captured.out + tui.expect_exact(["in_love?", "Format: bool", "(Y/n)"]) + tui.sendline() + tui.expect_exact( + ["If you have a name, tell me now.", "your_name?", "Format: str", name] ) + tui.sendline() + tui.expect_exact(["Secret enemy name", "your_enemy?", "Format: str", "Bowser"]) + tui.sendline() + tui.expect_exact(["what_enemy_does?", "Format: str", f"Bowser hates {name}"]) + tui.sendline() + tui.expect_exact(["Overwrite", ".copier-answers.yml", "[Y/n]"]) + tui.sendline() + tui.expect_exact(pexpect.EOF) + assert "_commit: v2" in Path(".copier-answers.yml").read_text() + + +@pytest.mark.parametrize( + "question_2_when, asks", + ( + (True, True), + (False, False), + ("trUe", True), + ("faLse", False), + ("Yes", True), + ("nO", False), + ("Y", True), + ("N", False), + ("on", True), + ("off", False), + ("[[ question_1 ]]", True), + ("[[ not question_1 ]]", False), + ("[% if question_1 %]YES[% endif %]", True), + ("[% if question_1 %]FALSE[% endif %]", False), + ), +) +def test_when(tmp_path_factory, question_2_when, spawn, asks): + """Test that the 2nd question is skipped or not, properly.""" + template, subproject = ( + tmp_path_factory.mktemp("template"), + tmp_path_factory.mktemp("subproject"), + ) + questions = { + "question_1": {"type": "bool", "default": True}, + "question_2": {"default": "something", "when": question_2_when}, + } + build_file_tree( + { + template / "copier.yml": yaml.dump(questions), + template + / "[[ _copier_conf.answers_file ]].tmpl": "[[ _copier_answers|to_nice_yaml ]]", + } + ) + tui = spawn([COPIER_PATH, str(template), str(subproject)], timeout=10) + tui.expect_exact(["question_1?", "Format: bool", "(Y/n)"]) + tui.sendline() + if asks: + tui.expect_exact(["question_2?", "Format: yaml"]) + tui.sendline() + tui.expect_exact(pexpect.EOF) + answers = yaml.safe_load((subproject / ".copier-answers.yml").read_text()) + assert answers == { + "_src_path": str(template), + "question_1": True, + "question_2": "something", + } + + +def test_placeholder(tmp_path_factory, spawn): + template, subproject = ( + tmp_path_factory.mktemp("template"), + tmp_path_factory.mktemp("subproject"), + ) + build_file_tree( + { + template + / "copier.yml": yaml.dump( + { + "question_1": "answer 1", + "question_2": { + "type": "str", + "help": "write a list of answers", + "placeholder": "Write something like [[ question_1 ]], but better", + }, + } + ), + template + / "[[ _copier_conf.answers_file ]].tmpl": "[[ _copier_answers|to_nice_yaml ]]", + } + ) + tui = spawn([COPIER_PATH, str(template), str(subproject)], timeout=10) + tui.expect_exact(["question_1?", "Format: str", "answer 1"]) + tui.sendline() + tui.expect_exact( + ["question_2?", "Format: yaml", "Write something like answer 1, but better"] + ) + tui.sendline() + tui.expect_exact(pexpect.EOF) + answers = yaml.safe_load((subproject / ".copier-answers.yml").read_text()) + assert answers == { + "_src_path": str(template), + "question_1": "answer 1", + "question_2": None, + } + + +@pytest.mark.parametrize("type_", ("str", "yaml", "json")) +def test_multiline(tmp_path_factory, spawn, type_): + template, subproject = ( + tmp_path_factory.mktemp("template"), + tmp_path_factory.mktemp("subproject"), + ) + build_file_tree( + { + template + / "copier.yml": yaml.dump( + { + "question_1": "answer 1", + "question_2": {"type": type_}, + "question_3": {"type": type_, "multiline": True}, + "question_4": { + "type": type_, + "multiline": "[[ question_1 == 'answer 1' ]]", + }, + "question_5": { + "type": type_, + "multiline": "[[ question_1 != 'answer 1' ]]", + }, + } + ), + template + / "[[ _copier_conf.answers_file ]].tmpl": "[[ _copier_answers|to_nice_yaml ]]", + } + ) + tui = spawn([COPIER_PATH, str(template), str(subproject)], timeout=10) + tui.expect_exact(["question_1?", "Format: str", "answer 1"]) + tui.sendline() + tui.expect_exact(["question_2?", f"Format: {type_}"]) + tui.sendline('"answer 2"') + tui.expect_exact(["question_3?", f"Format: {type_}"]) + tui.sendline('"answer 3"') + tui.send(Keyboard.Alt + Keyboard.Enter) + tui.expect_exact(["question_4?", f"Format: {type_}"]) + tui.sendline('"answer 4"') + tui.send(Keyboard.Alt + Keyboard.Enter) + tui.expect_exact(["question_5?", f"Format: {type_}"]) + tui.sendline('"answer 5"') + tui.expect_exact(pexpect.EOF) + answers = yaml.safe_load((subproject / ".copier-answers.yml").read_text()) + if type_ == "str": + assert answers == { + "_src_path": str(template), + "question_1": "answer 1", + "question_2": '"answer 2"', + "question_3": ('"answer 3"\n'), + "question_4": ('"answer 4"\n'), + "question_5": ('"answer 5"'), + } + else: + assert answers == { + "_src_path": str(template), + "question_1": "answer 1", + "question_2": ("answer 2"), + "question_3": ("answer 3"), + "question_4": ("answer 4"), + "question_5": ("answer 5"), + } diff --git a/tests/test_templated_prompt.py b/tests/test_templated_prompt.py index 1c67ac17b..2937fae4e 100644 --- a/tests/test_templated_prompt.py +++ b/tests/test_templated_prompt.py @@ -1,15 +1,13 @@ -import io -from collections import ChainMap -from datetime import datetime - +import pexpect import pytest +import yaml from copier.config.factory import filter_config, make_config from copier.config.objects import EnvOps from copier.config.user_data import InvalidTypeError, query_user_data -from copier.types import AnyByStrDict -answers_data: AnyByStrDict = {} +from .helpers import COPIER_PATH, build_file_tree + envops = EnvOps() main_default = "copier" main_question = {"main": {"default": main_default}} @@ -51,7 +49,7 @@ }, }, main_default, - ["(1) choice 1", "(2) copier", "Choice [1]"], + ["choice 1", "copier"], ), ( { @@ -101,7 +99,7 @@ }, }, "value 2", - ["(1) name 1", "(2) copier", "Choice [2]"], + ["name 1", "copier"], ), ( { @@ -111,27 +109,35 @@ }, }, "value 2", - ["(1) copier", "(2) copier", "Choice [2]"], + ["copier", "copier"], ), ], ) def test_templated_prompt( - questions_data, expected_value, expected_outputs, capsys, monkeypatch + questions_data, expected_value, expected_outputs, tmp_path_factory, spawn ): - monkeypatch.setattr("sys.stdin", io.StringIO("\n\n")) + template, subproject = ( + tmp_path_factory.mktemp("template"), + tmp_path_factory.mktemp("subproject"), + ) questions_combined = filter_config({**main_question, **questions_data})[1] - data = dict( - ChainMap( - query_user_data(questions_combined, {}, {}, True, envops), - {k: v["default"] for k, v in questions_combined.items()}, - ) + # There's always only 1 question; get its name + question_name = questions_data.copy().popitem()[0] + build_file_tree( + { + template / "copier.yml": yaml.dump(questions_combined), + template + / "[[ _copier_conf.answers_file ]].tmpl": "[[ _copier_answers|to_nice_yaml ]]", + } ) - captured = capsys.readouterr() - data.pop("main") - name, value = list(data.items())[0] - assert value == expected_value - for output in expected_outputs: - assert output in captured.out + tui = spawn([COPIER_PATH, str(template), str(subproject)], timeout=10) + tui.expect_exact(["main?", "Format: yaml", main_default]) + tui.sendline() + tui.expect_exact([f"{question_name}?"] + expected_outputs) + tui.sendline() + tui.expect_exact(pexpect.EOF) + answers = yaml.safe_load((subproject / ".copier-answers.yml").read_text()) + assert answers[question_name] == expected_value def test_templated_prompt_custom_envops(tmp_path): @@ -146,16 +152,11 @@ def test_templated_prompt_custom_envops(tmp_path): def test_templated_prompt_builtins(): data = query_user_data( - {"question": {"default": "[[ now() ]]"}}, answers_data, {}, False, envops + {"question": {"default": "[[ now() ]]"}}, {}, {}, {}, False, envops ) - assert isinstance(data["question"], datetime) data = query_user_data( - {"question": {"default": "[[ make_secret() ]]"}}, - answers_data, - {}, - False, - envops, + {"question": {"default": "[[ make_secret() ]]"}}, {}, {}, {}, False, envops, ) assert isinstance(data["question"], str) and len(data["question"]) == 128 @@ -163,27 +164,27 @@ def test_templated_prompt_builtins(): def test_templated_prompt_invalid(): # assert no exception in non-strict mode query_user_data( - {"question": {"default": "[[ not_valid ]]"}}, {}, answers_data, False, envops + {"question": {"default": "[[ not_valid ]]"}}, {}, {}, {}, False, envops, ) # assert no exception in non-strict mode query_user_data( - {"question": {"help": "[[ not_valid ]]"}}, {}, answers_data, False, envops + {"question": {"help": "[[ not_valid ]]"}}, {}, {}, {}, False, envops ) with pytest.raises(InvalidTypeError): query_user_data( - {"question": {"type": "[[ not_valid ]]"}}, {}, answers_data, False, envops + {"question": {"type": "[[ not_valid ]]"}}, {}, {}, {}, False, envops, ) # assert no exception in non-strict mode query_user_data( - {"question": {"choices": ["[[ not_valid ]]"]}}, {}, answers_data, False, envops + {"question": {"choices": ["[[ not_valid ]]"]}}, {}, {}, {}, False, envops, ) # TODO: uncomment this later when EnvOps supports setting the undefined behavior # envops.undefined = StrictUndefined # with pytest.raises(UserMessageError): # query_user_data( - # {"question": {"default": "[[ not_valid ]]"}}, answers_data, False, envops + # {"question": {"default": "[[ not_valid ]]"}}, {}, False, envops # ) diff --git a/tests/test_tools.py b/tests/test_tools.py index c1b5e0a28..021e50852 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from poethepoet.app import PoeThePoet from copier import tools from copier.config.factory import ConfigData, EnvOps @@ -60,3 +61,13 @@ def test_render(tmp_path): ) def test_create_path_filter(pattern, should_match): assert path_filter(pattern) == should_match + + +def test_lint(): + """Ensure source code formatting""" + PoeThePoet(Path("."))(["lint", "--show-diff-on-failure", "--color=always"]) + + +def test_types(): + """Ensure source code static typing.""" + PoeThePoet(Path("."))(["types"]) diff --git a/tests/test_updatediff.py b/tests/test_updatediff.py index 2b4efbfef..cb47eeac6 100644 --- a/tests/test_updatediff.py +++ b/tests/test_updatediff.py @@ -160,14 +160,15 @@ def test_updatediff(tmpdir): ) +# This fails on Windows because there's some problem while detecting +# the diff. It seems like an older Git version were being used, while +# that's not the case... +# FIXME Some generous Windows power user please fix this test! +@pytest.mark.xfail( + condition=platform.system() == "Windows", reason="Git broken on Windows?" +) def test_commit_hooks_respected(tmp_path: Path): """Commit hooks are taken into account when producing the update diff.""" - if platform.system() == "Windows": - # This fails on Windows because there's some problem while detecting - # the diff. It seems like an older Git version were being used, while - # that's not the case... - # FIXME Some generous Windows power user please fix this test! - pytest.skip("Skipping test that will fail on Windows") # Prepare source template v1 src, tmp_path = tmp_path / "src", tmp_path / "tmp_path" src.mkdir()