Skip to content

Commit

Permalink
refactor: annotation with __future__.annotations
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Carlos Muniz authored and regisb committed Jan 18, 2023
1 parent d629ca9 commit ac1a875
Show file tree
Hide file tree
Showing 23 changed files with 126 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [Improvement] Changes annotations from `typing` to use built-in generic types from `__future__.annotations` (by @Carlos-Muniz)
6 changes: 6 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
("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
Expand Down
6 changes: 3 additions & 3 deletions tests/commands/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import typing as t
from __future__ import annotations

import click.testing

Expand All @@ -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:
Expand Down
15 changes: 7 additions & 8 deletions tests/commands/test_compose.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
import unittest
from io import StringIO
Expand Down Expand Up @@ -62,9 +63,9 @@ def test_compose_local_tmp_generation(self, _mock_stdout: StringIO) -> None:
# 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": {
Expand All @@ -78,11 +79,9 @@ def test_compose_local_tmp_generation(self, _mock_stdout: StringIO) -> None:
}
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"]},
}
Expand Down
5 changes: 3 additions & 2 deletions tests/hooks/test_filters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
import unittest

Expand All @@ -23,14 +24,14 @@ def filter1(value: int) -> int:

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:
Expand Down
5 changes: 2 additions & 3 deletions tests/test_plugins_v0.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import typing as t
from unittest.mock import patch

Expand Down Expand Up @@ -197,9 +198,7 @@ def test_dict_plugin(self) -> None:
{"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)
Expand Down
3 changes: 2 additions & 1 deletion tutor/commands/cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import sys
import typing as t

Expand Down Expand Up @@ -61,7 +62,7 @@ def ensure_plugins_enabled(cls, ctx: click.Context) -> None:
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>
Expand Down
57 changes: 28 additions & 29 deletions tutor/commands/compose.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import os
import re
import typing as t
Expand All @@ -16,15 +17,15 @@
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:
"""
Expand Down Expand Up @@ -55,7 +56,7 @@ def update_docker_compose_tmp(
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": {},
}
Expand Down Expand Up @@ -134,20 +135,20 @@ def convert(
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".
"""
match = re.match(self.PARAM_REGEXP, value)
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"]))
Expand All @@ -159,11 +160,11 @@ def convert_explicit_form(self, value: str) -> t.List["MountParam.MountType"]:
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)
Expand All @@ -175,7 +176,7 @@ def convert_implicit_form(self, value: str) -> t.List["MountParam.MountType"]:

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:
Expand All @@ -197,7 +198,7 @@ def shell_complete(


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:
Expand Down Expand Up @@ -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}")
Expand All @@ -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:
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -295,7 +296,7 @@ def reboot(context: click.Context, detach: bool, services: t.List[str]) -> None:
)
@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:
Expand All @@ -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).
"""
Expand Down Expand Up @@ -345,8 +344,8 @@ def _mount_tmp_volumes(_job_name: str, *_args: t.Any, **_kwargs: t.Any) -> None:
@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():
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions tutor/commands/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import json
import typing as t

Expand Down Expand Up @@ -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)
Expand All @@ -36,7 +37,7 @@ def shell_complete(
@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
Expand All @@ -58,15 +59,15 @@ 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)
return result

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.
"""
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tutor/commands/dev.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import typing as t
from __future__ import annotations

import click

Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit ac1a875

Please sign in to comment.