Skip to content

Commit

Permalink
feat: Initial features
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Oct 2, 2020
1 parent f5224dd commit 3c395d3
Show file tree
Hide file tree
Showing 8 changed files with 491 additions and 240 deletions.
30 changes: 11 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
.DEFAULT_GOAL := help
SHELL := bash

INVOKE_OR_POETRY = $(shell command -v invoke &>/dev/null || echo poetry run) invoke
INVOKE_AND_POETRY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo poetry run) invoke

DUTY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo poetry run) duty
PYTHON_VERSIONS ?= 3.6 3.7 3.8

POETRY_TASKS = \
BASIC_DUTIES = \
changelog \
clean \
combine \
coverage \
docs \
Expand All @@ -17,34 +16,27 @@ POETRY_TASKS = \
format \
release

QUALITY_TASKS = \
QUALITY_DUTIES = \
check \
check-code-quality \
check-dependencies \
check-docs \
check-types \
test

INVOKE_TASKS = \
clean


.PHONY: help
help:
@$(INVOKE_OR_POETRY) --list
@$(DUTY) --list

.PHONY: setup
setup:
@env PYTHON_VERSIONS="$(PYTHON_VERSIONS)" bash scripts/setup.sh

.PHONY: $(POETRY_TASKS)
$(POETRY_TASKS):
@$(INVOKE_AND_POETRY) $@ $(args)
.PHONY: $(BASIC_DUTIES)
$(BASIC_DUTIES):
@$(DUTY) $@

.PHONY: $(QUALITY_TASKS)
$(QUALITY_TASKS):
@env PYTHON_VERSIONS="$(PYTHON_VERSIONS)" bash scripts/run_task.sh $(INVOKE_AND_POETRY) $@ $(args)
.PHONY: $(QUALITY_DUTIES)
$(QUALITY_DUTIES):
@env PYTHON_VERSIONS="$(PYTHON_VERSIONS)" bash scripts/multirun.sh duty $@

.PHONY: $(INVOKE_TASKS)
$(INVOKE_TASKS):
@$(INVOKE_OR_POETRY) $@ $(args)
240 changes: 240 additions & 0 deletions duties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"""Development tasks."""

import os
from pathlib import Path
from shutil import which

from duty import duty

PY_SRC_PATHS = (Path(_) for _ in ("src", "scripts", "tests", "duties.py"))
PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS)
PY_SRC = " ".join(PY_SRC_LIST)
TESTING = os.environ.get("TESTING", "0") in {"1", "true"}
CI = os.environ.get("CI", "0") in {"1", "true"}
WINDOWS = os.name == "nt"
PTY = not WINDOWS


@duty
def changelog(ctx):
"""
Update the changelog in-place with latest commits.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run(
[
"python",
"scripts/update_changelog.py",
"CHANGELOG.md",
"<!-- insertion marker -->",
r"^## \[v?(?P<version>[^\]]+)",
],
title="Updating changelog",
pty=PTY,
)


@duty(pre=["check_code_quality", "check_types", "check_docs", "check_dependencies"])
def check(ctx): # noqa: W0613 (no use for the context argument)
"""
Check it all!
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
""" # noqa: D400 (exclamation mark is funnier)


@duty
def check_code_quality(ctx):
"""
Check the code quality.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run(["flakehell", "lint", *PY_SRC_LIST], title="Checking code quality", pty=PTY)


@duty
def check_dependencies(ctx):
"""
Check for vulnerabilities in dependencies.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
safety = "safety" if which("safety") else "pipx run safety"
ctx.run(
"poetry export -f requirements.txt --without-hashes | " f"{safety} check --stdin --full-report",
title="Checking dependencies",
pty=PTY,
)


@duty
def check_docs(ctx):
"""
Check if the documentation builds correctly.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run(["mkdocs", "build", "-s"], title="Building documentation", pty=PTY)


@duty
def check_types(ctx):
"""
Check that the code is correctly typed.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run(["mypy", "--config-file", "config/mypy.ini", *PY_SRC_LIST], title="Type-checking", pty=PTY)


@duty(silent=True)
def clean(ctx):
"""
Delete temporary files.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run("rm -rf .coverage*")
ctx.run("rm -rf .mypy_cache")
ctx.run("rm -rf .pytest_cache")
ctx.run("rm -rf build")
ctx.run("rm -rf dist")
ctx.run("rm -rf pip-wheel-metadata")
ctx.run("rm -rf site")
ctx.run("find . -type d -name __pycache__ | xargs rm -rf")
ctx.run("find . -name '*.rej' -delete")


@duty
def docs_regen(ctx):
"""
Regenerate some documentation pages.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run(["python", "scripts/regen_docs.py"], title="Regenerating docfiles", pty=PTY)


# @duty(docs_regen)
@duty
def docs(ctx):
"""
Build the documentation locally.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run(["mkdocs", "build"])


# @duty(docs_regen)
@duty
def docs_serve(ctx, host="127.0.0.1", port=8000):
"""
Serve the documentation (localhost:8000).
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
host: The host to serve the docs from.
port: The port to serve the docs on.
"""
ctx.run(["mkdocs", "serve", "-a", f"{host}:{port}"])


