c561ff96c7d947261446c989de901cf5af2876d0 — JA Viljoen 3 months ago d7ae1cb
Remove type annotations

Mypy wasn't contributing much to this project,
and the type annotations reduced the readability of the code.
8 files changed, 32 insertions(+), 45 deletions(-)

M docs/doctasks.py
M requirements.txt
M tasks.py
M yatte/cli.py
D yatte/py.typed
M yatte/taskfile.py
M yatte/tasklist.py
M yatte/utils.py
M docs/doctasks.py => docs/doctasks.py +7 -8
@@ 3,9 3,8 @@ import xml.etree.ElementTree as xml
from datetime import date
from logging import error
from pathlib import Path
from typing import Set

import chevron  # type: ignore
import chevron

import yatte
from yatte import task

@@ 62,7 61,7 @@ def upload_docs():
# Helper functions

def pipe(cmd: str, input=None) -> str:
def pipe(cmd, input=None):
    """Run a shell command and return its stdout output.

    If `input` is not None, pipes in the text via stdin.

@@ 75,24 74,24 @@ def pipe(cmd: str, input=None) -> str:
    return p.stdout

def scdoc2man(scd: Path, man: Path):
def scdoc2man(scd, man):
    """Convert manual in scdoc format to man format."""
    stderr(f"$ chevron {scd} | scdoc > {man}")
    scdoc = chevron.render(scd.read_text(), {"version": yatte.__version__})
    pipe(f"scdoc > {man}", input=scdoc)

def cp(src: Path, dest: Path):
def cp(src, dest):
    if not dest.is_file() or is_newer(src, than=dest):
        run(f"cp -p {src} {dest}")

def uptodate(f: Path, deps: Set[Path]) -> bool:
def uptodate(f, deps):
    # like is_newer() but takes multiple files as 2nd arg.
    return f.is_file() and all(f.stat().st_mtime > d.stat().st_mtime for d in deps)

def render_page(page: Path, template: str, out_html: Path):
def render_page(page, template, out_html):
    """Inject page into template and write to HTML file."""
    content = page.read_text()

@@ 108,7 107,7 @@ def render_page(page: Path, template: str, out_html: Path):

def get_title(doc: str) -> str:
def get_title(doc):
    """Return body of first h1 element in an HTML document.

    The doc must be a well-formed XML snippet:

M requirements.txt => requirements.txt +0 -1
@@ 3,6 3,5 @@ isort~=5.10

M tasks.py => tasks.py +3 -11
@@ 20,12 20,6 @@ def install_dependencies():

def check_types():
    """Run type checker."""
    run("mypy .")

def run_linters():
    """Run linters."""

@@ 45,12 39,11 @@ def run_tests():

