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.P"),
("py:class", "tutor.hooks.filters.T"), ("py:class", "tutor.hooks.filters.T"),
("py:class", "tutor.hooks.actions.P"), ("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 # -- Sphinx-Click configuration

View File

@ -1,4 +1,4 @@
import typing as t from __future__ import annotations
import click.testing import click.testing
@ -12,13 +12,13 @@ class TestCommandMixin:
""" """
@staticmethod @staticmethod
def invoke(args: t.List[str]) -> click.testing.Result: def invoke(args: list[str]) -> click.testing.Result:
with temporary_root() as root: with temporary_root() as root:
return TestCommandMixin.invoke_in_root(root, args) return TestCommandMixin.invoke_in_root(root, args)
@staticmethod @staticmethod
def invoke_in_root( 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: ) -> click.testing.Result:
""" """
Use this method for commands that all need to run in the same root: 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 typing as t
import unittest import unittest
from io import StringIO from io import StringIO
@ -62,9 +63,9 @@ class ComposeTests(unittest.TestCase):
# Mount volumes # Mount volumes
compose.mount_tmp_volumes(mount_args, LocalContext("")) compose.mount_tmp_volumes(mount_args, LocalContext(""))
compose_file: t.Dict[str, t.Any] = hooks.Filters.COMPOSE_LOCAL_TMP.apply({}) compose_file: dict[str, t.Any] = hooks.Filters.COMPOSE_LOCAL_TMP.apply({})
actual_services: t.Dict[str, t.Any] = compose_file["services"] actual_services: dict[str, t.Any] = compose_file["services"]
expected_services: t.Dict[str, t.Any] = { expected_services: dict[str, t.Any] = {
"cms": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]}, "cms": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]},
"cms-worker": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]}, "cms-worker": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]},
"lms": { "lms": {
@ -78,11 +79,9 @@ class ComposeTests(unittest.TestCase):
} }
self.assertEqual(actual_services, expected_services) self.assertEqual(actual_services, expected_services)
compose_jobs_file: t.Dict[ compose_jobs_file = hooks.Filters.COMPOSE_LOCAL_JOBS_TMP.apply({})
str, t.Any actual_jobs_services = compose_jobs_file["services"]
] = hooks.Filters.COMPOSE_LOCAL_JOBS_TMP.apply({}) expected_jobs_services: dict[str, t.Any] = {
actual_jobs_services: t.Dict[str, t.Any] = compose_jobs_file["services"]
expected_jobs_services: t.Dict[str, t.Any] = {
"cms-job": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]}, "cms-job": {"volumes": ["/path/to/edx-platform:/openedx/edx-platform"]},
"lms-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 typing as t
import unittest import unittest
@ -23,14 +24,14 @@ class PluginFiltersTests(unittest.TestCase):
def test_add_items(self) -> None: def test_add_items(self) -> None:
@hooks.filters.add("tests:add-sheeps") @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] return sheeps + [0]
hooks.filters.add_item("tests:add-sheeps", 1) hooks.filters.add_item("tests:add-sheeps", 1)
hooks.filters.add_item("tests:add-sheeps", 2) hooks.filters.add_item("tests:add-sheeps", 2)
hooks.filters.add_items("tests:add-sheeps", [3, 4]) 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) self.assertEqual([0, 1, 2, 3, 4], sheeps)
def test_filter_callbacks(self) -> None: def test_filter_callbacks(self) -> None:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
""" """
Common jobs that must be added both to local, dev and k8s commands. Common jobs that must be added both to local, dev and k8s commands.
""" """
from __future__ import annotations
import functools import functools
import typing as t import typing as t
@ -49,7 +50,7 @@ def _add_core_init_tasks() -> None:
@click.command("init", help="Initialise all applications") @click.command("init", help="Initialise all applications")
@click.option("-l", "--limit", help="Limit initialisation to this service or plugin") @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...") fmt.echo_info("Initialising all services...")
filter_context = hooks.Contexts.APP(limit).name if limit else None filter_context = hooks.Contexts.APP(limit).name if limit else None
@ -99,7 +100,7 @@ def createuser(
password: str, password: str,
name: str, name: str,
email: str, email: str,
) -> t.Iterable[t.Tuple[str, str]]: ) -> t.Iterable[tuple[str, str]]:
""" """
Create an Open edX user Create an Open edX user
@ -127,7 +128,7 @@ u.save()"
@click.command(help="Import the demo course") @click.command(help="Import the demo course")
def importdemocourse() -> t.Iterable[t.Tuple[str, str]]: def importdemocourse() -> t.Iterable[tuple[str, str]]:
template = """ template = """
# Import demo course # Import demo course
git clone https://github.com/openedx/edx-demo-course --branch {{ OPENEDX_COMMON_VERSION }} --depth 1 ../edx-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") @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. 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)) 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. For each domain, get or create a Site object and assign the selected theme.
""" """
@ -231,7 +232,7 @@ P = ParamSpec("P")
def _patch_callback( 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]: ) -> t.Callable[P, None]:
""" """
Modify a subcommand callback function such that its results are processed by `do_callback`. Modify a subcommand callback function such that its results are processed by `do_callback`.
@ -247,7 +248,7 @@ def _patch_callback(
return new_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. 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 typing as t
import click import click
@ -70,7 +71,7 @@ def local(context: click.Context) -> None:
@click.pass_context @click.pass_context
def launch( def launch(
context: click.Context, context: click.Context,
mounts: t.Tuple[t.List[compose.MountParam.MountType]], mounts: tuple[list[compose.MountParam.MountType]],
non_interactive: bool, non_interactive: bool,
pullimages: bool, pullimages: bool,
) -> None: ) -> None:

View File

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

View File

@ -1,5 +1,5 @@
from __future__ import annotations
import os import os
import typing as t
from tutor import env, exceptions, fmt, hooks, plugins, serialize, utils from tutor import env, exceptions, fmt, hooks, plugins, serialize, utils
from tutor.types import Config, ConfigValue, cast_config, get_typed from tutor.types import Config, ConfigValue, cast_config, get_typed
@ -108,7 +108,7 @@ def get_base() -> Config:
Entries in this configuration are unrendered. Entries in this configuration are unrendered.
""" """
base = get_template("base.yml") 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_UNIQUE.apply(extra_config)
extra_config = hooks.Filters.CONFIG_OVERRIDES.apply(extra_config) extra_config = hooks.Filters.CONFIG_OVERRIDES.apply(extra_config)
for name, value in 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)) 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 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 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 os
import re import re
import shutil import shutil
@ -111,7 +112,7 @@ class Renderer:
The elements of `prefix` must contain only "/", and not os.sep. The elements of `prefix` must contain only "/", and not os.sep.
""" """
full_prefix = "/".join(prefix) 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: for template in env_templates:
if template.startswith(full_prefix): if template.startswith(full_prefix):
# Exclude templates that match certain patterns # 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. # The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0" __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. 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: def __init__(self, name: str) -> None:
self.name = name self.name = name
self.callbacks: t.List[ActionCallback[P]] = [] self.callbacks: list[ActionCallback[P]] = []
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}('{self.name}')" 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. # The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0" __license__ = "Apache 2.0"
@ -6,7 +8,7 @@ from contextlib import contextmanager
class Context: class Context:
CURRENT: t.List[str] = [] CURRENT: list[str] = []
def __init__(self, name: str): def __init__(self, name: str):
self.name = name 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. # The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0" __license__ = "Apache 2.0"
@ -58,11 +60,11 @@ class Filter(t.Generic[T, P]):
they are adding and calling filter callbacks correctly. 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: def __init__(self, name: str) -> None:
self.name = name self.name = name
self.callbacks: t.List[FilterCallback[T, P]] = [] self.callbacks: list[FilterCallback[T, P]] = []
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}('{self.name}')" 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 # The methods below are specific to filters which take lists as first arguments
def add_item( 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: ) -> None:
self.add_items([item], priority=priority) self.add_items([item], priority=priority)
def add_items( 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: ) -> None:
# Unfortunately we have to type-ignore this line. If not, mypy complains with: # 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. # 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. # Get rid of this statement after Python 3.7 EOL.
@self.add(priority=priority) # type: ignore @self.add(priority=priority) # type: ignore
def callback( def callback(values: list[E], *_args: P.args, **_kwargs: P.kwargs) -> list[E]:
values: t.List[E], *_args: P.args, **_kwargs: P.kwargs
) -> t.List[E]:
return values + items return values + items
def iterate( 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]: ) -> t.Iterator[E]:
yield from self.iterate_from_context(None, *args, **kwargs) yield from self.iterate_from_context(None, *args, **kwargs)
def iterate_from_context( def iterate_from_context(
self: "Filter[t.List[E], P]", self: "Filter[list[E], P]",
context: t.Optional[str], context: t.Optional[str],
*args: P.args, *args: P.args,
**kwargs: P.kwargs, **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) 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. 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 import typing as t
from typing_extensions import Protocol from typing_extensions import Protocol
@ -14,7 +15,7 @@ class PrioritizedCallback(Protocol):
TPrioritized = t.TypeVar("TPrioritized", bound=PrioritizedCallback) 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 # I wish we could use bisect.insort_right here but the `key=` parameter
# is unsupported in Python 3.9 # is unsupported in Python 3.9
position = 0 position = 0

View File

@ -1,6 +1,7 @@
""" """
Provide API for plugin features. Provide API for plugin features.
""" """
from __future__ import annotations
import typing as t import typing as t
from copy import deepcopy from copy import deepcopy
@ -20,7 +21,7 @@ def _convert_plugin_patches() -> None:
This action is run after plugins have been loaded. 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: for name, content in patches:
hooks.Filters.ENV_PATCH(name).add_item(content) 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()) 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. Iterate on the information of all installed plugins.
Yields (<plugin name>, <info>) tuples. 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] return info[0]
yield from sorted(hooks.Filters.PLUGINS_INFO.iterate(), key=plugin_info_name) 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 re
import typing as t import typing as t
@ -36,7 +37,7 @@ def parse(v: t.Union[str, t.IO[str]]) -> t.Any:
return v 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. 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. # The Tutor plugin system is licensed under the terms of the Apache 2.0 license.
__license__ = "Apache 2.0" __license__ = "Apache 2.0"
@ -38,9 +40,9 @@ T = t.TypeVar("T")
def get_typed( def get_typed(
config: t.Dict[str, t.Any], config: dict[str, t.Any],
key: str, key: str,
expected_type: t.Type[T], expected_type: type[T],
default: t.Optional[T] = None, default: t.Optional[T] = None,
) -> T: ) -> T:
value = config.get(key, default) value = config.get(key, default)