diff --git a/changelog.d/20230110_130740_cmuniz_built_in_generic_types.md b/changelog.d/20230110_130740_cmuniz_built_in_generic_types.md new file mode 100644 index 0000000..25b31a8 --- /dev/null +++ b/changelog.d/20230110_130740_cmuniz_built_in_generic_types.md @@ -0,0 +1 @@ +- [Improvement] Changes annotations from `typing` to use built-in generic types from `__future__.annotations` (by @Carlos-Muniz) diff --git a/docs/conf.py b/docs/conf.py index 068538f..1f17b4a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 diff --git a/tests/commands/base.py b/tests/commands/base.py index e68dc29..868e00d 100644 --- a/tests/commands/base.py +++ b/tests/commands/base.py @@ -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: diff --git a/tests/commands/test_compose.py b/tests/commands/test_compose.py index aeb91e0..9eb505e 100644 --- a/tests/commands/test_compose.py +++ b/tests/commands/test_compose.py @@ -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"]}, } diff --git a/tests/hooks/test_filters.py b/tests/hooks/test_filters.py index 796ff72..4c23310 100644 --- a/tests/hooks/test_filters.py +++ b/tests/hooks/test_filters.py @@ -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: diff --git a/tests/test_plugins_v0.py b/tests/test_plugins_v0.py index 85dddb9..311cac6 100644 --- a/tests/test_plugins_v0.py +++ b/tests/test_plugins_v0.py @@ -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) diff --git a/tutor/commands/cli.py b/tutor/commands/cli.py index 758ff1a..98da421 100755 --- a/tutor/commands/cli.py +++ b/tutor/commands/cli.py @@ -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 diff --git a/tutor/commands/compose.py b/tutor/commands/compose.py index a1e24f7..921ff3c 100644 --- a/tutor/commands/compose.py +++ b/tutor/commands/compose.py @@ -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. diff --git a/tutor/commands/config.py b/tutor/commands/config.py index f48bf85..3d65801 100644 --- a/tutor/commands/config.py +++ b/tutor/commands/config.py @@ -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 = 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) diff --git a/tutor/commands/dev.py b/tutor/commands/dev.py index 908b75b..0fcb58d 100644 --- a/tutor/commands/dev.py +++ b/tutor/commands/dev.py @@ -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: diff --git a/tutor/commands/images.py b/tutor/commands/images.py index e6e7895..abc9207 100644 --- a/tutor/commands/images.py +++ b/tutor/commands/images.py @@ -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]: """ diff --git a/tutor/commands/jobs.py b/tutor/commands/jobs.py index 9840307..de80725 100644 --- a/tutor/commands/jobs.py +++ b/tutor/commands/jobs.py @@ -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. diff --git a/tutor/commands/local.py b/tutor/commands/local.py index 409e16e..140edc0 100644 --- a/tutor/commands/local.py +++ b/tutor/commands/local.py @@ -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: diff --git a/tutor/commands/plugins.py b/tutor/commands/plugins.py index ed67149..225be11 100644 --- a/tutor/commands/plugins.py +++ b/tutor/commands/plugins.py @@ -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}...") diff --git a/tutor/config.py b/tutor/config.py index b77a494..5512b84 100644 --- a/tutor/config.py +++ b/tutor/config.py @@ -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 diff --git a/tutor/env.py b/tutor/env.py index b3e2299..908a38e 100644 --- a/tutor/env.py +++ b/tutor/env.py @@ -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 diff --git a/tutor/hooks/actions.py b/tutor/hooks/actions.py index 1ef6db4..ccaf467 100644 --- a/tutor/hooks/actions.py +++ b/tutor/hooks/actions.py @@ -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}')" diff --git a/tutor/hooks/contexts.py b/tutor/hooks/contexts.py index 0ac9821..75a3182 100644 --- a/tutor/hooks/contexts.py +++ b/tutor/hooks/contexts.py @@ -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 diff --git a/tutor/hooks/filters.py b/tutor/hooks/filters.py index 85fa294..4aa10f9 100644 --- a/tutor/hooks/filters.py +++ b/tutor/hooks/filters.py @@ -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. diff --git a/tutor/hooks/priorities.py b/tutor/hooks/priorities.py index 3c43d53..c493bdb 100644 --- a/tutor/hooks/priorities.py +++ b/tutor/hooks/priorities.py @@ -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 diff --git a/tutor/plugins/__init__.py b/tutor/plugins/__init__.py index cc5c5d2..fd137be 100644 --- a/tutor/plugins/__init__.py +++ b/tutor/plugins/__init__.py @@ -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 (, ) 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) diff --git a/tutor/serialize.py b/tutor/serialize.py index 415f75c..a838d40 100644 --- a/tutor/serialize.py +++ b/tutor/serialize.py @@ -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 = command line arguments. diff --git a/tutor/types.py b/tutor/types.py index 4f77205..c6156b3 100644 --- a/tutor/types.py +++ b/tutor/types.py @@ -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)