def check():
    """lint + typecheck + test"""
    """lint + test"""
    cmds = [
        "isort --check .",
        "black --check .",
        "flake8 .",
        "mypy .",
        "pytest -q .",

@@ 64,7 57,7 @@ def format():

sys.path.insert(0, "docs")
import doctasks  # type: ignore  # noqa: E402 F401
import doctasks  # noqa: E402 F401


@@ 76,7 69,6 @@ def pypi():
def clean():
    """Remove build/test artefacts."""
    run("rm -rf .mypy_cache")
    run("rm -rf .pytest_cache")
    run("rm -rf dist")
    run("rm -rf docs/_built")

@@ 85,6 77,6 @@ def clean():
# Helper functions

def check_installed(cmd: str):
def check_installed(cmd):
    if shutil.which(cmd) is None:
        warning("%r is required for some tasks but is not installed.", cmd)

M yatte/cli.py => yatte/cli.py +1 -1
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")

def parse_args() -> Namespace:
def parse_args():
    """Sets up the CLI and returns the arguments provided by the user."""
    ns = Namespace()
    ns.task_file = getenv(TASKFILE_ENVVAR, "tasks.py")

D yatte/py.typed => yatte/py.typed +0 -0
M yatte/taskfile.py => yatte/taskfile.py +2 -2
@@ 13,7 13,7 @@ class TaskfileImportError(ImportError):

def load_taskfile(path: str):
def load_taskfile(path):
    """Load the tasks from the given file.

    Nothing is returned but the tasks are registered on the `Task` class

@@ 41,7 41,7 @@ def load_taskfile(path: str):
        raise TaskfileImportError(f"Failed to import {path!r} as a Python module")

    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)  # type: ignore

    # Note: The module doesn't need to be added to `sys.modules`,
    # since it won't actually be imported by name / import path.

M yatte/tasklist.py => yatte/tasklist.py +11 -12
@@ 2,7 2,6 @@
from __future__ import annotations

from inspect import getdoc, getfile, signature
from typing import Callable, Dict

from .taskfile import load_taskfile

@@ 14,9 13,9 @@ class Task:
    the task name and the first line of the function docstring.

    _instances: list[Task] = []
    _instances = []

    def __init__(self, name: str, fn: Callable):
    def __init__(self, name, fn):
        self.name = name
        self.fn = fn

@@ 33,21 32,21 @@ class Task:


    def __repr__(self) -> str:
    def __repr__(self):
        return f"Task({self.name!r}: <{getfile(self.fn)}>:{self.fn.__qualname__})"

    def __str__(self) -> str:
    def __str__(self):
        arglist = " ".join(self.args)
        signature = f"{self.name} {arglist}"
        return f"{signature:<30} {self.doc}"

    def args(self) -> tuple[str, ...]:
    def args(self):
        """The names of the function parameters"""
        return tuple(signature(self.fn).parameters)

    def doc(self) -> str:
    def doc(self):
        """The first line of the function docstring"""
        docstring = getdoc(self.fn) or ""
        return docstring and docstring.splitlines()[0]

@@ 59,7 58,7 @@ class ArgCountError(TypeError):

class TaskList(Dict[str, Task]):
class TaskList(dict):
    """A mapping of Tasks indexed on task name

    When instantiating this class,

@@ 69,21 68,21 @@ class TaskList(Dict[str, Task]):
    def __init__(self):
        super().__init__({t.name: t for t in Task._instances})

    def __str__(self) -> str:
    def __str__(self):
        return "\n".join(map(str, self.values())) or "<No tasks defined>"

    def load_from(cls, task_file: str) -> TaskList:
    def load_from(cls, task_file):
        """Load Tasks defined in task_file into a TaskList."""
        # Import task_file, registering Tasks in Task._instances.
        return TaskList()

def task(name: str) -> Callable:
def task(name):
    """A decorator for turning functions into Tasks"""

    def make_task(fn: Callable) -> Task:
    def make_task(fn):
        return Task(name, fn)

    return make_task

M yatte/utils.py => yatte/utils.py +8 -10
@@ 4,17 4,15 @@ import subprocess
import sys
from concurrent.futures import ProcessPoolExecutor
from logging import error
from pathlib import Path
from shlex import quote
from typing import Iterable, List, Union

def stderr(s: str):
def stderr(s):
    """Print a string to stderr."""
    print(s, file=sys.stderr)

def run(cmd: Union[str, List[str]]):
def run(cmd):
    """Run a shell command."""
    if isinstance(cmd, list):
        cmd = " ".join(map(quote, cmd))

@@ 27,7 25,7 @@ def run(cmd: Union[str, List[str]]):
        raise SystemExit(e.returncode)

def runp(cmds: Iterable[str]):
def runp(cmds):
    """Run shell commands in parallel."""
    with ProcessPoolExecutor() as executor:
        retcodes = list(executor.map(_run, cmds))

@@ 36,7 34,7 @@ def runp(cmds: Iterable[str]):
        raise SystemExit(max(retcodes))

def _run(cmd: str) -> int:
def _run(cmd):
    """Run a shell command and print its output upon completion."""
    p = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    stderr(f"$ {cmd}")

@@ 53,24 51,24 @@ def _run(cmd: str) -> int:
    return p.returncode

def mkdir(d: Path):
def mkdir(d):
    """Create directory d if it doesn't already exist."""
    if not d.is_dir():
        run(["mkdir", "-p", str(d)])

def cp(src: Path, dest: Path):
def cp(src, dest):
    """Copy src to dest if dest doesn't already exist."""
    if not dest.is_file():
        run(["cp", "-p", str(src), str(dest)])

def is_newer(f: Path, than: Path) -> bool:
def is_newer(f, than):
    """Return True if f exists and is newer than the second argument."""
    return f.is_file() and f.stat().st_mtime > than.stat().st_mtime

def check_envvars(names: set) -> set:
def check_envvars(names):
    """Return the environment variables in names that are undefined."""
    return names - set(os.environ)