From b7e86e50bf8815df8d7b90c3b622ef13e14bf843 Mon Sep 17 00:00:00 2001
From: Reid 'arrdem' McKenzie <me@arrdem.com>
Date: Thu, 2 Sep 2021 22:10:12 -0600
Subject: [PATCH] Get black working as a linter

---
 tools/black/BUILD       | 10 ++++++
 tools/black/__main__.py | 57 +++++++++++++++++++++++++++++++++
 tools/black/black.bzl   | 71 +++++++++++++++++++++++++++++++++++++++++
 tools/flake8/flake8.bzl |  2 +-
 4 files changed, 139 insertions(+), 1 deletion(-)
 create mode 100644 tools/black/BUILD
 create mode 100644 tools/black/__main__.py
 create mode 100644 tools/black/black.bzl

diff --git a/tools/black/BUILD b/tools/black/BUILD
new file mode 100644
index 0000000..a6aa310
--- /dev/null
+++ b/tools/black/BUILD
@@ -0,0 +1,10 @@
+py_binary(
+    name = "black",
+    main = "__main__.py",
+    deps = [
+        py_requirement("black"),
+    ],
+    visibility = [
+        "//visibility:public"
+    ],
+)
diff --git a/tools/black/__main__.py b/tools/black/__main__.py
new file mode 100644
index 0000000..c4a3e54
--- /dev/null
+++ b/tools/black/__main__.py
@@ -0,0 +1,57 @@
+"""A shim to black which knows how to tee output."""
+
+import argparse
+from contextlib import contextmanager
+import sys
+
+from black import patched_main
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--output-file", default=None)
+
+
+class Tee(object):
+    """Tee all I/O to a file and stdout."""
+
+    def __init__(self, name, mode):
+        self._file = open(name, mode)
+        self._stdout = sys.stdout
+
+    def __enter__(self):
+        sys.stdout = self
+        return self
+
+    def __exit__(self, *args, **kwargs):
+        sys.stdout = self._stdout
+        self.close()
+
+    def write(self, data):
+        self._file.write(data)
+        self._stdout.write(data)
+
+    def flush(self):
+        self._file.flush()
+        self._stdout.flush()
+
+    def close(self):
+        self._file.close()
+
+
+@contextmanager
+def nullctx():
+    yield
+
+
+if __name__ == "__main__":
+    opts, args = parser.parse_known_args()
+
+    if opts.output_file:
+        print("Teeig output....")
+        ctx = Tee(opts.output_file, "w")
+    else:
+        ctx = nullctx()
+
+    with ctx:
+        sys.argv = [sys.argv[0]] + args
+        patched_main()
diff --git a/tools/black/black.bzl b/tools/black/black.bzl
new file mode 100644
index 0000000..96a96e9
--- /dev/null
+++ b/tools/black/black.bzl
@@ -0,0 +1,71 @@
+"""Linting for Python using Aspects."""
+
+# Hacked up from https://github.com/bazelbuild/rules_rust/blob/main/rust/private/clippy.bzl
+#
+# Usage:
+#   bazel build --aspects="//tools/flake8:flake8.bzl%flake8_aspect" --output_groups=flake8_checks <target|pattern>
+#
+# Note that the build directive can be inserted to .bazelrc to make it part of the default behavior
+
+def _black_aspect_impl(target, ctx):
+    if hasattr(ctx.rule.attr, 'srcs'):
+        black = ctx.attr._black.files_to_run
+        config = ctx.attr._config.files.to_list()[0]
+
+        files = []
+        for src in ctx.rule.attr.srcs:
+            for f in src.files.to_list():
+                if f.extension == "py":
+                    files.append(f)
+
+        if files:
+            report = ctx.actions.declare_file(ctx.label.name + ".black.report")
+        else:
+            return []
+
+        args = ["--check", "--output-file", report.path]
+        for f in files:
+            args.append(f.path)
+
+        ctx.actions.run(
+            executable = black,
+            inputs = files,
+            tools = ctx.attr._config.files.to_list() + ctx.attr._black.files.to_list(),
+            arguments = args,
+            outputs = [report],
+            mnemonic = "Black",
+        )
+
+        return [
+            OutputGroupInfo(black_checks = depset([report]))
+        ]
+
+    return []
+
+
+black_aspect = aspect(
+    implementation = _black_aspect_impl,
+    attr_aspects = ['deps'],
+    attrs = {
+        '_black': attr.label(default=":black"),
+        '_config': attr.label(
+            default="//:setup.cfg",
+            executable=False,
+            allow_single_file=True
+        ),
+    }
+)
+
+
+def _black_rule_impl(ctx):
+    ready_targets = [dep for dep in ctx.attr.deps if "black_checks" in dir(dep[OutputGroupInfo])]
+    files = depset([], transitive = [dep[OutputGroupInfo].black_checks for dep in ready_targets])
+    return [DefaultInfo(files = files)]
+
+
+black = rule(
+    implementation = _black_rule_impl,
+    attrs = {
+        'deps' : attr.label_list(aspects = [black_aspect]),
+    },
+)
diff --git a/tools/flake8/flake8.bzl b/tools/flake8/flake8.bzl
index 2e97623..9295d98 100644
--- a/tools/flake8/flake8.bzl
+++ b/tools/flake8/flake8.bzl
@@ -19,7 +19,7 @@ def _flake8_aspect_impl(target, ctx):
                     files.append(f)
 
         if files:
-            report = ctx.actions.declare_file(ctx.label.name + ".report")
+            report = ctx.actions.declare_file(ctx.label.name + ".flake.report")
         else:
             return []