diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c63dba736 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto eol=lf + +*.py text +*.rst text +*.toml text +*.yml text +*.md text + +*.png binary +*.jpg binary diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 93302c18a..2b14e83e1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -93,5 +93,5 @@ We like to keep a clean history, so squash-rebase merges are preferred for the _ ### How to create release 1. Create a branch off of _development_ like **release/1.0.0** and add a commit to bump the version in `pyproject.toml` and `__version__.py`. -2. Merge it into both _main_ and _release_ +2. Merge it into both _main_ and _development_ 3. Create a GitHub release from the head of main, following the existing convention for naming and release notes format, and the GitHub CI will do the rest. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dcfb6bd9..d4631bc8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: strategy: matrix: os: [Ubuntu, MacOS, Windows] - python-version: [ '3.6', '3.7' , '3.8' ] + python-version: ['3.6', '3.7', '3.8', '3.9'] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index ec817a71b..f43a99562 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +notes/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -74,3 +76,4 @@ dmypy.json # Misc /TODO.md Dockerfile +.vscode/ \ No newline at end of file diff --git a/README.rst b/README.rst index bb1c5a392..6ac3ec80c 100644 --- a/README.rst +++ b/README.rst @@ -14,27 +14,29 @@ A task runner that works well with poetry. :language: toml .. role:: python(code) :language: python +.. |•| unicode:: ✅ 0xA0 0xA0 + :trim: Features ======== -✅ Straight forward declaration of project tasks in your pyproject.toml (kind of like npm scripts) +|•| Straight forward declaration of project tasks in your pyproject.toml (kind of like npm scripts) -✅ Task are run in poetry's virtualenv by default +|•| Task are run in poetry's virtualenv (or another env you specify) -✅ Shell completion of task names (and global options too for zsh) +|•| Shell completion of task names (and global options too for zsh) -✅ Tasks can be commands (with or without a shell) or references to python functions (like tool.poetry.scripts) +|•| Tasks can be commands (with or without a shell) or references to python functions (like tool.poetry.scripts) -✅ Short and sweet commands with extra arguments passed to the task :bash:`poe [options] task [task_args]` +|•| Short and sweet commands with extra arguments passed to the task :bash:`poe [options] task [task_args]` -✅ Tasks can specify and reference environment variables as if they were evaluated by a shell +|•| Tasks can specify and reference environment variables as if they were evaluated by a shell -✅ Tasks are self documenting, with optional help messages (just run poe without arguments) +|•| Tasks are self documenting, with optional help messages (just run poe without arguments) -✅ Tasks can be defined as a sequence of other tasks +|•| Tasks can be defined as a sequence of other tasks -✅ Can also be configured to execute tasks with any virtualenv (not just poetry) +|•| Works with .env files Installation @@ -46,7 +48,7 @@ Into your project (so it works inside poetry shell): poetry add --dev poethepoet -And into your default python environment (so it works outside of poetry shell) +And into any python environment (so it works outside of poetry shell) .. code-block:: bash @@ -55,7 +57,8 @@ And into your default python environment (so it works outside of poetry shell) Enable tab completion for your shell ------------------------------------ -Poe comes with tab completion scripts for bash, zsh, and fish to save you keystrokes. How to install them will depend on your shell setup. +Poe comes with tab completion scripts for bash, zsh, and fish to save you keystrokes. +How to install them will depend on your shell setup. Zsh ~~~ @@ -70,7 +73,9 @@ Zsh mkdir -p ~/.zfunc/ poe _zsh_completion > ~/.zfunc/_poetry -Note that you'll need to start a new shell for the new completion script to be loaded. If it still doesn't work try adding a call to :bash:`compinit` to the end of your zshrc file. +Note that you'll need to start a new shell for the new completion script to be loaded. +If it still doesn't work try adding a call to :bash:`compinit` to the end of your zshrc +file. Bash ~~~~ @@ -109,9 +114,9 @@ Define tasks in your pyproject.toml .. code-block:: toml [tool.poe.tasks] - test = "pytest --cov=poethepoet" # simple command based task - mksandwich = { script = "my_package.sandwich:build" } # python script based task - tunnel = { shell = "ssh -N -L 0.0.0.0:8080:$PROD:8080 $PROD &" } # (posix) shell script based task + test = "pytest --cov=poethepoet" # simple command based task + serve = { script = "my_app.service:run(debug=True)" } # python script based task + tunnel = { shell = "ssh -N -L 0.0.0.0:8080:$PROD:8080 $PROD &" } # (posix) shell based task Run tasks with the poe cli -------------------------- @@ -120,7 +125,7 @@ Run tasks with the poe cli poe test -Additional arguments are passed to the task so +By default additional arguments are passed to the task so .. code-block:: bash @@ -150,8 +155,8 @@ Though it that case you might like to do :bash:`alias poe='poetry run poe'`. Types of task ============= -There are four types of task: simple commands (cmd), python scripts (script), shell -scripts (shell), and composite tasks (sequence). +There are four types of task: simple commands *(cmd)*, python scripts *(script)*, shell +scripts *(shell)*, and sequence tasks *(sequence)*. - **Command tasks** contain a single command that will be executed without a shell. This covers most basic use cases for example: @@ -178,13 +183,16 @@ scripts (shell), and composite tasks (sequence). [tool.poe.tasks] fetch-assets = { "script" = "my_package.assets:fetch" } - fetch-images = { "script" = "my_package.assets:fetch(only='images')" } + fetch-images = { "script" = "my_package.assets:fetch(only='images', log=environ['LOG_PATH'])" } As in the second example, is it possible to hard code literal arguments to the target - callable. + callable. In fact a subset of python syntax, operators, and globals can be used inline + to define the arguments to the function using normal python syntax, including environ + (from the os package) to access environment variables that are available to the task. - If extra arguments are passed to task on the command line, then they will be available - to the called python function via :python:`sys.argv`. + If extra arguments are passed to task on the command line (and no CLI args are + declared), then they will be available within the called python function via + :python:`sys.argv`. - **Shell tasks** are similar to simple command tasks except that they are executed inside a new shell, and can consist of multiple separate commands, command @@ -201,7 +209,10 @@ scripts (shell), and composite tasks (sequence). - **Composite tasks** are defined as a sequence of other tasks as an array. - By default the contents of the array are interpreted as references to other tasks (actually a ref task type), though this behaviour can be altered by setting the global :toml:`default_array_item_task_type` option to the name of another task type such as _cmd_, or by setting the :toml:`default_item_type` option locally on the sequence task. + By default the contents of the array are interpreted as references to other tasks + (actually a ref task type), though this behaviour can be altered by setting the global + :toml:`default_array_item_task_type` option to the name of another task type such as + *cmd*, or by setting the :toml:`default_item_type` option locally on the sequence task. **An example task with references** @@ -214,7 +225,9 @@ scripts (shell), and composite tasks (sequence). _publish = "poetry publish" release = ["test", "build", "_publish"] - Note that tasks with names prefixed with :code:`_` are not included in the documentation or directly executable, but can be useful for cases where a task is only needed for a sequence. + Note that tasks with names prefixed with :code:`_` are not included in the + documentation or directly executable, but can be useful for cases where a task is only + needed for referencing from another task. **An example task with inline tasks expressed via inline tables** @@ -226,6 +239,21 @@ scripts (shell), and composite tasks (sequence). { ref = "_publish" }, ] + **An example task with inline tasks expressed via an array of tables** + + .. code-block:: toml + + [tool.poe.tasks] + + [[tool.poe.tasks.release]] + cmd = "pytest --cov=src" + + [[tool.poe.tasks.release]] + script = "devtasks:build" + + [[tool.poe.tasks.release]] + ref = "_publish" + **An example task with inline script subtasks using default_item_type** .. code-block:: toml @@ -237,13 +265,25 @@ scripts (shell), and composite tasks (sequence). ] release.default_item_type = "script" - A failure (non-zero result) will result in the rest of the tasks in the sequence not being executed, unless the :toml:`ignore_fail` option is set on the task like so: + A failure (non-zero result) will result in the rest of the tasks in the sequence not + being executed, unless the :toml:`ignore_fail` option is set on the task to + :toml:`true` or :toml:`"return_zero"` like so: .. code-block:: toml [tool.poe.tasks] attempts.sequence = ["task1", "task2", "task3"] - attempts.ignore_fail = true + attempts.ignore_fail = "return_zero" + + If you want to run all the subtasks in the sequence but return non-zero result in the + end of the sequence if any of the subtasks have failed you can set :toml:`ignore_fail` + option to the :toml:`return_non_zero` value like so: + + .. code-block:: toml + + [tool.poe.tasks] + attempts.sequence = ["task1", "task2", "task3"] + attempts.ignore_fail = "return_non_zero" Task level configuration ======================== @@ -251,9 +291,10 @@ Task level configuration Task help text -------------- -You can specifiy help text to be shown alongside the task name in the list of available tasks (such as when executing poe with no arguments), by adding a help key like so: +You can specify help text to be shown alongside the task name in the list of available +tasks (such as when executing poe with no arguments), by adding a help key like so: - .. code-block:: toml +.. code-block:: toml [tool.poe.tasks] style = {cmd = "black . --check --diff", help = "Check code style"} @@ -261,15 +302,249 @@ You can specifiy help text to be shown alongside the task name in the list of av Environment variables --------------------- -You can specify arbitrary environment variables to be set for a task by providing the env key like so: +You can specify arbitrary environment variables to be set for a task by providing the +env key like so: - .. code-block:: toml +.. code-block:: toml [tool.poe.tasks] serve.script = "myapp:run" - serve.env = { PORT = 9001 } + serve.env = { PORT = "9001" } + +Notice this example uses deep keys which can be more convenient but aren't as well +supported by some toml implementations. + +The above example can be modified to only set the `PORT` variable if it is not already +set by replacing the last line with the following: + +.. code-block:: toml + + serve.env.PORT.default = "9001" + + +You can also specify an env file (with bash-like syntax) to load per task like so: + +.. code-block:: bash + + # .env + STAGE=dev + PASSWORD='!@#$%^&*(' + +.. code-block:: toml + + [tool.poe.tasks] + serve.script = "myapp:run" + serve.envfile = ".env" + +Declaring CLI arguments +----------------------- + +By default extra arguments passed to the poe CLI following the task name are appended to +the end of a cmd task, or exposed as sys.argv in a script task (but will cause an error +for shell or sequence tasks). Alternatively it is possible to define named arguments +that a task should accept, which will be documented in the help for that task, and +exposed to the task in a way the makes the most sense for that task type. + +In general named arguments can take one of the following three forms: + +- **positional arguments** which are provided directly following the name of the task like + :bash:`poe task-name arg-value` + +- **option arguments** which are provided like + :bash:`poe task-name --option-name arg-value` + +- **flags** which are either provided or not, but don't accept a value like + :bash:`poe task-name --flag` + +The value for the named argument is then accessible by name within the task content, +though exactly how will depend on the type of the task as detailed below. + + +Configuration syntax +~~~~~~~~~~~~~~~~~~~~ + +Named arguments are configured by declaring the *args* task option as either an array or +a subtable. + + +Array configuration syntax +"""""""""""""""""""""""""" + +The array form may contain string items which are interpreted as an option argument with +the given name. + +.. code-block:: toml + + [tool.poe.tasks.serve] + cmd = "myapp:run" + args = ["host", "port"] + +This example can be invoked as + +.. code-block:: bash + + poe serve --host 0.0.0.0 --port 8001 + +Items in the array can also be inline tables to allow for more configuration to be +provided to the task like so: + +.. code-block:: toml + + [tool.poe.tasks.serve] + cmd = "myapp:run" + args = [{ name = "host", default = "localhost" }, { name = "port", default = "9000" }] + +You can also use the toml syntax for an array of tables like so: + +.. code-block:: toml + + [tool.poe.tasks.serve] + cmd = "myapp:run" + help = "Run the application server" + + [[tool.poe.tasks.serve.args]] + name = "host" + options = ["-h", "--host"] + help = "The host on which to expose the service" + default = "localhost" + + [[tool.poe.tasks.serve.args]] + name = "port" + options = ["-p", "--port"] + help = "The port on which to expose the service" + default = "8000" + + +Table configuration syntax +"""""""""""""""""""""""""" + +You can also use the toml syntax for subtables like so: + +.. code-block:: toml + + [tool.poe.tasks.serve] + cmd = "myapp:run" + help = "Run the application server" + + [tool.poe.tasks.serve.args.host] + options = ["-h", "--host"] + help = "The host on which to expose the service" + default = "localhost" + + [tool.poe.tasks.serve.args.port] + options = ["-p", "--port"] + help = "The port on which to expose the service" + default = "8000" + +When using this form the *name* option is no longer applicable because the key for the +argument within the args table is taken as the name. + + +Task argument options +~~~~~~~~~~~~~~~~~~~~~ + +Named arguments support the following configuration options: -Notice this example uses deep keys which can be more convenient but aren't as well supported by some toml implementations. +- **default** : Union[str, int, float, bool] + The value to use if the argument is not provided. This option has no effect if the + required option is set to true. + +- **help** : str + A short description of the argument to include in the documentation of the task. + +- **name** : str + The name of the task. Only applicable when *args* is an array. + +- **options** : List[str] + A list of options to accept for this argument, similar to + `argsparse name or flags `_. + If not provided then the name of the argument is used. You can use this option to + expose a different name to the CLI vs the name that is used inside the task, or to + specify long and short forms of the CLI option, e.g. ["-h", "--help"]. + +- **positional** : bool + If set to true then the argument becomes a position argument instead of an option + argument. Note that positional arguments may not have type *bool*. + +- **required** : bool + If true then not providing the argument will result in an error. Arguments are not + required by default. + +- **type** : str + The type that the provided value will be cast to. The set of acceptable options is + {"string", "float", "integer", "boolean"}. If not provided then the default behaviour + is to keep values as strings. Setting the type to "bool" makes the resulting argument + a flag that if provided will set the value to the boolean opposite of the default + value – i.e. *true* if no default value is given, or false if :toml:`default = true`. + + +Arguments for cmd and shell tasks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For cmd and shell tasks the values are exposed to the task as environment variables. For +example given the following configuration: + +.. code-block:: toml + + [tool.poe.tasks.passby] + shell = """ + echo "hello $planet"; + echo "goodbye $planet"; + """ + help = "Pass by a planet!" + + [[tool.poe.tasks.passby.args]] + name = "planet" + help = "Name of the planet to pass" + default = "earth" + options = ["-p", "--planet"] + +The resulting task can be run like: + +.. code-block:: bash + + poe passby --planet mars + +Arguments for script tasks +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Arguments can be defined for script tasks in the same way, but how they are exposed to +the underlying python function depends on how the script is defined. + +In the following example, since no parenthesis are included for the referenced function, +all provided args will be passed to the function as kwargs: + +.. code-block:: toml + + [tool.poe.tasks] + build = { script = "project.util:build", args = ["dest", "version"] } + +You can also control exactly how values are passed to the python function as +demonstrated in the following example: + +.. code-block:: toml + + [tool.poe.tasks] + build = { script = "project.util:build(dest, build_version=version, verbose=True)", args = ["dest", "version"] + +Arguments for sequence tasks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Arguments can be passed to the tasks referenced from a sequence task as in the following +example. + +.. code-block:: toml + + [tool.poe.tasks] + build = { script = "util:build_app", args = [{ name = "target", positional = true }] } + + [tool.poe.tasks.check] + sequence = ["build ${target}", { script = "util:run_tests(environ['target'])" }] + args = ["target"] + +This works by setting the argument values as environment variables for the subtasks, +which can be read at runtime, but also referenced in the task definition as +demonstrated in the above example for a *ref* task and *script* task. Project-wide configuration options ================================== @@ -277,7 +552,8 @@ Project-wide configuration options Global environment variables ---------------------------- -You can configure environment variables to be set for all poe tasks in the pyproject.toml file by specifying :toml:`tool.poe.env` like so +You can configure environment variables to be set for all poe tasks in the +pyproject.toml file by specifying :toml:`tool.poe.env` like so .. code-block:: toml @@ -285,16 +561,60 @@ You can configure environment variables to be set for all poe tasks in the pypro VAR1 = "FOO" VAR2 = "BAR" +As for the task level option, you can indicated that a variable should only be set if +not already set like so: + +.. code-block:: toml + + [tool.poe.env] + VAR1.default = "FOO" + +You can also specify an env file (with bash-like syntax) to load for all tasks like so: + +.. code-block:: bash + + # .env + STAGE=dev + PASSWORD='!@#$%^&*(' + +.. code-block:: toml + + [tool.poe] + envfile = ".env" + +Default command verbosity +------------------------- + +You can alter the verbosity level for poe commands by passing :bash:`--quiet` / +:bash:`-q` (which decreases verbosity) or :bash:`--verbose` / :bash:`-v` (which +increases verbosity) on the CLI. + +If you want to change the default verbosity level for all commands, you can use +the :toml:`tool.poe.verbose` option in pyproject.toml like so: + +.. code-block:: toml + + [tool.poe] + verbosity = -1 + +:toml:`-1` is the quietest and :toml:`1` is the most verbose. :toml:`0` is the +default. + +Note that the command line arguments are incremental: :bash:`-q` subtracts one +from the default verbosity, and :bash:`-v` adds one. So setting the default +verbosity to :toml:`-1` and passing :bash:`-v -v` on the command line is +equivalent to setting the verbosity to :toml:`0` and just passing :bash:`-v`. + Run poe from anywhere --------------------- By default poe will detect when you're inside a project with a pyproject.toml in the -root. However if you want to run it from elsewhere that is supported too by using the -:bash:`--root` option to specify an alternate location for the toml file. The task will run -with the given location as the current working directory. +root. However if you want to run it from elsewhere then that is supported by using the +:bash:`--root` option to specify an alternate location for the toml file. The task will +run with the given location as the current working directory. -In all cases the path to project root (where the pyproject.toml resides) will be available -as :bash:`$POE_ROOT` within the command line and process. +In all cases the path to project root (where the pyproject.toml resides) will be +available as :bash:`$POE_ROOT` within the command line and process. Change the default task type ---------------------------- @@ -319,18 +639,22 @@ pyproject.toml like so: my_cmd_task = { "cmd" = "cmd args" } my_script_task = "my_package.my_module:run" - Change the executor type ------------------------ -You can configure poe to use a specific executor by setting :toml:`tool.poe.executor.type`. Valid valued include: +You can configure poe to use a specific executor by setting +:toml:`tool.poe.executor.type`. Valid values include: - - auto: to automatically use the most appropriate of the following executors in order - - poetry: to run tasks in the poetry managed environment - - virtualenv: to run tasks in the indicated virtualenv (or else "./.venv" if present) - - simple: to run tasks without doing any specific environment setup +- **auto**: to automatically use the most appropriate of the following executors in order +- **poetry**: to run tasks in the poetry managed environment +- **virtualenv**: to run tasks in the indicated virtualenv (or else "./.venv" if present) +- **simple**: to run tasks without doing any specific environment setup -For example the following configuration will cause poe to ignore the poetry environment (if present), and instead use the virtualenv at the given location relative to the parent directory. +The default behaviour is auto. + +For example the following configuration will cause poe to ignore the poetry environment +(if present), and instead use the virtualenv at the given location relative to the +parent directory. .. code-block:: toml @@ -338,41 +662,83 @@ For example the following configuration will cause poe to ignore the poetry envi type = "virtualenv" location = "myvenv" - See below for more details. Usage without poetry ==================== Poe the Poet was originally intended for use alongside poetry. But it works just as -well with any other kind of virtualenv, or standalone. This behaviour is configurable via the :toml:`tool.poe.executor` global option (see above). - -By default poe will run tasks in the poetry managed environment, if the pyproject.toml contains a :toml:`tool.poetry` section. If it doesn't then poe looks for a virtualenv to use from :bash:`./.venv` or :bash:`./venv` relative to the pyproject.toml file. Otherwise it falls back to running tasks without any special environment management. - -Contributing -============ +well with any other kind of virtualenv, or standalone. This behaviour is configurable +via the :toml:`tool.poe.executor` global option (see above). -There's plenty to do, come say hi in `the issues `_! 👋 - -Also check out the `CONTRIBUTING.MD `_ 🤓 +By default poe will run tasks in the poetry managed environment, if the pyproject.toml +contains a :toml:`tool.poetry` section. If it doesn't then poe looks for a virtualenv to +use from :bash:`./.venv` or :bash:`./venv` relative to the pyproject.toml file. +Otherwise it falls back to running tasks without any special environment management. -TODO -==== +Composing tasks into graphs (Experimental) +========================================== -☐ support declaring specific arguments for a task `#6 `_ +You can define tasks that depend on other tasks, and optionally capture and reuse the +output of those tasks, thus defining an execution graph of tasks. This is done by using +the *deps* task option, or if you want to capture the output of the upstream task to +pass it to the present task then specify the *uses* option, as demonstrated below. -☐ support conditional execution (a bit like make targets) `#12 `_ +.. code-block:: toml -☐ support verbose mode for documentation that shows task definitions + [tool.poe.tasks] + _website_bucket_name.shell = """ + aws cloudformation describe-stacks \ + --stack-name $AWS_SAM_STACK_NAME \ + --query "Stacks[0].Outputs[?(@.OutputKey == 'FrontendS3Bucket')].OutputValue" \ + | jq -cr 'select(0)[0]' + """ + + [tool.poe.tasks.build-backend] + help = "Build the backend" + sequence = [ + {cmd = "poetry export -f requirements.txt --output src/requirements.txt"}, + {cmd = "sam build"}, + ] + + [tool.poe.tasks.build-frontend] + help = "Build the frontend" + cmd = "npm --prefix client run build" + + [tool.poe.tasks.shipit] + help = "Build and deploy the app" + sequence = [ + "sam deploy --config-env $SAM_ENV_NAME", + "aws s3 sync --delete ./client/build s3://${BUCKET_NAME}" + ] + default_item_type = "cmd" + deps = ["build-frontend", "build-backend"] + uses = { BUCKET_NAME = "_website_bucket_name" } + +In this example the *shipit* task depends on the *build-frontend* *build-backend*, which +means that these tasks get executed before the *shipit* task. It also declares that it +uses the output of the hidden *_website_bucket_name* task, which means that this also +gets executed, but its output it captured and then made available to the *shipit* task +as the environment variable BUCKET_NAME. + +This feature is experimental. There may be edge cases that aren't handled well, so +feedback is requested. Some details of the implementation or API may be altered in +future versions. + +Supported python versions +========================= + +Poe the Poet officially supports python >3.6.2, and is tested with python 3.6 to 3.9 on +macOS, linux and windows. -☐ create documentation website `#11 `_ -☐ support third party task or executor types (e.g. pipenv) as plugins `#13 `_ +Contributing +============ -☐ provide poe as a poetry plugin `#14 `_ +There's plenty to do, come say hi in `the issues `_! 👋 -☐ maybe support plumbum based tasks +Also check out the `CONTRIBUTING.MD `_ 🤓 Licence ======= diff --git a/poethepoet/__version__.py b/poethepoet/__version__.py index 61fb31cae..ae6db5f17 100644 --- a/poethepoet/__version__.py +++ b/poethepoet/__version__.py @@ -1 +1 @@ -__version__ = "0.10.0" +__version__ = "0.11.0" diff --git a/poethepoet/app.py b/poethepoet/app.py index b385c622b..57fd51f15 100644 --- a/poethepoet/app.py +++ b/poethepoet/app.py @@ -1,11 +1,13 @@ import os from pathlib import Path import sys -from typing import Any, IO, MutableMapping, Optional, Sequence, Union +from typing import Any, Dict, IO, Mapping, Optional, Sequence, Tuple, Union from .config import PoeConfig from .context import RunContext from .exceptions import ExecutionError, PoeException from .task import PoeTask +from .task.args import PoeTaskArgs +from .task.graph import TaskExecutionGraph from .ui import PoeUi @@ -18,7 +20,7 @@ class PoeThePoet: def __init__( self, cwd: Path, - config: Optional[MutableMapping[str, Any]] = None, + config: Optional[Mapping[str, Any]] = None, output: IO = sys.stdout, ): self.cwd = cwd @@ -42,6 +44,8 @@ def __call__(self, cli_args: Sequence[str]) -> int: self.print_help(error=error) return 1 + self.ui.set_default_verbosity(self.config.verbosity) + if self.ui["help"]: self.print_help() return 0 @@ -49,17 +53,21 @@ def __call__(self, cli_args: Sequence[str]) -> int: if not self.resolve_task(): return 1 - return self.run_task() or 0 + assert self.task + if self.task.has_deps(): + return self.run_task_graph() or 0 + else: + return self.run_task() or 0 def resolve_task(self) -> bool: - task = self.ui["task"] + task = tuple(self.ui["task"]) if not task: self.print_help(info="No task specified.") return False task_name = task[0] if task_name not in self.config.tasks: - self.print_help(error=PoeException(f"Unrecognised task {task_name!r}"),) + self.print_help(error=PoeException(f"Unrecognised task {task_name!r}")) return False if task_name.startswith("_"): @@ -70,22 +78,24 @@ def resolve_task(self) -> bool: ) return False - self.task = PoeTask.from_config(task_name, config=self.config, ui=self.ui) + self.task = PoeTask.from_config( + task_name, config=self.config, ui=self.ui, invocation=task + ) return True - def run_task(self) -> Optional[int]: + def run_task(self, context: Optional[RunContext] = None) -> Optional[int]: _, *extra_args = self.ui["task"] + if context is None: + context = RunContext( + config=self.config, + ui=self.ui, + env=os.environ, + dry=self.ui["dry_run"], + poe_active=os.environ.get("POE_ACTIVE"), + ) try: assert self.task - return self.task.run( - context=RunContext( - config=self.config, - env=os.environ, - dry=self.ui["dry_run"], - poe_active=os.environ.get("POE_ACTIVE"), - ), - extra_args=extra_args, - ) + return self.task.run(context=context, extra_args=extra_args) except PoeException as error: self.print_help(error=error) return 1 @@ -93,6 +103,40 @@ def run_task(self) -> Optional[int]: self.ui.print_error(error=error) return 1 + def run_task_graph(self) -> Optional[int]: + assert self.task + context = RunContext( + config=self.config, + ui=self.ui, + env=os.environ, + dry=self.ui["dry_run"], + poe_active=os.environ.get("POE_ACTIVE"), + ) + graph = TaskExecutionGraph(self.task, context) + plan = graph.get_execution_plan() + + for stage in plan: + for task in stage: + if task == self.task: + # The final sink task gets special treatment + return self.run_task(context) + + try: + task_result = task.run( + context=context, extra_args=task.invocation[1:] + ) + if task_result: + raise ExecutionError( + f"Task graph aborted after failed task {task.name!r}" + ) + except PoeException as error: + self.print_help(error=error) + return 1 + except ExecutionError as error: + self.ui.print_error(error=error) + return 1 + return 0 + def print_help( self, info: Optional[str] = None, @@ -100,8 +144,15 @@ def print_help( ): if isinstance(error, str): error == PoeException(error) - tasks_help = { - task: (content.get("help", "") if isinstance(content, dict) else "") - for task, content in self.config.tasks.items() + tasks_help: Dict[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str]]]] = { + task_name: ( + ( + content.get("help", ""), + PoeTaskArgs.get_help_content(content.get("args")), + ) + if isinstance(content, dict) + else ("", tuple()) + ) + for task_name, content in self.config.tasks.items() } self.ui.print_help(tasks=tasks_help, info=info, error=error) # type: ignore diff --git a/poethepoet/config.py b/poethepoet/config.py index 5025faedb..cfa9b5c7c 100644 --- a/poethepoet/config.py +++ b/poethepoet/config.py @@ -1,5 +1,5 @@ from pathlib import Path -import tomlkit +import tomli from typing import Any, Dict, Mapping, Optional, Union from .exceptions import PoeException @@ -15,7 +15,9 @@ class PoeConfig: "default_array_task_type": str, "default_array_item_task_type": str, "env": dict, + "envfile": str, "executor": dict, + "verbosity": int, } def __init__( @@ -48,9 +50,17 @@ def default_array_item_task_type(self) -> str: return self._poe.get("default_array_item_task_type", "ref") @property - def global_env(self) -> Dict[str, str]: + def global_env(self) -> Dict[str, Union[str, Dict[str, str]]]: return self._poe.get("env", {}) + @property + def global_envfile(self) -> Optional[str]: + return self._poe.get("envfile") + + @property + def verbosity(self) -> int: + return self._poe.get("verbosity", 0) + @property def project(self) -> Any: return self._project @@ -111,19 +121,31 @@ def validate(self): f"{self.default_array_item_task_type!r}" ) # Validate env value - env = self.global_env - if env: - for key, value in env.items(): - if not isinstance(value, str): + for key, value in self.global_env.items(): + if isinstance(value, dict): + if tuple(value.keys()) != ("default",) or not isinstance( + value["default"], str + ): raise PoeException( - f"Value of {key!r} in option `env` should be a string, but found {type(value)!r}" + f"Invalid declaration at {key!r} in option `env`: {value!r}" ) + elif not isinstance(value, str): + raise PoeException( + f"Value of {key!r} in option `env` should be a string, but found " + f"{type(value)!r}" + ) # Validate tasks for task_name, task_def in self.tasks.items(): error = PoeTask.validate_def(task_name, task_def, self) if error is None: continue raise PoeException(error) + # Validate default verbosity. + if self.verbosity < -1 or self.verbosity > 1: + raise PoeException( + f"Invalid value for option `verbosity`: {self.verbosity!r}. " + "Should be between -1 and 1." + ) def find_pyproject_toml(self, target_dir: Optional[str] = None) -> Path: """ @@ -159,9 +181,9 @@ def find_pyproject_toml(self, target_dir: Optional[str] = None) -> Path: @staticmethod def _read_pyproject(path: Path) -> Mapping[str, Any]: try: - with path.open(encoding="utf-8") as pyproj: - return tomlkit.parse(pyproj.read()) - except tomlkit.exceptions.TOMLKitError as error: + with path.open("rb") as pyproj: + return tomli.load(pyproj) + except tomli.TOMLDecodeError as error: raise PoeException(f"Couldn't parse toml file at {path}", error) from error except Exception as error: raise PoeException(f"Couldn't open file at {path}") from error diff --git a/poethepoet/context.py b/poethepoet/context.py index 0cc06e8fc..90bebf083 100644 --- a/poethepoet/context.py +++ b/poethepoet/context.py @@ -2,51 +2,160 @@ from typing import ( Any, Dict, + Mapping, MutableMapping, Optional, + Tuple, + Union, TYPE_CHECKING, ) +from .exceptions import ExecutionError from .executor import PoeExecutor +from .envfile import load_env_file if TYPE_CHECKING: from .config import PoeConfig + from .ui import PoeUi + +# TODO: think about factoring env var concerns out to a dedicated class class RunContext: config: "PoeConfig" + ui: "PoeUi" env: Dict[str, str] dry: bool poe_active: Optional[str] project_dir: Path multistage: bool = False exec_cache: Dict[str, Any] + captured_stdout: Dict[Tuple[str, ...], str] + _envfile_cache: Dict[str, Dict[str, str]] def __init__( self, config: "PoeConfig", - env: MutableMapping[str, str], + ui: "PoeUi", + env: Mapping[str, str], dry: bool, poe_active: Optional[str], ): self.config = config + self.ui = ui self.project_dir = Path(config.project_dir) - self.env = {**env, "POE_ROOT": str(config.project_dir)} self.dry = dry self.poe_active = poe_active self.exec_cache = {} + self.captured_stdout = {} + self._envfile_cache = {} + self.base_env = self.__build_base_env(env) + + def __build_base_env(self, env: Mapping[str, str]): + # Get env vars from envfile referenced in global options + result = dict(env) + + # Get env vars from envfile referenced in global options + if self.config.global_envfile is not None: + result.update(self.get_env_file(self.config.global_envfile)) + + # Get env vars from global options + self._update_env(result, self.config.global_env) + + result["POE_ROOT"] = str(self.config.project_dir) + return result + + @staticmethod + def _update_env( + env: MutableMapping[str, str], + extra_vars: Mapping[str, Union[str, Mapping[str, str]]], + ): + """ + Update the given env with the given extra_vars. If a value in extra_vars is + indicated as `default` then only copy it over if that key is not already set on + env. + """ + for key, value in extra_vars.items(): + if isinstance(value, str): + env[key] = value + elif key not in env: + env[key] = value["default"] + + @property + def executor_type(self) -> Optional[str]: + return self.config.executor["type"] + + def get_env( + self, + parent_env: Optional[Mapping[str, str]], + task_envfile: Optional[str], + task_env: Optional[Mapping[str, str]], + task_uses: Optional[Mapping[str, Tuple[str, ...]]] = None, + ) -> Dict[str, str]: + result = dict(self.base_env, **(parent_env or {})) + + # Include env vars from envfile referenced in task options + if task_envfile is not None: + result.update(self.get_env_file(task_envfile)) + + # Include env vars from task options + if task_env is not None: + self._update_env(result, task_env) - def get_env(self, env: MutableMapping[str, str]) -> Dict[str, str]: - return {**self.env, **env} + # Include env vars from dependencies + if task_uses is not None: + result.update(self.get_dep_values(task_uses)) + + return result + + def get_dep_values( + self, used_task_invocations: Mapping[str, Tuple[str, ...]] + ) -> Dict[str, str]: + """ + Get env vars from upstream tasks declared via the uses option + """ + return { + var_name: self.captured_stdout[invocation] + for var_name, invocation in used_task_invocations.items() + } def get_executor( self, - env: MutableMapping[str, str], - task_executor: Optional[Dict[str, str]] = None, + invocation: Tuple[str, ...], + env: Mapping[str, str], + task_options: Dict[str, Any], ) -> PoeExecutor: return PoeExecutor.get( + invocation=invocation, context=self, - env=self.get_env(env), + env=env, working_dir=self.project_dir, dry=self.dry, - executor_config=task_executor, + executor_config=task_options.get("executor"), + capture_stdout=task_options.get("capture_stdout", False), ) + + def get_env_file(self, envfile_path_str: str) -> Dict[str, str]: + if envfile_path_str in self._envfile_cache: + return self._envfile_cache[envfile_path_str] + + result = {} + + envfile_path = self.project_dir.joinpath(envfile_path_str) + if envfile_path.is_file(): + try: + with envfile_path.open() as envfile: + result = load_env_file(envfile) + except ValueError as error: + message = error.args[0] + raise ExecutionError( + f"Syntax error in referenced envfile: {envfile_path_str!r}; {message}" + ) from error + + else: + self.ui.print_msg( + f"Warning: Poe failed to locate envfile at {envfile_path_str!r}", + verbosity=1, + ) + + self._envfile_cache[envfile_path_str] = result + return result diff --git a/poethepoet/envfile.py b/poethepoet/envfile.py new file mode 100644 index 000000000..7bf96458e --- /dev/null +++ b/poethepoet/envfile.py @@ -0,0 +1,176 @@ +from enum import Enum +import re +from typing import Dict, List, Optional, TextIO + + +class ParserException(ValueError): + def __init__(self, message: str, position: int): + super().__init__(message) + self.message = message + self.position = position + + +class ParserState(Enum): + # Scanning for a new assignment + SCAN_VAR_NAME = 0 + # In a value with no quoting + SCAN_VALUE = 1 + # Inside single quotes + IN_SINGLE_QUOTE = 2 + # Inside double quotes + IN_DOUBLE_QUOTE = 3 + + +def load_env_file(envfile: TextIO) -> Dict[str, str]: + """ + Parses variable assignments from the given string. Expects a subset of bash syntax. + """ + + content_lines = envfile.readlines() + try: + return parse_env_file("".join(content_lines)) + except ParserException as error: + line_num, position = _get_line_number(content_lines, error.position) + raise ValueError(f"{error.message} at line {line_num} position {position}.") + + +VARNAME_PATTERN = r"^[\s\t;]*(?:export[\s\t]+)?([a-zA-Z_][a-zA-Z_0-9]*)" +ASSIGNMENT_PATTERN = f"{VARNAME_PATTERN}=" +COMMENT_SUFFIX_PATTERN = r"^[\s\t;]*\#.*?\n" +WHITESPACE_PATTERN = r"^[\s\t;]*" +UNQUOTED_VALUE_PATTERN = r"^(.*?)(?:(\t|\s|;|'|\"|\\+))" +SINGLE_QUOTE_VALUE_PATTERN = r"^((?:.|\n)*?)'" +DOUBLE_QUOTE_VALUE_PATTERN = r"^((?:.|\n)*?)(\"|\\+)" + + +def parse_env_file(content: str): + content = content + "\n" + result = {} + cursor = 0 + state = ParserState.SCAN_VAR_NAME + var_name: Optional[str] = "" + var_content = [] + + while cursor < len(content): + if state == ParserState.SCAN_VAR_NAME: + # scan for new variable assignment + match = re.search(ASSIGNMENT_PATTERN, content[cursor:], re.MULTILINE) + + if match is None: + comment_match = re.match(COMMENT_SUFFIX_PATTERN, content[cursor:]) + if comment_match: + cursor += comment_match.end() + continue + + if ( + re.match(WHITESPACE_PATTERN, content[cursor:], re.MULTILINE).end() # type: ignore + == len(content) - cursor + ): + # The rest of the input is whitespace or semicolons + break + + # skip any immediate whitespace + cursor += re.match( # type: ignore + r"[\s\t\n]*", content[cursor:] + ).span()[1] + + var_name_match = re.match(VARNAME_PATTERN, content[cursor:]) + if var_name_match: + cursor += var_name_match.span()[1] + raise ParserException( + f"Expected assignment operator", + cursor, + ) + + raise ParserException(f"Expected variable assignment", cursor) + + var_name = match.group(1) + cursor += match.end() + state = ParserState.SCAN_VALUE + + if state == ParserState.SCAN_VALUE: + # collect up until the first quote, whitespace, or group of backslashes + match = re.search(UNQUOTED_VALUE_PATTERN, content[cursor:], re.MULTILINE) + assert match + new_var_content, match_terminator = match.groups() + var_content.append(new_var_content) + cursor += len(new_var_content) + + if match_terminator.isspace() or match_terminator == ";": + assert var_name + result[var_name] = "".join(var_content) + var_name = None + var_content = [] + state = ParserState.SCAN_VAR_NAME + continue + + if match_terminator == "'": + cursor += 1 + state = ParserState.IN_SINGLE_QUOTE + + elif match_terminator == '"': + cursor += 1 + state = ParserState.IN_DOUBLE_QUOTE + continue + + else: + # We found one or more backslashes + num_backslashes = len(match_terminator) + # Keep the excess (escaped) backslashes + var_content.append("\\" * (num_backslashes // 2)) + cursor += num_backslashes + + if num_backslashes % 2 > 0: + # Odd number of backslashes, means the next char is escaped + next_char = content[cursor] + var_content.append(next_char) + cursor += 1 + continue + + if state == ParserState.IN_SINGLE_QUOTE: + # collect characters up until a single quote + match = re.search( + SINGLE_QUOTE_VALUE_PATTERN, content[cursor:], re.MULTILINE + ) + if match is None: + raise ParserException(f"Unmatched single quote", cursor - 1) + var_content.append(match.group(1)) + cursor += match.end() + state = ParserState.SCAN_VALUE + continue + + if state == ParserState.IN_DOUBLE_QUOTE: + # collect characters up until a run of backslashes or double quote + match = re.search( + DOUBLE_QUOTE_VALUE_PATTERN, content[cursor:], re.MULTILINE + ) + if match is None: + raise ParserException(f"Unmatched double quote", cursor - 1) + new_var_content, backslashes_or_dquote = match.groups() + var_content.append(new_var_content) + cursor += match.end() + + if backslashes_or_dquote == '"': + state = ParserState.SCAN_VALUE + continue + + # Keep the excess (escaped) backslashes + var_content.append("\\" * (len(backslashes_or_dquote) // 2)) + + if len(backslashes_or_dquote) % 2 == 0: + # whatever follows is escaped + next_char = content[cursor] + var_content.append(next_char) + cursor += 1 + + return result + + +def _get_line_number(lines: List[str], position: int): + line_num = 1 + for line in lines: + if len(line) > position: + break + line_num += 1 + position -= len(line) + return line_num, position diff --git a/poethepoet/exceptions.py b/poethepoet/exceptions.py index b301b158f..d327f3f4b 100644 --- a/poethepoet/exceptions.py +++ b/poethepoet/exceptions.py @@ -5,6 +5,14 @@ def __init__(self, msg, *args): self.args = (msg, *args) +class CyclicDependencyError(PoeException): + pass + + +class ScriptParseError(PoeException): + pass + + class ExecutionError(RuntimeError): def __init__(self, msg, *args): self.msg = msg diff --git a/poethepoet/executor/base.py b/poethepoet/executor/base.py index b11a345e0..8494c1238 100644 --- a/poethepoet/executor/base.py +++ b/poethepoet/executor/base.py @@ -1,7 +1,18 @@ import signal from subprocess import Popen, PIPE import sys -from typing import Any, Dict, MutableMapping, Optional, Sequence, Type, TYPE_CHECKING +from typing import ( + Any, + Dict, + Mapping, + MutableMapping, + Optional, + Sequence, + Tuple, + Type, + TYPE_CHECKING, + Union, +) from ..exceptions import PoeException from ..virtualenv import Virtualenv @@ -9,7 +20,7 @@ from pathlib import Path from ..context import RunContext -# TODO: maybe invert the control so the executor is given a task to run +# TODO: maybe invert the control so the executor is given a task to run? class MetaPoeExecutor(type): @@ -38,50 +49,57 @@ class PoeExecutor(metaclass=MetaPoeExecutor): def __init__( self, + invocation: Tuple[str, ...], context: "RunContext", - options: MutableMapping[str, str], - env: MutableMapping[str, str], + options: Mapping[str, str], + env: Mapping[str, str], working_dir: Optional["Path"] = None, dry: bool = False, + capture_stdout: Union[str, bool] = False, ): + self.invocation = invocation self.context = context self.options = options self.working_dir = working_dir self.env = env self.dry = dry + self.capture_stdout = capture_stdout @classmethod def get( cls, + invocation: Tuple[str, ...], context: "RunContext", - env: MutableMapping[str, str], + env: Mapping[str, str], working_dir: Optional["Path"] = None, dry: bool = False, - executor_config: Optional[Dict[str, str]] = None, + executor_config: Optional[Mapping[str, str]] = None, + capture_stdout: Union[str, bool] = False, ) -> "PoeExecutor": """""" # use task specific executor config or fallback to global options = executor_config or context.config.executor return cls._resolve_implementation(context, executor_config)( - context, options, env, working_dir, dry + invocation, context, options, env, working_dir, dry, capture_stdout ) @classmethod def _resolve_implementation( - cls, context: "RunContext", executor_config: Optional[Dict[str, str]] + cls, context: "RunContext", executor_config: Optional[Mapping[str, str]] ): """ Resolve to an executor class, either as specified in the available config or by making some reasonable assumptions based on visible features of the environment """ + config_executor_type = context.executor_type if executor_config: if executor_config["type"] not in cls.__executor_types: raise PoeException( f"Cannot instantiate unknown executor {executor_config['type']!r}" ) return cls.__executor_types[executor_config["type"]] - elif context.config.executor["type"] == "auto": + elif config_executor_type == "auto": if "poetry" in context.config.project["tool"]: # Looks like this is a poetry project! return cls.__executor_types["poetry"] @@ -92,14 +110,13 @@ def _resolve_implementation( # Fallback to not using any particular environment return cls.__executor_types["simple"] else: - if context.config.executor["type"] not in cls.__executor_types: + if config_executor_type not in cls.__executor_types: raise PoeException( - f"Cannot instantiate unknown executor" - + repr(context.config.executor["type"]) + f"Cannot instantiate unknown executor" + repr(config_executor_type) ) - return cls.__executor_types[context.config.executor["type"]] + return cls.__executor_types[config_executor_type] - def execute(self, cmd: Sequence[str], input: Optional[bytes] = None,) -> int: + def execute(self, cmd: Sequence[str], input: Optional[bytes] = None) -> int: raise NotImplementedError def _exec_via_subproc( @@ -107,7 +124,7 @@ def _exec_via_subproc( cmd: Sequence[str], *, input: Optional[bytes] = None, - env: Optional[MutableMapping[str, str]] = None, + env: Optional[Mapping[str, str]] = None, shell: bool = False, ) -> int: if self.dry: @@ -116,6 +133,11 @@ def _exec_via_subproc( popen_kwargs["env"] = self.env if env is None else env if input is not None: popen_kwargs["stdin"] = PIPE + if self.capture_stdout: + if isinstance(self.capture_stdout, str): + popen_kwargs["stdout"] = open(self.capture_stdout, "wb") + else: + popen_kwargs["stdout"] = PIPE if self.working_dir is not None: popen_kwargs["cwd"] = self.working_dir @@ -131,7 +153,10 @@ def handle_signal(signum, _frame): old_signal_handler = signal.signal(signal.SIGINT, handle_signal) # send data to the subprocess and wait for it to finish - proc.communicate(input) + (captured_stdout, _) = proc.communicate(input) + + if self.capture_stdout == True: + self.context.captured_stdout[self.invocation] = captured_stdout.decode() # restore signal handler signal.signal(signal.SIGINT, old_signal_handler) diff --git a/poethepoet/helpers/__init__.py b/poethepoet/helpers/__init__.py new file mode 100644 index 000000000..cfc906287 --- /dev/null +++ b/poethepoet/helpers/__init__.py @@ -0,0 +1,5 @@ +import re + + +def is_valid_env_var(var_name: str) -> bool: + return bool(re.match("^[a-zA-Z_][a-zA-Z0-9_]*$", var_name)) diff --git a/poethepoet/helpers/env.py b/poethepoet/helpers/env.py new file mode 100644 index 000000000..cfcf7a253 --- /dev/null +++ b/poethepoet/helpers/env.py @@ -0,0 +1,48 @@ +import re +from typing import Mapping + +_SHELL_VAR_PATTERN = re.compile( + # Matches shell variable patterns, distinguishing escaped examples (to be ignored) + # There may be a more direct way to doing this + r"(?:" + r"(?:[^\\]|^)(?:\\(?:\\{2})*)\$([\w\d_]+)|" # $VAR preceded by an odd num of \ + r"(?:[^\\]|^)(?:\\(?:\\{2})*)\$\{([\w\d_]+)\}|" # ${VAR} preceded by an odd num of \ + r"\$([\w\d_]+)|" # $VAR + r"\${([\w\d_]+)}" # ${VAR} + r")" +) + + +def resolve_envvars(content: str, env: Mapping[str, str]) -> str: + """ + Template in ${environmental} $variables from env as if we were in a shell + + Supports escaping of the $ if preceded by an odd number of backslashes, in which + case the backslash immediately precending the $ is removed. This is an + intentionally very limited implementation of escaping semantics for the sake of + usability. + """ + cursor = 0 + resolved_parts = [] + for match in _SHELL_VAR_PATTERN.finditer(content): + groups = match.groups() + # the first two groups match escaped varnames so should be ignored + var_name = groups[2] or groups[3] + escaped_var_name = groups[0] or groups[1] + if var_name: + var_value = env.get(var_name) + resolved_parts.append(content[cursor : match.start()]) + cursor = match.end() + if var_value is not None: + resolved_parts.append(var_value) + elif escaped_var_name: + # Remove the effective escape char + resolved_parts.append(content[cursor : match.start()]) + cursor = match.end() + matched = match.string[match.start() : match.end()] + if matched[0] == "\\": + resolved_parts.append(matched[1:]) + else: + resolved_parts.append(matched[0:1] + matched[2:]) + resolved_parts.append(content[cursor:]) + return "".join(resolved_parts) diff --git a/poethepoet/helpers/python.py b/poethepoet/helpers/python.py new file mode 100644 index 000000000..c6a22cb4b --- /dev/null +++ b/poethepoet/helpers/python.py @@ -0,0 +1,219 @@ +""" +Helper functions for parsing python code, as required by ScriptTask +""" + +import ast +from itertools import chain +import re +import sys +from typing import Container, Iterator, List, Tuple +from ..exceptions import ScriptParseError + + +_BUILTINS_WHITELIST = { + "abs", + "all", + "any", + "ascii", + "bin", + "chr", + "dir", + "divmod", + "environ", + "format", + "getattr", + "hasattr", + "hex", + "iter", + "len", + "max", + "min", + "next", + "oct", + "ord", + "pow", + "repr", + "round", + "sorted", + "sum", + "None", + "Ellipsis", + "False", + "True", + "bool", + "memoryview", + "bytearray", + "bytes", + "complex", + "dict", + "enumerate", + "filter", + "float", + "frozenset", + "int", + "list", + "map", + "range", + "reversed", + "set", + "slice", + "str", + "tuple", + "type", + "zip", +} + + +Substitution = Tuple[Tuple[int, int], str] + + +def resolve_function_call( + source: str, arguments: Container[str], args_prefix: str = "__args." +): + """ + Validate function call and substitute references to arguments with their namespaced + counterparts (e.g. `my_arg` => `args.my_arg`). + """ + + call_node = parse_and_validate(source) + + substitutions: List[Substitution] = [] + + # Collect all the variables + name_nodes: Iterator[ast.Name] = chain( + ( + node + for arg in call_node.args + for node in ast.walk(arg) + if isinstance(node, ast.Name) + ), + ( + node + for kwarg in call_node.keywords + for node in ast.walk(kwarg.value) + if isinstance(node, ast.Name) + ), + ) + for node in name_nodes: + if node.id in _BUILTINS_WHITELIST: + # builtin values have precedence over unqualified args + continue + if node.id in arguments: + substitutions.append( + (_get_name_node_abs_range(source, node), args_prefix + node.id) + ) + else: + raise ScriptParseError( + "Invalid variable reference in script: " + + _get_name_source_segment(source, node) + ) + + # Prefix references to arguments with args_prefix + return _apply_substitutions(source, substitutions) + + +def parse_and_validate(source: str): + """ + Parse the given source into an ast, validate that is consists of a single function + call, and return the Call node. + """ + + try: + module = ast.parse(source) + except SyntaxError as error: + raise ScriptParseError(f"Invalid script content: {source}") from error + + if len(module.body) != 1: + raise ScriptParseError( + f"Expected a single python expression, instead got: {source}" + ) + + first_statement = module.body[0] + if not isinstance(first_statement, ast.Expr): + raise ScriptParseError(f"Expected a function call, instead got: {source}") + + call_node = first_statement.value + if not isinstance(call_node, ast.Call): + raise ScriptParseError(f"Expected a function call, instead got: {source}") + + node = call_node.func + while isinstance(node, ast.Attribute): + node = node.value + if not isinstance(node, ast.Name): + raise ScriptParseError(f"Invalid function reference in: {source}") + + return call_node + + +def _apply_substitutions(content: str, subs: List[Substitution]): + """ + Returns a copy of content with all of the substitutions applied. + Uses a single pass for efficiency. + """ + cursor = 0 + segments: List[str] = [] + + for ((start, end), replacement) in sorted(subs, key=lambda x: x[0][0]): + in_between = content[cursor:start] + segments.extend((in_between, replacement)) + cursor += len(in_between) + (end - start) + + segments.append(content[cursor:]) + + return "".join(segments) + + +# This pattern matches the sequence of chars from the begining of the string that are +# *probably* a valid identifier +IDENTIFIER_PATTERN = r"[^\s\!-\/\:-\@\[-\^\{-\~`]+" + + +def _get_name_node_abs_range(source: str, node: ast.Name): + """ + Find the absolute start and end offsets of the given name node in the source. + """ + + source_lines = re.findall(r".*?(?:\r\n|\r|\n)", source + "\n") + prev_lines_offset = sum(len(line) for line in source_lines[: node.lineno - 1]) + own_line_offset = len( + source_lines[node.lineno - 1].encode()[: node.col_offset].decode() + ) + total_start_chars_offset = prev_lines_offset + own_line_offset + + name_content = re.match( # type: ignore + IDENTIFIER_PATTERN, source[total_start_chars_offset:] + ).group() + while not name_content.isidentifier() and name_content: + name_content = name_content[:-1] + + return (total_start_chars_offset, total_start_chars_offset + len(name_content)) + + +def _get_name_source_segment(source: str, node: ast.Name): + """ + Before python 3.8 the ast module didn't allow for easily identifying the source + segment of a node, so this function provides this functionality specifically for + name nodes as needed here. + + The fallback logic is specialised for name nodes which cannot span multiple lines + and must be valid identifiers. It is expected to be correct in all cases, and + performant in common cases. + """ + if sys.version_info.minor >= 8: + return ast.get_source_segment(source, node) # type: ignore + + partial_result = ( + re.split(r"(?:\r\n|\r|\n)", source)[node.lineno - 1] + .encode()[node.col_offset :] + .decode() + ) + + # The name probably extends to the first ascii char outside of [a-zA-Z\d_] + # regex will always match with valid arguments to this function + partial_result = re.match(IDENTIFIER_PATTERN, partial_result).group() # type: ignore + + # This bit is a nasty hack, but probably always gets skipped + while not partial_result.isidentifier() and partial_result: + partial_result = partial_result[:-1] + + return partial_result diff --git a/poethepoet/task/args.py b/poethepoet/task/args.py new file mode 100644 index 000000000..796041d5b --- /dev/null +++ b/poethepoet/task/args.py @@ -0,0 +1,245 @@ +import argparse +from typing import ( + Any, + Dict, + List, + Optional, + Sequence, + Set, + Tuple, + Type, + Union, + TYPE_CHECKING, +) + +if TYPE_CHECKING: + from ..config import PoeConfig + +ArgParams = Dict[str, Any] +ArgsDef = Union[List[str], List[ArgParams], Dict[str, ArgParams]] + +arg_param_schema: Dict[str, Union[Type, Tuple[Type, ...]]] = { + "default": (str, int, float, bool), + "help": str, + "name": str, + "options": (list, tuple), + "positional": (bool, str), + "required": bool, + "type": str, +} +arg_types: Dict[str, Type] = { + "string": str, + "float": float, + "integer": int, + "boolean": bool, +} + + +class PoeTaskArgs: + _args: Tuple[ArgParams, ...] + + def __init__(self, args_def: ArgsDef, task_name: str): + self._args = self._normalize_args_def(args_def) + self._task_name = task_name + + @classmethod + def _normalize_args_def(cls, args_def: ArgsDef) -> Tuple[ArgParams, ...]: + """ + args_def can be defined as a dictionary of ArgParams, or a list of strings, or + ArgParams. Here we normalize it to a list of ArgParams, assuming that it has + already been validated. + """ + result = [] + if isinstance(args_def, list): + for item in args_def: + if isinstance(item, str): + result.append({"name": item, "options": (f"--{item}",)}) + else: + result.append( + dict( + item, + options=cls._get_arg_options_list(item), + ) + ) + else: + for name, params in args_def.items(): + result.append( + dict( + params, + name=name, + options=cls._get_arg_options_list(params, name), + ) + ) + return tuple(result) + + @staticmethod + def _get_arg_options_list(arg: ArgParams, name: Optional[str] = None): + position = arg.get("positional", False) + name = name or arg["name"] + if position: + if isinstance(position, str): + return [position] + return [name] + return tuple(arg.get("options", (f"--{name}",))) + + @classmethod + def get_help_content( + cls, args_def: Optional[ArgsDef] + ) -> List[Tuple[Tuple[str, ...], str]]: + if args_def is None: + return [] + return [ + (arg["options"], arg.get("help", "")) + for arg in cls._normalize_args_def(args_def) + ] + + @classmethod + def validate_def(cls, task_name: str, args_def: ArgsDef) -> Optional[str]: + arg_names: Set[str] = set() + if isinstance(args_def, list): + for item in args_def: + # can be a list of strings (just arg name) or ArgConfig dictionaries + if isinstance(item, str): + arg_name = item + elif isinstance(item, dict): + arg_name = item.get("name", "") + error = cls._validate_params(item, arg_name, task_name) + if error: + return error + else: + return f"Arg {item!r} of task {task_name!r} has invlaid type" + error = cls._validate_name(arg_name, task_name, arg_names) + if error: + return error + elif isinstance(args_def, dict): + for arg_name, params in args_def.items(): + error = cls._validate_name(arg_name, task_name, arg_names) + if error: + return error + if "name" in params: + return ( + f"Unexpected 'name' option for arg {arg_name!r} of task " + f"{task_name!r}" + ) + error = cls._validate_params(params, arg_name, task_name) + if error: + return error + error = cls._validate_type(params, arg_name, task_name) + if error: + return error + return None + + @classmethod + def _validate_name( + cls, name: Any, task_name: str, arg_names: Set[str] + ) -> Optional[str]: + if not isinstance(name, str): + return f"Arg name {name!r} of task {task_name!r} should be a string" + if not name.replace("-", "_").isidentifier(): + return ( + f"Arg name {name!r} of task {task_name!r} is not a valid 'identifier'" + f"see the following documentation for details" + f"https://docs.python.org/3/reference/lexical_analysis.html#identifiers" + ) + if name in arg_names: + return f"Duplicate arg name {name!r} for task {task_name!r}" + arg_names.add(name) + return None + + @classmethod + def _validate_params( + cls, params: ArgParams, arg_name: str, task_name: str + ) -> Optional[str]: + for param, value in params.items(): + if param not in arg_param_schema: + return ( + f"Invalid option {param!r} for arg {arg_name!r} of task " + f"{task_name!r}" + ) + if not isinstance(value, arg_param_schema[param]): + return ( + f"Invalid value for option {param!r} of arg {arg_name!r} of" + f" task {task_name!r}" + ) + + positional = params.get("positional", False) + if positional: + if params.get("type") == "boolean": + return ( + f"Positional argument {arg_name!r} of task {task_name!r} may not" + "have type 'boolean'" + ) + if params.get("options") is not None: + return ( + f"Positional argument {arg_name!r} of task {task_name!r} may not" + "have options defined" + ) + if isinstance(positional, str) and not positional.isidentifier(): + return ( + f"positional name {positional!r} for arg {arg_name!r} of task " + f"{task_name!r} is not a valid 'identifier' see the following " + "documentation for details" + "https://docs.python.org/3/reference/lexical_analysis.html#identifiers" + ) + + return None + + @classmethod + def _validate_type( + cls, params: ArgParams, arg_name: str, task_name: str + ) -> Optional[str]: + if "type" in params and params["type"] not in arg_types: + return ( + f"{params['type']!r} is not a valid type for arg {arg_name!r} of task " + f"{task_name!r}. Choose one of " + "{" + f'{" ".join(sorted(str_type for str_type in arg_types.keys()))}' + "}" + ) + return None + + def build_parser(self) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog=f"poe {self._task_name}", add_help=False, allow_abbrev=False + ) + for arg in self._args: + parser.add_argument( + *arg["options"], + **self._get_argument_params(arg), + ) + return parser + + def _get_argument_params(self, arg: ArgParams): + default = arg.get("default") + result = { + "default": default, + "help": arg.get("help", ""), + } + + required = arg.get("required", False) + arg_type = str(arg.get("type")) + + if arg.get("positional", False): + if not required: + result["nargs"] = "?" + else: + result["dest"] = arg["name"] + result["required"] = required + + if arg_type == "boolean": + result["action"] = "store_false" if default else "store_true" + else: + result["type"] = arg_types.get(arg_type, str) + + return result + + def parse(self, extra_args: Sequence[str]): + parsed_args = vars(self.build_parser().parse_args(extra_args)) + # Ensure positional args are still exposed by name even if the were parsed with + # alternate identifiers + for arg in self._args: + if isinstance(arg.get("positional"), str): + parsed_args[arg["name"]] = parsed_args[arg["positional"]] + del parsed_args[arg["positional"]] + # args named with dash case are converted to snake case before being exposed + return {name.replace("-", "_"): value for name, value in parsed_args.items()} diff --git a/poethepoet/task/base.py b/poethepoet/task/base.py index 71644ec20..cad333281 100644 --- a/poethepoet/task/base.py +++ b/poethepoet/task/base.py @@ -1,18 +1,23 @@ import re +import shlex import sys from typing import ( Any, Dict, - Iterable, + Iterator, List, - MutableMapping, + Mapping, Optional, + Sequence, Tuple, Type, TYPE_CHECKING, Union, ) +from .args import PoeTaskArgs from ..exceptions import PoeException +from ..helpers import is_valid_env_var +from ..helpers.env import resolve_envvars if TYPE_CHECKING: from ..context import RunContext @@ -23,16 +28,6 @@ TaskDef = Union[str, Dict[str, Any], List[Union[str, Dict[str, Any]]]] _TASK_NAME_PATTERN = re.compile(r"^\w[\w\d\-\_\+\:]*$") -_SHELL_VAR_PATTERN = re.compile( - # Matches shell variable patterns, distinguishing escaped examples (to be ignored) - # There may be a more direct way to doing this - r"(?:" - r"(?:[^\\]|^)(?:\\(?:\\{2})*)\$([\w\d_]+)|" # $VAR preceded by an odd num of \ - r"(?:[^\\]|^)(?:\\(?:\\{2})*)\$\{([\w\d_]+)\}|" # ${VAR} preceded by an odd num of \ - r"\$([\w\d_]+)|" # $VAR - r"\${([\w\d_]+)}" # ${VAR} - r")" -) class MetaPoeTask(type): @@ -58,11 +53,24 @@ class PoeTask(metaclass=MetaPoeTask): content: TaskContent options: Dict[str, Any] - __options__: Dict[str, Type] = {} + __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} __content_type__: Type = str - __base_options: Dict[str, Type] = {"env": dict, "executor": dict, "help": str} + __base_options: Dict[str, Union[Type, Tuple[Type, ...]]] = { + "args": (dict, list), + "capture_stdout": (str), + "deps": list, + "env": dict, + "envfile": str, + "executor": dict, + "help": str, + "uses": dict, + } __task_types: Dict[str, Type["PoeTask"]] = {} + __upstream_invocations: Optional[ + Dict[str, Union[List[Tuple[str, ...]], Dict[str, Tuple[str, ...]]]] + ] = None + def __init__( self, name: str, @@ -70,20 +78,41 @@ def __init__( options: Dict[str, Any], ui: "PoeUi", config: "PoeConfig", + invocation: Tuple[str, ...], + capture_stdout: bool = False, ): self.name = name self.content = content.strip() if isinstance(content, str) else content - self.options = options + if capture_stdout: + self.options = dict(options, capture_stdout=True) + else: + self.options = options self._ui = ui self._config = config self._is_windows = sys.platform == "win32" + self.invocation = invocation + self.named_args = self._parse_named_args(invocation[1:]) @classmethod - def from_config(cls, task_name: str, config: "PoeConfig", ui: "PoeUi") -> "PoeTask": + def from_config( + cls, + task_name: str, + config: "PoeConfig", + ui: "PoeUi", + invocation: Tuple[str, ...], + capture_stdout: Optional[bool] = None, + ) -> "PoeTask": task_def = config.tasks.get(task_name) if not task_def: raise PoeException(f"Cannot instantiate unknown task {task_name!r}") - return cls.from_def(task_def, task_name, config, ui) + return cls.from_def( + task_def, + task_name, + config, + ui, + invocation=invocation, + capture_stdout=capture_stdout, + ) @classmethod def from_def( @@ -92,103 +121,177 @@ def from_def( task_name: str, config: "PoeConfig", ui: "PoeUi", + invocation: Tuple[str, ...], array_item: Union[bool, str] = False, + capture_stdout: Optional[bool] = None, ) -> "PoeTask": - if array_item: - if isinstance(task_def, str): - task_type = ( + task_type = cls.resolve_task_type(task_def, config, array_item) + if task_type is None: + # Something is wrong with this task_def + raise cls.Error(cls.validate_def(task_name, task_def, config)) + + options: Dict[str, Any] = {} + if capture_stdout is not None: + # Override config because we want to specifically capture the stdout of this + # task for internal use + options["capture_stdout"] = capture_stdout + + if isinstance(task_def, (str, list)): + return cls.__task_types[task_type]( + name=task_name, + content=task_def, + options=options, + ui=ui, + config=config, + invocation=invocation, + ) + + assert isinstance(task_def, dict) + options = dict(task_def, **options) + content = options.pop(task_type) + return cls.__task_types[task_type]( + name=task_name, + content=content, + options=options, + ui=ui, + config=config, + invocation=invocation, + ) + + @classmethod + def resolve_task_type( + cls, + task_def: TaskDef, + config: "PoeConfig", + array_item: Union[bool, str] = False, + ) -> Optional[str]: + if isinstance(task_def, str): + if array_item: + return ( array_item if isinstance(array_item, str) else config.default_array_item_task_type ) - return cls.__task_types[task_type]( - name=task_name, content=task_def, options={}, ui=ui, config=config - ) - else: - if isinstance(task_def, str): - return cls.__task_types[config.default_task_type]( - name=task_name, content=task_def, options={}, ui=ui, config=config - ) - if isinstance(task_def, list): - return cls.__task_types[config.default_array_task_type]( - name=task_name, content=task_def, options={}, ui=ui, config=config - ) + else: + return config.default_task_type - assert isinstance(task_def, dict) - task_type_keys = set(task_def.keys()).intersection(cls.__task_types) - if len(task_type_keys) == 1: - task_type_key = next(iter(task_type_keys)) - options = dict(task_def) - content = options.pop(task_type_key) - return cls.__task_types[task_type_key]( - name=task_name, content=content, options=options, ui=ui, config=config - ) + elif isinstance(task_def, list): + return config.default_array_task_type + + elif isinstance(task_def, dict): + task_type_keys = set(task_def.keys()).intersection(cls.__task_types) + if len(task_type_keys) == 1: + return next(iter(task_type_keys)) + + return None + + def _parse_named_args(self, extra_args: Sequence[str]) -> Optional[Dict[str, str]]: + args_def = self.options.get("args") + if args_def: + return PoeTaskArgs(args_def, self.name).parse(extra_args) + return None - # Something is wrong with this task_def - raise cls.Error(cls.validate_def(task_name, task_def, config)) + def add_named_args_to_env( + self, env: Mapping[str, str] + ) -> Tuple[Mapping[str, str], bool]: + if self.named_args is None: + return env, False + return ( + dict( + env, + **( + { + key: str(value) + for key, value in self.named_args.items() + if value is not None + } + ), + ), + bool(self.named_args), + ) def run( self, context: "RunContext", - extra_args: Iterable[str], - env: Optional[MutableMapping[str, str]] = None, + extra_args: Sequence[str] = tuple(), + env: Optional[Mapping[str, str]] = None, ) -> int: """ Run this task """ - env = dict(env or {}, **self._config.global_env) - if self.options.get("env"): - env = dict(env, **self.options["env"]) - return self._handle_run(context, extra_args, env) + upstream_invocations = self._get_upstream_invocations(context) + return self._handle_run( + context, + extra_args, + context.get_env( + env, + self.options.get("envfile"), + self.options.get("env"), + upstream_invocations["uses"], + ), + ) def _handle_run( self, context: "RunContext", - extra_args: Iterable[str], - env: MutableMapping[str, str], + extra_args: Sequence[str], + env: Mapping[str, str], ) -> int: """ - _handle_run must be implemented by a subclass and return a single executor result. + _handle_run must be implemented by a subclass and return a single executor + result. """ raise NotImplementedError - @staticmethod - def _resolve_envvars( - content: str, context: "RunContext", env: MutableMapping[str, str] - ) -> str: - """ - Template in ${environmental} $variables from env as if we were in a shell + def iter_upstream_tasks( + self, context: "RunContext" + ) -> Iterator[Tuple[str, "PoeTask"]]: + invocations = self._get_upstream_invocations(context) + for invocation in invocations["deps"]: + yield ("", self._instantiate_dep(invocation, capture_stdout=False)) + for key, invocation in invocations["uses"].items(): + yield (key, self._instantiate_dep(invocation, capture_stdout=True)) - Supports escaping of the $ if preceded by an odd number of backslashes, in which - case the backslash immediately precending the $ is removed. This is an - intentionally very limited implementation of escaping semantics for the sake of - usability. + def _get_upstream_invocations(self, context: "RunContext"): """ - env = context.get_env(env) - cursor = 0 - resolved_parts = [] - for match in _SHELL_VAR_PATTERN.finditer(content): - groups = match.groups() - # the first two groups match escaped varnames so should be ignored - var_name = groups[2] or groups[3] - escaped_var_name = groups[0] or groups[1] - if var_name: - var_value = env.get(var_name) - resolved_parts.append(content[cursor : match.start()]) - cursor = match.end() - if var_value is not None: - resolved_parts.append(var_value) - elif escaped_var_name: - # Remove the effective escape char - resolved_parts.append(content[cursor : match.start()]) - cursor = match.end() - matched = match.string[match.start() : match.end()] - if matched[0] == "\\": - resolved_parts.append(matched[1:]) - else: - resolved_parts.append(matched[0:1] + matched[2:]) - resolved_parts.append(content[cursor:]) - return "".join(resolved_parts) + NB. this memoization assumes the context (and contained env vars) will be the + same in all instances for the lifetime of this object. Whilst this should be OK + for all corrent usecases is it strictly speaking something that this object + should not know enough to safely assume. So we probably want to revisit this. + """ + if self.__upstream_invocations is None: + env: Mapping + env = context.get_env( + {}, self.options.get("envfile"), self.options.get("env") + ) + env, _ = self.add_named_args_to_env(env) + + self.__upstream_invocations = { + "deps": [ + tuple(shlex.split(resolve_envvars(task_ref, env))) + for task_ref in self.options.get("deps", tuple()) + ], + "uses": { + key: tuple(shlex.split(resolve_envvars(task_ref, env))) + for key, task_ref in self.options.get("uses", {}).items() + }, + } + + return self.__upstream_invocations + + def _instantiate_dep( + self, invocation: Tuple[str, ...], capture_stdout: bool + ) -> "PoeTask": + return self.from_config( + invocation[0], + config=self._config, + ui=self._ui, + invocation=invocation, + capture_stdout=capture_stdout, + ) + + def has_deps(self) -> bool: + return bool(self.options.get("deps", False) or self.options.get("uses", False)) @classmethod def validate_def( @@ -197,7 +300,6 @@ def validate_def( """ Check the given task name and definition for validity and return a message describing the first encountered issue if any. - If raize is True then the issue is raised as an exception. """ if not (task_name[0].isalpha() or task_name[0] == "_"): return ( @@ -211,47 +313,76 @@ def validate_def( ) elif isinstance(task_def, dict): task_type_keys = set(task_def.keys()).intersection(cls.__task_types) - if len(task_type_keys) == 1: - task_type_key = next(iter(task_type_keys)) - task_content = task_def[task_type_key] - task_type = cls.__task_types[task_type_key] - if not isinstance(task_content, task_type.__content_type__): - return ( - f"Invalid task: {task_name!r}. {task_type} value must be a " - f"{task_type.__content_type__}" + if len(task_type_keys) != 1: + return ( + f"Invalid task: {task_name!r}. Task definition must include exactly" + f" one task key from {set(cls.__task_types)!r}" + ) + task_type_key = next(iter(task_type_keys)) + task_content = task_def[task_type_key] + task_type = cls.__task_types[task_type_key] + if not isinstance(task_content, task_type.__content_type__): + return ( + f"Invalid task: {task_name!r}. {task_type} value must be a " + f"{task_type.__content_type__}" + ) + else: + for key in set(task_def) - {task_type_key}: + expected_type = cls.__base_options.get( + key, task_type.__options__.get(key) ) + if expected_type is None: + return ( + f"Invalid task: {task_name!r}. Unrecognised option " + f"{key!r} for task of type: {task_type_key}." + ) + elif not isinstance(task_def[key], expected_type): + return ( + f"Invalid task: {task_name!r}. Option {key!r} should " + f"have a value of type {expected_type!r}" + ) else: - for key in set(task_def) - {task_type_key}: - expected_type = cls.__base_options.get( - key, task_type.__options__.get(key) + if hasattr(task_type, "_validate_task_def"): + task_type_issue = task_type._validate_task_def( + task_name, task_def, config ) - if expected_type is None: - return ( - f"Invalid task: {task_name!r}. Unrecognised option " - f"{key!r} for task of type: {task_type_key}." - ) - elif not isinstance(task_def[key], expected_type): - return ( - f"Invalid task: {task_name!r}. Option {key!r} should " - f"have a value of type {expected_type!r}" - ) - else: - if hasattr(task_type, "_validate_task_def"): - task_type_issue = task_type._validate_task_def( - task_name, task_def, config - ) - if task_type_issue: - return task_type_issue - if "\n" in task_def.get("help", ""): - return ( - f"Invalid task: {task_name!r}. Help messages cannot contain " - "line breaks" - ) - else: + if task_type_issue: + return task_type_issue + + if "args" in task_def: + return PoeTaskArgs.validate_def(task_name, task_def["args"]) + + if "\n" in task_def.get("help", ""): return ( - f"Invalid task: {task_name!r}. Task definition must include exactly" - f" one task key from {set(cls.__task_types)!r}" + f"Invalid task: {task_name!r}. Help messages cannot contain " + "line breaks" ) + + all_task_names = set(config.tasks) + + if "deps" in task_def: + for dep in task_def["deps"]: + dep_task_name = dep.split(" ", 1)[0] + if dep_task_name not in all_task_names: + return ( + f"Invalid task: {task_name!r}. deps options contains " + f"reference to unknown task: {dep_task_name!r}" + ) + + if "uses" in task_def: + for key, dep in task_def["uses"].items(): + if not is_valid_env_var(key): + return ( + f"Invalid task: {task_name!r} uses options contains invalid" + f" key: {key!r}" + ) + dep_task_name = dep.split(" ", 1)[0] + if dep_task_name not in all_task_names: + return ( + f"Invalid task: {task_name!r}. uses options contains " + f"reference to unknown task: {dep_task_name!r}" + ) + return None @classmethod @@ -259,7 +390,7 @@ def is_task_type( cls, task_def_key: str, content_type: Optional[Type] = None ) -> bool: """ - Checks whether the given key identified a known task type. + Checks whether the given key identifies a known task type. Optionally also check whether the given content_type matches the type of content for this tasks type. """ @@ -295,7 +426,10 @@ def _print_action(self, action: str, dry: bool): Print the action taken by a task just before executing it. """ min_verbosity = -1 if dry else 0 - self._ui.print_msg(f"Poe => {action}", min_verbosity) + arrow = "<=" if self.options.get("capture_stdout") else "=>" + self._ui.print_msg( + f"Poe {arrow} {action}", min_verbosity + ) class Error(Exception): pass diff --git a/poethepoet/task/cmd.py b/poethepoet/task/cmd.py index bce8e690b..8a9fb6a63 100644 --- a/poethepoet/task/cmd.py +++ b/poethepoet/task/cmd.py @@ -3,12 +3,15 @@ import shlex from typing import ( Dict, - Iterable, - MutableMapping, + Mapping, + Sequence, Type, + Tuple, TYPE_CHECKING, + Union, ) from .base import PoeTask +from ..helpers.env import resolve_envvars if TYPE_CHECKING: from ..config import PoeConfig @@ -26,27 +29,31 @@ class CmdTask(PoeTask): content: str __key__ = "cmd" - __options__: Dict[str, Type] = {} + __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} def _handle_run( self, context: "RunContext", - extra_args: Iterable[str], - env: MutableMapping[str, str], + extra_args: Sequence[str], + env: Mapping[str, str], ) -> int: - cmd = (*self._resolve_args(context, env), *extra_args) + env, has_named_args = self.add_named_args_to_env(env) + if has_named_args: + # If named arguments are defined then it doesn't make sense to pass extra + # args to the command, because they've already been parsed + cmd = self._resolve_args(context, env) + else: + cmd = (*self._resolve_args(context, env), *extra_args) self._print_action(" ".join(cmd), context.dry) - return context.get_executor(env, self.options.get("executor")).execute(cmd) + return context.get_executor(self.invocation, env, self.options).execute(cmd) - def _resolve_args( - self, context: "RunContext", env: MutableMapping[str, str], - ): + def _resolve_args(self, context: "RunContext", env: Mapping[str, str]): # Parse shell command tokens and check if they're quoted if self._is_windows: cmd_tokens = ( (compat_token, bool(_QUOTED_TOKEN_PATTERN.match(compat_token))) for compat_token in shlex.split( - self._resolve_envvars(self.content, context, env), + resolve_envvars(self.content, env), posix=False, comments=True, ) @@ -56,12 +63,12 @@ def _resolve_args( (posix_token, bool(_QUOTED_TOKEN_PATTERN.match(compat_token))) for (posix_token, compat_token) in zip( shlex.split( - self._resolve_envvars(self.content, context, env), + resolve_envvars(self.content, env), posix=True, comments=True, ), shlex.split( - self._resolve_envvars(self.content, context, env), + resolve_envvars(self.content, env), posix=False, comments=True, ), diff --git a/poethepoet/task/graph.py b/poethepoet/task/graph.py new file mode 100644 index 000000000..db11a81cc --- /dev/null +++ b/poethepoet/task/graph.py @@ -0,0 +1,141 @@ +from typing import Dict, Set, List, Tuple +from ..context import RunContext +from ..exceptions import CyclicDependencyError +from .base import PoeTask + + +class TaskExecutionNode: + task: PoeTask + direct_dependants: List["TaskExecutionNode"] + direct_dependencies: Set[Tuple[str, ...]] + path_dependants: Tuple[str, ...] + capture_stdout: bool + + def __init__( + self, + task: PoeTask, + direct_dependants: List["TaskExecutionNode"], + path_dependants: Tuple[str, ...], + capture_stdout: bool = False, + ): + self.task = task + self.direct_dependants = direct_dependants + self.direct_dependencies = set() + self.path_dependants = (task.name, *path_dependants) + self.capture_stdout = capture_stdout + + def is_source(self): + return not self.task.has_deps() + + @property + def identifier(self) -> Tuple[str, ...]: + return self.task.invocation + + +class TaskExecutionGraph: + """ + A directed-acyclic execution graph of tasks, with a single sink node, and any number + of source nodes. Non-source nodes may have multiple upstream nodes, and non-sink + nodes may have multiple downstream nodes. + + A task/node may appear twice in the graph, if one instance has captured output, and + one does not. Nodes are deduplicated to enforce this. + """ + + _context: RunContext + sink: TaskExecutionNode + sources: List[TaskExecutionNode] + captured_tasks: Dict[Tuple[str, ...], TaskExecutionNode] + uncaptured_tasks: Dict[Tuple[str, ...], TaskExecutionNode] + + def __init__( + self, + sink_task: PoeTask, + context: RunContext, + ): + self._context = context + self.sink = TaskExecutionNode(sink_task, [], tuple()) + self.sources = [] + self.captured_tasks = {} + self.uncaptured_tasks = {} + + # Build graph + self._resolve_node_deps(self.sink) + + def get_execution_plan(self) -> List[List[PoeTask]]: + """ + Derive an execution plan from the DAG in terms of stages consisting of tasks + that could theoretically be parallelized. + """ + # TODO: if we parallelize tasks then this should be modified to support lazy + # scheduling + + stages: List[List[TaskExecutionNode]] = [self.sources] + visited = set(source.identifier for source in self.sources) + + while True: + next_stage = [] + for node in stages[-1]: + for dep_node in node.direct_dependants: + if ( + dep_node.identifier in visited + or not dep_node.direct_dependencies.issubset(visited) + ): + # We've already added this node OR some dependencies of dep_node + # have not been added so we can't add it yet + continue + + next_stage.append(dep_node) + visited.add(dep_node.identifier) + + if not next_stage: + break + + stages.append(next_stage) + + return [[node.task for node in stage] for stage in stages] + + def _resolve_node_deps(self, node: TaskExecutionNode): + """ + Build a DAG of tasks by depth-first traversal of the dependency tree starting + from the sink node. + """ + for key, task in node.task.iter_upstream_tasks(self._context): + node.direct_dependencies.add(task.invocation) + + if task.invocation in node.path_dependants: + raise CyclicDependencyError( + f"Encountered cyclic task dependency with task: {task.name!r}" + ) + + # a non empty key indicates output is captured + capture_stdout = bool(key) + + # Check if a node already exists for this task + if capture_stdout: + if task.invocation in self.captured_tasks: + # reuse instance of task with captured output + self.captured_tasks[task.invocation].direct_dependants.append(node) + continue + elif task.invocation in self.uncaptured_tasks: + # reuse instance of task with uncaptured output + self.uncaptured_tasks[task.invocation].direct_dependants.append(node) + continue + + # This task has not been encountered before via another path + new_node = TaskExecutionNode( + task, [node], node.path_dependants, capture_stdout + ) + + # Keep track of this task/node so it can be found by other dependants + if capture_stdout: + self.captured_tasks[task.invocation] = new_node + else: + self.uncaptured_tasks[task.invocation] = new_node + + if new_node.is_source(): + # Track this node as having no dependencies + self.sources.append(new_node) + else: + # Recurse immediately for DFS + self._resolve_node_deps(new_node) diff --git a/poethepoet/task/ref.py b/poethepoet/task/ref.py index d6062e85a..cdb06a1c6 100644 --- a/poethepoet/task/ref.py +++ b/poethepoet/task/ref.py @@ -1,13 +1,17 @@ +import shlex from typing import ( Any, Dict, - Iterable, - MutableMapping, + Mapping, Optional, + Sequence, Type, + Tuple, TYPE_CHECKING, + Union, ) from .base import PoeTask +from ..helpers.env import resolve_envvars if TYPE_CHECKING: from ..config import PoeConfig @@ -19,24 +23,23 @@ class RefTask(PoeTask): A task consisting of a reference to another task """ - # TODO: support extending/overriding env or other configuration of the referenced task - content: str __key__ = "ref" - __options__: Dict[str, Type] = {} + __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} def _handle_run( self, context: "RunContext", - extra_args: Iterable[str], - env: MutableMapping[str, str], + extra_args: Sequence[str], + env: Mapping[str, str], ) -> int: """ Lookup and delegate to the referenced task """ - task = self.from_config(self.content, self._config, ui=self._ui) - return task.run(context=context, extra_args=extra_args, env=env,) + invocation = tuple(shlex.split(resolve_envvars(self.content, env))) + task = self.from_config(invocation[0], self._config, self._ui, invocation) + return task.run(context=context, extra_args=extra_args, env=env) @classmethod def _validate_task_def( @@ -46,7 +49,9 @@ def _validate_task_def( Check the given task definition for validity specific to this task type and return a message describing the first encountered issue if any. """ - if task_def["ref"] not in config.tasks: - return f"Task {task_name!r} contains reference to unkown task {task_def['ref']!r}" + task_ref = task_def["ref"] + + if shlex.split(task_ref)[0] not in config.tasks: + return f"Task {task_name!r} contains reference to unkown task {task_ref!r}" return None diff --git a/poethepoet/task/script.py b/poethepoet/task/script.py index d2c03a58a..29d14120e 100644 --- a/poethepoet/task/script.py +++ b/poethepoet/task/script.py @@ -1,89 +1,114 @@ -import re +import ast from typing import ( Any, Dict, - Iterable, + Mapping, Optional, - MutableMapping, + Sequence, Tuple, Type, TYPE_CHECKING, Union, ) from .base import PoeTask +from ..exceptions import ScriptParseError +from ..helpers.env import resolve_envvars +from ..helpers.python import ( + resolve_function_call, + parse_and_validate, +) if TYPE_CHECKING: from ..config import PoeConfig from ..context import RunContext -_FUNCTION_CALL_PATTERN = re.compile(r"^(.+)\((.*)\)\s*;?\s*$") - - class ScriptTask(PoeTask): """ A task consisting of a reference to a python script """ content: str + _callnode: ast.Call __key__ = "script" - __options__: Dict[str, Type] = {} + __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} def _handle_run( self, context: "RunContext", - extra_args: Iterable[str], - env: MutableMapping[str, str], + extra_args: Sequence[str], + env: Mapping[str, str], ) -> int: # TODO: check whether the project really does use src layout, and don't do # sys.path.append('src') if it doesn't - target_module, target_call = self._parse_content(self.content) + target_module, function_call = self.parse_script_content(self.named_args) argv = [ self.name, - *(self._resolve_envvars(token, context, env) for token in extra_args), + *(resolve_envvars(token, env) for token in extra_args), ] cmd = ( - "python", # TODO: pre-locate python from the target env? + "python", "-c", "import sys; " + "from os import environ; " "from importlib import import_module; " f"sys.argv = {argv!r}; sys.path.append('src');" - f"import_module('{target_module}').{target_call}", + f"\n{self.format_args_class(self.named_args)}" + f"import_module('{target_module}').{function_call}", ) + self._print_action(" ".join(argv), context.dry) - return context.get_executor(env, self.options.get("executor")).execute(cmd) + return context.get_executor(self.invocation, env, self.options).execute(cmd) @classmethod - def _parse_content(cls, call_ref: str) -> Union[Tuple[str, str], Tuple[None, None]]: + def _validate_task_def( + cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" + ) -> Optional[str]: + try: + target_module, target_ref = task_def["script"].split(":", 1) + if not target_ref.isidentifier(): + parse_and_validate(target_ref) + except (ValueError, ScriptParseError): + return ( + f"Task {task_name!r} contains invalid callable reference " + f"{task_def['script']!r} (expected something like `module:callable`" + " or `module:callable()`)" + ) + + return None + + def parse_script_content(self, args: Optional[Dict[str, Any]]) -> Tuple[str, str]: """ - Parse module and callable call out of a string like one of: - - "some_module:main" - - "some.module:main(foo='bar')" + Returns the module to load, and the function call to execute. + + Will raise an exception if the function call contains invalid syntax or references + variables that are not in scope. """ try: - target_module, target_ref = call_ref.split(":") + target_module, target_ref = self.content.split(":", 1) except ValueError: - return None, None + raise ScriptParseError(f"Invalid task content: {self.content!r}") if target_ref.isidentifier(): + if args: + return target_module, f"{target_ref}(**({args}))" return target_module, f"{target_ref}()" - call_match = _FUNCTION_CALL_PATTERN.match(target_ref) - if call_match: - callable_name, call_params = call_match.groups() - return target_module, f"{callable_name}({call_params})" - - return None, None + return target_module, resolve_function_call(target_ref, set(args or tuple())) - @classmethod - def _validate_task_def( - cls, task_name: str, task_def: Dict[str, Any], config: "PoeConfig" - ) -> Optional[str]: - target_module, target_call = cls._parse_content(task_def["script"]) - if not target_module or not target_call: - return ( - f"Task {task_name!r} contains invalid callable reference " - f"{task_def['script']!r} (expected something like `module:callable()`)" - ) - return None + @staticmethod + def format_args_class( + named_args: Optional[Dict[str, Any]], classname: str = "__args" + ) -> str: + """ + Generates source for a python class with the entries of the given dictionary + represented as class attributes. + """ + if named_args is None: + return "" + return ( + f"class {classname}:\n" + + "\n".join(f" {name} = {value!r}" for name, value in named_args.items()) + + "\n" + ) diff --git a/poethepoet/task/sequence.py b/poethepoet/task/sequence.py index 8b9d89a84..16b679847 100644 --- a/poethepoet/task/sequence.py +++ b/poethepoet/task/sequence.py @@ -1,10 +1,11 @@ from typing import ( Any, Dict, - Iterable, List, - MutableMapping, + Mapping, Optional, + Sequence, + Tuple, Type, TYPE_CHECKING, Union, @@ -28,7 +29,10 @@ class SequenceTask(PoeTask): __key__ = "sequence" __content_type__: Type = list - __options__: Dict[str, Type] = {"ignore_fail": bool, "default_item_type": str} + __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = { + "ignore_fail": (bool, str), + "default_item_type": str, + } def __init__( self, @@ -37,38 +41,55 @@ def __init__( options: Dict[str, Any], ui: "PoeUi", config: "PoeConfig", + invocation: Tuple[str, ...], + capture_stdout: bool = False, ): - super().__init__(name, content, options, ui, config) + assert capture_stdout == False + super().__init__(name, content, options, ui, config, invocation) + self.subtasks = [ self.from_def( task_def=item, - task_name=item if isinstance(item, str) else f"{name}[{index}]", + task_name=task_name, config=config, + invocation=(task_name,), ui=ui, array_item=self.options.get("default_item_type", True), ) for index, item in enumerate(self.content) + for task_name in (item if isinstance(item, str) else f"{name}[{index}]",) ] def _handle_run( self, context: "RunContext", - extra_args: Iterable[str], - env: MutableMapping[str, str], + extra_args: Sequence[str], + env: Mapping[str, str], ) -> int: - if any(arg.strip() for arg in extra_args): + env, has_named_args = self.add_named_args_to_env(env) + + if not has_named_args and any(arg.strip() for arg in extra_args): raise PoeException(f"Sequence task {self.name!r} does not accept arguments") if len(self.subtasks) > 1: # Indicate on the global context that there are multiple stages context.multistage = True + ignore_fail = self.options.get("ignore_fail") + non_zero_subtasks: List[str] = list() for subtask in self.subtasks: task_result = subtask.run(context=context, extra_args=tuple(), env=env) - if task_result and not self.options.get("ignore_fail"): + if task_result and not ignore_fail: raise ExecutionError( f"Sequence aborted after failed subtask {subtask.name!r}" ) + if task_result: + non_zero_subtasks.append(subtask.name) + + if non_zero_subtasks and ignore_fail == "return_non_zero": + raise ExecutionError( + f"Subtasks {', '.join(non_zero_subtasks)} returned non-zero exit status" + ) return 0 @classmethod @@ -83,4 +104,16 @@ def _validate_task_def( "Unsupported value for option `default_item_type` for task " f"{task_name!r}. Expected one of {cls.get_task_types(content_type=str)}" ) + ignore_fail = task_def.get("ignore_fail") + if ignore_fail is not None and ignore_fail not in ( + True, + False, + "return_zero", + "return_non_zero", + ): + return ( + "Unsupported value for option `ignore_fail` for task " + f'{task_name!r}. Expected one of (true, false, "return_zero", "return_non_zero")' + ) + return None diff --git a/poethepoet/task/shell.py b/poethepoet/task/shell.py index 5c4f1c86c..37011813d 100644 --- a/poethepoet/task/shell.py +++ b/poethepoet/task/shell.py @@ -1,7 +1,7 @@ import os import shutil import subprocess -from typing import Dict, Iterable, MutableMapping, Type, TYPE_CHECKING +from typing import Dict, Mapping, Sequence, Tuple, Type, TYPE_CHECKING, Union from ..exceptions import PoeException from .base import PoeTask @@ -18,15 +18,17 @@ class ShellTask(PoeTask): content: str __key__ = "shell" - __options__: Dict[str, Type] = {} + __options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {} def _handle_run( self, context: "RunContext", - extra_args: Iterable[str], - env: MutableMapping[str, str], + extra_args: Sequence[str], + env: Mapping[str, str], ) -> int: - if any(arg.strip() for arg in extra_args): + env, has_named_args = self.add_named_args_to_env(env) + + if not has_named_args and any(arg.strip() for arg in extra_args): raise PoeException(f"Shell task {self.name!r} does not accept arguments") if self._is_windows: @@ -36,7 +38,7 @@ def _handle_run( shell = [os.environ.get("SHELL", shutil.which("bash") or "/bin/bash")] self._print_action(self.content, context.dry) - return context.get_executor(env, self.options.get("executor")).execute( + return context.get_executor(self.invocation, env, self.options).execute( shell, input=self.content.encode() ) diff --git a/poethepoet/ui.py b/poethepoet/ui.py index a4e64d8df..b7e01146d 100644 --- a/poethepoet/ui.py +++ b/poethepoet/ui.py @@ -2,7 +2,7 @@ import os from pastel import Pastel import sys -from typing import IO, List, Mapping, Optional, Sequence, Union +from typing import IO, List, Mapping, Optional, Sequence, Tuple, Union from .exceptions import PoeException from .__version__ import __version__ @@ -32,6 +32,7 @@ def _init_colors(self): self._color.add_style("hl", "light_gray") self._color.add_style("em", "cyan") self._color.add_style("em2", "cyan", options="italic") + self._color.add_style("em3", "blue") self._color.add_style("h2", "default", options="bold") self._color.add_style("h2-dim", "default", options="dark") self._color.add_style("action", "light_blue") @@ -41,7 +42,7 @@ def __getitem__(self, key: str): """Provide easy access to arguments""" return getattr(self.args, key, None) - def build_parser(self): + def build_parser(self) -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="poe", description="Poe the Poet: A task runner that works well with poetry.", @@ -66,25 +67,22 @@ def build_parser(self): help="Print the version and exit", ) - verbosity_group = parser.add_mutually_exclusive_group() - verbosity_group.add_argument( + parser.add_argument( "-v", "--verbose", - dest="verbosity", - action="store_const", - metavar="verbose_mode", + dest="increase_verbosity", + action="count", default=0, - const=1, - help="More console spam", + help="Increase command output (repeatable)", ) - verbosity_group.add_argument( + + parser.add_argument( "-q", "--quiet", - dest="verbosity", - action="store_const", - metavar="quiet_mode", - const=-1, - help="Less console spam", + dest="decrease_verbosity", + action="count", + default=0, + help="Decrease command output (repeatable)", ) parser.add_argument( @@ -128,18 +126,22 @@ def build_parser(self): def parse_args(self, cli_args: Sequence[str]): self.parser = self.build_parser() self.args = self.parser.parse_args(cli_args) + self.verbosity: int = self["increase_verbosity"] - self["decrease_verbosity"] self._color.with_colors(self.args.ansi) + def set_default_verbosity(self, default_verbosity: int): + self.verbosity += default_verbosity + def print_help( self, - tasks: Optional[Mapping[str, str]] = None, + tasks: Optional[Mapping[str, Tuple[str, Sequence[Tuple[str, str]]]]] = None, info: Optional[str] = None, error: Optional[PoeException] = None, ): # TODO: See if this can be done nicely with a custom HelpFormatter # Ignore verbosity mode if help flag is set - verbosity = 0 if self["help"] else self["verbosity"] + verbosity = 0 if self["help"] else self.verbosity result: List[Union[str, Sequence[str]]] = [] if verbosity >= 0: @@ -179,15 +181,27 @@ def print_help( ) if tasks: - max_task_len = max(len(task) for task in tasks) + max_task_len = max( + max( + len(task), + max([len(", ".join(opts)) for (opts, _) in args] or (0,)) + 2, + ) + for task, (_, args) in tasks.items() + ) col_width = max(13, min(30, max_task_len)) tasks_section = ["

