refactor: annotation with __future__.annotations

Adds `from __future__ import annotations` to the top of every module,
right below the module's docstring. Replaces any usages of t.List,
t.Dict, t.Set, t.Tuple, and t.Type with their built-in equivalents:
list, dict, set, tuple, and type. Ensures that make test still passes
under Python 3.7, 3.8 and 3.9.
This commit is contained in:
Carlos Muniz 2023-01-17 13:57:23 -05:00 committed by Régis Behmo
parent d629ca932c
commit ac1a875f42
23 changed files with 126 additions and 105 deletions

View File

@ -0,0 +1 @@
- [Improvement] Changes annotations from `typing` to use built-in generic types from `__future__.annotations` (by @Carlos-Muniz)

View File

@ -38,6 +38,12 @@ nitpick_ignore = [
("py:class", "tutor.hooks.filters.P"),
("py:class", "tutor.hooks.filters.T"),
("py:class", "tutor.hooks.actions.P"),
("py:class", "P"),
("py:class", "P.args"),
("py:class", "P.kwargs"),
("py:class", "T"),
("py:class", "t.Any"),
("py:class", "t.Optional"),
]
# -- Sphinx-Click configuration

View File

@ -1,4 +1,4 @@
import typing as t
from __future__ import annotations
import click.testing
@ -12,13 +12,13 @@ class TestCommandMixin:
"""
@staticmethod
def invoke(args: t.List[str]) -> click.testing.Result:
def invoke(args: list[str]) -> click.testing.Result:
with temporary_root() as root:
return TestCommandMixin.invoke_in_root(root, args)
@staticmethod
def invoke_in_root(
root: str, args: t.List[str], catch_exceptions: bool = True
root: str, args: list[str], catch_exceptions: bool = True
) -> click.testing.Result:
"""
Use this method for commands that all need to run in the same root:

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
import unittest
from io import StringIO
@ -62,9 +63,9 @@ class ComposeTests(unittest.TestCase):
# Mount volumes
compose.mount_tmp_volumes(mount_args, LocalContext(""))
compose_file: t.Dict[str, t.Any] = hooks.Filters.COMPOSE_LOCAL_TMP.apply({})
actual_services: t.Dict[str, t.Any] = compose_file["services"]
expected_services: t.Dict[str, t.Any] = {
compose_file: dict[str, t.Any] = hooks.Filters.COMPOSE_LOCAL_TMP.apply({})
actual_services: dict[str, t.Any] = compose_file["services"]
expected_services: dict[str, t.Any] = {
"cms": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]},
"cms-worker": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]},
"lms": {
@ -78,11 +79,9 @@ class ComposeTests(unittest.TestCase):
}
self.assertEqual(actual_services, expected_services)
compose_jobs_file: t.Dict[
str, t.Any
] = hooks.Filters.COMPOSE_LOCAL_JOBS_TMP.apply({})
actual_jobs_services: t.Dict[str, t.Any] = compose_jobs_file["services"]
expected_jobs_services: t.Dict[str, t.Any] = {
compose_jobs_file = hooks.Filters.COMPOSE_LOCAL_JOBS_TMP.apply({})
actual_jobs_services = compose_jobs_file["services"]
expected_jobs_services: dict[str, t.Any] = {
"cms-job": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]},
"lms-job": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]},
}

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
import unittest
@ -23,14 +24,14 @@ class PluginFiltersTests(unittest.TestCase):
def test_add_items(self) -> None:
@hooks.filters.add("tests:add-sheeps")
def filter1(sheeps: t.List[int]) -> t.List[int]:
def filter1(sheeps: list[int]) -> list[int]:
return sheeps + [0]
hooks.filters.add_item("tests:add-sheeps", 1)
hooks.filters.add_item("tests:add-sheeps", 2)
hooks.filters.add_items("tests:add-sheeps", [3, 4])
sheeps: t.List[int] = hooks.filters.apply("tests:add-sheeps", [])
sheeps: list[int] = hooks.filters.apply("tests:add-sheeps", [])
self.assertEqual([0, 1, 2, 3, 4], sheeps)
def test_filter_callbacks(self) -> None:

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
from unittest.mock import patch
@ -197,9 +198,7 @@ class PluginsTests(PluginsTestCase):
{"name": "myplugin", "config": {"set": {"KEY": "value"}}, "version": "0.1"}
)
plugins.load("myplugin")
overriden_items: t.List[
t.Tuple[str, t.Any]
] = hooks.Filters.CONFIG_OVERRIDES.apply([])
overriden_items = hooks.Filters.CONFIG_OVERRIDES.apply([])
versions = list(plugins.iter_info())
self.assertEqual("myplugin", plugin.name)
self.assertEqual([("myplugin", "0.1")], versions)

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import sys
import typing as t
@ -61,7 +62,7 @@ class TutorCli(click.MultiCommand):
hooks.Actions.PROJECT_ROOT_READY.do(ctx.params["root"])
cls.IS_ROOT_READY = True
def list_commands(self, ctx: click.Context) -> t.List[str]:
def list_commands(self, ctx: click.Context) -> list[str]:
"""
This is run in the following cases:
- shell autocompletion: tutor <tab>

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import os
import re
import typing as t
@ -16,15 +17,15 @@ from tutor.exceptions import TutorError
from tutor.tasks import BaseComposeTaskRunner
from tutor.types import Config
COMPOSE_FILTER_TYPE: TypeAlias = "hooks.filters.Filter[t.Dict[str, t.Any], []]"
COMPOSE_FILTER_TYPE: TypeAlias = "hooks.filters.Filter[dict[str, t.Any], []]"
class ComposeTaskRunner(BaseComposeTaskRunner):
def __init__(self, root: str, config: Config):
super().__init__(root, config)
self.project_name = ""
self.docker_compose_files: t.List[str] = []
self.docker_compose_job_files: t.List[str] = []
self.docker_compose_files: list[str] = []
self.docker_compose_job_files: list[str] = []
def docker_compose(self, *command: str) -> int:
"""
@ -55,7 +56,7 @@ class ComposeTaskRunner(BaseComposeTaskRunner):
Update the contents of the docker-compose.tmp.yml and
docker-compose.jobs.tmp.yml files, which are generated at runtime.
"""
compose_base: t.Dict[str, t.Any] = {
compose_base: dict[str, t.Any] = {
"version": "{{ DOCKER_COMPOSE_VERSION }}",
"services": {},
}
@ -134,11 +135,11 @@ class MountParam(click.ParamType):
value: str,
param: t.Optional["click.Parameter"],
ctx: t.Optional[click.Context],
) -> t.List["MountType"]:
) -> list["MountType"]:
mounts = self.convert_explicit_form(value) or self.convert_implicit_form(value)
return mounts
def convert_explicit_form(self, value: str) -> t.List["MountParam.MountType"]:
def convert_explicit_form(self, value: str) -> list["MountParam.MountType"]:
"""
Argument is of the form "containers:/host/path:/container/path".
"""
@ -146,8 +147,8 @@ class MountParam(click.ParamType):
if not match:
return []
mounts: t.List["MountParam.MountType"] = []
services: t.List[str] = [
mounts: list["MountParam.MountType"] = []
services: list[str] = [
service.strip() for service in match["services"].split(",")
]
host_path = os.path.abspath(os.path.expanduser(match["host_path"]))
@ -159,11 +160,11 @@ class MountParam(click.ParamType):
mounts.append((service, host_path, container_path))
return mounts
def convert_implicit_form(self, value: str) -> t.List["MountParam.MountType"]:
def convert_implicit_form(self, value: str) -> list["MountParam.MountType"]:
"""
Argument is of the form "/host/path"
"""
mounts: t.List["MountParam.MountType"] = []
mounts: list["MountParam.MountType"] = []
host_path = os.path.abspath(os.path.expanduser(value))
for service, container_path in hooks.Filters.COMPOSE_MOUNTS.iterate(
os.path.basename(host_path)
@ -175,7 +176,7 @@ class MountParam(click.ParamType):
def shell_complete(
self, ctx: click.Context, param: click.Parameter, incomplete: str
) -> t.List[CompletionItem]:
) -> list[CompletionItem]:
"""
Mount argument completion works only for the single path (implicit) form. The
reason is that colons break words in bash completion:
@ -197,7 +198,7 @@ mount_option = click.option(
def mount_tmp_volumes(
all_mounts: t.Tuple[t.List[MountParam.MountType], ...],
all_mounts: tuple[list[MountParam.MountType], ...],
context: BaseComposeContext,
) -> None:
for mounts in all_mounts:
@ -230,8 +231,8 @@ def mount_tmp_volume(
@compose_tmp_filter.add()
def _add_mounts_to_docker_compose_tmp(
docker_compose: t.Dict[str, t.Any],
) -> t.Dict[str, t.Any]:
docker_compose: dict[str, t.Any],
) -> dict[str, t.Any]:
services = docker_compose.setdefault("services", {})
services.setdefault(service, {"volumes": []})
services[service]["volumes"].append(f"{host_path}:{container_path}")
@ -251,8 +252,8 @@ def start(
context: BaseComposeContext,
skip_build: bool,
detach: bool,
mounts: t.Tuple[t.List[MountParam.MountType]],
services: t.List[str],
mounts: tuple[list[MountParam.MountType]],
services: list[str],
) -> None:
command = ["up", "--remove-orphans"]
if not skip_build:
@ -269,7 +270,7 @@ def start(
@click.command(help="Stop a running platform")
@click.argument("services", metavar="service", nargs=-1)
@click.pass_obj
def stop(context: BaseComposeContext, services: t.List[str]) -> None:
def stop(context: BaseComposeContext, services: list[str]) -> None:
config = tutor_config.load(context.root)
context.job_runner(config).docker_compose("stop", *services)
@ -281,7 +282,7 @@ def stop(context: BaseComposeContext, services: t.List[str]) -> None:
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
@click.argument("services", metavar="service", nargs=-1)
@click.pass_context
def reboot(context: click.Context, detach: bool, services: t.List[str]) -> None:
def reboot(context: click.Context, detach: bool, services: list[str]) -> None:
context.invoke(stop, services=services)
context.invoke(start, detach=detach, services=services)
@ -295,7 +296,7 @@ fully stop the platform, use the 'reboot' command.""",
)
@click.argument("services", metavar="service", nargs=-1)
@click.pass_obj
def restart(context: BaseComposeContext, services: t.List[str]) -> None:
def restart(context: BaseComposeContext, services: list[str]) -> None:
config = tutor_config.load(context.root)
command = ["restart"]
if "all" in services:
@ -315,9 +316,7 @@ def restart(context: BaseComposeContext, services: t.List[str]) -> None:
@jobs.do_group
@mount_option
@click.pass_obj
def do(
context: BaseComposeContext, mounts: t.Tuple[t.List[MountParam.MountType]]
) -> None:
def do(context: BaseComposeContext, mounts: tuple[list[MountParam.MountType]]) -> None:
"""
Run a custom job in the right container(s).
"""
@ -345,8 +344,8 @@ def do(
@click.pass_context
def run(
context: click.Context,
mounts: t.Tuple[t.List[MountParam.MountType]],
args: t.List[str],
mounts: tuple[list[MountParam.MountType]],
args: list[str],
) -> None:
extra_args = ["--rm"]
if not utils.is_a_tty():
@ -411,7 +410,7 @@ def copyfrom(
)
@click.argument("args", nargs=-1, required=True)
@click.pass_context
def execute(context: click.Context, args: t.List[str]) -> None:
def execute(context: click.Context, args: list[str]) -> None:
context.invoke(dc_command, command="exec", args=args)
@ -454,9 +453,9 @@ def status(context: click.Context) -> None:
@click.pass_obj
def dc_command(
context: BaseComposeContext,
mounts: t.Tuple[t.List[MountParam.MountType]],
mounts: tuple[list[MountParam.MountType]],
command: str,
args: t.List[str],
args: list[str],
) -> None:
mount_tmp_volumes(mounts, context)
config = tutor_config.load(context.root)
@ -465,8 +464,8 @@ def dc_command(
@hooks.Filters.COMPOSE_MOUNTS.add()
def _mount_edx_platform(
volumes: t.List[t.Tuple[str, str]], name: str
) -> t.List[t.Tuple[str, str]]:
volumes: list[tuple[str, str]], name: str
) -> list[tuple[str, str]]:
"""
When mounting edx-platform with `--mount=/path/to/edx-platform`, bind-mount the host
repo in the lms/cms containers.

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import json
import typing as t
@ -27,7 +28,7 @@ class ConfigKeyParamType(click.ParamType):
def shell_complete(
self, ctx: click.Context, param: click.Parameter, incomplete: str
) -> t.List[click.shell_completion.CompletionItem]:
) -> list[click.shell_completion.CompletionItem]:
return [
click.shell_completion.CompletionItem(key)
for key, _value in self._shell_complete_config_items(ctx, incomplete)
@ -36,7 +37,7 @@ class ConfigKeyParamType(click.ParamType):
@staticmethod
def _shell_complete_config_items(
ctx: click.Context, incomplete: str
) -> t.List[t.Tuple[str, ConfigValue]]:
) -> list[tuple[str, ConfigValue]]:
# Here we want to auto-complete the name of the config key. For that we need to
# figure out the list of enabled plugins, and for that we need the project root.
# The project root would ordinarily be stored in ctx.obj.root, but during
@ -58,7 +59,7 @@ class ConfigKeyValParamType(ConfigKeyParamType):
name = "configkeyval"
def convert(self, value: str, param: t.Any, ctx: t.Any) -> t.Tuple[str, t.Any]:
def convert(self, value: str, param: t.Any, ctx: t.Any) -> tuple[str, t.Any]:
result = serialize.parse_key_value(value)
if result is None:
self.fail(f"'{value}' is not of the form 'key=value'.", param, ctx)
@ -66,7 +67,7 @@ class ConfigKeyValParamType(ConfigKeyParamType):
def shell_complete(
self, ctx: click.Context, param: click.Parameter, incomplete: str
) -> t.List[click.shell_completion.CompletionItem]:
) -> list[click.shell_completion.CompletionItem]:
"""
Nice and friendly <KEY>=<VAL> auto-completion.
"""
@ -117,7 +118,7 @@ def save(
context: Context,
interactive: bool,
set_vars: Config,
unset_vars: t.List[str],
unset_vars: list[str],
env_only: bool,
) -> None:
config = tutor_config.load_minimal(context.root)