# @duty(docs_regen)
@duty
def docs_deploy(ctx):
"""
Deploy the documentation on GitHub pages.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run("mkdocs gh-deploy")


@duty
def format(ctx): # noqa: W0622 (we don't mind shadowing the format builtin)
"""
Run formatting tools on the code.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run(
["autoflake", "-ir", "--exclude", "tests/fixtures", "--remove-all-unused-imports", *PY_SRC_LIST],
title="Removing unused imports",
pty=PTY,
)
ctx.run(["isort", "-y", "-rc", *PY_SRC_LIST], title="Ordering imports", pty=PTY)
ctx.run(["black", *PY_SRC_LIST], title="Formatting code", pty=PTY)


@duty
def release(ctx, version):
"""
Release a new Python package.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
version: The new version number to use.
"""
ctx.run(f"poetry version {version}", title="Bumping version in pyproject.toml", pty=PTY)
ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY)
ctx.run(f"git commit -m 'chore: Prepare release {version}'", title="Committing changes", pty=PTY)
ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY)
if not TESTING:
ctx.run("git push", title="Pushing commits", pty=False)
ctx.run("git push --tags", title="Pushing tags", pty=False)
ctx.run("poetry build", title="Building dist/wheel", pty=PTY)
ctx.run("poetry publish", title="Publishing version", pty=PTY)
ctx.run("poetry run mkdocs gh-deploy", title="Deploying docs", pty=PTY)


@duty
def combine(ctx):
"""
Combine coverage data from multiple runs.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run("coverage combine --rcfile=config/coverage.ini")


@duty
def coverage(ctx):
"""
Report coverage as text and HTML.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
"""
ctx.run("coverage report --rcfile=config/coverage.ini")
ctx.run("coverage html --rcfile=config/coverage.ini")


@duty(pre=[duty(lambda ctx: ctx.run("rm -f .coverage", silent=True))])
def test(ctx, match=""):
"""
Run the test suite.
Arguments:
ctx: The [context][duties.logic.Context] instance (passed automatically).
match: A pytest expression to filter selected tests.
"""
ctx.run(
["pytest", "-c", "config/pytest.ini", "-n", "auto", "-k", match, *PY_SRC_LIST],
title="Running tests",
pty=PTY,
)
6 changes: 3 additions & 3 deletions scripts/run_task.sh → scripts/multirun.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ if [ -n "${PYTHON_VERSIONS}" ]; then
echo "> Environment for Python ${python_version} not created, skipping" >&2
poetry env remove "${python_version}" &>/dev/null || true
else
echo "> $@ (Python ${python_version})"
"$@"
echo "> poetry run $@ (Python ${python_version})"
poetry run "$@"
fi
else
echo "> poetry env use ${python_version}: Python version not available?" >&2
fi
done
else
"$@"
poetry run "$@"
fi
4 changes: 3 additions & 1 deletion src/duty/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

from typing import List

__all__: List[str] = [] # noqa: WPS410 (the only __variable__ we use)
from duty.logic import duty

__all__: List[str] = ["duty"] # noqa: WPS410 (the only __variable__ we use)
51 changes: 48 additions & 3 deletions src/duty/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,27 @@
"""Module that contains the command line application."""

import argparse
from typing import List, Optional
import importlib.util
from typing import Dict, List, Optional

from duty import logic


def load_duties(path: str) -> Dict[str, logic.Duty]:
"""
Load duties from a Python file.
Arguments:
path: The path to the Python file to load.
Returns:
The loaded duties.
"""
logic.duties.clear()
spec = importlib.util.spec_from_file_location("duty.loaded", path)
duties = importlib.util.module_from_spec(spec)
spec.loader.exec_module(duties) # type: ignore
return logic.duties


def get_parser() -> argparse.ArgumentParser:
Expand All @@ -22,7 +42,12 @@ def get_parser() -> argparse.ArgumentParser:
Returns:
An argparse parser.
"""
return argparse.ArgumentParser(prog="duty")
parser = argparse.ArgumentParser(prog="duty")
parser.add_argument(
"-d", "--duties-file", nargs=1, default="duties.py", help="Python file where the duties are defined."
)
parser.add_argument("DUTIES", metavar="DUTY", nargs="+")
return parser


def main(args: Optional[List[str]] = None) -> int:
Expand All @@ -39,5 +64,25 @@ def main(args: Optional[List[str]] = None) -> int:
"""
parser = get_parser()
opts = parser.parse_args(args=args)
print(opts) # noqa: WPS421 (side-effect in main is fine)

duties = load_duties(opts.duties_file)

selection = []
duty_name: str = ""
for arg in opts.DUTIES:
if "=" in arg:
duty_args.update(dict([arg.split("=", 1)]))
else:
if duty_name:
selection.append((duty_name, duty_args))
duty_args: Dict[str, str] = {}
duty_name = arg
selection.append((duty_name, duty_args))

for duty_name, duty_args in selection:
try:
duties[duty_name].run(**duty_args)
except logic.DutyFailure as failure:
return failure.code

return 0
Loading

0 comments on commit 3c395d3

Please sign in to comment.