CONFIGURED TASKS

"] - for task, help_text in tasks.items(): + for task, (help_text, args_help) in tasks.items(): if task.startswith("_"): continue tasks_section.append( f" {self._padr(task, col_width)} {help_text}" ) + for (options, arg_help_text) in args_help: + tasks_section.append( + " " + f"{self._padr(', '.join(options), col_width - 2)}" + f" {arg_help_text}" + ) result.append(tasks_section) else: result.append("NO TASKS CONFIGURED") @@ -208,7 +222,7 @@ def _padr(text: str, width: int): return text + " " * (width - len(text)) def print_msg(self, message: str, verbosity=0, end="\n"): - if verbosity <= self["verbosity"]: + if verbosity <= self.verbosity: self._print(message, end=end) def print_error(self, error: Exception): @@ -217,7 +231,7 @@ def print_error(self, error: Exception): self._print(f" From: {error.cause} ") # type: ignore def print_version(self): - if self["verbosity"] >= 0: + if self.verbosity >= 0: result = f"Poe the poet - version: {__version__}\n" else: result = f"{__version__}\n" diff --git a/poethepoet/virtualenv.py b/poethepoet/virtualenv.py index 316121105..fe2833795 100644 --- a/poethepoet/virtualenv.py +++ b/poethepoet/virtualenv.py @@ -1,7 +1,7 @@ import os from pathlib import Path import sys -from typing import Dict, MutableMapping +from typing import Dict, Mapping class Virtualenv: @@ -74,7 +74,7 @@ def valid(self) -> bool: ) ) - def get_env_vars(self, base_env: MutableMapping[str, str]) -> Dict[str, str]: + def get_env_vars(self, base_env: Mapping[str, str]) -> Dict[str, str]: path_delim = ";" if self._is_windows else ":" result = dict( base_env, diff --git a/poetry.lock b/poetry.lock index 041cbdfc4..c77d6c753 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,24 +1,16 @@ -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "astroid" -version = "2.4.2" +version = "2.8.4" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false -python-versions = ">=3.5" +python-versions = "~=3.6" [package.dependencies] -lazy-object-proxy = ">=1.4.0,<1.5.0" -six = ">=1.12,<2.0" +lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} -wrapt = ">=1.11,<2.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<1.14" [[package]] name = "atomicwrites" @@ -30,37 +22,72 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.3.0" +version = "21.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -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"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "backports.cached-property" +version = "1.0.1" +description = "cached_property() - computed once per instance, cached as attribute" +category = "dev" +optional = false +python-versions = ">=3.6.0" + +[package.dependencies] +typing = {version = ">=3.6", markers = "python_version < \"3.7\""} + +[[package]] +name = "backports.entry-points-selectable" +version = "1.1.0" +description = "Compatibility shim providing selectable entry points for older implementations" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] [[package]] name = "black" -version = "19.10b0" +version = "21.10b0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.2" [package.dependencies] -appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" -pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" -typed-ast = ">=1.4.0" +click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0,<1" +platformdirs = ">=2" +regex = ">=2020.1.8" +tomli = ">=0.2.6,<2.0.0" +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} +typing-extensions = [ + {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, + {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, +] [package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +python2 = ["typed-ast (>=1.4.3)"] +uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "blessings" @@ -75,47 +102,59 @@ six = "*" [[package]] name = "bpython" -version = "0.19" -description = "Fancy Interface to the Python Interpreter" +version = "0.22.1" +description = "" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] -curtsies = ">=0.1.18" +"backports.cached-property" = {version = "*", markers = "python_version < \"3.8\""} +curtsies = ">=0.3.5" +cwcwidth = "*" +dataclasses = {version = "*", markers = "python_version < \"3.7\""} greenlet = "*" pygments = "*" +pyxdg = "*" requests = "*" -six = ">=1.5" +typing-extensions = "*" [package.extras] -jedi = ["jedi"] +clipboard = ["pyperclip"] +jedi = ["jedi (>=0.16)"] urwid = ["urwid"] watch = ["watchdog"] [[package]] name = "certifi" -version = "2020.12.5" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = "*" [[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" +name = "charset-normalizer" +version = "2.0.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "7.1.2" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" @@ -127,38 +166,50 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "5.3.1" +version = "6.1.1" 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" +python-versions = ">=3.6" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] -toml = ["toml"] +toml = ["tomli"] [[package]] name = "curtsies" -version = "0.3.5" +version = "0.3.10" description = "Curses-like terminal wrapper, with colored strings!" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] +"backports.cached-property" = {version = "*", markers = "python_version < \"3.8\""} blessings = ">=1.5" cwcwidth = "*" [[package]] name = "cwcwidth" -version = "0.1.1" +version = "0.1.5" description = "Python bindings for wc(s)width" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.6" + +[[package]] +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" +category = "dev" +optional = false +python-versions = ">=3.6, <3.7" [[package]] name = "distlib" -version = "0.3.1" +version = "0.3.3" description = "Distribution utilities" category = "dev" optional = false @@ -166,7 +217,7 @@ python-versions = "*" [[package]] name = "docutils" -version = "0.16" +version = "0.18" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false @@ -174,15 +225,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "filelock" -version = "3.0.12" +version = "3.3.2" description = "A platform independent file lock." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] [[package]] name = "greenlet" -version = "1.0.0" +version = "1.1.2" description = "Lightweight in-process concurrent programming" category = "dev" optional = false @@ -193,15 +248,15 @@ docs = ["sphinx"] [[package]] name = "idna" -version = "2.10" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "3.4.0" +version = "4.8.1" description = "Read metadata from Python packages" category = "dev" optional = false @@ -213,43 +268,53 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "importlib-resources" -version = "5.1.0" +version = "5.4.0" description = "Read resources from Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" [[package]] name = "isort" -version = "5.7.0" +version = "5.10.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] name = "lazy-object-proxy" -version = "1.4.3" +version = "1.6.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "mccabe" @@ -259,17 +324,9 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "more-itertools" -version = "8.6.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "mypy" -version = "0.770" +version = "0.910" description = "Optional static typing for Python" category = "dev" optional = false @@ -277,11 +334,13 @@ python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" +toml = "*" +typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] name = "mypy-extensions" @@ -293,14 +352,14 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.8" +version = "21.2" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3" [[package]] name = "pastel" @@ -312,37 +371,50 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" -version = "1.10.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pygments" -version = "2.7.4" +version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -350,18 +422,20 @@ python-versions = ">=3.5" [[package]] name = "pylint" -version = "2.6.0" +version = "2.11.1" description = "python code static checker" category = "dev" optional = false -python-versions = ">=3.5.*" +python-versions = "~=3.6" [package.dependencies] -astroid = ">=2.4.0,<=2.5" +astroid = ">=2.8.0,<2.9" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" toml = ">=0.7.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [[package]] name = "pyparsing" @@ -373,45 +447,52 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "5.4.3" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" +attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = ">=4.0.0" +iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" [package.extras] -checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-cov" -version = "2.11.1" +version = "3.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -coverage = ">=5.2.1" +coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pyxdg" +version = "0.27" +description = "PyXDG contains implementations of freedesktop.org standards in python." +category = "dev" +optional = false +python-versions = "*" [[package]] name = "regex" -version = "2020.11.13" +version = "2021.11.2" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -419,21 +500,21 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.1" +version = "2.26.0" description = "Python HTTP for Humans." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "rstcheck" @@ -448,7 +529,7 @@ docutils = ">=0.7" [[package]] name = "six" -version = "1.15.0" +version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false @@ -463,16 +544,16 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] -name = "tomlkit" -version = "0.7.0" -description = "Style preserving TOML library" +name = "tomli" +version = "1.2.2" +description = "A lil' TOML parser" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [[package]] name = "tox" -version = "3.21.2" +version = "3.24.4" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -495,15 +576,23 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytes [[package]] name = "typed-ast" -version = "1.4.2" +version = "1.4.3" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = "*" [[package]] -name = "typing-extensions" +name = "typing" version = "3.7.4.3" +description = "Type Hints for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" category = "dev" optional = false @@ -511,7 +600,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.2" +version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -524,77 +613,74 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.4.0" +version = "20.10.0" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -appdirs = ">=1.4.3,<2" +"backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" -filelock = ">=3.0.0,<4" +filelock = ">=3.2,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} +platformdirs = ">=2,<3" six = ">=1.9.0,<2" [package.extras] -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)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] - -[[package]] -name = "wcwidth" -version = "0.2.5" -description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" -optional = false -python-versions = "*" +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +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)", "packaging (>=20.0)"] [[package]] name = "wrapt" -version = "1.12.1" +version = "1.13.3" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.4.0" +version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.6" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" -python-versions = "^3.6" -content-hash = "e58e5ee40815ee6a32da05425c249c81b960a7a1fa15dca832741b9e6a5dd430" +python-versions = "^3.6.2" +content-hash = "0cd2e421d5b9e9478afe31ce24bdb19034ac534d0da1d0035d632ff0ea9d4b28" [metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] astroid = [ - {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, - {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, + {file = "astroid-2.8.4-py3-none-any.whl", hash = "sha256:0755c998e7117078dcb7d0bda621391dd2a85da48052d948c7411ab187325346"}, + {file = "astroid-2.8.4.tar.gz", hash = "sha256:1e83a69fd51b013ebf5912d26b9338d6643a55fec2f20c787792680610eed4a2"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +"backports.cached-property" = [ + {file = "backports.cached-property-1.0.1.tar.gz", hash = "sha256:1a5ef1e750f8bc7d0204c807aae8e0f450c655be0cf4b30407a35fd4bb27186c"}, + {file = "backports.cached_property-1.0.1-py3-none-any.whl", hash = "sha256:687b5fe14be40aadcf547cae91337a1fdb84026046a39370274e54d3fe4fb4f9"}, +] +"backports.entry-points-selectable" = [ + {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, + {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, ] black = [ - {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, + {file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"}, + {file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"}, ] blessings = [ {file = "blessings-1.7-py2-none-any.whl", hash = "sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e"}, @@ -602,365 +688,469 @@ blessings = [ {file = "blessings-1.7.tar.gz", hash = "sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d"}, ] bpython = [ - {file = "bpython-0.19-py2.py3-none-any.whl", hash = "sha256:95d95783bfadfa0a25300a648de5aba4423b0ee76b034022a81dde2b5e853c00"}, - {file = "bpython-0.19.tar.gz", hash = "sha256:476ce09a896c4d34bf5e56aca64650c56fdcfce45781a20dc1521221df8cc49c"}, + {file = "bpython-0.22.1-py3-none-any.whl", hash = "sha256:27ab07d64b9f0268990612aa5c1ab7be8705aa819b14b72c9e4b3b0418458937"}, + {file = "bpython-0.22.1.tar.gz", hash = "sha256:1fb1e0a52332579fc4e3dcf75e21796af67aae2be460179ecfcce9530a49a200"}, ] certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +charset-normalizer = [ + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, ] click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-5.3.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b"}, - {file = "coverage-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297"}, - {file = "coverage-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7"}, - {file = "coverage-5.3.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b"}, - {file = "coverage-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7"}, - {file = "coverage-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72"}, - {file = "coverage-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277"}, - {file = "coverage-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f"}, - {file = "coverage-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c"}, - {file = "coverage-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e"}, - {file = "coverage-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2"}, - {file = "coverage-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879"}, - {file = "coverage-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830"}, - {file = "coverage-5.3.1-cp38-cp38-win32.whl", hash = "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"}, - {file = "coverage-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606"}, - {file = "coverage-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d"}, - {file = "coverage-5.3.1-cp39-cp39-win32.whl", hash = "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98"}, - {file = "coverage-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1"}, - {file = "coverage-5.3.1-pp36-none-any.whl", hash = "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3"}, - {file = "coverage-5.3.1-pp37-none-any.whl", hash = "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c"}, - {file = "coverage-5.3.1.tar.gz", hash = "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b"}, + {file = "coverage-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9"}, + {file = "coverage-6.1.1-cp310-cp310-win32.whl", hash = "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026"}, + {file = "coverage-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9"}, + {file = "coverage-6.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489"}, + {file = "coverage-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003"}, + {file = "coverage-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b"}, + {file = "coverage-6.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b"}, + {file = "coverage-6.1.1-cp37-cp37m-win32.whl", hash = "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685"}, + {file = "coverage-6.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926"}, + {file = "coverage-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad"}, + {file = "coverage-6.1.1-cp38-cp38-win32.whl", hash = "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b"}, + {file = "coverage-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54"}, + {file = "coverage-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a"}, + {file = "coverage-6.1.1-cp39-cp39-win32.whl", hash = "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0"}, + {file = "coverage-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b"}, + {file = "coverage-6.1.1-pp36-none-any.whl", hash = "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f"}, + {file = "coverage-6.1.1-pp37-none-any.whl", hash = "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07"}, + {file = "coverage-6.1.1-pp38-none-any.whl", hash = "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5"}, + {file = "coverage-6.1.1.tar.gz", hash = "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0"}, ] curtsies = [ - {file = "curtsies-0.3.5.tar.gz", hash = "sha256:a587ff3335667a32be7afed163f60a1c82c5d9c848d8297534a06fd29de20dbd"}, + {file = "curtsies-0.3.10.tar.gz", hash = "sha256:11efbb153d9cb22223dd9a44041ea0c313b8411e246e7f684aa843f6aa9c1600"}, ] cwcwidth = [ - {file = "cwcwidth-0.1.1.tar.gz", hash = "sha256:042cdf80d80a836935f700d8e1c34270f82a627fc07f7b5ec1e8cec486e1d755"}, + {file = "cwcwidth-0.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6fbbdbc742d78d732f0cfd3f69672f3805ec6c766f14460f8c392f624ea7af09"}, + {file = "cwcwidth-0.1.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77c0492c65555dfd022635ffc83365bbf2b15e95fde030b95ee9b2409ce98909"}, + {file = "cwcwidth-0.1.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a6a1b285655c7c86ceebe241f722fe19ccdef4891f7dced8cd818c966b6bb9d9"}, + {file = "cwcwidth-0.1.5-cp36-cp36m-win32.whl", hash = "sha256:d3dc22afbaeba3b3a168e6a15e194d55fb63f01720feee53051b62f0f1c6165f"}, + {file = "cwcwidth-0.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:672b0d4b9d39642c023a80daf4a8623b3aa63a31275c20f705569e3b50021b06"}, + {file = "cwcwidth-0.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1777c325f7153b4e430d540a3ae2e8595b8117c10e5e3c2f65862af95ff94f5"}, + {file = "cwcwidth-0.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ae134791ea6024bc0397f6b6a15d398f31167ad4b71190278e9c02178dad6948"}, + {file = "cwcwidth-0.1.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:268775831c952d45cf8165bc9168585868e933a87b72f6f1bd35230ad5206f1f"}, + {file = "cwcwidth-0.1.5-cp37-cp37m-win32.whl", hash = "sha256:4c3ab17f0ee34d71b7de12d543eb40f1a6aae9339c9221b96b06970e6299bd10"}, + {file = "cwcwidth-0.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:106efcbe26c495720cdfd4694e4c191ce1a3c1268b2d90356c875fd109d78a46"}, + {file = "cwcwidth-0.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a25161e4302413b8b4c3379b29c1adcbe04c05380a6dfc8e4eb0e3938e4b3f9"}, + {file = "cwcwidth-0.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a6385cfdb3f0cfd4f42ab73d2c965e8f133f4f78ad56a7495f6e5924165a6c73"}, + {file = "cwcwidth-0.1.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f57a3784c799553a67d5ebc2c4854d4cc95e8c2c01f10861b97a0a829c09c36e"}, + {file = "cwcwidth-0.1.5-cp38-cp38-win32.whl", hash = "sha256:3fe12ba738548ff5ad3081621d22b7bff97fa78258c44a6586e97ec4f1430739"}, + {file = "cwcwidth-0.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:b05739a439f815c85b47abfb3fea301354762950142539d13263a353f21a90f6"}, + {file = "cwcwidth-0.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21701b63cc4e287a197e65c033583bbea674ec7176f983c5fff77fa82118d440"}, + {file = "cwcwidth-0.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:66addcb77a16d3e1ab817bea337ffb2509c57af6d91f5515c37811b081e7a448"}, + {file = "cwcwidth-0.1.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1dc35642fd1a26ce63236c18d7d4a32909be2b1e99b272be95859872f97d1d28"}, + {file = "cwcwidth-0.1.5-cp39-cp39-win32.whl", hash = "sha256:9321274eca7fd4323a3f0eb472d08a08dfa7e2e0d797b713167d5106e41c92ad"}, + {file = "cwcwidth-0.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:23fa1021135b8b99e9d4478097ce42aaa17d957c037d5a76e990de20262d76d4"}, + {file = "cwcwidth-0.1.5.tar.gz", hash = "sha256:2c840e7d85f6de45c45986b416d79312c91882e1121b78d4c347e49c4238c09d"}, +] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] distlib = [ - {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, - {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, + {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, + {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, ] docutils = [ - {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, - {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, + {file = "docutils-0.18-py2.py3-none-any.whl", hash = "sha256:a31688b2ea858517fa54293e5d5df06fbb875fb1f7e4c64529271b77781ca8fc"}, + {file = "docutils-0.18.tar.gz", hash = "sha256:c1d5dab2b11d16397406a282e53953fe495a46d69ae329f55aa98a5c4e3c5fbb"}, ] filelock = [ - {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, - {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, + {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, + {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, ] greenlet = [ - {file = "greenlet-1.0.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2"}, - {file = "greenlet-1.0.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c"}, - {file = "greenlet-1.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203"}, - {file = "greenlet-1.0.0-cp27-cp27m-win32.whl", hash = "sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476"}, - {file = "greenlet-1.0.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b"}, - {file = "greenlet-1.0.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379"}, - {file = "greenlet-1.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce"}, - {file = "greenlet-1.0.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c"}, - {file = "greenlet-1.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5"}, - {file = "greenlet-1.0.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f"}, - {file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6"}, - {file = "greenlet-1.0.0-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683"}, - {file = "greenlet-1.0.0-cp35-cp35m-win32.whl", hash = "sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70"}, - {file = "greenlet-1.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770"}, - {file = "greenlet-1.0.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee"}, - {file = "greenlet-1.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd"}, - {file = "greenlet-1.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a"}, - {file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d"}, - {file = "greenlet-1.0.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2"}, - {file = "greenlet-1.0.0-cp36-cp36m-win32.whl", hash = "sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7"}, - {file = "greenlet-1.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be"}, - {file = "greenlet-1.0.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef"}, - {file = "greenlet-1.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128"}, - {file = "greenlet-1.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7"}, - {file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c"}, - {file = "greenlet-1.0.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f"}, - {file = "greenlet-1.0.0-cp37-cp37m-win32.whl", hash = "sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c"}, - {file = "greenlet-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85"}, - {file = "greenlet-1.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7"}, - {file = "greenlet-1.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06"}, - {file = "greenlet-1.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36"}, - {file = "greenlet-1.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378"}, - {file = "greenlet-1.0.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196"}, - {file = "greenlet-1.0.0-cp38-cp38-win32.whl", hash = "sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e"}, - {file = "greenlet-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c"}, - {file = "greenlet-1.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243"}, - {file = "greenlet-1.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df"}, - {file = "greenlet-1.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218"}, - {file = "greenlet-1.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7"}, - {file = "greenlet-1.0.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664"}, - {file = "greenlet-1.0.0-cp39-cp39-win32.whl", hash = "sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139"}, - {file = "greenlet-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0"}, - {file = "greenlet-1.0.0.tar.gz", hash = "sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8"}, + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] importlib-metadata = [ - {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, - {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, ] importlib-resources = [ - {file = "importlib_resources-5.1.0-py3-none-any.whl", hash = "sha256:885b8eae589179f661c909d699a546cf10d83692553e34dca1bf5eb06f7f6217"}, - {file = "importlib_resources-5.1.0.tar.gz", hash = "sha256:bfdad047bce441405a49cf8eb48ddce5e56c696e185f59147a8b79e75e9e6380"}, + {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, + {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"}, - {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"}, + {file = "isort-5.10.0-py3-none-any.whl", hash = "sha256:1a18ccace2ed8910bd9458b74a3ecbafd7b2f581301b0ab65cfdd4338272d76f"}, + {file = "isort-5.10.0.tar.gz", hash = "sha256:e52ff6d38012b131628cf0f26c51e7bd3a7c81592eefe3ac71411e692f1b9345"}, ] lazy-object-proxy = [ - {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, + {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -more-itertools = [ - {file = "more-itertools-8.6.0.tar.gz", hash = "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf"}, - {file = "more_itertools-8.6.0-py3-none-any.whl", hash = "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330"}, -] mypy = [ - {file = "mypy-0.770-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600"}, - {file = "mypy-0.770-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754"}, - {file = "mypy-0.770-cp35-cp35m-win_amd64.whl", hash = "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65"}, - {file = "mypy-0.770-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce"}, - {file = "mypy-0.770-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761"}, - {file = "mypy-0.770-cp36-cp36m-win_amd64.whl", hash = "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2"}, - {file = "mypy-0.770-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8"}, - {file = "mypy-0.770-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913"}, - {file = "mypy-0.770-cp37-cp37m-win_amd64.whl", hash = "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9"}, - {file = "mypy-0.770-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1"}, - {file = "mypy-0.770-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27"}, - {file = "mypy-0.770-cp38-cp38-win_amd64.whl", hash = "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"}, - {file = "mypy-0.770-py3-none-any.whl", hash = "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164"}, - {file = "mypy-0.770.tar.gz", hash = "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae"}, + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ - {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, - {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, + {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, + {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, ] pastel = [ {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, ] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pygments = [ - {file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"}, - {file = "Pygments-2.7.4.tar.gz", hash = "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"}, + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pylint = [ - {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, - {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, + {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, + {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, ] 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-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ - {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, - {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pyxdg = [ + {file = "pyxdg-0.27-py2.py3-none-any.whl", hash = "sha256:2d6701ab7c74bbab8caa6a95e0a0a129b1643cf6c298bf7c569adec06d0709a0"}, + {file = "pyxdg-0.27.tar.gz", hash = "sha256:80bd93aae5ed82435f20462ea0208fb198d8eec262e831ee06ce9ddb6b91c5a5"}, ] regex = [ - {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, - {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, - {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, - {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, - {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, - {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, - {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, - {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, - {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, - {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, - {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, - {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, - {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, - {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, - {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, - {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, - {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, + {file = "regex-2021.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:897c539f0f3b2c3a715be651322bef2167de1cdc276b3f370ae81a3bda62df71"}, + {file = "regex-2021.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:886f459db10c0f9d17c87d6594e77be915f18d343ee138e68d259eb385f044a8"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075b0fdbaea81afcac5a39a0d1bb91de887dd0d93bf692a5dd69c430e7fc58cb"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6238d30dcff141de076344cf7f52468de61729c2f70d776fce12f55fe8df790"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fab29411d75c2eb48070020a40f80255936d7c31357b086e5931c107d48306e"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0148988af0182a0a4e5020e7c168014f2c55a16d11179610f7883dd48ac0ebe"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be30cd315db0168063a1755fa20a31119da91afa51da2907553493516e165640"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e9cec3a62d146e8e122d159ab93ac32c988e2ec0dcb1e18e9e53ff2da4fbd30c"}, + {file = "regex-2021.11.2-cp310-cp310-win32.whl", hash = "sha256:41c66bd6750237a8ed23028a6c9173dc0c92dc24c473e771d3bfb9ee817700c3"}, + {file = "regex-2021.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:0075fe4e2c2720a685fef0f863edd67740ff78c342cf20b2a79bc19388edf5db"}, + {file = "regex-2021.11.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ed3465acf8c7c10aa2e0f3d9671da410ead63b38a77283ef464cbb64275df58"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab1fea8832976ad0bebb11f652b692c328043057d35e9ebc78ab0a7a30cf9a70"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1e44d860345ab5d4f533b6c37565a22f403277f44c4d2d5e06c325da959883"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9486ebda015913909bc28763c6b92fcc3b5e5a67dee4674bceed112109f5dfb8"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20605bfad484e1341b2cbfea0708e4b211d233716604846baa54b94821f487cb"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f20f9f430c33597887ba9bd76635476928e76cad2981643ca8be277b8e97aa96"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d85ca137756d62c8138c971453cafe64741adad1f6a7e63a22a5a8abdbd19fa"}, + {file = "regex-2021.11.2-cp36-cp36m-win32.whl", hash = "sha256:af23b9ca9a874ef0ec20e44467b8edd556c37b0f46f93abfa93752ea7c0e8d1e"}, + {file = "regex-2021.11.2-cp36-cp36m-win_amd64.whl", hash = "sha256:070336382ca92c16c45b4066c4ba9fa83fb0bd13d5553a82e07d344df8d58a84"}, + {file = "regex-2021.11.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef4e53e2fdc997d91f5b682f81f7dc9661db9a437acce28745d765d251902d85"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35ed5714467fc606551db26f80ee5d6aa1f01185586a7bccd96f179c4b974a11"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee36d5113b6506b97f45f2e8447cb9af146e60e3f527d93013d19f6d0405f3b"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fba661a4966adbd2c3c08d3caad6822ecb6878f5456588e2475ae23a6e47929"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f9d16f7970791f17ecce7e7f101548314ed1ee2583d4268601f30af3170856"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6a28e87ba69f3a4f30d775b179aac55be1ce59f55799328a0d9b6df8f16b39d"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9267e4fba27e6dd1008c4f2983cc548c98b4be4444e3e342db11296c0f45512f"}, + {file = "regex-2021.11.2-cp37-cp37m-win32.whl", hash = "sha256:d4bfe3bc3976ccaeb4ae32f51e631964e2f0e85b2b752721b7a02de5ce3b7f27"}, + {file = "regex-2021.11.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2bb7cae741de1aa03e3dd3a7d98c304871eb155921ca1f0d7cc11f5aade913fd"}, + {file = "regex-2021.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:23f93e74409c210de4de270d4bf88fb8ab736a7400f74210df63a93728cf70d6"}, + {file = "regex-2021.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8ee91e1c295beb5c132ebd78616814de26fedba6aa8687ea460c7f5eb289b72"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3ff69ab203b54ce5c480c3ccbe959394ea5beef6bd5ad1785457df7acea92e"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3c00cb5c71da655e1e5161481455479b613d500dd1bd252aa01df4f037c641f"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf35e16f4b639daaf05a2602c1b1d47370e01babf9821306aa138924e3fe92"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb11c982a849dc22782210b01d0c1b98eb3696ce655d58a54180774e4880ac66"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e3755e0f070bc31567dfe447a02011bfa8444239b3e9e5cca6773a22133839"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0621c90f28d17260b41838b22c81a79ff436141b322960eb49c7b3f91d1cbab6"}, + {file = "regex-2021.11.2-cp38-cp38-win32.whl", hash = "sha256:8fbe1768feafd3d0156556677b8ff234c7bf94a8110e906b2d73506f577a3269"}, + {file = "regex-2021.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9ee98d658a146cb6507be720a0ce1b44f2abef8fb43c2859791d91aace17cd5"}, + {file = "regex-2021.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3794cea825f101fe0df9af8a00f9fad8e119c91e39a28636b95ee2b45b6c2e5"}, + {file = "regex-2021.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3576e173e7b4f88f683b4de7db0c2af1b209bb48b2bf1c827a6f3564fad59a97"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4f4810117a9072a5aa70f7fea5f86fa9efbe9a798312e0a05044bd707cc33"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5930d334c2f607711d54761956aedf8137f83f1b764b9640be21d25a976f3a4"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:956187ff49db7014ceb31e88fcacf4cf63371e6e44d209cf8816cd4a2d61e11a"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e095f7f96a4b9f24b93c2c915f31a5201a6316618d919b0593afb070a5270e"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56735c35a3704603d9d7b243ee06139f0837bcac2171d9ba1d638ce1df0742a"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adf35d88d9cffc202e6046e4c32e1e11a1d0238b2fcf095c94f109e510ececea"}, + {file = "regex-2021.11.2-cp39-cp39-win32.whl", hash = "sha256:30fe317332de0e50195665bc61a27d46e903d682f94042c36b3f88cb84bd7958"}, + {file = "regex-2021.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:85289c25f658e3260b00178757c87f033f3d4b3e40aa4abdd4dc875ff11a94fb"}, + {file = "regex-2021.11.2.tar.gz", hash = "sha256:5e85dcfc5d0f374955015ae12c08365b565c6f1eaf36dd182476a4d8e5a1cdb7"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] rstcheck = [ {file = "rstcheck-3.3.1.tar.gz", hash = "sha256:92c4f79256a54270e0402ba16a2f92d0b3c15c8f4410cb9c57127067c215741f"}, ] six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -tomlkit = [ - {file = "tomlkit-0.7.0-py2.py3-none-any.whl", hash = "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831"}, - {file = "tomlkit-0.7.0.tar.gz", hash = "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"}, +tomli = [ + {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, + {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, ] tox = [ - {file = "tox-3.21.2-py2.py3-none-any.whl", hash = "sha256:0aa777ee466f2ef18e6f58428c793c32378779e0a321dbb8934848bc3e78998c"}, - {file = "tox-3.21.2.tar.gz", hash = "sha256:f501808381c01c6d7827c2f17328be59c0a715046e94605ddca15fb91e65827d"}, + {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"}, + {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"}, ] typed-ast = [ - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, - {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, - {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, - {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, - {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, - {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, - {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, - {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, - {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, - {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, - {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, - {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, - {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, - {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, - {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, - {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] +typing = [ + {file = "typing-3.7.4.3-py2-none-any.whl", hash = "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"}, + {file = "typing-3.7.4.3.tar.gz", hash = "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ - {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, - {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] virtualenv = [ - {file = "virtualenv-20.4.0-py2.py3-none-any.whl", hash = "sha256:227a8fed626f2f20a6cdb0870054989f82dd27b2560a911935ba905a2a5e0034"}, - {file = "virtualenv-20.4.0.tar.gz", hash = "sha256:219ee956e38b08e32d5639289aaa5bd190cfbe7dafcb8fa65407fca08e808f9c"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, + {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, + {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, ] wrapt = [ - {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] zipp = [ - {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, - {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] diff --git a/pyproject.toml b/pyproject.toml index e31dbe30c..08c6cd278 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,55 +1,80 @@ [tool.poetry] -name = "poethepoet" -version = "0.10.0" +name = "poethepoet" +version = "0.11.0" description = "A task runner that works well with poetry." -authors = ["Nat Noordanus "] -readme = "README.rst" -license = "MIT" -repository = "https://github.com/nat-n/poethepoet" -homepage = "https://github.com/nat-n/poethepoet" +authors = ["Nat Noordanus "] +readme = "README.rst" +license = "MIT" +repository = "https://github.com/nat-n/poethepoet" +homepage = "https://github.com/nat-n/poethepoet" [tool.poetry.dependencies] -python = "^3.6" -pastel = "^0.2.0" -tomlkit = ">=0.6.0,<1.0.0" +python = "^3.6.2" +pastel = "^0.2.1" +tomli = "^1.2.2" [tool.poetry.dev-dependencies] -black = "^19.10b0" -bpython = "^0.19" -mypy = "^0.770" -pylint = "^2.5.2" -pytest = "^5.2" -pytest-cov = "^2.9.0" -tox = "^3.15.2" -rstcheck = "^3.3.1" +black = "^21.10b0" +bpython = "^0.22.1" +mypy = "^0.910" +pylint = "^2.11.1" +pytest = "^6.2.5" +pytest-cov = "^3.0.0" +rstcheck = "^3.3.1" +tox = "^3.24.4" [tool.poetry.scripts] poe = "poethepoet:main" + [tool.poe.tasks] -# Dev actions -format = { cmd = "black .", help = "Run black on the code base" } -clean = """ - # multiline commands including comments work too! - rm -rf .coverage - .mypy_cache - .pytest_cache - ./**/__pycache__ - dist - htmlcov - ./tests/fixtures/simple_project/venv - ./tests/fixtures/venv_project/myvenv -""" -# Code quality checks -test = "pytest --cov=poethepoet" -types = "mypy poethepoet --ignore-missing-imports" -lint = "pylint poethepoet" -style = "black . --check --diff" -check-docs = "rstcheck README.rst" -# Run all checks on the code base -check = ["check-docs", "style", "types", "lint", "test"] -# poeception -poe = { script = "poethepoet:main", help = "Execute poe programmatically as a poe task" } + + [tool.poe.tasks.format] + help = "Run black on the code base" + cmd = "black ." + + [tool.poe.tasks.clean] + help = "Remove generated files" + cmd = """ + # multiline commands including comments work too! + rm -rf .coverage + .mypy_cache + .pytest_cache + ./**/__pycache__ + dist + htmlcov + ./tests/fixtures/simple_project/venv + ./tests/fixtures/venv_project/myvenv + """ + + [tool.poe.tasks.test] + help = "Run unit and feature tests" + cmd = "pytest --cov=poethepoet" + + [tool.poe.tasks.types] + help = "Run the type checker" + cmd = "mypy poethepoet --ignore-missing-imports" + + [tool.poe.tasks.lint] + help = "Run the linter" + cmd = "pylint poethepoet" + + [tool.poe.tasks.style] + help = "Validate code style" + cmd = "black . --check --diff" + + [tool.poe.tasks.check-docs] + help = "Validate rst syntax in the docs" + cmd = "rstcheck README.rst" + + [tool.poe.tasks.check] + help = "Run all checks on the code base" + sequence = ["check-docs", "style", "types", "lint", "test"] + + [tool.poe.tasks.poe] + help = "Execute poe from this repo (useful for testing)" + script = "poethepoet:main" + [tool.tox] legacy_tox_ini = """ @@ -63,9 +88,11 @@ commands = poe test """ + [tool.coverage.report] -omit=["**/site-packages/**"] +omit = ["**/site-packages/**"] + [build-system] -requires = ["poetry>=0.12"] +requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" diff --git a/tests/conftest.py b/tests/conftest.py index 2fc04cda6..89fa80a99 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,12 +6,13 @@ from poethepoet.app import PoeThePoet from poethepoet.virtualenv import Virtualenv import pytest +import re import shutil from subprocess import PIPE, Popen import sys from tempfile import TemporaryDirectory -import tomlkit -from typing import Any, List, Mapping, Optional +import tomli +from typing import Any, Dict, List, Mapping, Optional import venv import virtualenv @@ -27,32 +28,43 @@ def is_windows(): @pytest.fixture def pyproject(): with PROJECT_TOML.open("r") as toml_file: - return tomlkit.parse(toml_file.read()) + return tomli.load(toml_file) @pytest.fixture -def poe_project_path(): - return PROJECT_ROOT - - -@pytest.fixture -def dummy_project_path(): - return PROJECT_ROOT.joinpath("tests", "fixtures", "dummy_project") +def projects(): + """ + General purpose provider of paths to test projects with the conventional layout + """ + base_path = PROJECT_ROOT / "tests" / "fixtures" + projects = { + re.match(r"^([_\w]+)_project", path.name).groups()[0]: path.resolve() + for path in base_path.glob("*_project") + } + projects.update( + { + f"{project_key}/" + + re.match(r"^([_\w]+).toml$", path.name).groups()[0]: path + for project_key, project_path in projects.items() + for path in project_path.glob("*.toml") + } + ) + return projects @pytest.fixture -def venv_project_path(): - return PROJECT_ROOT.joinpath("tests", "fixtures", "venv_project") +def poe_project_path(): + return PROJECT_ROOT @pytest.fixture -def simple_project_path(): - return PROJECT_ROOT.joinpath("tests", "fixtures", "simple_project") +def low_verbosity_project_path(): + return PROJECT_ROOT.joinpath("tests", "fixtures", "low_verbosity") @pytest.fixture -def scripts_project_path(): - return PROJECT_ROOT.joinpath("tests", "fixtures", "scripts_project") +def high_verbosity_project_path(): + return PROJECT_ROOT.joinpath("tests", "fixtures", "high_verbosity") @pytest.fixture(scope="function") @@ -63,11 +75,23 @@ def temp_file(tmp_path): yield tmpfilepath -PoeRunResult = namedtuple("PoeRunResult", ("code", "capture", "stdout", "stderr")) +class PoeRunResult( + namedtuple("PoeRunResult", ("code", "path", "capture", "stdout", "stderr")) +): + def __str__(self): + return ( + "PoeRunResult(\n" + f" code={self.code!r},\n" + f" path={self.path},\n" + f" capture=`{self.capture}`,\n" + f" stdout=`{self.stdout}`,\n" + f" stderr=`{self.stderr}`,\n" + ")" + ) @pytest.fixture(scope="function") -def run_poe_subproc(dummy_project_path, temp_file, tmp_path, is_windows): +def run_poe_subproc(projects, temp_file, tmp_path, is_windows): coverage_setup = ( "from coverage import Coverage;" fr'Coverage(data_file=r\"{PROJECT_ROOT.joinpath(".coverage")}\").start();' @@ -75,7 +99,7 @@ def run_poe_subproc(dummy_project_path, temp_file, tmp_path, is_windows): shell_cmd_template = ( 'python -c "' "{coverage_setup}" - "import tomlkit;" + "import tomli;" "from poethepoet.app import PoeThePoet;" "from pathlib import Path;" r"poe = PoeThePoet(cwd=r\"{cwd}\", config={config}, output={output});" @@ -85,16 +109,19 @@ def run_poe_subproc(dummy_project_path, temp_file, tmp_path, is_windows): def run_poe_subproc( *run_args: str, - cwd: str = dummy_project_path, + cwd: str = projects["example"], config: Optional[Mapping[str, Any]] = None, coverage: bool = not is_windows, - ) -> str: + env: Dict[str, str] = None, + project: Optional[str] = None, + ) -> PoeRunResult: + cwd = projects.get(project, cwd) if config is not None: config_path = tmp_path.joinpath("tmp_test_config_file") with config_path.open("w+") as config_file: toml.dump(config, config_file) config_file.seek(0) - config_arg = fr"tomlkit.parse(open(r\"{config_path}\", \"r\").read())" + config_arg = fr"tomli.load(open(r\"{config_path}\", \"r\"))" else: config_arg = "None" @@ -106,7 +133,7 @@ def run_poe_subproc( output=fr"open(r\"{temp_file}\", \"w\")", ) - env = dict(os.environ) + env = dict(os.environ, **(env or {})) if coverage: env["COVERAGE_PROCESS_START"] = str(PROJECT_TOML) @@ -118,6 +145,7 @@ def run_poe_subproc( result = PoeRunResult( code=poeproc.returncode, + path=cwd, capture=captured_output, stdout=task_out.decode().replace("\r\n", "\n"), stderr=task_err.decode().replace("\r\n", "\n"), @@ -129,17 +157,19 @@ def run_poe_subproc( @pytest.fixture(scope="function") -def run_poe(capsys, dummy_project_path): +def run_poe(capsys, projects): def run_poe( *run_args: str, - cwd: str = dummy_project_path, + cwd: str = projects["example"], config: Optional[Mapping[str, Any]] = None, - ) -> str: + project: Optional[str] = None, + ) -> PoeRunResult: + cwd = projects.get(project, cwd) output_capture = StringIO() poe = PoeThePoet(cwd=cwd, config=config, output=output_capture) result = poe(run_args) output_capture.seek(0) - return PoeRunResult(result, output_capture.read(), *capsys.readouterr()) + return PoeRunResult(result, cwd, output_capture.read(), *capsys.readouterr()) return run_poe @@ -155,12 +185,14 @@ def esc_prefix(is_windows): @pytest.fixture(scope="function") -def run_poe_main(capsys, dummy_project_path): +def run_poe_main(capsys, projects): def run_poe_main( *cli_args: str, - cwd: str = dummy_project_path, + cwd: str = projects["example"], config: Optional[Mapping[str, Any]] = None, - ) -> str: + project: Optional[str] = None, + ) -> PoeRunResult: + cwd = projects.get(project, cwd) from poethepoet import main prev_cwd = os.getcwd() @@ -168,7 +200,7 @@ def run_poe_main( sys.argv = ("poe", *cli_args) result = main() os.chdir(prev_cwd) - return PoeRunResult(result, "", *capsys.readouterr()) + return PoeRunResult(result, cwd, "", *capsys.readouterr()) return run_poe_main @@ -202,7 +234,10 @@ def use_venv( ) # create new venv - venv.EnvBuilder(symlinks=True, with_pip=True,).create(str(location)) + venv.EnvBuilder( + symlinks=True, + with_pip=True, + ).create(str(location)) if contents: install_into_virtualenv(location, contents) @@ -246,7 +281,8 @@ def use_virtualenv( @pytest.fixture def with_virtualenv_and_venv(use_venv, use_virtualenv): def with_virtualenv_and_venv( - location: Path, contents: Optional[List[str]] = None, + location: Path, + contents: Optional[List[str]] = None, ): with use_venv(location, contents, require_empty=True): yield diff --git a/tests/fixtures/cmds_project/pyproject.toml b/tests/fixtures/cmds_project/pyproject.toml new file mode 100644 index 000000000..c4e09117a --- /dev/null +++ b/tests/fixtures/cmds_project/pyproject.toml @@ -0,0 +1,10 @@ +tool.poe.tasks.show_env = "env" + +[tool.poe.tasks.echo] +cmd = "echo POE_ROOT:$POE_ROOT ${BEST_PASSWORD}, task_args:" +help = "It says what you say" +env = { BEST_PASSWORD = "Password1" } + +[tool.poe.tasks.greet] +shell = "echo $formal_greeting $subject" +args = ["formal-greeting", "subject"] diff --git a/tests/fixtures/default_value_project/pyproject.toml b/tests/fixtures/default_value_project/pyproject.toml new file mode 100644 index 000000000..035c16869 --- /dev/null +++ b/tests/fixtures/default_value_project/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poe.env] +ONE = "!one!" +TWO = "nope" +THREE = "!three!" +FOUR.default = "!four!" +FIVE.default = "nope" +SIX.default = "!six!" + +[tool.poe.tasks.test] +cmd = "echo $ONE $TWO $THREE $FOUR $FIVE $SIX" +env.TWO = "!two!" +env.FIVE = "!five!" +env.SIX.default = "nope" diff --git a/tests/fixtures/envfile_project/credentials.env b/tests/fixtures/envfile_project/credentials.env new file mode 100644 index 000000000..650b81755 --- /dev/null +++ b/tests/fixtures/envfile_project/credentials.env @@ -0,0 +1,3 @@ +USER=admin +PASSWORD=12345 +HOST=dev.example.com diff --git a/tests/fixtures/envfile_project/prod.env b/tests/fixtures/envfile_project/prod.env new file mode 100644 index 000000000..464398b81 --- /dev/null +++ b/tests/fixtures/envfile_project/prod.env @@ -0,0 +1,2 @@ +HOST=prod.example.com +PATH_SUFFIX=/app diff --git a/tests/fixtures/envfile_project/pyproject.toml b/tests/fixtures/envfile_project/pyproject.toml new file mode 100644 index 000000000..b56abae3d --- /dev/null +++ b/tests/fixtures/envfile_project/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poe] +envfile = "credentials.env" + +[tool.poe.tasks.deploy-dev] +cmd = """ +echo "deploying to ${USER}:${PASSWORD}@${HOST}${PATH_SUFFIX}" +""" + + +[tool.poe.tasks.deploy-prod] +cmd = """ +echo "deploying to ${USER}:${PASSWORD}@${HOST}${PATH_SUFFIX}" +""" +envfile = "prod.env" diff --git a/tests/fixtures/dummy_project/dummy_package/__init__.py b/tests/fixtures/example_project/dummy_package/__init__.py similarity index 86% rename from tests/fixtures/dummy_project/dummy_package/__init__.py rename to tests/fixtures/example_project/dummy_package/__init__.py index 4887af0a8..ecec40fe8 100644 --- a/tests/fixtures/dummy_project/dummy_package/__init__.py +++ b/tests/fixtures/example_project/dummy_package/__init__.py @@ -7,7 +7,7 @@ def main(*args, greeting="hello", upper=False): print( greeting.upper(), *(arg.upper() for arg in args[1:]), - *(arg.upper() for arg in sys.argv[1:]) + *(arg.upper() for arg in sys.argv[1:]), ) else: print(greeting, *args, *sys.argv[1:]) diff --git a/tests/fixtures/dummy_project/pyproject.toml b/tests/fixtures/example_project/pyproject.toml similarity index 91% rename from tests/fixtures/dummy_project/pyproject.toml rename to tests/fixtures/example_project/pyproject.toml index 3e981050f..b01b39a47 100644 --- a/tests/fixtures/dummy_project/pyproject.toml +++ b/tests/fixtures/example_project/pyproject.toml @@ -12,17 +12,19 @@ packages = [ env.PLANET = "EARTH" env.DEST = "MARS" -# Alternate sub-table syntax for task with lots of config + [tool.poe.tasks.echo] cmd = "echo POE_ROOT:$POE_ROOT ${BEST_PASSWORD}, task_args:" help = "It says what you say" env = { BEST_PASSWORD = "Password1" } + [tool.poe.tasks] show_env = "env" greet = { script = "dummy_package:main" } greet-shouty = { script = "dummy_package:main(upper=True)" } -count = { shell = "echo 1 && echo 2 && echo $(python -c 'print(1 + 2)')" } + +count = { shell = "echo 1 && echo 2 && echo $(python -c 'print(1 + 2)')" } also_echo = { ref = "echo" } # Dotted key syntax for tasks with multiple keys specified @@ -56,6 +58,7 @@ travel = [ { script = "dummy_package:print_var('DEST')" } ] + [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/graphs_project/pyproject.toml b/tests/fixtures/graphs_project/pyproject.toml new file mode 100644 index 000000000..b975cd71a --- /dev/null +++ b/tests/fixtures/graphs_project/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poe.tasks] + +greet = "echo hello" + +_noop.shell = ":" +_about = "echo about" +_do_setup = "echo here we go..." + + + [tool.poe.tasks.think] + cmd = "echo Thinking $first_thing and $subject2" + deps = ["_noop", "_do_setup"] + uses = { first_thing = "_about $subject1" } + args = [{ name = "subject1", positional = true }, { name = "subject2", positional = true }] + + [tool.poe.tasks.deep-graph-with-args] + cmd = "echo $greeting1 and $greeting2" + deps = ["_do_setup", "think $subject1 $subject2"] + uses = { greeting1 = "greet $subject1", greeting2 = "greet $subject2"} + args = ["subject1", "subject2"] diff --git a/tests/fixtures/high_verbosity/pyproject.toml b/tests/fixtures/high_verbosity/pyproject.toml new file mode 100644 index 000000000..c226261da --- /dev/null +++ b/tests/fixtures/high_verbosity/pyproject.toml @@ -0,0 +1,5 @@ +[tool.poe] +verbosity = 1 + +[tool.poe.tasks] +test = "echo Hello there!" diff --git a/tests/fixtures/low_verbosity/pyproject.toml b/tests/fixtures/low_verbosity/pyproject.toml new file mode 100644 index 000000000..7592e8db4 --- /dev/null +++ b/tests/fixtures/low_verbosity/pyproject.toml @@ -0,0 +1,5 @@ +[tool.poe] +verbosity = -1 + +[tool.poe.tasks] +test = "echo Hello there!" diff --git a/tests/fixtures/scripts_project/bad_content.toml b/tests/fixtures/scripts_project/bad_content.toml new file mode 100644 index 000000000..08e19f7d3 --- /dev/null +++ b/tests/fixtures/scripts_project/bad_content.toml @@ -0,0 +1,3 @@ +[tool.poe.tasks.bad-type] +script = "dummy_package:main[greeting]" +args = { greeting = {type = "datetime"} } diff --git a/tests/fixtures/scripts_project/bad_type.toml b/tests/fixtures/scripts_project/bad_type.toml new file mode 100644 index 000000000..e0c7e4ebe --- /dev/null +++ b/tests/fixtures/scripts_project/bad_type.toml @@ -0,0 +1,3 @@ +[tool.poe.tasks.bad-type] +script = "dummy_package:main(greeting)" +args = { greeting = {type = "datetime"} } diff --git a/tests/fixtures/scripts_project/pkg/__init__.py b/tests/fixtures/scripts_project/pkg/__init__.py new file mode 100644 index 000000000..b266ac18e --- /dev/null +++ b/tests/fixtures/scripts_project/pkg/__init__.py @@ -0,0 +1,51 @@ +import sys +from typing import Any, Optional + + +def uprint(*objects, sep=" ", end="\n", file=sys.stdout): + enc = file.encoding + if enc == "UTF-8": + print(*objects, sep=sep, end=end, file=file) + else: + f = lambda obj: str(obj).encode(enc, errors="backslashreplace").decode(enc) + print(*map(f, objects), sep=sep, end=end, file=file) + + +def echo_args(): + print("hello", *sys.argv[1:]) + + +def describe_args(*args, **kwargs): + for value in args: + print(f"{type(value).__name__}: {value}") + for key, value in kwargs.items(): + print(f"{key} => {type(value).__name__}: {value}") + + +def greet( + greeting: str = "I'm sorry", user: str = "Dave", upper: bool = False, **kwargs +): + if upper: + uprint( + *( + str(subpart).upper() + for subpart in ( + greeting, + user, + *(val for val in kwargs.values() if isinstance(val, str)), + ) + ), + *( + val + for val in kwargs.values() + if val is not None and not isinstance(val, str) + ), + ) + else: + uprint(greeting, user, *kwargs.values()) + + +class Scripts: + class Deep: + def fun(self): + print("task!") diff --git a/tests/fixtures/scripts_project/pyproject.toml b/tests/fixtures/scripts_project/pyproject.toml index 5e1cdb64d..c32c9f3c6 100644 --- a/tests/fixtures/scripts_project/pyproject.toml +++ b/tests/fixtures/scripts_project/pyproject.toml @@ -5,7 +5,7 @@ description = "scripts-project" authors = ["Nat Noordanus "] packages = [ - { include = "scripts_pkg" } + { include = "pkg" } ] [tool.poe] @@ -13,11 +13,92 @@ default_task_type = "script" default_array_item_task_type = "cmd" [tool.poe.tasks] -greet = "scripts_pkg:main" - # Interpret subtasks as cmd instead of ref composite_task = ["echo Hello", "echo World!"] +# test_setting_default_task_type +echo-args = "pkg:echo_args" + +static-args-test = """ + pkg:describe_args( + 'pat a cake', 1, 1.2, False, True, + ..., hex(99), chr(55), next(zip(['first'], ({'second': "lol"}).items())), bool(1), + **({"spread": set((1,2,3,2,1))}), + thing="stuff", data1=b'stuff', data2=bytes((66,67,68)), eight=(int('8'), 8.0, 8), + ) +""" + +call_attrs = "pkg:Scripts.Deep.fun(0)" + +greet = "pkg:greet" + + [tool.poe.tasks.greet-passed-args] + # weird formatting is intentional + script = """ + pkg:greet( + greeting, user, + dêƒäült='👋', optional=optional, + upper=upper)""" + args = ["greeting", "user", "optional", "upper"] + + [tool.poe.tasks.greet-full-args] + script = "pkg:greet(greeting, user, upper=(not upper), fvar=user_height, ivar=age)" + + [tool.poe.tasks.greet-full-args.args.greeting] + options = ["--greeting", "-g"] + type = "string" + default = "hi" + + [tool.poe.tasks.greet-full-args.args.user] + # defaults to string if no type given + + [tool.poe.tasks.greet-full-args.args.upper] + default = true + type = "boolean" + + [tool.poe.tasks.greet-full-args.args.age] + options = ["--age", "-a"] + type = "integer" + + [tool.poe.tasks.greet-full-args.args.user_height] + options = ["--height", "-h"] + type = "float" + help = "The user's height in meters" + + [tool.poe.tasks.greet-strict] + script = "pkg:greet(greeting, user=name)" + help = "All arguments are required" + + [tool.poe.tasks.greet-strict.args.greeting] + default = "doesn't matter" + required = true + help = "this one is required" + + [tool.poe.tasks.greet-strict.args.name] + required = true + help = "and this one is required" + + [tool.poe.tasks.greet-positional] + script = "pkg:greet(greeting, user=username, upper=uppercase)" + + [[tool.poe.tasks.greet-positional.args]] + name = "greeting" + positional = true + default = "yo" + help = "this one is required" + + [[tool.poe.tasks.greet-positional.args]] + name = "username" + positional = "user" + required = true + help = "and this one is required" + + [[tool.poe.tasks.greet-positional.args]] + name = "uppercase" + options = ["--upper"] + type = "boolean" + + [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" diff --git a/tests/fixtures/scripts_project/scripts_pkg/__init__.py b/tests/fixtures/scripts_project/scripts_pkg/__init__.py deleted file mode 100644 index b8a1f43b1..000000000 --- a/tests/fixtures/scripts_project/scripts_pkg/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys - - -def main(): - print("hello", *sys.argv[1:]) diff --git a/tests/fixtures/sequences_project/my_package/__init__.py b/tests/fixtures/sequences_project/my_package/__init__.py new file mode 100644 index 000000000..ecec40fe8 --- /dev/null +++ b/tests/fixtures/sequences_project/my_package/__init__.py @@ -0,0 +1,18 @@ +import os +import sys + + +def main(*args, greeting="hello", upper=False): + if upper: + print( + greeting.upper(), + *(arg.upper() for arg in args[1:]), + *(arg.upper() for arg in sys.argv[1:]), + ) + else: + print(greeting, *args, *sys.argv[1:]) + + +def print_var(*var_names): + for var in var_names: + print(os.environ.get(var)) diff --git a/tests/fixtures/sequences_project/pyproject.toml b/tests/fixtures/sequences_project/pyproject.toml new file mode 100644 index 000000000..f7db17440 --- /dev/null +++ b/tests/fixtures/sequences_project/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poe.tasks] + +part1 = "echo 'Hello'" +_part2.cmd = "echo '${SUBJECT}!'" +_part2.env = { SUBJECT = "World" } +composite_task.sequence = [ + ["part1", "_part2"], + # wrapping in arrays means we can have different types of task in the sequence + [{cmd = "echo '${SMILEY}!'"}] +] +# env var is inherited by subtask +composite_task.env = { SMILEY = ":)" } + +also_composite_task = [["composite_task"]] # sequence in a sequence + +greet-multiple.sequence = ["my_package:main(environ.get('cat'))", "my_package:main(environ['mouse'])"] +greet-multiple.default_item_type = "script" +greet-multiple.env = { cat = "Tom" } +greet-multiple.args = ["mouse"] + + +travel = [ + { cmd = "echo 'from $PLANET to'" }, + { script = "my_package:print_var('DEST')" } +] diff --git a/tests/fixtures/shells_project/pyproject.toml b/tests/fixtures/shells_project/pyproject.toml new file mode 100644 index 000000000..d53a93fe4 --- /dev/null +++ b/tests/fixtures/shells_project/pyproject.toml @@ -0,0 +1,17 @@ +tool.poe.default_task_type = "shell" + +[tool.poe.tasks] + +count = "echo 1 && echo 2 && echo $(python -c 'print(1 + 2)')" + +sing = """ +echo "this is the story"; +echo "all about how" && # the last line won't run +echo "my life got flipped; + turned upside down" || +echo "bam bam baaam bam" +""" + +[tool.poe.tasks.greet] +shell = "echo $formal_greeting $subject" +args = ["formal-greeting", "subject"] diff --git a/tests/test_cli.py b/tests/test_cli.py index ab1b47f33..b89d9faa7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,8 +13,8 @@ def test_call_no_args(run_poe): assert "CONFIGURED TASKS\n echo" in result.capture, "echo task should be in help" -def test_call_with_root(run_poe, dummy_project_path): - result = run_poe("--root", str(dummy_project_path), cwd=".") +def test_call_with_root(run_poe, projects): + result = run_poe("--root", str(projects["example"]), cwd=".") assert result.code == 1, "Expected non-zero result" assert result.capture.startswith( "Poe the Poet - A task runner that works well with poetry." @@ -61,8 +61,40 @@ def test_version_option(run_poe): assert result.stderr == "" -def test_dry_run(run_poe_subproc, dummy_project_path): +def test_dry_run(run_poe_subproc): result = run_poe_subproc("-d", "show_env") assert result.capture == f"Poe => env\n" assert result.stdout == "" assert result.stderr == "" + + +def test_documentation_of_task_named_args(run_poe): + result = run_poe(project="scripts") + assert result.capture.startswith( + "Poe the Poet - A task runner that works well with poetry." + ), "Output should start with poe header line" + assert ( + "\nResult: No task specified.\n" in result.capture + ), "Output should include status message" + assert ( + "CONFIGURED TASKS\n" + " composite_task \n" + " echo-args \n" + " static-args-test \n" + " call_attrs \n" + " greet \n" + " greet-passed-args \n" + " --greeting \n" + " --user \n" + " --optional \n" + " --upper \n" + " greet-full-args \n" + " --greeting, -g \n" + " --user \n" + " --upper \n" + " --age, -a \n" + " --height, -h The user's height in meters\n" + " greet-strict All arguments are required\n" + " --greeting this one is required\n" + " --name and this one is required\n" + ) in result.capture diff --git a/tests/test_cmd_tasks.py b/tests/test_cmd_tasks.py new file mode 100644 index 000000000..658148b5e --- /dev/null +++ b/tests/test_cmd_tasks.py @@ -0,0 +1,24 @@ +def test_call_echo_task(run_poe_subproc, projects, esc_prefix): + result = run_poe_subproc("echo", "foo", "!", project="cmds") + assert ( + result.capture + == f"Poe => echo POE_ROOT:{projects['cmds']} Password1, task_args: foo !\n" + ) + assert result.stdout == f"POE_ROOT:{projects['cmds']} Password1, task_args: foo !\n" + assert result.stderr == "" + + +def test_setting_envvar_in_task(run_poe_subproc, projects): + result = run_poe_subproc("show_env", project="cmds") + assert result.capture == f"Poe => env\n" + assert f"POE_ROOT={projects['cmds']}" in result.stdout + assert result.stderr == "" + + +def test_cmd_task_with_dash_case_arg(run_poe_subproc): + result = run_poe_subproc( + "greet", "--formal-greeting=hey", "--subject=you", project="cmds" + ) + assert result.capture == f"Poe => echo $formal_greeting $subject\n" + assert result.stdout == "hey you\n" + assert result.stderr == "" diff --git a/tests/test_default_value.py b/tests/test_default_value.py new file mode 100644 index 000000000..3a85bdc0e --- /dev/null +++ b/tests/test_default_value.py @@ -0,0 +1,21 @@ +def test_global_envfile_and_default(run_poe_subproc): + result = run_poe_subproc("test", project="default_value") + assert "Poe => echo !one! !two! !three! !four! !five! !six!\n" in result.capture + assert result.stdout == "!one! !two! !three! !four! !five! !six!\n" + assert result.stderr == "" + + +def test_global_envfile_and_default_with_presets(run_poe_subproc): + env = { + "ONE": "111", + "TWO": "222", + "THREE": "333", + "FOUR": "444", + "FIVE": "555", + "SIX": "666", + } + + result = run_poe_subproc("test", project="default_value", env=env) + assert "Poe => echo !one! !two! !three! 444 !five! 666\n" in result.capture + assert result.stdout == "!one! !two! !three! 444 !five! 666\n" + assert result.stderr == "" diff --git a/tests/test_envfile.py b/tests/test_envfile.py new file mode 100644 index 000000000..9e5246f36 --- /dev/null +++ b/tests/test_envfile.py @@ -0,0 +1,38 @@ +import os +from pathlib import Path +import sys + + +def test_global_envfile_and_default(run_poe_subproc, is_windows): + result = run_poe_subproc("deploy-dev", project="envfile") + if is_windows: + # On windows shlex works in non-POSIX mode which results in quotes + assert ( + 'Poe => echo "deploying to admin:12345@dev.example.com"\n' in result.capture + ) + assert result.stdout == '"deploying to admin:12345@dev.example.com"\n' + assert result.stderr == "" + else: + assert ( + "Poe => echo deploying to admin:12345@dev.example.com\n" in result.capture + ) + assert result.stdout == "deploying to admin:12345@dev.example.com\n" + assert result.stderr == "" + + +def test_task_envfile_and_default(run_poe_subproc, is_windows): + result = run_poe_subproc("deploy-prod", project="envfile") + if is_windows: + assert ( + 'Poe => echo "deploying to admin:12345@prod.example.com/app"\n' + in result.capture + ) + assert result.stdout == '"deploying to admin:12345@prod.example.com/app"\n' + assert result.stderr == "" + else: + assert ( + "Poe => echo deploying to admin:12345@prod.example.com/app\n" + in result.capture + ) + assert result.stdout == "deploying to admin:12345@prod.example.com/app\n" + assert result.stderr == "" diff --git a/tests/test_executors.py b/tests/test_executors.py index a91c6804f..00d09fb04 100644 --- a/tests/test_executors.py +++ b/tests/test_executors.py @@ -4,12 +4,13 @@ PY_V = f"{sys.version_info.major}.{sys.version_info.minor}" -def test_virtualenv_executor_fails_without_venv_dir(run_poe_subproc, venv_project_path): - venv_path = venv_project_path.joinpath("myvenv") +def test_virtualenv_executor_fails_without_venv_dir(run_poe_subproc, projects): + venv_path = projects["venv"].joinpath("myvenv") + print("venv_path", venv_path) assert ( not venv_path.is_dir() ), f"This test requires the virtualenv not to already exist at {venv_path}!" - result = run_poe_subproc("show_env", cwd=venv_project_path) + result = run_poe_subproc("show_env", project="venv") assert ( f"Error: Could not find valid virtualenv at configured location: {venv_path}" in result.capture @@ -19,41 +20,41 @@ def test_virtualenv_executor_fails_without_venv_dir(run_poe_subproc, venv_projec def test_virtualenv_executor_activates_venv( - run_poe_subproc, with_virtualenv_and_venv, venv_project_path + run_poe_subproc, with_virtualenv_and_venv, projects ): - venv_path = venv_project_path.joinpath("myvenv") + venv_path = projects["venv"].joinpath("myvenv") for _ in with_virtualenv_and_venv(venv_path): - result = run_poe_subproc("show_env", cwd=venv_project_path) + result = run_poe_subproc("show_env", project="venv") assert result.capture == "Poe => env\n" assert f"VIRTUAL_ENV={venv_path}" in result.stdout assert result.stderr == "" def test_virtualenv_executor_provides_access_to_venv_content( - run_poe_subproc, with_virtualenv_and_venv, venv_project_path + run_poe_subproc, with_virtualenv_and_venv, projects ): # version 1.0.0 of flask isn't around much - venv_path = venv_project_path.joinpath("myvenv") + venv_path = projects["venv"].joinpath("myvenv") for _ in with_virtualenv_and_venv(venv_path, ["flask==1.0.0"]): # binaries from the venv are directly callable - result = run_poe_subproc("server-version", cwd=venv_project_path) + result = run_poe_subproc("server-version", project="venv") assert result.capture == "Poe => flask --version\n" assert "Flask 1.0" in result.stdout assert result.stderr == "" # python packages from the venv are importable - result = run_poe_subproc("flask-version", cwd=venv_project_path) + result = run_poe_subproc("flask-version", project="venv") assert result.capture == "Poe => flask-version\n" assert result.stdout == "1.0\n" assert result.stderr == "" # binaries from the venv are on the path - result = run_poe_subproc("server-version2", cwd=venv_project_path) + result = run_poe_subproc("server-version2", project="venv") assert result.capture == "Poe => server-version2\n" assert "Flask 1.0" in result.stdout assert result.stderr == "" def test_detect_venv( - simple_project_path, + projects, run_poe_subproc, install_into_virtualenv, with_virtualenv_and_venv, @@ -63,16 +64,16 @@ def test_detect_venv( If no executor is specified and no poetry config is present but a local venv is found then use it! """ - venv_path = simple_project_path.joinpath("venv") + venv_path = projects["simple"].joinpath("venv") for _ in with_virtualenv_and_venv(venv_path): - result = run_poe_subproc("detect_flask", cwd=simple_project_path) + result = run_poe_subproc("detect_flask", project="simple") assert result.capture == "Poe => detect_flask\n" assert result.stdout == "No flask found\n" assert result.stderr == "" # if we install flask into this virtualenv then we should get a different result install_into_virtualenv(venv_path, ["flask==1.0.0"]) - result = run_poe_subproc("detect_flask", cwd=simple_project_path) + result = run_poe_subproc("detect_flask", project="simple") assert result.capture == "Poe => detect_flask\n" assert result.stdout.startswith("Flask found at ") if is_windows: @@ -86,11 +87,11 @@ def test_detect_venv( assert result.stderr == "" -def test_simple_exector(simple_project_path, run_poe_subproc): +def test_simple_exector(run_poe_subproc): """ The task should execute but not find flask from a local venv """ - result = run_poe_subproc("detect_flask", cwd=simple_project_path) + result = run_poe_subproc("detect_flask", project="simple") assert result.capture == "Poe => detect_flask\n" assert result.stdout == "No flask found\n" or not result.stdout.endswith( f"/tests/fixtures/simple_project/venv/lib/python{PY_V}/site-packages/flask/__init__.py\n" diff --git a/tests/test_graph_execution.py b/tests/test_graph_execution.py new file mode 100644 index 000000000..195c53d0c --- /dev/null +++ b/tests/test_graph_execution.py @@ -0,0 +1,14 @@ +def test_call_attr_func(run_poe_subproc): + result = run_poe_subproc("deep-graph-with-args", project="graphs") + assert result.capture == ( + "Poe => echo here we go...\n" + "Poe => :\n" + "Poe <= echo about\n" + "Poe <= echo hello\n" + "Poe => echo Thinking about and\n" + "Poe => echo hello and hello\n" + ) + assert result.stdout == ( + "here we go...\n" "Thinking about and\n" "hello and hello\n" + ) + assert result.stderr == "" diff --git a/tests/test_ignore_fail.py b/tests/test_ignore_fail.py new file mode 100644 index 000000000..0580c8396 --- /dev/null +++ b/tests/test_ignore_fail.py @@ -0,0 +1,68 @@ +import pytest + + +@pytest.fixture +def generate_pyproject(tmp_path): + """Return function which generates pyproject.toml with a given ignore_fail value.""" + + def generator(ignore_fail): + project_tmpl = """ + [tool.poe.tasks] + task_1 = { shell = "echo 'task 1 error'; exit 1;" } + task_2 = { shell = "echo 'task 2 error'; exit 1;" } + task_3 = { shell = "echo 'task 3 success'; exit 0;" } + + [tool.poe.tasks.all_tasks] + sequence = ["task_1", "task_2", "task_3"] + """ + if isinstance(ignore_fail, bool) and ignore_fail: + project_tmpl += "\nignore_fail = true" + elif not isinstance(ignore_fail, bool): + project_tmpl += f'\nignore_fail = "{ignore_fail}"' + with open(tmp_path / "pyproject.toml", "w") as fp: + fp.write(project_tmpl) + + return tmp_path + + return generator + + +@pytest.mark.parametrize("fail_value", [True, "return_zero"]) +def test_full_ignore(generate_pyproject, run_poe, fail_value): + project_path = generate_pyproject(ignore_fail=fail_value) + result = run_poe("all_tasks", cwd=project_path) + assert result.code == 0, "Expected zero result" + assert "task 1 error" in result.capture, "Expected first task in log" + assert "task 2 error" in result.capture, "Expected second task in log" + assert "task 3 success" in result.capture, "Expected third task in log" + + +def test_without_ignore(generate_pyproject, run_poe): + project_path = generate_pyproject(ignore_fail=False) + result = run_poe("all_tasks", cwd=project_path) + assert result.code == 1, "Expected non-zero result" + assert "task 1 error" in result.capture, "Expected first task in log" + assert "task 2 error" not in result.capture, "Second task shouldn't run" + assert "task 3 success" not in result.capture, "Third task shouldn't run" + assert "Sequence aborted after failed subtask 'task_1'" in result.capture + + +def test_return_non_zero(generate_pyproject, run_poe): + project_path = generate_pyproject(ignore_fail="return_non_zero") + result = run_poe("all_tasks", cwd=project_path) + assert result.code == 1, "Expected non-zero result" + assert "task 1 error" in result.capture, "Expected first task in log" + assert "task 2 error" in result.capture, "Expected second task in log" + assert "task 3 success" in result.capture, "Expected third task in log" + assert "Subtasks task_1, task_2 returned non-zero exit status" in result.capture + + +def test_invalid_ingore_value(generate_pyproject, run_poe): + project_path = generate_pyproject(ignore_fail="invalid_value") + result = run_poe("all_tasks", cwd=project_path) + assert result.code == 1, "Expected non-zero result" + assert ( + "Unsupported value for option `ignore_fail` for task 'all_tasks'." + ' Expected one of (true, false, "return_zero", "return_non_zero")' + in result.capture + ) diff --git a/tests/test_poe_config.py b/tests/test_poe_config.py index 22af4236a..40d5371ef 100644 --- a/tests/test_poe_config.py +++ b/tests/test_poe_config.py @@ -3,20 +3,21 @@ import tempfile -def test_setting_default_task_type(run_poe_subproc, scripts_project_path, esc_prefix): +def test_setting_default_task_type(run_poe_subproc, projects, esc_prefix): + # Also tests passing of extra_args to sys.argv result = run_poe_subproc( - "greet", + "echo-args", "nat,", r"welcome to " + esc_prefix + "${POE_ROOT}", - cwd=scripts_project_path, + project="scripts", ) - assert result.capture == f"Poe => greet nat, welcome to {scripts_project_path}\n" - assert result.stdout == f"hello nat, welcome to {scripts_project_path}\n" + assert result.capture == f"Poe => echo-args nat, welcome to {projects['scripts']}\n" + assert result.stdout == f"hello nat, welcome to {projects['scripts']}\n" assert result.stderr == "" -def test_setting_default_array_item_task_type(run_poe_subproc, scripts_project_path): - result = run_poe_subproc("composite_task", cwd=scripts_project_path,) +def test_setting_default_array_item_task_type(run_poe_subproc): + result = run_poe_subproc("composite_task", project="scripts") assert result.capture == f"Poe => echo Hello\nPoe => echo World!\n" assert result.stdout == f"Hello\nWorld!\n" assert result.stderr == "" @@ -31,3 +32,52 @@ def test_setting_global_env_vars(run_poe_subproc, is_windows): assert result.capture == f"Poe => echo from EARTH to\nPoe => travel[1]\n" assert result.stdout == f"from EARTH to\nMARS\n" assert result.stderr == "" + + +def test_setting_default_verbosity(run_poe_subproc, low_verbosity_project_path): + result = run_poe_subproc( + "test", + cwd=low_verbosity_project_path, + ) + assert result.capture == "" + assert result.stdout == "Hello there!\n" + assert result.stderr == "" + + +def test_override_default_verbosity(run_poe_subproc, low_verbosity_project_path): + result = run_poe_subproc( + "-v", + "-v", + "test", + cwd=low_verbosity_project_path, + ) + assert result.capture == "Poe => echo Hello there!\n" + assert result.stdout == "Hello there!\n" + assert result.stderr == "" + + +def test_partially_decrease_verbosity(run_poe_subproc, high_verbosity_project_path): + result = run_poe_subproc( + "-q", + "test", + cwd=high_verbosity_project_path, + ) + assert result.capture == "Poe => echo Hello there!\n" + assert result.stdout == "Hello there!\n" + assert result.stderr == "" + + +def test_decrease_verbosity(run_poe_subproc, projects, is_windows): + result = run_poe_subproc( + "-q", + "part1", + cwd=projects["example"], + ) + assert result.capture == "" + assert result.stderr == "" + if is_windows: + # On Windows, "echo 'Hello'" results in "'Hello'". + assert result.stdout == "'Hello'\n" + else: + # On UNIX, "echo 'Hello'" results in just "Hello". + assert result.stdout == "Hello\n" diff --git a/tests/test_script_tasks.py b/tests/test_script_tasks.py new file mode 100644 index 000000000..a18b94832 --- /dev/null +++ b/tests/test_script_tasks.py @@ -0,0 +1,254 @@ +import difflib + + +def test_script_task_with_hard_coded_args(run_poe_subproc, projects, esc_prefix): + result = run_poe_subproc("static-args-test", project="scripts") + assert result.capture == f"Poe => static-args-test\n" + assert result.stdout == ( + "str: pat a cake\n" + "int: 1\n" + "float: 1.2\n" + "bool: False\n" + "bool: True\n" + "ellipsis: Ellipsis\n" + "str: 0x63\n" + "str: 7\n" + "tuple: ('first', ('second', 'lol'))\n" + "bool: True\n" + "spread => set: {1, 2, 3}\n" + "thing => str: stuff\n" + "data1 => bytes: b'stuff'\n" + "data2 => bytes: b'BCD'\n" + "eight => tuple: (8, 8.0, 8)\n" + ) + assert result.stderr == "" + + +def test_call_attr_func(run_poe_subproc): + result = run_poe_subproc("call_attrs", project="scripts") + assert result.capture == "Poe => call_attrs\n" + assert result.stdout == "task!\n" + assert result.stderr == "" + + +def test_automatic_kwargs_from_args(run_poe_subproc): + result = run_poe_subproc("greet", project="scripts") + assert result.capture == "Poe => greet\n" + assert result.stdout == "I'm sorry Dave\n" + assert result.stderr == "" + + +def test_script_task_with_cli_args(run_poe_subproc, is_windows): + result = run_poe_subproc( + "greet-passed-args", + "--greeting=hello", + "--user=nat", + project="scripts", + ) + assert ( + result.capture == f"Poe => greet-passed-args --greeting=hello --user=nat\n" + ), [ + li + for li in difflib.ndiff( + result.capture, f"Poe => greet-passed-args --greeting=hello --user=nat\n" + ) + if li[0] != " " + ] + if is_windows: + assert result.stdout == "hello nat \\U0001f44b None\n" + else: + assert result.stdout == "hello nat 👋 None\n" + assert result.stderr == "" + + +def test_script_task_with_args_optional(run_poe_subproc, projects, is_windows): + named_args_project_path = projects["scripts"] + result = run_poe_subproc( + "greet-passed-args", + "--greeting=hello", + "--user=nat", + f"--optional=welcome to {named_args_project_path}", + project="scripts", + ) + assert result.capture == ( + f"Poe => greet-passed-args --greeting=hello --user=nat --optional=welcome " + f"to {named_args_project_path}\n" + ) + + if is_windows: + assert ( + result.stdout + == f"hello nat \\U0001f44b welcome to {named_args_project_path}\n" + ) + else: + assert result.stdout == f"hello nat 👋 welcome to {named_args_project_path}\n" + + assert result.stderr == "" + + +def test_script_task_default_arg(run_poe_subproc): + result = run_poe_subproc("greet-full-args", project="scripts") + assert result.capture == f"Poe => greet-full-args\n" + # hi is the default value for --greeting + assert result.stdout == "hi None None None\n" + assert result.stderr == "" + + +def test_script_task_include_boolean_flag_and_numeric_args(run_poe_subproc): + result = run_poe_subproc( + "greet-full-args", + "--greeting=hello", + "--user=nat", + "--upper", + "--age=42", + "--height=1.23", + project="scripts", + ) + assert result.capture == ( + "Poe => greet-full-args --greeting=hello --user=nat --upper --age=42 " + "--height=1.23\n" + ) + assert result.stdout == "HELLO NAT 1.23 42\n" + assert result.stderr == "" + + +def test_script_task_with_short_args(run_poe_subproc): + result = run_poe_subproc( + "greet-full-args", + "-g=Ciao", + "--user=toni", + "-a", + "109", + "-h=1.09", + project="scripts", + ) + assert result.capture == ( + "Poe => greet-full-args -g=Ciao --user=toni -a 109 -h=1.09\n" + ) + assert result.stdout == "Ciao toni 1.09 109\n" + assert result.stderr == "" + + +def test_wrong_args_passed(run_poe_subproc): + base_error = ( + "usage: poe greet-full-args [--greeting GREETING] [--user USER] [--upper]\n" + " [--age AGE] [--height USER_HEIGHT]\n" + "poe greet-full-args: error:" + ) + + result = run_poe_subproc("greet-full-args", "--age=lol", project="scripts") + assert result.capture == "" + assert result.stdout == "" + assert result.stderr == ( + f"{base_error} argument --age/-a: invalid int value: 'lol'\n" + ) + + result = run_poe_subproc("greet-full-args", "--age", project="scripts") + assert result.capture == "" + assert result.stdout == "" + assert result.stderr == (f"{base_error} argument --age/-a: expected one argument\n") + + result = run_poe_subproc("greet-full-args", "--age 3 2 1", project="scripts") + assert result.capture == "" + assert result.stdout == "" + assert result.stderr == (f"{base_error} unrecognized arguments: --age 3 2 1\n") + + result = run_poe_subproc("greet-full-args", "--potatoe", project="scripts") + assert result.capture == "" + assert result.stdout == "" + assert result.stderr == (f"{base_error} unrecognized arguments: --potatoe\n") + + +def test_required_args(run_poe_subproc): + result = run_poe_subproc( + "greet-strict", "--greeting=yo", "--name", "dude", project="scripts" + ) + assert result.capture == "Poe => greet-strict --greeting=yo --name dude\n" + assert result.stdout == "yo dude\n" + assert result.stderr == "" + + result = run_poe_subproc("greet-strict", project="scripts") + assert result.capture == "" + assert result.stdout == "" + assert result.stderr == ( + "usage: poe greet-strict --greeting GREETING --name NAME\npoe greet-strict: " + "error: the following arguments are required: --greeting, --name\n" + ) + + +def test_script_task_bad_type(run_poe_subproc, projects): + result = run_poe_subproc( + f'--root={projects["scripts/bad_type"]}', + "bad-type", + "--greeting=hello", + ) + assert ( + "Error: 'datetime' is not a valid type for arg 'greeting' of task 'bad-type'. " + "Choose one of {boolean float integer string} \n" in result.capture + ) + assert result.stdout == "" + assert result.stderr == "" + + +def test_script_task_bad_content(run_poe_subproc, projects): + result = run_poe_subproc( + f'--root={projects["scripts/bad_type"]}', + "bad-content", + "--greeting=hello", + ) + assert ( + "Error: Task 'bad-type' contains invalid callable reference " + "'dummy_package:main[greeting]' " + "(expected something like `module:callable` or `module:callable()`)" + ) + assert result.stdout == "" + assert result.stderr == "" + + +def test_script_with_positional_args(run_poe_subproc): + result = run_poe_subproc("greet-positional", "help!", "Santa", project="scripts") + assert result.capture == "Poe => greet-positional help! Santa\n" + assert result.stdout == "help! Santa\n" + assert result.stderr == "" + + # Ommission of optional positional arg + result = run_poe_subproc("greet-positional", "Santa", project="scripts") + assert result.capture == "Poe => greet-positional Santa\n" + assert result.stdout == "yo Santa\n" + assert result.stderr == "" + + # Ommission of required positional arg + result = run_poe_subproc("greet-positional", project="scripts") + assert result.capture == "" + assert result.stdout == "" + assert result.stderr == ( + "usage: poe greet-positional [--upper] [greeting] user\n" + "poe greet-positional: error: the following arguments are required: user\n" + ) + + # Too many positional args + result = run_poe_subproc( + "greet-positional", "plop", "plop", "plop", project="scripts" + ) + assert result.capture == "" + assert result.stdout == "" + assert result.stderr == ( + "usage: poe greet-positional [--upper] [greeting] user\n" + "poe greet-positional: error: unrecognized arguments: plop\n" + ) + + +def test_script_with_positional_args_and_options(run_poe_subproc): + result = run_poe_subproc( + "greet-positional", "help!", "Santa", "--upper", project="scripts" + ) + assert result.capture == "Poe => greet-positional help! Santa --upper\n" + assert result.stdout == "HELP! SANTA\n" + assert result.stderr == "" + + result = run_poe_subproc( + "greet-positional", "--upper", "help!", "Santa", project="scripts" + ) + assert result.capture == "Poe => greet-positional --upper help! Santa\n" + assert result.stdout == "HELP! SANTA\n" + assert result.stderr == "" diff --git a/tests/test_sequence_tasks.py b/tests/test_sequence_tasks.py new file mode 100644 index 000000000..4f8ae0911 --- /dev/null +++ b/tests/test_sequence_tasks.py @@ -0,0 +1,46 @@ +def test_sequence_task(run_poe_subproc, esc_prefix, is_windows): + result = run_poe_subproc("composite_task", project="sequences") + if is_windows: + # On windows shlex works in non-POSIX mode which results in quotes + assert ( + result.capture + == f"Poe => echo 'Hello'\nPoe => echo 'World!'\nPoe => echo ':)!'\n" + ) + assert result.stdout == f"'Hello'\n'World!'\n':)!'\n" + else: + assert ( + result.capture + == f"Poe => echo Hello\nPoe => echo World!\nPoe => echo :)!\n" + ) + assert result.stdout == f"Hello\nWorld!\n:)!\n" + assert result.stderr == "" + + +def test_another_sequence_task(run_poe_subproc, esc_prefix, is_windows): + # This should be exactly the same as calling the composite_task task directly + result = run_poe_subproc("also_composite_task", project="sequences") + if is_windows: + # On windows shlex works in non-POSIX mode which results in quotes + assert ( + result.capture + == f"Poe => echo 'Hello'\nPoe => echo 'World!'\nPoe => echo ':)!'\n" + ) + assert result.stdout == f"'Hello'\n'World!'\n':)!'\n" + else: + assert ( + result.capture + == f"Poe => echo Hello\nPoe => echo World!\nPoe => echo :)!\n" + ) + assert result.stdout == f"Hello\nWorld!\n:)!\n" + assert result.stderr == "" + + +def test_a_script_sequence_task_with_args(run_poe_subproc, esc_prefix): + # This should be exactly the same as calling the composite_task task directly + result = run_poe_subproc("greet-multiple", "--mouse=Jerry", project="sequences") + assert ( + result.capture + == f"Poe => my_package:main(environ.get('cat'))\nPoe => my_package:main(environ['mouse'])\n" + ) + assert result.stdout == f"hello Tom\nhello Jerry\n" + assert result.stderr == "" diff --git a/tests/test_shell_task.py b/tests/test_shell_task.py new file mode 100644 index 000000000..80b07ebfe --- /dev/null +++ b/tests/test_shell_task.py @@ -0,0 +1,42 @@ +def test_shell_task(run_poe_subproc): + result = run_poe_subproc("count", project="shells") + assert ( + result.capture + == f"Poe => echo 1 && echo 2 && echo $(python -c 'print(1 + 2)')\n" + ) + assert result.stdout == "1\n2\n3\n" + assert result.stderr == "" + + +def test_shell_task_raises_given_extra_args(run_poe): + result = run_poe("count", "bla", project="shells") + assert f"\n\nError: Shell task 'count' does not accept arguments" in result.capture + assert result.stdout == "" + assert result.stderr == "" + + +def test_multiline_non_default_type_task(run_poe_subproc): + # This should be exactly the same as calling the echo task directly + result = run_poe_subproc("sing", project="shells") + assert result.capture == ( + f'Poe => echo "this is the story";\n' + 'echo "all about how" && # the last line won\'t run\n' + 'echo "my life got flipped;\n' + ' turned upside down" ||\necho "bam bam baaam bam"\n' + ) + assert result.stdout == ( + f"this is the story\n" + "all about how\n" + "my life got flipped;\n" + " turned upside down\n" + ) + assert result.stderr == "" + + +def test_shell_task_with_dash_case_arg(run_poe_subproc): + result = run_poe_subproc( + "greet", "--formal-greeting=hey", "--subject=you", project="shells" + ) + assert result.capture == (f"Poe => echo $formal_greeting $subject\n") + assert result.stdout == "hey you\n" + assert result.stderr == "" diff --git a/tests/test_task_running.py b/tests/test_task_running.py index dc63a5e2e..418aceb01 100644 --- a/tests/test_task_running.py +++ b/tests/test_task_running.py @@ -1,152 +1,11 @@ -import pytest - - -def test_call_echo_task(run_poe_subproc, dummy_project_path, esc_prefix): - # The $ has to be escaped or it'll be evaluated by the outer shell and poe will - # never see it - result = run_poe_subproc("echo", "foo", "!") - assert ( - result.capture - == f"Poe => echo POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n" - ) - assert ( - result.stdout == f"POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n" - ) - assert result.stderr == "" - - -def test_setting_envvar_in_task(run_poe_subproc, dummy_project_path): - # The $ has to be escaped or it'll be evaluated by the outer shell and poe will - # never see it - result = run_poe_subproc("show_env") - assert result.capture == f"Poe => env\n" - assert f"POE_ROOT={dummy_project_path}" in result.stdout - assert result.stderr == "" - - -def test_shell_task(run_poe_subproc): - result = run_poe_subproc("count") - assert ( - result.capture - == f"Poe => echo 1 && echo 2 && echo $(python -c 'print(1 + 2)')\n" - ) - assert result.stdout == "1\n2\n3\n" - assert result.stderr == "" - - -def test_shell_task_raises_given_extra_args(run_poe): - result = run_poe("count", "bla") - assert f"\n\nError: Shell task 'count' does not accept arguments" in result.capture - assert result.stdout == "" - assert result.stderr == "" - - -def test_script_task(run_poe_subproc, dummy_project_path, esc_prefix): - # The $ has to be escaped or it'll be evaluated by the outer shell and poe will - # never see it - result = run_poe_subproc( - "greet", "nat,", r"welcome to " + esc_prefix + "${POE_ROOT}" - ) - assert result.capture == f"Poe => greet nat, welcome to {dummy_project_path}\n" - assert result.stdout == f"hello nat, welcome to {dummy_project_path}\n" - assert result.stderr == "" - - -def test_script_task_with_hard_coded_args( - run_poe_subproc, dummy_project_path, esc_prefix -): - # The $ has to be escaped or it'll be evaluated by the outer shell and poe will - # never see it - result = run_poe_subproc( - "greet-shouty", "nat,", r"welcome to " + esc_prefix + "${POE_ROOT}" - ) - assert ( - result.capture == f"Poe => greet-shouty nat, welcome to {dummy_project_path}\n" - ) - assert result.stdout == f"hello nat, welcome to {dummy_project_path}\n".upper() - assert result.stderr == "" - - -def test_ref_task(run_poe_subproc, dummy_project_path, esc_prefix): +def test_ref_task(run_poe_subproc, projects, esc_prefix): # This should be exactly the same as calling the echo task directly result = run_poe_subproc("also_echo", "foo", "!") assert ( result.capture - == f"Poe => echo POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n" - ) - assert ( - result.stdout == f"POE_ROOT:{dummy_project_path} Password1, task_args: foo !\n" - ) - assert result.stderr == "" - - -def test_multiline_non_default_type_task( - run_poe_subproc, dummy_project_path, esc_prefix -): - # This should be exactly the same as calling the echo task directly - result = run_poe_subproc("sing") - assert result.capture == ( - f'Poe => echo "this is the story";\n' - 'echo "all about how" && # the last line won\'t run\n' - 'echo "my life got flipped;\n' - ' turned upside down" ||\necho "bam bam baaam bam"\n' + == f"Poe => echo POE_ROOT:{projects['example']} Password1, task_args: foo !\n" ) - assert result.stdout == ( - f"this is the story\n" - "all about how\n" - "my life got flipped;\n" - " turned upside down\n" - ) - assert result.stderr == "" - - -def test_sequence_task(run_poe_subproc, dummy_project_path, esc_prefix, is_windows): - result = run_poe_subproc("composite_task") - if is_windows: - # On windows shlex works in non-POSIX mode which results in quotes - assert ( - result.capture - == f"Poe => echo 'Hello'\nPoe => echo 'World!'\nPoe => echo ':)!'\n" - ) - assert result.stdout == f"'Hello'\n'World!'\n':)!'\n" - else: - assert ( - result.capture - == f"Poe => echo Hello\nPoe => echo World!\nPoe => echo :)!\n" - ) - assert result.stdout == f"Hello\nWorld!\n:)!\n" - assert result.stderr == "" - - -def test_another_sequence_task( - run_poe_subproc, dummy_project_path, esc_prefix, is_windows -): - # This should be exactly the same as calling the composite_task task directly - result = run_poe_subproc("also_composite_task") - if is_windows: - # On windows shlex works in non-POSIX mode which results in quotes - assert ( - result.capture - == f"Poe => echo 'Hello'\nPoe => echo 'World!'\nPoe => echo ':)!'\n" - ) - assert result.stdout == f"'Hello'\n'World!'\n':)!'\n" - else: - assert ( - result.capture - == f"Poe => echo Hello\nPoe => echo World!\nPoe => echo :)!\n" - ) - assert result.stdout == f"Hello\nWorld!\n:)!\n" - assert result.stderr == "" - - -def test_a_script_sequence_task( - run_poe_subproc, dummy_project_path, esc_prefix, is_windows -): - # This should be exactly the same as calling the composite_task task directly - result = run_poe_subproc("greet-multiple") assert ( - result.capture - == f"Poe => dummy_package:main('Tom')\nPoe => dummy_package:main('Jerry')\n" + result.stdout == f"POE_ROOT:{projects['example']} Password1, task_args: foo !\n" ) - assert result.stdout == f"hello Tom\nhello Jerry\n" assert result.stderr == "" diff --git a/tests/unit/test_parse_env_file.py b/tests/unit/test_parse_env_file.py new file mode 100644 index 000000000..65bea8e87 --- /dev/null +++ b/tests/unit/test_parse_env_file.py @@ -0,0 +1,133 @@ +from poethepoet.envfile import parse_env_file +import pytest + +valid_examples = [ + ( + """ + # empty + """, + {}, + ), + ( + """ + # single word values + WORD=something + WORD_WITH_HASH=some#thing + NUMBER=0 + EMOJI=😃😃 + DOUBLE_QUOTED_WORD="something" + SINGLE_QUOTED_WORD='something' + """, + { + "WORD": "something", + "WORD_WITH_HASH": "some#thing", + "NUMBER": "0", + "EMOJI": "😃😃", + "DOUBLE_QUOTED_WORD": "something", + "SINGLE_QUOTED_WORD": "something", + }, + ), + ( + """ + # multiword values + WORD=some\\ thing # and trailing comments + DOUBLE_QUOTED_WORD="some thing" + SINGLE_QUOTED_WORD='some thing' + """, + { + "WORD": r"some thing", + "DOUBLE_QUOTED_WORD": "some thing", + "SINGLE_QUOTED_WORD": "some thing", + }, + ), + ( + """ + # values with line breaks + WORD=some\\ +thing + DOUBLE_QUOTED_WORD="some + thing" + SINGLE_QUOTED_WORD='some + thing' + """, + { + "WORD": "some\nthing", + "DOUBLE_QUOTED_WORD": "some\n thing", + "SINGLE_QUOTED_WORD": "some\n thing", + }, + ), + ( + """ + # without linebreak between vars + FOO=BAR BAR=FOO + """, + {"FOO": "BAR", "BAR": "FOO"}, + ), + ( + """ + # with semicolons + ; FOO=BAR;BAR=FOO ; + ; + BAZ="2;'2"#; + \tQUX=3\t; + """, + {"FOO": "BAR", "BAR": "FOO", "BAZ": "2;'2#", "QUX": "3"}, + ), + ( + r""" + # with extra backslashes + FOO=a\\\ b + BAR='a\\\ b' + BAZ="a\\\ b" + """, + {"FOO": r"a\ b", "BAR": r"a\\\ b", "BAZ": r"a\ b"}, + ), + ( # a value with many parts and some empty vars + r"""FOO=a\\\ b'a\\\ b'"a\\\ b"#"#"'\'' ;'#;\t + BAR= + BAZ= # still empty + QUX=""", + {"FOO": r"a\ ba\\\ ba\ b##\ ;#", "BAR": "", "BAZ": "", "QUX": ""}, + ), + # export keyword is allowed + ( + """export answer=42 + export \t question=undefined + export\tdinner=chicken + """, + {"answer": "42", "question": "undefined", "dinner": "chicken"}, + ), +] + + +invalid_examples = [ + "foo = bar", + "foo =bar", + "foo= bar", + "foo\t=\tbar", + "foo\t=bar", + "foo=\tbar", + "foo= 'bar", + 'foo= "bar"', + "foo", + "foo;", + "8oo=bar", + "foo@=bar", + '"foo@"=bar', + "'foo@'=bar", + r"foo\=bar", + r"foo\==bar", + r"export;foo=bar", + r"export\nfoo=bar", +] + + +@pytest.mark.parametrize("example", valid_examples) +def test_parse_valid_env_files(example): + assert parse_env_file(example[0]) == example[1] + + +@pytest.mark.parametrize("example", invalid_examples) +def test_parse_invalid_env_files(example): + with pytest.raises(ValueError): + parse_env_file(example)