View File

@ -1,4 +1,4 @@
import typing as t
from __future__ import annotations
import click
@ -70,7 +70,7 @@ def launch(
context: click.Context,
non_interactive: bool,
pullimages: bool,
mounts: t.Tuple[t.List[compose.MountParam.MountType]],
mounts: tuple[list[compose.MountParam.MountType]],
) -> None:
compose.mount_tmp_volumes(mounts, context.obj)
try:

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
import click
@ -21,9 +22,9 @@ VENDOR_IMAGES = [
@hooks.Filters.IMAGES_BUILD.add()
def _add_core_images_to_build(
build_images: t.List[t.Tuple[str, t.Tuple[str, ...], str, t.Tuple[str, ...]]],
build_images: list[tuple[str, tuple[str, ...], str, tuple[str, ...]]],
config: Config,
) -> t.List[t.Tuple[str, t.Tuple[str, ...], str, t.Tuple[str, ...]]]:
) -> list[tuple[str, tuple[str, ...], str, tuple[str, ...]]]:
"""
Add base images to the list of Docker images to build on `tutor build all`.
"""
@ -35,8 +36,8 @@ def _add_core_images_to_build(
@hooks.Filters.IMAGES_PULL.add()
def _add_images_to_pull(
remote_images: t.List[t.Tuple[str, str]], config: Config
) -> t.List[t.Tuple[str, str]]:
remote_images: list[tuple[str, str]], config: Config
) -> list[tuple[str, str]]:
"""
Add base and vendor images to the list of Docker images to pull on `tutor pull all`.
"""
@ -50,8 +51,8 @@ def _add_images_to_pull(
@hooks.Filters.IMAGES_PUSH.add()
def _add_core_images_to_push(
remote_images: t.List[t.Tuple[str, str]], config: Config
) -> t.List[t.Tuple[str, str]]:
remote_images: list[tuple[str, str]], config: Config
) -> list[tuple[str, str]]:
"""
Add base images to the list of Docker images to push on `tutor push all`.
"""
@ -100,12 +101,12 @@ def images_command() -> None:
@click.pass_obj
def build(
context: Context,
image_names: t.List[str],
image_names: list[str],
no_cache: bool,
build_args: t.List[str],
add_hosts: t.List[str],
build_args: list[str],
add_hosts: list[str],
target: str,
docker_args: t.List[str],
docker_args: list[str],
) -> None:
config = tutor_config.load(context.root)
command_args = []
@ -132,7 +133,7 @@ def build(
@click.command(short_help="Pull images from the Docker registry")
@click.argument("image_names", metavar="image", nargs=-1)
@click.pass_obj
def pull(context: Context, image_names: t.List[str]) -> None:
def pull(context: Context, image_names: list[str]) -> None:
config = tutor_config.load_full(context.root)
for image in image_names:
for tag in find_remote_image_tags(config, hooks.Filters.IMAGES_PULL, image):
@ -142,7 +143,7 @@ def pull(context: Context, image_names: t.List[str]) -> None:
@click.command(short_help="Push images to the Docker registry")
@click.argument("image_names", metavar="image", nargs=-1)
@click.pass_obj
def push(context: Context, image_names: t.List[str]) -> None:
def push(context: Context, image_names: list[str]) -> None:
config = tutor_config.load_full(context.root)
for image in image_names:
for tag in find_remote_image_tags(config, hooks.Filters.IMAGES_PUSH, image):
@ -152,7 +153,7 @@ def push(context: Context, image_names: t.List[str]) -> None:
@click.command(short_help="Print tag associated to a Docker image")
@click.argument("image_names", metavar="image", nargs=-1)
@click.pass_obj
def printtag(context: Context, image_names: t.List[str]) -> None:
def printtag(context: Context, image_names: list[str]) -> None:
config = tutor_config.load_full(context.root)
for image in image_names:
for _name, _path, tag, _args in find_images_to_build(config, image):
@ -161,7 +162,7 @@ def printtag(context: Context, image_names: t.List[str]) -> None:
def find_images_to_build(
config: Config, image: str
) -> t.Iterator[t.Tuple[str, t.Tuple[str, ...], str, t.Tuple[str, ...]]]:
) -> t.Iterator[tuple[str, tuple[str, ...], str, tuple[str, ...]]]:
"""
Iterate over all images to build.
@ -182,7 +183,7 @@ def find_images_to_build(
def find_remote_image_tags(
config: Config,
filtre: "hooks.filters.Filter[t.List[t.Tuple[str, str]], [Config]]",
filtre: "hooks.filters.Filter[list[tuple[str, str]], [Config]]",
image: str,
) -> t.Iterator[str]:
"""

