From ccfe4b2c53fb5735c7e258ffa5b6ece18b2b7c06 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Thu, 24 Sep 2020 12:22:15 +0200 Subject: [PATCH] Use PyInquirer --- .pre-commit-config.yaml | 6 + copier/config/factory.py | 56 +++++++++ copier/config/objects.py | 240 ++++++++++++++++++++++++++++++++++++- copier/config/user_data.py | 212 ++------------------------------ copier/tools.py | 42 ++++++- poetry.lock | 46 +++++-- pyproject.toml | 6 +- 7 files changed, 385 insertions(+), 223 deletions(-) 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 c7f436821..7e95c8fa4 100644 --- a/copier/config/factory.py +++ b/copier/config/factory.py @@ -21,6 +21,58 @@ __all__ = ("make_config",) +# def build_question( +# question: str, +# type_name: str, +# type_fn: Callable, +# secret: bool = False, +# placeholder: OptStr = None, +# default: Any = None, +# choices: Any = None, +# extra_help: OptStr = None, +# ) -> AnyByStrDict: +# """Build one question definition. + +# Get the question from copier.yml and return a question definition in the +# format expected by PyInquirer. +# """ +# # Generate message to ask the user +# emoji = "🕵️" if secret else "🎤" +# lexer_map = {"json": JsonLexer, "yaml": YamlLexer} +# lexer = lexer_map.get(type_name) +# question_type = "input" +# if type_name == "bool": +# question_type = "confirm" +# elif choices: +# question_type = "list" +# elif secret: +# question_type = "password" +# # Hints for the multiline input user +# multiline = lexer is not None +# # Convert default to string +# to_str_map: Dict[str, Callable[[Any], str]] = { +# "json": lambda obj: json.dumps(obj, indent=2), +# "yaml": to_nice_yaml, +# } +# to_str_fn = to_str_map.get(type_name, str) +# # Allow placeholder YAML comments +# default_str = to_str_fn(default) +# question_dict = { +# "type": question_type, +# "name": question, +# "message": extra_help, +# "default": default_str, +# # "lexer": lexer and PygmentsLexer(lexer), +# "mouse_support": True, +# "multiline": multiline, +# "validator": Validator.from_callable(abstract_validator(type_fn)), +# "qmark": emoji, +# } +# if choices: +# question_dict["choices"] = choices +# return prompt([question_dict]) + + def filter_config(data: AnyByStrDict) -> Tuple[AnyByStrDict, AnyByStrDict]: """Separates config and questions data.""" conf_data: AnyByStrDict = {"secret_questions": set()} @@ -47,6 +99,10 @@ def verify_minimum_version(version_str: str) -> None: # so instead we do a lazy import here from .. import __version__ + # Disable check when running copier as editable installation + if __version__ == "0.0.0": + return + if version.parse(__version__) < version.parse(version_str): raise UserMessageError( f"This template requires Copier version >= {version_str}, " diff --git a/copier/config/objects.py b/copier/config/objects.py index 999e3d6d1..02a103f15 100644 --- a/copier/config/objects.py +++ b/copier/config/objects.py @@ -1,15 +1,34 @@ """Pydantic models, exceptions and default values.""" - import datetime +import json from collections import ChainMap +from contextlib import suppress from copy import deepcopy from hashlib import sha512 from os import urandom from pathlib import Path -from typing import Any, ChainMap as t_ChainMap, Sequence, Tuple, Union +from typing import ( + Any, + Callable, + ChainMap as t_ChainMap, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, + Union, +) -from pydantic import BaseModel, Extra, StrictBool, validator +from jinja2 import UndefinedError +from jinja2.sandbox import SandboxedEnvironment +from prompt_toolkit.lexers import PygmentsLexer +from prompt_toolkit.validation import Validator +from pydantic import BaseModel, Extra, Field, StrictBool, validator +from pygments.lexers.data import JsonLexer, YamlLexer +from PyInquirer.prompt import prompt +from ..tools import cast_answer_type, force_str_end, parse_yaml_string from ..types import AnyByStrDict, OptStr, PathSeq, StrOrPathSeq, StrSeq # Default list of files in the template to exclude from the rendered project @@ -31,12 +50,19 @@ DEFAULT_TEMPLATES_SUFFIX = ".tmpl" +TYPE_COPIER2PYTHON: Dict[str, Callable] = { + "bool": bool, + "float": float, + "int": int, + "json": json.loads, + "str": str, + "yaml": parse_yaml_string, +} + class UserMessageError(Exception): """Exit the program giving a message to the user.""" - pass - class NoSrcPathError(UserMessageError): pass @@ -152,3 +178,207 @@ def data(self) -> t_ChainMap[str, Any]: class Config: allow_mutation = False anystr_strip_whitespace = True + + +class Question(BaseModel): + choices: Union[Dict[Any, Any], List[Any]] = Field(default_factory=list) + default: Any = None + help_text: str = "" + multiline: Optional[bool] = None + 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 TYPE_COPIER2PYTHON else "yaml" + if v not in TYPE_COPIER2PYTHON: + raise ValueError("Invalid question type") + return v + + def _iter_choices(self) -> Iterable[dict]: + 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): + yield choice + continue + # However, a choice can also be a single value... + name = value = choice + # ... or a value pair + if isinstance(choice, (tuple, list)): + name, value = choice + # The name must always be a str + name = str(name) + yield {"name": name, "value": value} + + def get_default(self) -> Union[str, bool]: + try: + result = self.questionary.answers_forced.get( + self.var_name, self.questionary.answers_last[self.var_name] + ) + except KeyError: + result = self.render_value(self.default) + result = cast_answer_type(result, self.get_type_fn()) + if self.type_name == "bool": + return bool(result) + if result is None: + return "" + return str(result) + + def get_choices(self) -> List[AnyByStrDict]: + result = [] + for choice in self._iter_choices(): + formatted_choice = { + key: self.render_value(value) for key, value in choice.items() + } + result.append(formatted_choice) + return result + + def get_filter(self, answer) -> Any: + return cast_answer_type(answer, self.get_type_fn()) + + def get_message(self) -> str: + 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: + return self.render_value(self.placeholder) + + def get_pyinquirer_structure(self): + lexer = None + result = { + "default": self.get_default(), + "filter": self.get_filter, + "message": self.get_message(), + "mouse_support": True, + "name": self.var_name, + "qmark": "🕵️" if self.secret else "🎤", + "validator": Validator.from_callable(self.get_validator), + "when": self.get_when, + } + multiline = self.multiline + pyinquirer_type = "input" + if self.type_name == "bool": + pyinquirer_type = "confirm" + if self.choices: + pyinquirer_type = "list" + result["choices"] = self.get_choices() + if pyinquirer_type == "input": + if self.secret: + pyinquirer_type = "password" + elif self.type_name == "yaml": + lexer = PygmentsLexer(YamlLexer) + elif self.type_name == "json": + lexer = PygmentsLexer(JsonLexer) + placeholder = self.get_placeholder() + if placeholder: + result["placeholder"] = placeholder + multiline = multiline or ( + multiline is None and self.type_name in {"yaml", "json"} + ) + result.update({"type": pyinquirer_type, "lexer": lexer, "multiline": multiline}) + from pprint import pprint + + pprint(result) + return result + + def get_type_fn(self) -> Callable: + return TYPE_COPIER2PYTHON.get(self.type_name, parse_yaml_string) + + def get_validator(self, document) -> bool: + type_fn = self.get_type_fn() + try: + type_fn(document) + return True + except Exception: + return False + + def get_when(self, answers) -> bool: + 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, parse_yaml_string) + 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()) + except UndefinedError as error: + raise UserMessageError(str(error)) from error + + +class Questionary(BaseModel): + 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]: + return ChainMap(self.answers_user, self.answers_last, self.answers_forced) + + def get_answers(self) -> AnyByStrDict: + return prompt( + (question.get_pyinquirer_structure() for question in self.questions), + answers=self.answers_user, + raise_keyboard_interrupt=True, + ) + + +Question.update_forward_refs() diff --git a/copier/config/user_data.py b/copier/config/user_data.py index 999936be9..21a7a96c8 100644 --- a/copier/config/user_data.py +++ b/copier/config/user_data.py @@ -1,29 +1,15 @@ """Functions used to load user data.""" -import json import re -import sys -from collections import ChainMap -from functools import partial from pathlib import Path -from typing import Any, Callable, Dict import yaml from iteration_utilities import deepflatten -from jinja2 import UndefinedError -from jinja2.sandbox import SandboxedEnvironment -from plumbum.cli.terminal import ask, choose -from plumbum.colors import bold, info, italics, reverse, warn -from prompt_toolkit import prompt -from prompt_toolkit.formatted_text import ANSI -from prompt_toolkit.lexers import PygmentsLexer -from prompt_toolkit.validation import Validator -from pygments.lexers.data import JsonLexer, YamlLexer from yamlinclude import YamlIncludeConstructor -from ..tools import force_str_end, get_jinja_env, printf_exception, to_nice_yaml -from ..types import AnyByStrDict, Choices, OptStr, OptStrOrPath, PathSeq, StrOrPath -from .objects import DEFAULT_DATA, EnvOps, UserMessageError +from ..tools import get_jinja_env, printf_exception +from ..types import AnyByStrDict, OptStrOrPath, PathSeq, StrOrPath +from .objects import EnvOps, Question, Questionary __all__ = ("load_config_data", "query_user_data") @@ -50,78 +36,6 @@ class InvalidTypeError(TypeError): pass -def ask_interactively( - question: str, - type_name: str, - type_fn: Callable, - secret: bool = False, - placeholder: OptStr = None, - default: Any = None, - choices: Any = None, - extra_help: OptStr = None, -) -> Any: - """Ask one question interactively to the user.""" - # Generate message to ask the user - message = "" - if extra_help: - message = force_str_end(f"\n{info & italics | extra_help}{message}") - emoji = "🕵️" if secret else "🎤" - message += f"{bold | question}? Format: {type_name}\n{emoji} " - lexer_map = {"json": JsonLexer, "yaml": YamlLexer} - lexer = lexer_map.get(type_name) - # HACK https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1071 - # FIXME When fixed, use prompt toolkit too for choices and bools - # Use the correct method to ask - if type_name == "bool": - return ask(message, default) - if choices: - return choose(message, choices, default) - # Hints for the multiline input user - multiline = lexer is not None - if multiline: - message += f"- Finish with {reverse | 'Esc, ↵'} or {reverse | 'Meta + ↵'}\n" - # Convert default to string - to_str_map: Dict[str, Callable[[Any], str]] = { - "json": lambda obj: json.dumps(obj, indent=2), - "yaml": to_nice_yaml, - } - to_str_fn = to_str_map.get(type_name, str) - # Allow placeholder YAML comments - default_str = to_str_fn(default) - if placeholder and type_name == "yaml": - prefixed_default_str = force_str_end(placeholder) + default_str - if yaml.safe_load(prefixed_default_str) == default: - default_str = prefixed_default_str - else: - print(warn | "Placeholder text alters value!", file=sys.stderr) - return prompt( - ANSI(message), - default=default_str, - is_password=secret, - lexer=lexer and PygmentsLexer(lexer), - mouse_support=True, - multiline=multiline, - validator=Validator.from_callable(abstract_validator(type_fn)), - ) - - -def abstract_validator(type_fn: Callable) -> Callable: - """Produce a validator for the given type. - - Params: - type_fn: A callable that converts text into the expected type. - """ - - def _validator(text: str): - try: - type_fn(text) - return True - except Exception: - return False - - return _validator - - def load_yaml_data(conf_path: Path, quiet: bool = False) -> AnyByStrDict: """Load the `copier.yml` file. @@ -157,18 +71,6 @@ def load_yaml_data(conf_path: Path, quiet: bool = False) -> AnyByStrDict: raise InvalidConfigFileError(conf_path, quiet) from e -def parse_yaml_string(string: str) -> Any: - """Parse a YAML string and raise a ValueError if parsing failed. - - This method is needed because :meth:`prompt` requires a ``ValueError`` - to repeat falied questions. - """ - try: - return yaml.safe_load(string) - except yaml.error.YAMLError as error: - raise ValueError(str(error)) - - def load_config_data( src_path: StrOrPath, quiet: bool = False, _warning: bool = True ) -> AnyByStrDict: @@ -199,53 +101,6 @@ def load_answersfile_data( return {} -def cast_answer_type(answer: Any, type_fn: Callable) -> Any: - """Cast answer to expected type.""" - # Skip casting None into "None" - if type_fn is str and answer is None: - return answer - # Parse correctly bools as 1, true, yes... - if type_fn is bool and isinstance(answer, str): - return parse_yaml_string(answer) - try: - return type_fn(answer) - except (TypeError, AttributeError): - # JSON or YAML failed because it wasn't a string; no need to convert - 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 - - def query_user_data( questions_data: AnyByStrDict, last_answers_data: AnyByStrDict, @@ -254,60 +109,11 @@ def query_user_data( 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), + questionary = Questionary( + answers_forced=forced_answers_data, + answers_last=last_answers_data, + env=get_jinja_env(envops=envops), ) - _render_choices = partial( - render_choices, - env=env, - context=ChainMap(result, forced_answers_data, defaults, DEFAULT_DATA), - ) - 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: - extra_help = details.get("help") - if extra_help: - extra_help = _render_value(extra_help) - answer = ask_interactively( - question, - type_name, - type_fn, - details.get("secret", False), - _render_value(details.get("placeholder")), - answer, - _render_choices(details.get("choices")), - _render_value(details.get("help")), - ) - 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 b5ac0763d..016a7fa30 100644 --- a/copier/tools.py +++ b/copier/tools.py @@ -6,17 +6,17 @@ import sys import unicodedata from pathlib import Path -from typing import Any, Dict, List, Optional, TextIO, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TextIO, Union import colorama import pathspec +import yaml from jinja2 import FileSystemLoader from jinja2.sandbox import SandboxedEnvironment from packaging import version from pydantic import StrictBool from yaml import safe_dump -from .config.objects import ConfigData, EnvOps from .types import ( AnyByStrDict, CheckPathFunc, @@ -29,6 +29,9 @@ T, ) +if TYPE_CHECKING: + from .config.objects import ConfigData, EnvOps + __all__ = ("Style", "printf") colorama.init() @@ -48,6 +51,21 @@ class Style: NO_VALUE: object = object() +def cast_answer_type(answer: Any, type_fn: Callable) -> Any: + """Cast answer to expected type.""" + # Skip casting None into "None" + if type_fn is str and answer is None: + return answer + # Parse correctly bools as 1, true, yes... + if type_fn is bool and isinstance(answer, str): + return parse_yaml_string(answer) + try: + return type_fn(answer) + except (TypeError, AttributeError): + # JSON or YAML failed because it wasn't a string; no need to convert + return answer + + def printf( action: str, msg: Any = "", @@ -79,6 +97,18 @@ def printf_exception( print(HLINE, file=sys.stderr) +def parse_yaml_string(string: str) -> Any: + """Parse a YAML string and raise a ValueError if parsing failed. + + This method is needed because :meth:`prompt` requires a ``ValueError`` + to repeat falied questions. + """ + try: + return yaml.safe_load(string) + except yaml.error.YAMLError as error: + raise ValueError(str(error)) + + def required(value: T, **kwargs: Any) -> T: if not value: raise ValueError() @@ -110,7 +140,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 +161,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 @@ -195,7 +225,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 295c4dd09..0f304a12e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -45,6 +45,17 @@ dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +[[package]] +category = "dev" +description = "Removes unused imports and unused variables" +name = "autoflake" +optional = false +python-versions = "*" +version = "1.4" + +[package.dependencies] +pyflakes = ">=1.1.0" + [[package]] category = "dev" description = "Specifications for callback functions passed in to an API" @@ -725,7 +736,7 @@ description = "Library for building powerful interactive command lines in Python name = "prompt-toolkit" optional = false python-versions = ">=3.6.1" -version = "3.0.6" +version = "3.0.7" [package.dependencies] wcwidth = "*" @@ -787,7 +798,24 @@ description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false python-versions = ">=3.5" -version = "2.6.1" +version = "2.7.1" + +[[package]] +category = "main" +description = "A Python module for collection of common interactive command line user interfaces, based on Inquirer.js" +name = "PyInquirer" +optional = false +python-versions = ">=3.6.1" +version = "1.0.3" + +[package.dependencies] +Pygments = ">=2.2.0" +prompt_toolkit = ">=3.0.0,<3.1.0" + +[package.source] +reference = "46282964b4af5fc495084cb66bb1ea108050c859" +type = "git" +url = "https://github.com/Yajo/PyInquirer.git" [[package]] category = "main" @@ -1065,7 +1093,7 @@ testing = ["jaraco.itertools", "func-timeout"] docs = ["mkdocstrings", "mkdocs-material"] [metadata] -content-hash = "6029d208164be0e6295fc8545a7a9a332c9a91ddf7e67ed1ee534fb7c5844eb8" +content-hash = "d6a2cbdb772ae892d8d7f6ceed35e432d08fdfdfc23b6ebd9b575975596490f9" lock-version = "1.0" python-versions = "^3.6.1" @@ -1090,6 +1118,9 @@ attrs = [ {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, ] +autoflake = [ + {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, +] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, @@ -1394,8 +1425,8 @@ pre-commit = [ {file = "pre_commit-2.1.1.tar.gz", hash = "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.6-py3-none-any.whl", hash = "sha256:683397077a64cd1f750b71c05afcfc6612a7300cb6932666531e5a54f38ea564"}, - {file = "prompt_toolkit-3.0.6.tar.gz", hash = "sha256:7630ab85a23302839a0f26b31cc24f518e6155dea1ed395ea61b42c45941b6a6"}, + {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"}, @@ -1433,9 +1464,10 @@ pyflakes = [ {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pygments = [ - {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, - {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, + {file = "Pygments-2.7.1-py3-none-any.whl", hash = "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998"}, + {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"}, ] +PyInquirer = [] 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"}, diff --git a/pyproject.toml b/pyproject.toml index 6ce874522..6d102e730 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,16 +31,17 @@ iteration_utilities = "^0.10.1" jinja2 = "^2.11.2" pathspec = "^0.8.0" plumbum = "^1.6.9" -prompt_toolkit = "^3.0.6" pydantic = "^1.5.1" regex = "^2020.6.8" -pygments = "^2.6.1" pyyaml = "^5.3.1" pyyaml-include = "^1.2" # packaging is needed when installing from PyPI packaging = "^20.4" mkdocstrings = {version = "^0.13.1", optional = true} mkdocs-material = {version = "^5.5.2", optional = true} +# HACK https://github.com/CITGuru/PyInquirer/pull/136 +# TODO Install from upstream when merged +pyinquirer = {git = "https://github.com/Yajo/PyInquirer.git", rev = "46282964b4af5fc495084cb66bb1ea108050c859"} [tool.poetry.extras] docs = ["mkdocstrings", "mkdocs-material"] @@ -66,6 +67,7 @@ pytest-timeout = "^1.4.1" # 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" [tool.poe.tasks] clean.script = "devtasks:clean"