Skip to content

Commit

Permalink
Upgraade ruff and fix resulting issues
Browse files Browse the repository at this point in the history
Make options annotation parsing work with common ForwardRefs
nat-n committed Nov 24, 2024
1 parent 0d5025e commit 8179477
Showing 47 changed files with 283 additions and 393 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
name: CI

on: [push, pull_request]
on:
pull_request:
branches:
- main
- development
push:
branches:
- main
- development

jobs:

12 changes: 6 additions & 6 deletions docs/global_options.rst
Original file line number Diff line number Diff line change
@@ -4,25 +4,25 @@ Global options
The following options can be set for all tasks in a project directly under ``[tool.poe]``. These are called global options, in contrast with :doc:`task options<tasks/options>`.


**env** : ``Dict[str, str]`` :ref:`📖<Global environment variables>`
**env** : ``dict[str, str]`` :ref:`📖<Global environment variables>`
Define environment variables to be exposed to all tasks. These can be :ref:`extended on the task level<Setting task specific environment variables>`.

**envfile** : ``str`` | ``List[str]`` :ref:`📖<Global environment variables>`
**envfile** : ``str`` | ``list[str]`` :ref:`📖<Global environment variables>`
Link to one or more files defining environment variables to be exposed to all tasks.

**executor** : ``Dict[str, str]`` :ref:`📖<Change the executor type>`
**executor** : ``dict[str, str]`` :ref:`📖<Change the executor type>`
Override the default behavior for selecting an executor for tasks in this project.

**include** : ``str`` | ``List[str]`` | ``Dict[str, str]`` :doc:`📖<../guides/include_guide>`
**include** : ``str`` | ``list[str]`` | ``dict[str, str]`` :doc:`📖<../guides/include_guide>`
Specify one or more other toml or json files to load tasks from.

**shell_interpreter** : ``str`` | ``List[str]`` :ref:`📖<Change the default shell interpreter>`
**shell_interpreter** : ``str`` | ``list[str]`` :ref:`📖<Change the default shell interpreter>`
Change the default interpreter to use for executing :doc:`shell tasks<../tasks/task_types/shell>`.

**poetry_command** : ``str`` :ref:`📖<Configuring the plugin>`
Change the name of the task poe registers with poetry when used as a plugin.

**poetry_hooks** : ``Dict[str, str]`` :ref:`📖<Hooking into poetry commands>`
**poetry_hooks** : ``dict[str, str]`` :ref:`📖<Hooking into poetry commands>`
Register tasks to run automatically before or after other poetry CLI commands.

**verbosity** : ``int``
2 changes: 1 addition & 1 deletion docs/guides/args_guide.rst
Original file line number Diff line number Diff line change
@@ -143,7 +143,7 @@ Named arguments support the following configuration options:
- **name** : ``str``
The name of the task. Only applicable when *args* is an array.