View File

@ -1,6 +1,7 @@
"""
Common jobs that must be added both to local, dev and k8s commands.
"""
from __future__ import annotations
import functools
import typing as t
@ -49,7 +50,7 @@ def _add_core_init_tasks() -> None:
@click.command("init", help="Initialise all applications")
@click.option("-l", "--limit", help="Limit initialisation to this service or plugin")
def initialise(limit: t.Optional[str]) -> t.Iterator[t.Tuple[str, str]]:
def initialise(limit: t.Optional[str]) -> t.Iterator[tuple[str, str]]:
fmt.echo_info("Initialising all services...")
filter_context = hooks.Contexts.APP(limit).name if limit else None
@ -99,7 +100,7 @@ def createuser(
password: str,
name: str,
email: str,
) -> t.Iterable[t.Tuple[str, str]]:
) -> t.Iterable[tuple[str, str]]:
"""
Create an Open edX user
@ -127,7 +128,7 @@ u.save()"
@click.command(help="Import the demo course")
def importdemocourse() -> t.Iterable[t.Tuple[str, str]]:
def importdemocourse() -> t.Iterable[tuple[str, str]]:
template = """
# Import demo course
git clone https://github.com/openedx/edx-demo-course --branch {{ OPENEDX_COMMON_VERSION }} --depth 1 ../edx-demo-course
@ -150,7 +151,7 @@ python ./manage.py cms import ../data ../edx-demo-course
),
)
@click.argument("theme_name")
def settheme(domains: t.List[str], theme_name: str) -> t.Iterable[t.Tuple[str, str]]:
def settheme(domains: list[str], theme_name: str) -> t.Iterable[tuple[str, str]]:
"""
Assign a theme to the LMS and the CMS.
@ -159,7 +160,7 @@ def settheme(domains: t.List[str], theme_name: str) -> t.Iterable[t.Tuple[str, s
yield ("lms", set_theme_template(theme_name, domains))
def set_theme_template(theme_name: str, domain_names: t.List[str]) -> str:
def set_theme_template(theme_name: str, domain_names: list[str]) -> str:
"""
For each domain, get or create a Site object and assign the selected theme.
"""
@ -231,7 +232,7 @@ P = ParamSpec("P")
def _patch_callback(
job_name: str, func: t.Callable[P, t.Iterable[t.Tuple[str, str]]]
job_name: str, func: t.Callable[P, t.Iterable[tuple[str, str]]]
) -> t.Callable[P, None]:
"""
Modify a subcommand callback function such that its results are processed by `do_callback`.
@ -247,7 +248,7 @@ def _patch_callback(
return new_callback
def do_callback(service_commands: t.Iterable[t.Tuple[str, str]]) -> None:
def do_callback(service_commands: t.Iterable[tuple[str, str]]) -> None:
"""
This function must be added as a callback to all `do` subcommands.

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
import click
@ -70,7 +71,7 @@ def local(context: click.Context) -> None:
@click.pass_context
def launch(
context: click.Context,
mounts: t.Tuple[t.List[compose.MountParam.MountType]],
mounts: tuple[list[compose.MountParam.MountType]],
non_interactive: bool,
pullimages: bool,
) -> None:

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import os
import typing as t
import urllib.request
@ -22,13 +23,13 @@ class PluginName(click.ParamType):
def shell_complete(
self, ctx: click.Context, param: click.Parameter, incomplete: str
) -> t.List[click.shell_completion.CompletionItem]:
) -> list[click.shell_completion.CompletionItem]:
return [
click.shell_completion.CompletionItem(name)
for name in self.get_names(incomplete)
]
def get_names(self, incomplete: str) -> t.List[str]:
def get_names(self, incomplete: str) -> list[str]:
candidates = []
if self.allow_all:
candidates.append("all")
@ -67,7 +68,7 @@ def list_command() -> None:
@click.command(help="Enable a plugin")
@click.argument("plugin_names", metavar="plugin", nargs=-1, type=PluginName())
@click.pass_obj
def enable(context: Context, plugin_names: t.List[str]) -> None:
def enable(context: Context, plugin_names: list[str]) -> None:
config = tutor_config.load_minimal(context.root)
for plugin in plugin_names:
plugins.load(plugin)
@ -87,10 +88,10 @@ def enable(context: Context, plugin_names: t.List[str]) -> None:
"plugin_names", metavar="plugin", nargs=-1, type=PluginName(allow_all=True)
)
@click.pass_obj
def disable(context: Context, plugin_names: t.List[str]) -> None:
def disable(context: Context, plugin_names: list[str]) -> None:
config = tutor_config.load_minimal(context.root)
disable_all = "all" in plugin_names
disabled: t.List[str] = []
disabled: list[str] = []
for plugin in tutor_config.get_enabled_plugins(config):
if disable_all or plugin in plugin_names:
fmt.echo_info(f"Disabling plugin {plugin}...")

View File

@ -1,5 +1,5 @@
from __future__ import annotations
import os
import typing as t
from tutor import env, exceptions, fmt, hooks, plugins, serialize, utils
from tutor.types import Config, ConfigValue, cast_config, get_typed
@ -108,7 +108,7 @@ def get_base() -> Config:
Entries in this configuration are unrendered.
"""
base = get_template("base.yml")
extra_config: t.List[t.Tuple[str, ConfigValue]] = []
extra_config: list[tuple[str, ConfigValue]] = []
extra_config = hooks.Filters.CONFIG_UNIQUE.apply(extra_config)
extra_config = hooks.Filters.CONFIG_OVERRIDES.apply(extra_config)
for name, value in extra_config:
@ -269,7 +269,7 @@ def enable_plugins(config: Config) -> None:
plugins.load_all(get_enabled_plugins(config))
def get_enabled_plugins(config: Config) -> t.List[str]:
def get_enabled_plugins(config: Config) -> list[str]:
"""
Return the list of plugins that are enabled, as per the configuration. Note that
this may differ from the list of loaded plugins. For instance when a plugin is

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import os
import re
import shutil
@ -111,7 +112,7 @@ class Renderer:
The elements of `prefix` must contain only "/", and not os.sep.
"""
full_prefix = "/".join(prefix)
env_templates: t.List[str] = self.environment.loader.list_templates()
env_templates: list[str] = self.environment.loader.list_templates()
for template in env_templates:
if template.startswith(full_prefix):
# Exclude templates that match certain patterns

View File

@ -1,3 +1,5 @@
from __future__ import annotations
# The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0"
@ -53,11 +55,11 @@ class Action(t.Generic[P]):
This strong typing makes it easier for plugin developers to quickly check whether they are adding and calling action callbacks correctly.
"""
INDEX: t.Dict[str, "Action[t.Any]"] = {}
INDEX: dict[str, "Action[t.Any]"] = {}
def __init__(self, name: str) -> None:
self.name = name
self.callbacks: t.List[ActionCallback[P]] = []
self.callbacks: list[ActionCallback[P]] = []
def __repr__(self) -> str:
return f"{self.__class__.__name__}('{self.name}')"

View File

@ -1,3 +1,5 @@
from __future__ import annotations
# The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0"
@ -6,7 +8,7 @@ from contextlib import contextmanager
class Context:
CURRENT: t.List[str] = []
CURRENT: list[str] = []
def __init__(self, name: str):
self.name = name

View File

@ -1,3 +1,5 @@
from __future__ import annotations
# The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0"
@ -58,11 +60,11 @@ class Filter(t.Generic[T, P]):
they are adding and calling filter callbacks correctly.
"""
INDEX: t.Dict[str, "Filter[t.Any, t.Any]"] = {}
INDEX: dict[str, "Filter[t.Any, t.Any]"] = {}
def __init__(self, name: str) -> None:
self.name = name
self.callbacks: t.List[FilterCallback[T, P]] = []
self.callbacks: list[FilterCallback[T, P]] = []
def __repr__(self) -> str:
return f"{self.__class__.__name__}('{self.name}')"
@ -143,12 +145,12 @@ class Filter(t.Generic[T, P]):
# The methods below are specific to filters which take lists as first arguments
def add_item(
self: "Filter[t.List[E], P]", item: E, priority: t.Optional[int] = None
self: "Filter[list[E], P]", item: E, priority: t.Optional[int] = None
) -> None:
self.add_items([item], priority=priority)
def add_items(
self: "Filter[t.List[E], P]", items: t.List[E], priority: t.Optional[int] = None
self: "Filter[list[E], P]", items: list[E], priority: t.Optional[int] = None
) -> None:
# Unfortunately we have to type-ignore this line. If not, mypy complains with:
#
@ -158,18 +160,16 @@ class Filter(t.Generic[T, P]):
# But we are unable to mark arguments positional-only (by adding / after values arg) in Python 3.7.
# Get rid of this statement after Python 3.7 EOL.
@self.add(priority=priority) # type: ignore
def callback(
values: t.List[E], *_args: P.args, **_kwargs: P.kwargs
) -> t.List[E]:
def callback(values: list[E], *_args: P.args, **_kwargs: P.kwargs) -> list[E]:
return values + items
def iterate(
self: "Filter[t.List[E], P]", *args: P.args, **kwargs: P.kwargs
self: "Filter[list[E], P]", *args: P.args, **kwargs: P.kwargs
) -> t.Iterator[E]:
yield from self.iterate_from_context(None, *args, **kwargs)
def iterate_from_context(
self: "Filter[t.List[E], P]",
self: "Filter[list[E], P]",
context: t.Optional[str],
*args: P.args,
**kwargs: P.kwargs,
@ -268,7 +268,7 @@ def add_item(name: str, item: T, priority: t.Optional[int] = None) -> None:
get(name).add_item(item, priority=priority)
def add_items(name: str, items: t.List[T], priority: t.Optional[int] = None) -> None:
def add_items(name: str, items: list[T], priority: t.Optional[int] = None) -> None:
"""
Convenience function to add multiple item to a filter that returns a list of items.

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
from typing_extensions import Protocol
@ -14,7 +15,7 @@ class PrioritizedCallback(Protocol):
TPrioritized = t.TypeVar("TPrioritized", bound=PrioritizedCallback)
def insert_callback(callback: TPrioritized, callbacks: t.List[TPrioritized]) -> None:
def insert_callback(callback: TPrioritized, callbacks: list[TPrioritized]) -> None:
# I wish we could use bisect.insort_right here but the `key=` parameter
# is unsupported in Python 3.9
position = 0

View File

@ -1,6 +1,7 @@
"""
Provide API for plugin features.
"""
from __future__ import annotations
import typing as t
from copy import deepcopy
@ -20,7 +21,7 @@ def _convert_plugin_patches() -> None:
This action is run after plugins have been loaded.
"""
patches: t.Iterable[t.Tuple[str, str]] = hooks.Filters.ENV_PATCHES.iterate()
patches: t.Iterable[tuple[str, str]] = hooks.Filters.ENV_PATCHES.iterate()
for name, content in patches:
hooks.Filters.ENV_PATCH(name).add_item(content)
@ -44,14 +45,14 @@ def iter_installed() -> t.Iterator[str]:
yield from sorted(hooks.Filters.PLUGINS_INSTALLED.iterate())
def iter_info() -> t.Iterator[t.Tuple[str, t.Optional[str]]]:
def iter_info() -> t.Iterator[tuple[str, t.Optional[str]]]:
"""
Iterate on the information of all installed plugins.
Yields (<plugin name>, <info>) tuples.
"""
def plugin_info_name(info: t.Tuple[str, t.Optional[str]]) -> str:
def plugin_info_name(info: tuple[str, t.Optional[str]]) -> str:
return info[0]
yield from sorted(hooks.Filters.PLUGINS_INFO.iterate(), key=plugin_info_name)

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import re
import typing as t
@ -36,7 +37,7 @@ def parse(v: t.Union[str, t.IO[str]]) -> t.Any:
return v
def parse_key_value(text: str) -> t.Optional[t.Tuple[str, t.Any]]:
def parse_key_value(text: str) -> t.Optional[tuple[str, t.Any]]:
"""
Parse <KEY>=<YAML VALUE> command line arguments.

View File

@ -1,3 +1,5 @@
from __future__ import annotations
# The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0"
@ -38,9 +40,9 @@ T = t.TypeVar("T")
def get_typed(
config: t.Dict[str, t.Any],
config: dict[str, t.Any],
key: str,
expected_type: t.Type[T],
expected_type: type[T],
default: t.Optional[T] = None,
) -> T:
value = config.get(key, default)