- **options** : ``List[str]``
- **options** : ``list[str]``
A list of options to accept for this argument, similar to `argsparse name or flags <https://docs.python.org/3/library/argparse.html#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``
10 changes: 5 additions & 5 deletions docs/tasks/options.rst
Original file line number Diff line number Diff line change
@@ -9,23 +9,23 @@ The following options can be configured on your tasks and are not specific to an
**help** : ``str`` | ``int`` :doc:`📖<../guides/help_guide>`
Help text to be displayed next to the task name in the documentation when poe is run without specifying a task.

**args** : ``Dict[str, dict]`` | ``List[Union[str, dict]]`` :doc:`📖<../guides/args_guide>`
**args** : ``dict[str, dict]`` | ``list[Union[str, dict]]`` :doc:`📖<../guides/args_guide>`
Define CLI options, positional arguments, or flags that this task should accept.

**env** : ``Dict[str, str]`` :ref:`📖<Setting task specific environment variables>`
**env** : ``dict[str, str]`` :ref:`📖<Setting task specific environment variables>`
A map of environment variables to be set for this task.

**envfile** : ``str`` | ``List[str]`` :ref:`📖<Loading environment variables from an env file>`
**envfile** : ``str`` | ``list[str]`` :ref:`📖<Loading environment variables from an env file>`
Provide one or more env files to be loaded before running this task.

**cwd** : ``str`` :ref:`📖<Running a task with a specific working directory>`
Specify the current working directory that this task should run with. The given path is resolved relative to the parent directory of the ``pyproject.toml``, or it may be absolute.
Resolves environment variables in the format ``${VAR_NAME}``.

**deps** : ``List[str]`` :doc:`📖<../guides/composition_guide>`
**deps** : ``list[str]`` :doc:`📖<../guides/composition_guide>`
A list of task invocations that will be executed before this one.

**uses** : ``Dict[str, str]`` :doc:`📖<../guides/composition_guide>`
**uses** : ``dict[str, str]`` :doc:`📖<../guides/composition_guide>`
Allows this task to use the output of other tasks which are executed first.
The value is a map where the values are invocations of the other tasks, and the keys are environment variables by which the results of those tasks will be accessible in this task.

2 changes: 1 addition & 1 deletion docs/tasks/task_types/expr.rst
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ Available task options

The following options are also accepted:

**imports** : ``List[str]`` :ref:`📖<Referencing imported modules in an expression>`
**imports** : ``list[str]`` :ref:`📖<Referencing imported modules in an expression>`
A list of modules to import for use in the expression.

**assert** : ``bool`` :ref:`📖<Fail if the expression result is falsey>`
2 changes: 1 addition & 1 deletion docs/tasks/task_types/shell.rst
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ Available task options

The following options are also accepted:

**interpreter** : ``str`` | ``List[str]`` :ref:`📖<Using a different shell interpreter>`
**interpreter** : ``str`` | ``list[str]`` :ref:`📖<Using a different shell interpreter>`
Specify the shell interpreter that this task should execute with, or a list of interpreters in order of preference.


19 changes: 5 additions & 14 deletions poethepoet/app.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import os
import sys
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import (
IO,
TYPE_CHECKING,
Any,
Dict,
Mapping,
Optional,
Sequence,
Tuple,
Union,
)
from typing import IO, TYPE_CHECKING, Any, Optional, Union

from .exceptions import ExecutionError, PoeException

@@ -57,7 +48,7 @@ class PoeThePoet:
ui: "PoeUi"
config: "PoeConfig"

_task_specs: Optional[Dict[str, "PoeTask.TaskSpec"]] = None
_task_specs: Optional[dict[str, "PoeTask.TaskSpec"]] = None

def __init__(
self,
@@ -242,8 +233,8 @@ def print_help(
if isinstance(error, str):
error = PoeException(error)

tasks_help: Dict[
str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]
tasks_help: dict[
str, tuple[str, Sequence[tuple[tuple[str, ...], str, str]]]
] = {
task_name: (
(
7 changes: 5 additions & 2 deletions poethepoet/completion/zsh.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Any, Iterable, Set
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from collections.abc import Iterable


def get_zsh_completion_script(name: str = "") -> str:
@@ -50,7 +53,7 @@ def format_exclusions(excl_option_strings):
tuple(),
)
# collect all option strings that are exclusive with this one
excl_option_strings: Set[str] = {
excl_option_strings: set[str] = {
option_string
for excl_option in excl_options
for option_string in excl_option.option_strings
2 changes: 1 addition & 1 deletion poethepoet/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .config import PoeConfig
from .partition import KNOWN_SHELL_INTERPRETERS, ConfigPartition

__all__ = ["PoeConfig", "ConfigPartition", "KNOWN_SHELL_INTERPRETERS"]
__all__ = ["KNOWN_SHELL_INTERPRETERS", "ConfigPartition", "PoeConfig"]
23 changes: 7 additions & 16 deletions poethepoet/config/config.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
from collections.abc import Iterator, Mapping, Sequence
from os import environ
from pathlib import Path
from typing import (
Any,
Dict,
Iterator,
List,
Mapping,
Optional,
Sequence,
Tuple,
Union,
)
from typing import Any, Optional, Union

from ..exceptions import ConfigValidationError, PoeException
from .file import PoeConfigFile
@@ -21,12 +12,12 @@

class PoeConfig:
_project_config: ProjectConfig
_included_config: List[IncludedConfig]
_included_config: list[IncludedConfig]

"""
The filenames to look for when loading config
"""
_config_filenames: Tuple[str, ...] = (
_config_filenames: tuple[str, ...] = (
"pyproject.toml",
"poe_tasks.toml",
"poe_tasks.yaml",
@@ -61,7 +52,7 @@ def __init__(

def lookup_task(
self, name: str
) -> Union[Tuple[Mapping[str, Any], ConfigPartition], Tuple[None, None]]:
) -> Union[tuple[Mapping[str, Any], ConfigPartition], tuple[None, None]]:
task = self._project_config.get("tasks", {}).get(name, None)
if task is not None:
return task, self._project_config
@@ -95,7 +86,7 @@ def task_names(self) -> Iterator[str]:
yield from result

@property
def tasks(self) -> Dict[str, Any]:
def tasks(self) -> dict[str, Any]:
result = dict(self._project_config.get("tasks", {}))
for config in self._included_config:
for task_name, task_def in config.get("tasks", {}).items():
@@ -117,7 +108,7 @@ def default_array_item_task_type(self) -> str:
return self._project_config.options.default_array_item_task_type

@property
def shell_interpreter(self) -> Tuple[str, ...]:
def shell_interpreter(self) -> tuple[str, ...]:
raw_value = self._project_config.options.shell_interpreter
if isinstance(raw_value, list):
return tuple(raw_value)
9 changes: 2 additions & 7 deletions poethepoet/config/file.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
from collections.abc import Iterator, Mapping, Sequence
from pathlib import Path
from typing import (
Any,
Iterator,
Mapping,
Optional,
Sequence,
)
from typing import Any, Optional

from ..exceptions import PoeException

15 changes: 4 additions & 11 deletions poethepoet/config/partition.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
from collections.abc import Mapping, Sequence
from pathlib import Path
from types import MappingProxyType
from typing import (
Any,
Mapping,
Optional,
Sequence,
Type,
TypedDict,
Union,
)
from typing import Any, Optional, TypedDict, Union

from ..exceptions import ConfigValidationError
from ..options import NoValue, PoeOptions
@@ -42,7 +35,7 @@ class ConfigPartition:
project_dir: Path
_cwd: Optional[Path]

ConfigOptions: Type[PoeOptions]
ConfigOptions: type[PoeOptions]
is_primary: bool = False

def __init__(
@@ -115,7 +108,7 @@ def normalize(
raise ConfigValidationError("Expected ")

# Normalize include option:
# > Union[str, Sequence[str], Mapping[str, str]] => List[dict]
# > Union[str, Sequence[str], Mapping[str, str]] => list[dict]
if "include" in config:
includes: Any = []
include_option = config.get("include", None)
3 changes: 2 additions & 1 deletion poethepoet/config/primitives.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections.abc import Mapping
from types import MappingProxyType
from typing import Mapping, TypedDict
from typing import TypedDict

EmptyDict: Mapping = MappingProxyType({})

17 changes: 9 additions & 8 deletions poethepoet/context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from collections.abc import Mapping
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, Optional, Union

if TYPE_CHECKING:
from .config import PoeConfig
@@ -17,8 +18,8 @@ class RunContext:
poe_active: Optional[str]
project_dir: Path
multistage: bool = False
exec_cache: Dict[str, Any]
captured_stdout: Dict[Tuple[str, ...], str]
exec_cache: dict[str, Any]
captured_stdout: dict[tuple[str, ...], str]

def __init__(
self,
@@ -52,8 +53,8 @@ def __init__(
)

def _get_dep_values(
self, used_task_invocations: Mapping[str, Tuple[str, ...]]
) -> Dict[str, str]:
self, used_task_invocations: Mapping[str, tuple[str, ...]]
) -> dict[str, str]:
"""
Get env vars from upstream tasks declared via the uses option.
"""
@@ -62,7 +63,7 @@ def _get_dep_values(
for var_name, invocation in used_task_invocations.items()
}

def save_task_output(self, invocation: Tuple[str, ...], captured_stdout: bytes):
def save_task_output(self, invocation: tuple[str, ...], captured_stdout: bytes):
"""
Store the stdout data from a task so that it can be reused by other tasks
"""
@@ -76,7 +77,7 @@ def save_task_output(self, invocation: Tuple[str, ...], captured_stdout: bytes):
else:
raise

def get_task_output(self, invocation: Tuple[str, ...]):
def get_task_output(self, invocation: tuple[str, ...]):
"""
Get the stored stdout data from a task so that it can be reused by other tasks
@@ -87,7 +88,7 @@ def get_task_output(self, invocation: Tuple[str, ...]):

def get_executor(
self,
invocation: Tuple[str, ...],
invocation: tuple[str, ...],
env: "EnvVarsManager",
working_dir: Path,
*,
6 changes: 3 additions & 3 deletions poethepoet/env/cache.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from os import environ
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Optional, Union
from typing import TYPE_CHECKING, Optional, Union

from ..exceptions import ExecutionError

@@ -11,15 +11,15 @@


class EnvFileCache:
_cache: Dict[str, Dict[str, str]] = {}
_cache: dict[str, dict[str, str]] = {}
_ui: Optional["PoeUi"]
_project_dir: Path

def __init__(self, project_dir: Path, ui: Optional["PoeUi"]):
self._project_dir = project_dir
self._ui = ui

def get(self, envfile: Union[str, Path]) -> Dict[str, str]:
def get(self, envfile: Union[str, Path]) -> dict[str, str]:
"""
Parse, cache, and return the environment variables from the envfile at the
given path. The path is used as the cache key.
9 changes: 5 additions & 4 deletions poethepoet/env/manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from collections.abc import Mapping
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union
from typing import TYPE_CHECKING, Any, Optional, Union

from .template import apply_envvars_to_template

@@ -13,7 +14,7 @@
class EnvVarsManager(Mapping):
_config: "PoeConfig"
_ui: Optional["PoeUi"]
_vars: Dict[str, str]
_vars: dict[str, str]
envfiles: "EnvFileCache"

def __init__( # TODO: check if we still need all these args!
@@ -77,7 +78,7 @@ def set(self, key: str, value: str):

def apply_env_config(
self,
envfile: Optional[Union[str, List[str]]],
envfile: Optional[Union[str, list[str]]],
config_env: Optional[Mapping[str, Union[str, Mapping[str, str]]]],
config_dir: Path,
config_working_dir: Path,
@@ -123,7 +124,7 @@ def apply_env_config(

def update(self, env_vars: Mapping[str, Any]):
# ensure all values are strings
str_vars: Dict[str, str] = {}
str_vars: dict[str, str] = {}
for key, value in env_vars.items():
if isinstance(value, list):
str_vars[key] = " ".join(str(item) for item in value)
3 changes: 2 additions & 1 deletion poethepoet/env/parse.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from collections.abc import Iterable, Sequence
from enum import Enum
from typing import Iterable, Optional, Sequence
from typing import Optional


class ParseError(ValueError):
2 changes: 1 addition & 1 deletion poethepoet/env/template.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import Mapping
from collections.abc import Mapping

_SHELL_VAR_PATTERN = re.compile(
# Matches shell variable patterns, distinguishing escaped examples (to be ignored)
25 changes: 7 additions & 18 deletions poethepoet/executor/base.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import os
import shutil
import sys
from collections.abc import Mapping, MutableMapping, Sequence
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
Mapping,
MutableMapping,
Optional,
Sequence,
Tuple,
Type,
Union,
)
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union

from ..exceptions import ConfigValidationError, ExecutionError, PoeException

@@ -50,12 +39,12 @@ class PoeExecutor(metaclass=MetaPoeExecutor):

working_dir: Optional[Path]

__executor_types: ClassVar[Dict[str, Type["PoeExecutor"]]] = {}
__executor_types: ClassVar[dict[str, type["PoeExecutor"]]] = {}
__key__: ClassVar[Optional[str]] = None

def __init__(
self,
invocation: Tuple[str, ...],
invocation: tuple[str, ...],
context: "RunContext",
options: Mapping[str, str],
env: "EnvVarsManager",
@@ -88,7 +77,7 @@ def works_with_context(cls, context: "RunContext") -> bool:
@classmethod
def get(
cls,
invocation: Tuple[str, ...],
invocation: tuple[str, ...],
context: "RunContext",
executor_config: Mapping[str, str],
env: "EnvVarsManager",
@@ -268,7 +257,7 @@ def handle_sigint(signum, _frame):
return proc.returncode

@classmethod
def validate_config(cls, config: Dict[str, Any]):
def validate_config(cls, config: dict[str, Any]):
if "type" not in config:
raise ConfigValidationError(
"Missing required key 'type' from executor option",
@@ -294,7 +283,7 @@ def validate_config(cls, config: Dict[str, Any]):
cls.__executor_types[executor_type].validate_executor_config(config)

@classmethod
def validate_executor_config(cls, config: Dict[str, Any]):
def validate_executor_config(cls, config: dict[str, Any]):
"""To be overridden by subclasses if they accept options"""
extra_options = set(config.keys()) - {"type"}
if extra_options:
5 changes: 3 additions & 2 deletions poethepoet/executor/poetry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Sequence
from os import environ
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Optional, Sequence, Type
from typing import TYPE_CHECKING, Optional

from ..exceptions import ExecutionError
from .base import PoeExecutor
@@ -16,7 +17,7 @@ class PoetryExecutor(PoeExecutor):
"""

__key__ = "poetry"
__options__: Dict[str, Type] = {}
__options__: dict[str, type] = {}

@classmethod
def works_with_context(cls, context: "RunContext") -> bool:
4 changes: 1 addition & 3 deletions poethepoet/executor/simple.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Dict, Type

from .base import PoeExecutor


@@ -9,4 +7,4 @@ class SimpleExecutor(PoeExecutor):
"""

__key__ = "simple"
__options__: Dict[str, Type] = {}
__options__: dict[str, type] = {}
7 changes: 4 additions & 3 deletions poethepoet/executor/virtualenv.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Type
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Optional

from ..exceptions import ConfigValidationError, ExecutionError
from .base import PoeExecutor
@@ -14,7 +15,7 @@ class VirtualenvExecutor(PoeExecutor):
"""

__key__ = "virtualenv"
__options__: Dict[str, Type] = {"location": str}
__options__: dict[str, type] = {"location": str}

@classmethod
def works_with_context(cls, context: "RunContext") -> bool:
@@ -74,7 +75,7 @@ def _resolve_virtualenv(self) -> "Virtualenv":
)

@classmethod
def validate_executor_config(cls, config: Dict[str, Any]):
def validate_executor_config(cls, config: dict[str, Any]):
"""
Validate that location is a string if given and no other options are given.
"""
16 changes: 4 additions & 12 deletions poethepoet/helpers/command/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import re
from collections.abc import Iterable, Iterator, Mapping
from glob import escape
from typing import (
TYPE_CHECKING,
Iterable,
Iterator,
List,
Mapping,
Optional,
Tuple,
cast,
)
from typing import TYPE_CHECKING, Optional, cast

from .ast import Comment

@@ -33,7 +25,7 @@ def resolve_command_tokens(
lines: Iterable["Line"],
env: Mapping[str, str],
config: Optional["ParseConfig"] = None,
) -> Iterator[Tuple[str, bool]]:
) -> Iterator[tuple[str, bool]]:
"""
Generates a sequence of tokens, and indicates for each whether it includes glob
patterns that are not escaped or quoted. In case there are glob patterns in the
@@ -72,7 +64,7 @@ def finalize_token(token_parts):
continue

# For each token part indicate whether it is a glob
token_parts: List[Tuple[str, bool]] = []
token_parts: list[tuple[str, bool]] = []
for segment in word:
for element in segment:
if isinstance(element, ParamExpansion):
17 changes: 9 additions & 8 deletions poethepoet/helpers/command/ast.py
Original file line number Diff line number Diff line change
@@ -20,7 +20,8 @@
DOUBLE_QUOTED_CONTENT : /([^\$"]|\[\$"])+/
"""

from typing import Iterable, List, Literal, Optional, Tuple, Union, cast
from collections.abc import Iterable
from typing import Literal, Optional, Union, cast

from .ast_core import ContentNode, ParseConfig, ParseCursor, ParseError, SyntaxNode

@@ -32,7 +33,7 @@

class SingleQuotedText(ContentNode):
def _parse(self, chars: ParseCursor):
content: List[str] = []
content: list[str] = []
for char in chars:
if char == "'":
self._content = "".join(content)
@@ -44,7 +45,7 @@ def _parse(self, chars: ParseCursor):

class DoubleQuotedText(ContentNode):
def _parse(self, chars: ParseCursor):
content: List[str] = []
content: list[str] = []
for char in chars:
if char == "\\":
# backslash is only special if escape is necessary
@@ -63,7 +64,7 @@ def _parse(self, chars: ParseCursor):

class UnquotedText(ContentNode):
def _parse(self, chars: ParseCursor):
content: List[str] = []
content: list[str] = []
for char in chars:
if char == "\\":
# Backslash is always an escape when outside of quotes
@@ -171,7 +172,7 @@ def _parse(self, chars: ParseCursor):

if char == "[":
# Match pattern [groups]
group_chars: List[str] = []
group_chars: list[str] = []
chars.take()
for char in chars:
if char == "]":
@@ -205,7 +206,7 @@ def param_name(self) -> str:
def _parse(self, chars: ParseCursor):
assert chars.take() == "$"

param: List[str] = []
param: list[str] = []
if chars.peek() == "{":
chars.take()
for char in chars:
@@ -356,7 +357,7 @@ def __consume_unquoted(self, chars):

class Word(SyntaxNode[Segment]):
@property
def segments(self) -> Tuple[Segment, ...]:
def segments(self) -> tuple[Segment, ...]:
return tuple(self._children)

def _parse(self, chars: ParseCursor):
@@ -378,7 +379,7 @@ class Line(SyntaxNode[Union[Word, Comment]]):
_terminator: str

@property
def words(self) -> Tuple[Word, ...]:
def words(self) -> tuple[Word, ...]:
if self._children and isinstance(self._children[-1], Comment):
return tuple(cast(Iterable[Word], self._children[:-1]))
return tuple(cast(Iterable[Word], self._children))
30 changes: 10 additions & 20 deletions poethepoet/helpers/command/ast_core.py
Original file line number Diff line number Diff line change
@@ -4,18 +4,8 @@
"""

from abc import ABC, abstractmethod
from typing import (
IO,
Dict,
Generic,
Iterator,
List,
Optional,
Tuple,
Type,
TypeVar,
cast,
)
from collections.abc import Iterator
from typing import IO, Generic, Optional, TypeVar, cast


class ParseCursor:
@@ -30,7 +20,7 @@ class ParseCursor:
_line: int
_position: int
_source: Iterator[str]
_pushback_stack: List[str]
_pushback_stack: list[str]

def __init__(self, source: Iterator[str]):
self._source = source
@@ -88,18 +78,18 @@ def __bool__(self):


class ParseConfig:
substitute_nodes: Dict[Type["AstNode"], Type["AstNode"]]
substitute_nodes: dict[type["AstNode"], type["AstNode"]]
line_separators: str

def __init__(
self,
substitute_nodes: Optional[Dict[Type["AstNode"], Type["AstNode"]]] = None,
substitute_nodes: Optional[dict[type["AstNode"], type["AstNode"]]] = None,
line_separators="",
):
self.substitute_nodes = substitute_nodes or {}
self.line_separators = line_separators

def resolve_node_cls(self, klass: Type["AstNode"]) -> Type["AstNode"]:
def resolve_node_cls(self, klass: type["AstNode"]) -> type["AstNode"]:
return self.substitute_nodes.get(klass, klass)


@@ -130,17 +120,17 @@ def __len__(self):


class SyntaxNode(AstNode, Generic[T]):
_children: List[T]
_children: list[T]

def get_child_node_cls(self, node_type: Type[AstNode]) -> Type[T]:
def get_child_node_cls(self, node_type: type[AstNode]) -> type[T]:
"""
Apply Node class substitution for the given node AstNode if specified in
the ParseConfig.
"""
return cast(Type[T], self.config.resolve_node_cls(node_type))
return cast(type[T], self.config.resolve_node_cls(node_type))

@property
def children(self) -> Tuple["SyntaxNode", ...]:
def children(self) -> tuple["SyntaxNode", ...]:
return tuple(getattr(self, "_children", tuple()))

def pretty(self, indent: int = 0, increment: int = 4):
4 changes: 2 additions & 2 deletions poethepoet/helpers/git.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import shutil
from pathlib import Path
from subprocess import PIPE, Popen
from typing import Optional, Tuple
from typing import Optional


class GitRepo:
@@ -55,7 +55,7 @@ def _resolve_main_path(self) -> Optional[Path]:
return Path(captured_stdout.decode().strip().split("\n")[0])
return None

def _exec(self, *args: str) -> Tuple[Popen, bytes]:
def _exec(self, *args: str) -> tuple[Popen, bytes]:
proc = Popen(
["git", *args],
cwd=self._seed_path,
70 changes: 14 additions & 56 deletions poethepoet/helpers/python.py
Original file line number Diff line number Diff line change
@@ -5,19 +5,8 @@

import ast
import re
import sys
from typing import (
Any,
Collection,
Container,
Dict,
Iterator,
List,
NamedTuple,
Optional,
Tuple,
cast,
)
from collections.abc import Collection, Container, Iterator
from typing import Any, NamedTuple, Optional, cast

from ..exceptions import ExpressionParseError

@@ -75,7 +64,7 @@
}


Substitution = Tuple[Tuple[int, int], str]
Substitution = tuple[tuple[int, int], str]


class FunctionCall(NamedTuple):
@@ -85,8 +74,8 @@ class FunctionCall(NamedTuple):

expression: str
function_ref: str
referenced_args: Tuple[str, ...] = tuple()
referenced_globals: Tuple[str, ...] = tuple()
referenced_args: tuple[str, ...] = tuple()
referenced_globals: tuple[str, ...] = tuple()

@classmethod
def parse(
@@ -100,9 +89,9 @@ def parse(
root_node = cast(ast.Call, parse_and_validate(source, True, "script"))
name_nodes = _validate_nodes_and_get_names(root_node, source)

substitutions: List[Substitution] = []
referenced_args: List[str] = []
referenced_globals: List[str] = []
substitutions: list[Substitution] = []
referenced_args: list[str] = []
referenced_globals: list[str] = []
for node in name_nodes:
if node.id in arguments:
substitutions.append(
@@ -114,7 +103,7 @@ def parse(
else:
raise ExpressionParseError(
"Invalid variable reference in script: "
+ _get_name_source_segment(source, node)
f"{ast.get_source_segment(source, node)}"
)

# Prefix references to arguments with args_prefix
@@ -151,7 +140,7 @@ def resolve_expression(
root_node = parse_and_validate(source, False, "expr")
name_nodes = _validate_nodes_and_get_names(root_node, source)

substitutions: List[Substitution] = []
substitutions: list[Substitution] = []
for node in name_nodes:
if node.id in arguments:
substitutions.append(
@@ -160,7 +149,7 @@ def resolve_expression(
elif node.id not in _ALLOWED_BUILTINS and node.id not in allowed_vars:
raise ExpressionParseError(
"Invalid variable reference in expr: "
+ _get_name_source_segment(source, node)
f"{ast.get_source_segment(source, node)}"
)

# Prefix references to arguments with args_prefix
@@ -204,7 +193,7 @@ def parse_and_validate(
return root_node


def format_class(attrs: Optional[Dict[str, Any]], classname: str = "__args") -> str:
def format_class(attrs: 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. Output is a one-liner.
@@ -313,13 +302,13 @@ def _validate_nodes_and_get_names(
)


def _apply_substitutions(content: str, subs: List[Substitution]) -> str:
def _apply_substitutions(content: str, subs: list[Substitution]) -> str:
"""
Returns a copy of content with all of the substitutions applied.
Uses a single pass for efficiency.
"""
cursor = 0
segments: List[str] = []
segments: list[str] = []

for (start, end), replacement in sorted(subs, key=lambda x: x[0][0]):
in_between = content[cursor:start]
@@ -357,37 +346,6 @@ def _get_name_node_abs_range(source: str, node: ast.Name):
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 >= (3, 8):
return ast.get_source_segment(source, node)

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
# type: ignore[union-attr]
partial_result = re.match(IDENTIFIER_PATTERN, partial_result).group()

# 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


def _clean_linebreaks(expression: str):
"""
Strip out any new lines because they can be problematic on windows
9 changes: 7 additions & 2 deletions poethepoet/options/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

from keyword import iskeyword
from typing import Any, Mapping, Sequence, get_type_hints
from typing import TYPE_CHECKING, Any, get_type_hints

if TYPE_CHECKING:
from collections.abc import Mapping, Sequence

from ..exceptions import ConfigValidationError
from .annotations import TypeAnnotation
@@ -186,7 +189,9 @@ def get_fields(cls) -> dict[str, TypeAnnotation]:
annotations = {}
for base_cls in cls.__bases__:
annotations.update(get_type_hints(base_cls))
annotations.update(get_type_hints(cls))
annotations.update(
get_type_hints(cls, globalns=TypeAnnotation.get_type_hint_globals())
)

cls.__annotations = {
key: TypeAnnotation.parse(type_)
32 changes: 23 additions & 9 deletions poethepoet/options/annotations.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from __future__ import annotations

import collections
import sys
import typing
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
from typing import (
Any,
Iterator,
Literal,
Mapping,
MutableMapping,
Sequence,
Optional,
Union,
get_args,
get_origin,
@@ -22,6 +20,22 @@ class TypeAnnotation:
enforcing pythonic type annotations for PoeOptions.
"""

@classmethod
def get_type_hint_globals(cls):
return {
"Any": Any,
"Optional": Optional,
"Mapping": Mapping,
"MutableMapping": MutableMapping,
"typing.Mapping": typing.Mapping,
"typing.MutableMapping": typing.MutableMapping,
"Sequence": Sequence,
"typing.Sequence": typing.Sequence,
"Literal": Literal,
"Union": Union,
"TypeAnnotation": cls,
}

@staticmethod
def parse(annotation: Any):
origin = get_origin(annotation)
@@ -33,16 +47,16 @@ def parse(annotation: Any):
dict,
Mapping,
MutableMapping,
collections.abc.Mapping,
collections.abc.MutableMapping,
typing.Mapping,
typing.MutableMapping,
):
return DictType(annotation)

elif annotation is list or origin in (
list,
tuple,
Sequence,
collections.abc.Sequence,
typing.Sequence,
):
return ListType(annotation)

@@ -89,7 +103,7 @@ class DictType(TypeAnnotation):
def __init__(self, annotation: Any):
super().__init__(annotation)
if args := get_args(annotation):
assert args[0] == str
assert args[0] is str
self._value_type = TypeAnnotation.parse(get_args(annotation)[1])
else:
self._value_type = AnyType()
10 changes: 5 additions & 5 deletions poethepoet/plugin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List
from typing import TYPE_CHECKING, Any

from cleo.commands.command import Command
from cleo.events.console_command_event import ConsoleCommandEvent
@@ -183,7 +183,7 @@ def _register_command(
)

def _register_command_event_handler(
self, application: Application, hooks: Dict[str, str]
self, application: Application, hooks: dict[str, str]
):
if not hooks:
return
@@ -210,7 +210,7 @@ def _register_command_event_handler(
)

def _get_command_event_handler(
self, hooks: Dict[str, str], application: Application
self, hooks: dict[str, str], application: Application
):
def command_event_handler(
event: ConsoleCommandEvent,
@@ -235,7 +235,7 @@ def command_event_handler(

return command_event_handler

def _monkey_patch_cleo(self, prefix: str, task_names: List[str]):
def _monkey_patch_cleo(self, prefix: str, task_names: list[str]):
"""
Cleo is quite opinionated about CLI structure and loose about how options are
used, and so doesn't currently support individual commands having their own way
@@ -279,7 +279,7 @@ def _run(self, io):
cleo.application.Application._run = _run


def _index_of_first_non_option(tokens: List[str]):
def _index_of_first_non_option(tokens: list[str]):
"""
Find the index of the first token that doesn't start with `-`
Returns len(tokens) if none is found.
3 changes: 2 additions & 1 deletion poethepoet/task/args.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Literal, Mapping, Optional, Sequence, Union
from typing import TYPE_CHECKING, Any, Literal, Optional, Union

if TYPE_CHECKING:
from argparse import ArgumentParser
from collections.abc import Mapping, Sequence

from ..env.manager import EnvVarsManager

51 changes: 19 additions & 32 deletions poethepoet/task/base.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import re
import sys
from collections.abc import Iterator, Mapping, Sequence
from os import environ
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
Iterator,
List,
Mapping,
NamedTuple,
Optional,
Sequence,
Tuple,
Type,
Union,
)
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, Optional, Union

from ..config.primitives import EmptyDict, EnvDefault
from ..exceptions import ConfigValidationError, PoeException
@@ -59,7 +46,7 @@ def __init__(cls, *args):


class TaskSpecFactory:
__cache: Dict[str, "PoeTask.TaskSpec"]
__cache: dict[str, "PoeTask.TaskSpec"]
config: "PoeConfig"

def __init__(self, config: "PoeConfig"):
@@ -167,7 +154,7 @@ def from_task(cls, parent_task: "PoeTask"):

class PoeTask(metaclass=MetaPoeTask):
__key__: ClassVar[str]
__content_type__: ClassVar[Type] = str
__content_type__: ClassVar[type] = str

class TaskOptions(PoeOptions):
args: Optional[Union[dict, list]] = None
@@ -190,7 +177,7 @@ class TaskSpec:
name: str
content: TaskContent
options: "PoeTask.TaskOptions"
task_type: Type["PoeTask"]
task_type: type["PoeTask"]
source: "ConfigPartition"
parent: Optional["PoeTask.TaskSpec"] = None

@@ -199,7 +186,7 @@ class TaskSpec:
def __init__(
self,
name: str,
task_def: Dict[str, Any],
task_def: dict[str, Any],
factory: TaskSpecFactory,
source: "ConfigPartition",
parent: Optional["PoeTask.TaskSpec"] = None,
@@ -210,7 +197,7 @@ def __init__(
self.source = source
self.parent = parent

def _parse_options(self, task_def: Dict[str, Any]):
def _parse_options(self, task_def: dict[str, Any]):
try:
return next(
self.task_type.TaskOptions.parse(
@@ -259,7 +246,7 @@ def args(self) -> Optional["PoeTaskArgs"]:

def create_task(
self,
invocation: Tuple[str, ...],
invocation: tuple[str, ...],
ctx: TaskContext,
capture_stdout: Union[str, bool] = False,
) -> "PoeTask":
@@ -349,17 +336,17 @@ def _task_validations(self, config: "PoeConfig", task_specs: TaskSpecFactory):

spec: TaskSpec
ctx: TaskContext
_parsed_args: Optional[Tuple[Dict[str, str], Tuple[str, ...]]] = None
_parsed_args: Optional[tuple[dict[str, str], tuple[str, ...]]] = None

__task_types: ClassVar[Dict[str, Type["PoeTask"]]] = {}
__task_types: ClassVar[dict[str, type["PoeTask"]]] = {}
__upstream_invocations: Optional[
Dict[str, Union[List[Tuple[str, ...]], Dict[str, Tuple[str, ...]]]]
dict[str, Union[list[tuple[str, ...]], dict[str, tuple[str, ...]]]]
] = None

def __init__(
self,
spec: TaskSpec,
invocation: Tuple[str, ...],
invocation: tuple[str, ...],
ctx: TaskContext,
capture_stdout: Union[str, bool] = False,
):
@@ -374,7 +361,7 @@ def name(self):
return self.spec.name

@classmethod
def lookup_task_spec_cls(cls, task_key: str) -> Type[TaskSpec]:
def lookup_task_spec_cls(cls, task_key: str) -> type[TaskSpec]:
return cls.__task_types[task_key].TaskSpec

@classmethod
@@ -406,15 +393,15 @@ def resolve_task_type(

def _parse_named_args(
self, extra_args: Sequence[str], env: "EnvVarsManager"
) -> Optional[Dict[str, str]]:
) -> Optional[dict[str, str]]:
if task_args := self.spec.args:
return task_args.parse(extra_args, env, self.ctx.ui.program_name)

return None

def get_parsed_arguments(
self, env: "EnvVarsManager"
) -> Tuple[Dict[str, str], Tuple[str, ...]]:
) -> tuple[dict[str, str], tuple[str, ...]]:
if self._parsed_args is None:
all_args = self.invocation[1:]

@@ -517,7 +504,7 @@ def get_working_dir(

def iter_upstream_tasks(
self, context: "RunContext"
) -> Iterator[Tuple[str, "PoeTask"]]:
) -> Iterator[tuple[str, "PoeTask"]]:
invocations = self._get_upstream_invocations(context)
for invocation in invocations["deps"]:
yield ("", self._instantiate_dep(invocation, capture_stdout=False))
@@ -553,7 +540,7 @@ def _get_upstream_invocations(self, context: "RunContext"):
return self.__upstream_invocations

def _instantiate_dep(
self, invocation: Tuple[str, ...], capture_stdout: bool
self, invocation: tuple[str, ...], capture_stdout: bool
) -> "PoeTask":
return self.ctx.specs.get(invocation[0]).create_task(
invocation=invocation,
@@ -573,7 +560,7 @@ def has_deps(self) -> bool:

@classmethod
def is_task_type(
cls, task_def_key: str, content_type: Optional[Type] = None
cls, task_def_key: str, content_type: Optional[type] = None
) -> bool:
"""
Checks whether the given key identifies a known task type.
@@ -586,7 +573,7 @@ def is_task_type(
)

@classmethod
def get_task_types(cls, content_type: Optional[Type] = None) -> Tuple[str, ...]:
def get_task_types(cls, content_type: Optional[type] = None) -> tuple[str, ...]:
if content_type:
return tuple(
task_type
19 changes: 5 additions & 14 deletions poethepoet/task/expr.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import re
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Mapping,
Optional,
Sequence,
Tuple,
Union,
)
from collections.abc import Iterable, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Optional, Union

from ..exceptions import ConfigValidationError, ExpressionParseError
from .base import PoeTask
@@ -102,10 +93,10 @@ def _handle_run(

def parse_content(
self,
args: Optional[Dict[str, Any]],
args: Optional[dict[str, Any]],
env: "EnvVarsManager",
imports=Iterable[str],
) -> Tuple[str, Dict[str, str]]:
) -> tuple[str, dict[str, str]]:
"""
Returns the expression to evaluate and the subset of env vars that it references
@@ -145,7 +136,7 @@ def _substitute_env_vars(cls, content: str, env: Mapping[str, str]):
# Spy on access to the env, so that instead of replacing template ${keys} with
# the corresponding value, replace them with a python name and keep track of
# referenced env vars.
accessed_vars: Dict[str, str] = {}
accessed_vars: dict[str, str] = {}

def getitem_spy(obj: SpyDict, key: str, value: str):
accessed_vars[key] = value
24 changes: 12 additions & 12 deletions poethepoet/task/graph.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
from typing import TYPE_CHECKING

from ..exceptions import CyclicDependencyError

@@ -9,16 +9,16 @@

class TaskExecutionNode:
task: "PoeTask"
direct_dependants: List["TaskExecutionNode"]
direct_dependencies: Set[Tuple[str, ...]]
path_dependants: Tuple[str, ...]
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, ...],
direct_dependants: list["TaskExecutionNode"],
path_dependants: tuple[str, ...],
capture_stdout: bool = False,
):
self.task = task
@@ -31,7 +31,7 @@ def is_source(self):
return not self.task.has_deps()

@property
def identifier(self) -> Tuple[str, ...]:
def identifier(self) -> tuple[str, ...]:
return self.task.invocation


@@ -47,9 +47,9 @@ class TaskExecutionGraph:

_context: "RunContext"
sink: TaskExecutionNode
sources: List[TaskExecutionNode]
captured_tasks: Dict[Tuple[str, ...], TaskExecutionNode]
uncaptured_tasks: Dict[Tuple[str, ...], TaskExecutionNode]
sources: list[TaskExecutionNode]
captured_tasks: dict[tuple[str, ...], TaskExecutionNode]
uncaptured_tasks: dict[tuple[str, ...], TaskExecutionNode]

def __init__(
self,
@@ -65,15 +65,15 @@ def __init__(
# Build graph
self._resolve_node_deps(self.sink)

def get_execution_plan(self) -> List[List["PoeTask"]]:
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]
stages: list[list[TaskExecutionNode]] = [self.sources]
visited = {source.identifier for source in self.sources}

while True:
6 changes: 3 additions & 3 deletions poethepoet/task/script.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import shlex
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
from typing import TYPE_CHECKING, Any, Optional

from ..exceptions import ConfigValidationError, ExpressionParseError
from .base import PoeTask
@@ -108,8 +108,8 @@ def _handle_run(
).execute(cmd, use_exec=self.spec.options.get("use_exec", False))

def parse_content(
self, args: Optional[Dict[str, Any]]
) -> Tuple[str, "FunctionCall"]:
self, args: Optional[dict[str, Any]]
) -> tuple[str, "FunctionCall"]:
"""
Returns the module to load, and the function call to execute.
25 changes: 7 additions & 18 deletions poethepoet/task/sequence.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
List,
Literal,
Optional,
Sequence,
Tuple,
Type,
Union,
)
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Union

from ..exceptions import ConfigValidationError, ExecutionError, PoeException
from .base import PoeTask, TaskContext
@@ -27,10 +16,10 @@ class SequenceTask(PoeTask):
A task consisting of a sequence of other tasks
"""

content: List[Union[str, Dict[str, Any]]]
content: list[Union[str, dict[str, Any]]]

__key__ = "sequence"
__content_type__: ClassVar[Type] = list
__content_type__: ClassVar[type] = list

class TaskOptions(PoeTask.TaskOptions):
ignore_fail: Literal[True, False, "return_zero", "return_non_zero"] = False
@@ -57,7 +46,7 @@ class TaskSpec(PoeTask.TaskSpec):
def __init__(
self,
name: str,
task_def: Dict[str, Any],
task_def: dict[str, Any],
factory: "TaskSpecFactory",
source: "ConfigPartition",
parent: Optional["PoeTask.TaskSpec"] = None,
@@ -116,7 +105,7 @@ def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"):
def __init__(
self,
spec: TaskSpec,
invocation: Tuple[str, ...],
invocation: tuple[str, ...],
ctx: TaskContext,
capture_stdout: bool = False,
):
@@ -146,7 +135,7 @@ def _handle_run(
context.multistage = True

ignore_fail = self.spec.options.ignore_fail
non_zero_subtasks: List[str] = list()
non_zero_subtasks: list[str] = list()
for subtask in self.subtasks:
try:
task_result = subtask.run(context=context, parent_env=env)
16 changes: 5 additions & 11 deletions poethepoet/task/shell.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import re
from collections.abc import Sequence
from os import environ
from typing import (
TYPE_CHECKING,
List,
Optional,
Sequence,
Tuple,
Union,
)
from typing import TYPE_CHECKING, Optional, Union

from ..exceptions import ConfigValidationError, PoeException
from .base import PoeTask
@@ -96,15 +90,15 @@ def _handle_run(
interpreter_cmd, input=content.encode()
)

def _get_interpreter_config(self) -> Tuple[str, ...]:
result: Union[str, Tuple[str, ...]] = self.spec.options.get(
def _get_interpreter_config(self) -> tuple[str, ...]:
result: Union[str, tuple[str, ...]] = self.spec.options.get(
"interpreter", self.ctx.config.shell_interpreter
)
if isinstance(result, str):
return (result,)
return tuple(result)

def resolve_interpreter_cmd(self) -> Optional[List[str]]:
def resolve_interpreter_cmd(self) -> Optional[list[str]]:
"""
Return a formatted command for the first specified interpreter that can be
located.
29 changes: 10 additions & 19 deletions poethepoet/task/switch.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
Literal,
MutableMapping,
Optional,
Tuple,
Type,
Union,
)
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Union

from ..exceptions import ConfigValidationError, ExecutionError, PoeException
from .base import PoeTask, TaskContext

if TYPE_CHECKING:
from collections.abc import MutableMapping

from ..config import ConfigPartition, PoeConfig
from ..context import RunContext
from ..env.manager import EnvVarsManager
@@ -32,7 +23,7 @@ class SwitchTask(PoeTask):
"""

__key__ = "switch"
__content_type__: ClassVar[Type] = list
__content_type__: ClassVar[type] = list

class TaskOptions(PoeTask.TaskOptions):
control: Union[str, dict]
@@ -66,13 +57,13 @@ def normalize(

class TaskSpec(PoeTask.TaskSpec):
control_task_spec: PoeTask.TaskSpec
case_task_specs: Tuple[Tuple[Tuple[Any, ...], PoeTask.TaskSpec], ...]
case_task_specs: tuple[tuple[tuple[Any, ...], PoeTask.TaskSpec], ...]
options: "SwitchTask.TaskOptions"

def __init__(
self,
name: str,
task_def: Dict[str, Any],
task_def: dict[str, Any],
factory: "TaskSpecFactory",
source: "ConfigPartition",
parent: Optional["PoeTask.TaskSpec"] = None,
@@ -154,19 +145,19 @@ def _task_validations(self, config: "PoeConfig", task_specs: "TaskSpecFactory"):

spec: TaskSpec
control_task: PoeTask
switch_tasks: Dict[str, PoeTask]
switch_tasks: dict[str, PoeTask]

def __init__(
self,
spec: TaskSpec,
invocation: Tuple[str, ...],
invocation: tuple[str, ...],
ctx: TaskContext,
capture_stdout: bool = False,
):
super().__init__(spec, invocation, ctx, capture_stdout)

control_task_name = f"{spec.name}[__control__]"
control_invocation: Tuple[str, ...] = (control_task_name,)
control_invocation: tuple[str, ...] = (control_task_name,)
options = self.spec.options
if options.get("args"):
control_invocation = (*control_invocation, *invocation[1:])
@@ -179,7 +170,7 @@ def __init__(

self.switch_tasks = {}
for case_keys, case_spec in spec.case_task_specs:
task_invocation: Tuple[str, ...] = (f"{spec.name}[{','.join(case_keys)}]",)
task_invocation: tuple[str, ...] = (f"{spec.name}[{','.join(case_keys)}]",)
if options.get("args"):
task_invocation = (*task_invocation, *invocation[1:])

7 changes: 4 additions & 3 deletions poethepoet/ui.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import sys
from typing import IO, TYPE_CHECKING, List, Mapping, Optional, Sequence, Tuple, Union
from collections.abc import Mapping, Sequence
from typing import IO, TYPE_CHECKING, Optional, Union

from .__version__ import __version__
from .exceptions import ConfigValidationError, ExecutionError, PoeException
@@ -176,7 +177,7 @@ def set_default_verbosity(self, default_verbosity: int):
def print_help(
self,
tasks: Optional[
Mapping[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]]
Mapping[str, tuple[str, Sequence[tuple[tuple[str, ...], str, str]]]]
] = None,
info: Optional[str] = None,
error: Optional[PoeException] = None,
@@ -186,7 +187,7 @@ def print_help(
# Ignore verbosity mode if help flag is set
verbosity = 0 if self["help"] else self.verbosity

result: List[Union[str, Sequence[str]]] = []
result: list[Union[str, Sequence[str]]] = []
if verbosity >= 0:
result.append(
(
4 changes: 2 additions & 2 deletions poethepoet/virtualenv.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import os
import shutil
import sys
from collections.abc import Mapping
from pathlib import Path
from typing import Dict, Mapping


class Virtualenv:
@@ -80,7 +80,7 @@ def valid(self) -> bool:
)
)

def get_env_vars(self, base_env: Mapping[str, str]) -> Dict[str, str]:
def get_env_vars(self, base_env: Mapping[str, str]) -> dict[str, str]:
bin_dir = str(self.bin_dir())
# Revert path update from existing virtualenv if applicable
path_var = os.environ.get("_OLD_VIRTUAL_PATH", "") or os.environ.get("PATH", "")
41 changes: 21 additions & 20 deletions poetry.lock
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ mypy = "^1.1.1"
pytest = "^7.1.2"
pytest-cov = "^3.0.0"
rstcheck = { version = "^6.2.4", python = "<4" }
ruff = "^0.0.291"
ruff = "^0.8.0"
types-pyyaml = "^6.0.12.20240808"

virtualenv = "^20.14.1"
@@ -166,7 +166,7 @@ markers = [
]


[tool.ruff]
[tool.ruff.lint]
select = [
"E", # error
"F", # pyflakes
25 changes: 13 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@
import sys
import time
import venv
from collections.abc import Mapping
from contextlib import contextmanager
from io import StringIO
from pathlib import Path
from subprocess import PIPE, Popen
from typing import Any, Dict, List, Mapping, NamedTuple, Optional
from typing import Any, NamedTuple, Optional

import pytest
import virtualenv
@@ -77,7 +78,7 @@ def high_verbosity_project_path():
return PROJECT_ROOT.joinpath("tests", "fixtures", "high_verbosity")


@pytest.fixture()
@pytest.fixture
def temp_file(tmp_path):
# not using NamedTemporaryFile here because it doesn't work on windows
tmpfilepath = tmp_path / "tmp_test_file"
@@ -110,7 +111,7 @@ def assert_no_err(self):
)


@pytest.fixture()
@pytest.fixture
def run_poe_subproc(projects, temp_file, tmp_path, is_windows):
coverage_setup = (
"from coverage import Coverage;"
@@ -137,7 +138,7 @@ def run_poe_subproc(
cwd: Optional[str] = None,
config: Optional[Mapping[str, Any]] = None,
coverage: bool = not is_windows,
env: Optional[Dict[str, str]] = None,
env: Optional[dict[str, str]] = None,
project: Optional[str] = None,
) -> PoeRunResult:
if cwd is None:
@@ -194,7 +195,7 @@ def run_poe_subproc(
return run_poe_subproc


@pytest.fixture()
@pytest.fixture
def run_poe(capsys, projects):
def run_poe(
*run_args: str,
@@ -220,7 +221,7 @@ def run_poe(
return run_poe


@pytest.fixture()
@pytest.fixture
def run_poe_main(capsys, projects):
def run_poe_main(
*cli_args: str,
@@ -245,7 +246,7 @@ def run_poe_main(
def run_poetry(use_venv, poe_project_path):
venv_location = poe_project_path / "tests" / "temp" / "poetry_venv"

def run_poetry(args: List[str], cwd: str, env: Optional[Dict[str, str]] = None):
def run_poetry(args: list[str], cwd: str, env: Optional[dict[str, str]] = None):
venv = Virtualenv(venv_location)

cmd = (venv.resolve_executable("python"), "-m", "poetry", *args)
@@ -291,7 +292,7 @@ def esc_prefix(is_windows):

@pytest.fixture(scope="session")
def install_into_virtualenv():
def install_into_virtualenv(location: Path, contents: List[str]):
def install_into_virtualenv(location: Path, contents: list[str]):
venv = Virtualenv(location)
Popen(
(venv.resolve_executable("pip"), "install", *contents),
@@ -308,7 +309,7 @@ def use_venv(install_into_virtualenv):
@contextmanager
def use_venv(
location: Path,
contents: Optional[List[str]] = None,
contents: Optional[list[str]] = None,
require_empty: bool = False,
):
did_exist = location.is_dir()
@@ -340,7 +341,7 @@ def use_virtualenv(install_into_virtualenv):
@contextmanager
def use_virtualenv(
location: Path,
contents: Optional[List[str]] = None,
contents: Optional[list[str]] = None,
require_empty: bool = False,
):
did_exist = location.is_dir()
@@ -385,7 +386,7 @@ def try_rm_dir(location: Path):
def with_virtualenv_and_venv(use_venv, use_virtualenv):
def with_virtualenv_and_venv(
location: Path,
contents: Optional[List[str]] = None,
contents: Optional[list[str]] = None,
):
with use_venv(location, contents, require_empty=True):
yield
@@ -396,7 +397,7 @@ def with_virtualenv_and_venv(
return with_virtualenv_and_venv


@pytest.fixture()
@pytest.fixture
def temp_pyproject(tmp_path):
"""Return function which generates pyproject.toml with the given content"""

6 changes: 3 additions & 3 deletions tests/test_executors.py
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ def test_virtualenv_executor_fails_without_venv_dir(run_poe_subproc, projects):
assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
def test_virtualenv_executor_activates_venv(
run_poe_subproc, with_virtualenv_and_venv, projects
):
@@ -33,7 +33,7 @@ def test_virtualenv_executor_activates_venv(
assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
def test_virtualenv_executor_provides_access_to_venv_content(
run_poe_subproc, with_virtualenv_and_venv, projects
):
@@ -62,7 +62,7 @@ def test_virtualenv_executor_provides_access_to_venv_content(
assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
def test_detect_venv(
projects,
run_poe_subproc,
2 changes: 1 addition & 1 deletion tests/test_ignore_fail.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest


@pytest.fixture()
@pytest.fixture
def generate_pyproject(temp_pyproject):
def generator(lvl1_ignore_fail=False, lvl2_ignore_fail=False):
def fmt_ignore_fail(value):
26 changes: 13 additions & 13 deletions tests/test_poetry_plugin.py
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ def _setup_poetry_project_with_prefix(run_poetry, projects):
run_poetry(["install"], cwd=projects["poetry_plugin/with_prefix"].parent)


@pytest.mark.slow()
@pytest.mark.slow
def test_poetry_help(run_poetry, projects):
result = run_poetry([], cwd=projects["poetry_plugin"])
assert result.stdout.startswith("Poetry (version ")
@@ -33,7 +33,7 @@ def test_poetry_help(run_poetry, projects):
os.environ.get("GITHUB_ACTIONS", "false") == "true",
reason="Skipping test the doesn't seem to work in GitHub Actions lately",
)
@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project")
def test_task_with_cli_dependency(run_poetry, projects, is_windows):
result = run_poetry(
@@ -58,7 +58,7 @@ def test_task_with_cli_dependency(run_poetry, projects, is_windows):
os.environ.get("GITHUB_ACTIONS", "false") == "true",
reason="Skipping test the doesn't seem to work in GitHub Actions lately",
)
@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project")
def test_task_with_lib_dependency(run_poetry, projects):
result = run_poetry(["poe", "cow-cheese"], cwd=projects["poetry_plugin"])
@@ -68,7 +68,7 @@ def test_task_with_lib_dependency(run_poetry, projects):
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project")
def test_task_accepts_any_args(run_poetry, projects):
result = run_poetry(
@@ -81,7 +81,7 @@ def test_task_accepts_any_args(run_poetry, projects):
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project_empty_prefix")
def test_poetry_help_without_poe_command_prefix(run_poetry, projects):
result = run_poetry([], cwd=projects["poetry_plugin/empty_prefix"].parent)
@@ -91,7 +91,7 @@ def test_poetry_help_without_poe_command_prefix(run_poetry, projects):
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project_empty_prefix")
def test_running_tasks_without_poe_command_prefix(run_poetry, projects):
result = run_poetry(
@@ -104,7 +104,7 @@ def test_running_tasks_without_poe_command_prefix(run_poetry, projects):
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project_empty_prefix")
def test_poetry_command_from_included_file_with_empty_prefix(run_poetry, projects):
result = run_poetry(
@@ -115,7 +115,7 @@ def test_poetry_command_from_included_file_with_empty_prefix(run_poetry, project
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project_empty_prefix")
def test_poetry_help_with_poe_command_prefix(run_poetry, projects):
result = run_poetry([], cwd=projects["poetry_plugin/with_prefix"].parent)
@@ -125,7 +125,7 @@ def test_poetry_help_with_poe_command_prefix(run_poetry, projects):
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project_with_prefix")
def test_running_tasks_with_poe_command_prefix(run_poetry, projects):
result = run_poetry(
@@ -138,7 +138,7 @@ def test_running_tasks_with_poe_command_prefix(run_poetry, projects):
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project_with_prefix")
def test_running_tasks_with_poe_command_prefix_missing_args(run_poetry, projects):
result = run_poetry(
@@ -149,7 +149,7 @@ def test_running_tasks_with_poe_command_prefix_missing_args(run_poetry, projects
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project")
def test_running_poetry_command_with_hooks(run_poetry, projects):
result = run_poetry(["env", "info"], cwd=projects["poetry_plugin"])
@@ -158,7 +158,7 @@ def test_running_poetry_command_with_hooks(run_poetry, projects):
# assert result.stderr == ""


@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project")
def test_running_poetry_command_with_hooks_with_directory(run_poetry, projects):
result = run_poetry(
@@ -175,7 +175,7 @@ def test_running_poetry_command_with_hooks_with_directory(run_poetry, projects):
os.environ.get("GITHUB_ACTIONS", "false") == "true",
reason="Skipping test the doesn't seem to work in GitHub Actions lately",
)
@pytest.mark.slow()
@pytest.mark.slow
@pytest.mark.usefixtures("_setup_poetry_project")
def test_task_with_cli_dependency_with_directory(run_poetry, projects, is_windows):
result = run_poetry(
6 changes: 3 additions & 3 deletions tests/test_scripts.py
Original file line number Diff line number Diff line change
@@ -23,15 +23,15 @@
}


@pytest.fixture()
@pytest.fixture
def test_file_tree_dir(poe_project_path):
path = poe_project_path / "tests" / "temp" / "rm_test"
path.mkdir(parents=True, exist_ok=True)
yield path
rmtree(path)


@pytest.fixture()
@pytest.fixture
def test_file_tree_nodes(test_file_tree_dir):
def _iter_dir(work_dir: Path, items: dict):
for node_name, content in items.items():
@@ -45,7 +45,7 @@ def _iter_dir(work_dir: Path, items: dict):
return tuple(_iter_dir(test_file_tree_dir, _test_file_tree))


@pytest.fixture()
@pytest.fixture
def test_dir_structure(test_file_tree_dir, test_file_tree_nodes):
"""
Stage a temporary directory structure full of files so we can delete some of them

0 comments on commit 8179477

Please sign in to comment.