Browse Source

Reformat code with black -S

Signed-off-by: Aarni Koskela <akx@iki.fi>
Aarni Koskela 5 năm trước cách đây
mục cha
commit
ea4ce19077

+ 0 - 1
python/labours/_vendor/swivel.py

@@ -59,7 +59,6 @@ import threading
 import time
 
 import numpy as np
-
 import tensorflow as tf
 from tensorflow.python.client import device_lib
 

+ 38 - 31
python/labours/burndown.py

@@ -1,5 +1,5 @@
 from datetime import datetime, timedelta
-from typing import TYPE_CHECKING, List, Tuple
+from typing import List, Tuple, TYPE_CHECKING
 import warnings
 
 import numpy
@@ -38,7 +38,7 @@ def fit_kaplan_meier(matrix: numpy.ndarray) -> 'KaplanMeierFitter':
     W.append(matrix[nnzind, -1])
     T = numpy.concatenate(T)
     E = numpy.ones(len(T), bool)
-    E[-nnzind.sum():] = 0
+    E[-nnzind.sum() :] = 0
     W = numpy.concatenate(W)
     if T.size == 0:
         return None
@@ -51,20 +51,17 @@ def print_survival_function(kmf: 'KaplanMeierFitter', sampling: int) -> None:
     sf.index = [timedelta(days=d) for d in sf.index * sampling]
     sf.columns = ["Ratio of survived lines"]
     try:
-        print(sf[len(sf) // 6::len(sf) // 6].append(sf.tail(1)))
+        print(sf[len(sf) // 6 :: len(sf) // 6].append(sf.tail(1)))
     except ValueError:
         pass
 
 
 def interpolate_burndown_matrix(
-    matrix: numpy.ndarray,
-    granularity: int,
-    sampling: int,
-    progress: bool = False
+    matrix: numpy.ndarray, granularity: int, sampling: int, progress: bool = False
 ) -> numpy.ndarray:
     daily = numpy.zeros(
-        (matrix.shape[0] * granularity, matrix.shape[1] * sampling),
-        dtype=numpy.float32)
+        (matrix.shape[0] * granularity, matrix.shape[1] * sampling), dtype=numpy.float32
+    )
     """
     ----------> samples, x
     |
@@ -88,7 +85,8 @@ def interpolate_burndown_matrix(
                     initial = daily[i][start_index - 1]
                     for j in range(start_index, (x + 1) * sampling):
                         daily[i][j] = initial * (
-                            1 + (k - 1) * (j - start_index + 1) / scale)
+                            1 + (k - 1) * (j - start_index + 1) / scale
+                        )
 
             def grow(finish_index: int, finish_val: float):
                 initial = matrix[y][x - 1] if x > 0 else 0
@@ -171,7 +169,9 @@ def interpolate_burndown_matrix(
                         # y*s <= (x+1)*g <= (y+1)*s < (y+2)*s
                         #           ^.........|_________|
                         k = (v2 - matrix[y][x + 1]) / sampling  # > 0
-                        peak = matrix[y][x] + k * ((x + 1) * sampling - (y + 1) * granularity)
+                        peak = matrix[y][x] + k * (
+                            (x + 1) * sampling - (y + 1) * granularity
+                        )
                         # peak > v2 > v1
                     else:
                         peak = v2
@@ -187,8 +187,10 @@ def interpolate_burndown_matrix(
 
 def import_pandas():
     import pandas
+
     try:
         from pandas.plotting import register_matplotlib_converters
+
         register_matplotlib_converters()
     except ImportError:
         pass
@@ -201,7 +203,7 @@ def load_burndown(
     matrix: numpy.ndarray,
     resample: str,
     report_survival: bool = True,
-    interpolation_progress: bool = False
+    interpolation_progress: bool = False,
 ) -> Tuple[str, numpy.ndarray, 'DatetimeIndex', List[int], int, int, str]:
     pandas = import_pandas()
 
@@ -226,43 +228,43 @@ def load_burndown(
             sampling=sampling,
             progress=interpolation_progress,
         )
-        daily[(last - start).days:] = 0
+        daily[(last - start).days :] = 0
         # Resample the bands
-        aliases = {
-            "year": "A",
-            "month": "M"
-        }
+        aliases = {"year": "A", "month": "M"}
         resample = aliases.get(resample, resample)
         periods = 0
         date_granularity_sampling = [start]
         while date_granularity_sampling[-1] < finish:
             periods += 1
             date_granularity_sampling = pandas.date_range(
-                start, periods=periods, freq=resample)
+                start, periods=periods, freq=resample
+            )
         if date_granularity_sampling[0] > finish:
             if resample == "A":
                 print("too loose resampling - by year, trying by month")
-                return load_burndown(header, name, matrix, "month", report_survival=False)
+                return load_burndown(
+                    header, name, matrix, "month", report_survival=False
+                )
             else:
                 raise ValueError("Too loose resampling: %s. Try finer." % resample)
         date_range_sampling = pandas.date_range(
             date_granularity_sampling[0],
             periods=(finish - date_granularity_sampling[0]).days,
-            freq="1D")
+            freq="1D",
+        )
         # Fill the new square matrix
         matrix = numpy.zeros(
             (len(date_granularity_sampling), len(date_range_sampling)),
-            dtype=numpy.float32)
+            dtype=numpy.float32,
+        )
         for i, gdt in enumerate(date_granularity_sampling):
-            istart = (date_granularity_sampling[i - 1] - start).days \
-                if i > 0 else 0
+            istart = (date_granularity_sampling[i - 1] - start).days if i > 0 else 0
             ifinish = (gdt - start).days
 
             for j, sdt in enumerate(date_range_sampling):
                 if (sdt - start).days >= istart:
                     break
-            matrix[i, j:] = \
-                daily[istart:ifinish, (sdt - start).days:].sum(axis=0)
+            matrix[i, j:] = daily[istart:ifinish, (sdt - start).days :].sum(axis=0)
         # Hardcode some cases to improve labels' readability
         if resample in ("year", "A"):
             labels = [dt.year for dt in date_granularity_sampling]
@@ -272,14 +274,19 @@ def load_burndown(
             labels = [dt.date() for dt in date_granularity_sampling]
     else:
         labels = [
-            "%s - %s" % ((start + timedelta(seconds=i * granularity * tick)).date(),
-            (
-                start + timedelta(seconds=(i + 1) * granularity * tick)).date())
-            for i in range(matrix.shape[0])]
+            "%s - %s"
+            % (
+                (start + timedelta(seconds=i * granularity * tick)).date(),
+                (start + timedelta(seconds=(i + 1) * granularity * tick)).date(),
+            )
+            for i in range(matrix.shape[0])
+        ]
         if len(labels) > 18:
             warnings.warn("Too many labels - consider resampling.")
         resample = "M"  # fake resampling type is checked while plotting
         date_range_sampling = pandas.date_range(
-            start + timedelta(seconds=sampling * tick), periods=matrix.shape[1],
-            freq="%dD" % sampling)
+            start + timedelta(seconds=sampling * tick),
+            periods=matrix.shape[1],
+            freq="%dD" % sampling,
+        )
     return name, matrix, date_range_sampling, labels, granularity, sampling, resample

+ 210 - 84
python/labours/cli.py

@@ -24,8 +24,10 @@ from labours.readers import read_input
 
 
 def list_matplotlib_styles() -> List[str]:
-    script = "import sys; from matplotlib import pyplot; " \
-             "sys.stdout.write(repr(pyplot.style.available))"
+    script = (
+        "import sys; from matplotlib import pyplot; "
+        "sys.stdout.write(repr(pyplot.style.available))"
+    )
     styles = eval(subprocess.check_output([sys.executable, "-c", script]))
     styles.remove("classic")
     return ["default", "classic"] + styles
@@ -33,53 +35,109 @@ def list_matplotlib_styles() -> List[str]:
 
 def parse_args() -> Namespace:
     parser = argparse.ArgumentParser()
-    parser.add_argument("-o", "--output", default="",
-                        help="Path to the output file/directory (empty for display). "
-                             "If the extension is JSON, the data is saved instead of "
-                             "the real image.")
-    parser.add_argument("-i", "--input", default="-",
-                        help="Path to the input file (- for stdin).")
-    parser.add_argument("-f", "--input-format", default="auto", choices=["yaml", "pb", "auto"])
-    parser.add_argument("--font-size", default=12, type=int,
-                        help="Size of the labels and legend.")
-    parser.add_argument("--style", default="ggplot", choices=list_matplotlib_styles(),
-                        help="Plot style to use.")
+    parser.add_argument(
+        "-o",
+        "--output",
+        default="",
+        help="Path to the output file/directory (empty for display). "
+        "If the extension is JSON, the data is saved instead of "
+        "the real image.",
+    )
+    parser.add_argument(
+        "-i", "--input", default="-", help="Path to the input file (- for stdin)."
+    )
+    parser.add_argument(
+        "-f", "--input-format", default="auto", choices=["yaml", "pb", "auto"]
+    )
+    parser.add_argument(
+        "--font-size", default=12, type=int, help="Size of the labels and legend."
+    )
+    parser.add_argument(
+        "--style",
+        default="ggplot",
+        choices=list_matplotlib_styles(),
+        help="Plot style to use.",
+    )
     parser.add_argument("--backend", help="Matplotlib backend to use.")
-    parser.add_argument("--background", choices=["black", "white"], default="white",
-                        help="Plot's general color scheme.")
+    parser.add_argument(
+        "--background",
+        choices=["black", "white"],
+        default="white",
+        help="Plot's general color scheme.",
+    )
     parser.add_argument("--size", help="Axes' size in inches, for example \"12,9\"")
-    parser.add_argument("--relative", action="store_true",
-                        help="Occupy 100%% height for every measurement.")
+    parser.add_argument(
+        "--relative",
+        action="store_true",
+        help="Occupy 100%% height for every measurement.",
+    )
     parser.add_argument("--tmpdir", help="Temporary directory for intermediate files.")
-    parser.add_argument("-m", "--mode", dest="modes", default=[], action="append",
-                        choices=["burndown-project", "burndown-file", "burndown-person",
-                                 "overwrites-matrix", "ownership", "couples-files",
-                                 "couples-people", "couples-shotness", "shotness", "sentiment",
-                                 "devs", "devs-efforts", "old-vs-new", "run-times",
-                                 "languages", "devs-parallel", "all"],
-                        help="What to plot. Can be repeated, e.g. "
-                             "-m burndown-project -m run-times")
     parser.add_argument(
-        "--resample", default="year",
+        "-m",
+        "--mode",
+        dest="modes",
+        default=[],
+        action="append",
+        choices=[
+            "burndown-project",
+            "burndown-file",
+            "burndown-person",
+            "overwrites-matrix",
+            "ownership",
+            "couples-files",
+            "couples-people",
+            "couples-shotness",
+            "shotness",
+            "sentiment",
+            "devs",
+            "devs-efforts",
+            "old-vs-new",
+            "run-times",
+            "languages",
+            "devs-parallel",
+            "all",
+        ],
+        help="What to plot. Can be repeated, e.g. " "-m burndown-project -m run-times",
+    )
+    parser.add_argument(
+        "--resample",
+        default="year",
         help="The way to resample the time series. Possible values are: "
-             "\"month\", \"year\", \"no\", \"raw\" and pandas offset aliases ("
-             "http://pandas.pydata.org/pandas-docs/stable/timeseries.html"
-             "#offset-aliases).")
-    dateutil_url = "https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse"
-    parser.add_argument("--start-date",
-                        help="Start date of time-based plots. Any format is accepted which is "
-                             "supported by %s" % dateutil_url)
-    parser.add_argument("--end-date",
-                        help="End date of time-based plots. Any format is accepted which is "
-                             "supported by %s" % dateutil_url)
-    parser.add_argument("--disable-projector", action="store_true",
-                        help="Do not run Tensorflow Projector on couples.")
-    parser.add_argument("--max-people", default=20, type=int,
-                        help="Maximum number of developers in overwrites matrix and people plots.")
-    parser.add_argument("--order-ownership-by-time", action="store_true",
-                        help="Sort developers in the ownership plot according to their first "
-                             "appearance in the history. The default is sorting by the number of "
-                             "commits.")
+        "\"month\", \"year\", \"no\", \"raw\" and pandas offset aliases ("
+        "http://pandas.pydata.org/pandas-docs/stable/timeseries.html"
+        "#offset-aliases).",
+    )
+    dateutil_url = (
+        "https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.parse"
+    )
+    parser.add_argument(
+        "--start-date",
+        help="Start date of time-based plots. Any format is accepted which is "
+        "supported by %s" % dateutil_url,
+    )
+    parser.add_argument(
+        "--end-date",
+        help="End date of time-based plots. Any format is accepted which is "
+        "supported by %s" % dateutil_url,
+    )
+    parser.add_argument(
+        "--disable-projector",
+        action="store_true",
+        help="Do not run Tensorflow Projector on couples.",
+    )
+    parser.add_argument(
+        "--max-people",
+        default=20,
+        type=int,
+        help="Maximum number of developers in overwrites matrix and people plots.",
+    )
+    parser.add_argument(
+        "--order-ownership-by-time",
+        action="store_true",
+        help="Sort developers in the ownership plot according to their first "
+        "appearance in the history. The default is sorting by the number of "
+        "commits.",
+    )
     args = parser.parse_args()
     return args
 
@@ -90,23 +148,35 @@ def main() -> None:
     header = reader.get_header()
     name = reader.get_name()
 
-    burndown_warning = "Burndown stats were not collected. Re-run hercules with --burndown."
-    burndown_files_warning = \
-        "Burndown stats for files were not collected. Re-run hercules with " \
+    burndown_warning = (
+        "Burndown stats were not collected. Re-run hercules with --burndown."
+    )
+    burndown_files_warning = (
+        "Burndown stats for files were not collected. Re-run hercules with "
         "--burndown --burndown-files."
-    burndown_people_warning = \
-        "Burndown stats for people were not collected. Re-run hercules with " \
+    )
+    burndown_people_warning = (
+        "Burndown stats for people were not collected. Re-run hercules with "
         "--burndown --burndown-people."
-    couples_warning = "Coupling stats were not collected. Re-run hercules with --couples."
-    shotness_warning = "Structural hotness stats were not collected. Re-run hercules with " \
-                       "--shotness. Also check --languages - the output may be empty."
-    sentiment_warning = "Sentiment stats were not collected. Re-run hercules with --sentiment."
+    )
+    couples_warning = (
+        "Coupling stats were not collected. Re-run hercules with --couples."
+    )
+    shotness_warning = (
+        "Structural hotness stats were not collected. Re-run hercules with "
+        "--shotness. Also check --languages - the output may be empty."
+    )
+    sentiment_warning = (
+        "Sentiment stats were not collected. Re-run hercules with --sentiment."
+    )
     devs_warning = "Devs stats were not collected. Re-run hercules with --devs."
 
     def run_times():
         rt = reader.get_run_times()
         pandas = import_pandas()
-        series = pandas.to_timedelta(pandas.Series(rt).sort_values(ascending=False), unit="s")
+        series = pandas.to_timedelta(
+            pandas.Series(rt).sort_values(ascending=False), unit="s"
+        )
         df = pandas.concat([series, series / series.sum()], axis=1)
         df.columns = ["time", "ratio"]
         print(df)
@@ -117,9 +187,16 @@ def main() -> None:
         except KeyError:
             print("project: " + burndown_warning)
             return
-        plot_burndown(args, "project",
-                      *load_burndown(full_header, *reader.get_project_burndown(),
-                                     resample=args.resample, interpolation_progress=True))
+        plot_burndown(
+            args,
+            "project",
+            *load_burndown(
+                full_header,
+                *reader.get_project_burndown(),
+                resample=args.resample,
+                interpolation_progress=True,
+            ),
+        )
 
     def files_burndown():
         try:
@@ -139,27 +216,44 @@ def main() -> None:
             print(burndown_warning)
             return
         try:
-            plot_many_burndown(args, "person", full_header, reader.get_people_burndown())
+            plot_many_burndown(
+                args, "person", full_header, reader.get_people_burndown()
+            )
         except KeyError:
             print("people: " + burndown_people_warning)
 
     def overwrites_matrix():
         try:
 
-            plot_overwrites_matrix(args, name, *load_overwrites_matrix(
-                *reader.get_people_interaction(), max_people=args.max_people))
+            plot_overwrites_matrix(
+                args,
+                name,
+                *load_overwrites_matrix(
+                    *reader.get_people_interaction(), max_people=args.max_people
+                ),
+            )
             people, matrix = load_overwrites_matrix(
-                *reader.get_people_interaction(), max_people=1000000, normalize=False)
+                *reader.get_people_interaction(), max_people=1000000, normalize=False
+            )
             from scipy.sparse import csr_matrix
+
             matrix = matrix[:, 1:]
             matrix = numpy.triu(matrix) + numpy.tril(matrix).T
             matrix = matrix + matrix.T
             matrix = csr_matrix(matrix)
             try:
-                write_embeddings("overwrites", args.output, not args.disable_projector,
-                                 *train_embeddings(people, matrix, tmpdir=args.tmpdir))
+                write_embeddings(
+                    "overwrites",
+                    args.output,
+                    not args.disable_projector,
+                    *train_embeddings(people, matrix, tmpdir=args.tmpdir),
+                )
             except AttributeError as e:
-                print("Training the embeddings is not possible: %s: %s", type(e).__name__, e)
+                print(
+                    "Training the embeddings is not possible: %s: %s",
+                    type(e).__name__,
+                    e,
+                )
         except KeyError:
             print("overwrites_matrix: " + burndown_people_warning)
 
@@ -170,33 +264,49 @@ def main() -> None:
             print(burndown_warning)
             return
         try:
-            plot_ownership(args, name, *load_ownership(
-                full_header, *reader.get_ownership_burndown(), max_people=args.max_people,
-                order_by_time=args.order_ownership_by_time))
+            plot_ownership(
+                args,
+                name,
+                *load_ownership(
+                    full_header,
+                    *reader.get_ownership_burndown(),
+                    max_people=args.max_people,
+                    order_by_time=args.order_ownership_by_time,
+                ),
+            )
         except KeyError:
             print("ownership: " + burndown_people_warning)
 
     def couples_files():
         try:
-            write_embeddings("files", args.output, not args.disable_projector,
-                             *train_embeddings(*reader.get_files_coocc(),
-                                               tmpdir=args.tmpdir))
+            write_embeddings(
+                "files",
+                args.output,
+                not args.disable_projector,
+                *train_embeddings(*reader.get_files_coocc(), tmpdir=args.tmpdir),
+            )
         except KeyError:
             print(couples_warning)
 
     def couples_people():
         try:
-            write_embeddings("people", args.output, not args.disable_projector,
-                             *train_embeddings(*reader.get_people_coocc(),
-                                               tmpdir=args.tmpdir))
+            write_embeddings(
+                "people",
+                args.output,
+                not args.disable_projector,
+                *train_embeddings(*reader.get_people_coocc(), tmpdir=args.tmpdir),
+            )
         except KeyError:
             print(couples_warning)
 
     def couples_shotness():
         try:
-            write_embeddings("shotness", args.output, not args.disable_projector,
-                             *train_embeddings(*reader.get_shotness_coocc(),
-                                               tmpdir=args.tmpdir))
+            write_embeddings(
+                "shotness",
+                args.output,
+                not args.disable_projector,
+                *train_embeddings(*reader.get_shotness_coocc(), tmpdir=args.tmpdir),
+            )
         except KeyError:
             print(shotness_warning)
 
@@ -214,7 +324,9 @@ def main() -> None:
         except KeyError:
             print(sentiment_warning)
             return
-        show_sentiment_stats(args, reader.get_name(), args.resample, reader.get_header()[0], data)
+        show_sentiment_stats(
+            args, reader.get_name(), args.resample, reader.get_header()[0], data
+        )
 
     def devs():
         try:
@@ -222,8 +334,13 @@ def main() -> None:
         except KeyError:
             print(devs_warning)
             return
-        show_devs(args, reader.get_name(), *reader.get_header(), *data,
-                  max_people=args.max_people)
+        show_devs(
+            args,
+            reader.get_name(),
+            *reader.get_header(),
+            *data,
+            max_people=args.max_people,
+        )
 
     def devs_efforts():
         try:
@@ -231,8 +348,13 @@ def main() -> None:
         except KeyError:
             print(devs_warning)
             return
-        show_devs_efforts(args, reader.get_name(), *reader.get_header(), *data,
-                          max_people=args.max_people)
+        show_devs_efforts(
+            args,
+            reader.get_name(),
+            *reader.get_header(),
+            *data,
+            max_people=args.max_people,
+        )
 
     def old_vs_new():
         try:
@@ -266,8 +388,12 @@ def main() -> None:
         except KeyError:
             print(devs_warning)
             return
-        show_devs_parallel(args, reader.get_name(), *reader.get_header(),
-                           load_devs_parallel(ownership, couples, devs, args.max_people))
+        show_devs_parallel(
+            args,
+            reader.get_name(),
+            *reader.get_header(),
+            load_devs_parallel(ownership, couples, devs, args.max_people),
+        )
 
     modes = {
         "run-times": run_times,
@@ -311,7 +437,7 @@ def main() -> None:
 
         print("Running: %s" % mode)
         # `args.mode` is required for path determination in the mode functions
-        args.mode = ("all" if all_mode else mode)
+        args.mode = "all" if all_mode else mode
         try:
             modes[mode]()
         except ImportError as ie:

+ 33 - 17
python/labours/embeddings.py

@@ -16,7 +16,7 @@ def train_embeddings(
     index: List[str],
     matrix: csr_matrix,
     tmpdir: None,
-    shard_size: int = IDEAL_SHARD_SIZE
+    shard_size: int = IDEAL_SHARD_SIZE,
 ) -> Tuple[List[Tuple[str, numpy.int64]], List[numpy.ndarray]]:
     import tensorflow as tf
     from labours._vendor import swivel
@@ -43,7 +43,9 @@ def train_embeddings(
     for i, j in enumerate(filtered):
         meta_index.append((index[j], matrix[i, i]))
     index = [mi[0] for mi in meta_index]
-    with tempfile.TemporaryDirectory(prefix="hercules_labours_", dir=tmpdir or None) as tmproot:
+    with tempfile.TemporaryDirectory(
+        prefix="hercules_labours_", dir=tmpdir or None
+    ) as tmproot:
         print("Writing Swivel metadata...")
         vocabulary = "\n".join(index)
         with open(os.path.join(tmproot, "row_vocab.txt"), "w") as out:
@@ -63,26 +65,36 @@ def train_embeddings(
         print("Writing Swivel shards...")
         for row in range(nshards):
             for col in range(nshards):
+
                 def _int64s(xs):
                     return tf.train.Feature(
-                        int64_list=tf.train.Int64List(value=list(xs)))
+                        int64_list=tf.train.Int64List(value=list(xs))
+                    )
 
                 def _floats(xs):
                     return tf.train.Feature(
-                        float_list=tf.train.FloatList(value=list(xs)))
+                        float_list=tf.train.FloatList(value=list(xs))
+                    )
 
                 indices_row = reorder[row::nshards]
                 indices_col = reorder[col::nshards]
                 shard = matrix[indices_row][:, indices_col].tocoo()
 
-                example = tf.train.Example(features=tf.train.Features(feature={
-                    "global_row": _int64s(indices_row),
-                    "global_col": _int64s(indices_col),
-                    "sparse_local_row": _int64s(shard.row),
-                    "sparse_local_col": _int64s(shard.col),
-                    "sparse_value": _floats(shard.data)}))
+                example = tf.train.Example(
+                    features=tf.train.Features(
+                        feature={
+                            "global_row": _int64s(indices_row),
+                            "global_col": _int64s(indices_col),
+                            "sparse_local_row": _int64s(shard.row),
+                            "sparse_local_col": _int64s(shard.col),
+                            "sparse_value": _floats(shard.data),
+                        }
+                    )
+                )
 
-                with open(os.path.join(tmproot, "shard-%03d-%03d.pb" % (row, col)), "wb") as out:
+                with open(
+                    os.path.join(tmproot, "shard-%03d-%03d.pb" % (row, col)), "wb"
+                ) as out:
                     out.write(example.SerializeToString())
         print("Training Swivel model...")
         swivel.FLAGS.submatrix_rows = shard_size
@@ -131,9 +143,10 @@ def train_embeddings(
                 for i, (lrow, lcol) in enumerate(zip(frow, fcol)):
                     prow, pcol = (l.split("\t", 1) for l in (lrow, lcol))
                     assert prow[0] == pcol[0]
-                    erow, ecol = \
-                        (numpy.fromstring(p[1], dtype=numpy.float32, sep="\t")
-                         for p in (prow, pcol))
+                    erow, ecol = (
+                        numpy.fromstring(p[1], dtype=numpy.float32, sep="\t")
+                        for p in (prow, pcol)
+                    )
                     embeddings.append((erow + ecol) / 2)
     return meta_index, embeddings
 
@@ -143,7 +156,7 @@ def write_embeddings(
     output: str,
     run_server: bool,
     index: List[Tuple[str, numpy.int64]],
-    embeddings: List[numpy.ndarray]
+    embeddings: List[numpy.ndarray],
 ) -> None:
     print("Writing Tensorflow Projector files...")
     if not output:
@@ -165,7 +178,8 @@ def write_embeddings(
     print("Wrote", dataf)
     jsonf = "%s_%s.json" % (output, name)
     with open(jsonf, "w") as fout:
-        fout.write("""{
+        fout.write(
+            """{
   "embeddings": [
     {
       "tensorName": "%s %s coupling",
@@ -175,7 +189,9 @@ def write_embeddings(
     }
   ]
 }
-""" % (output, name, len(embeddings), len(embeddings[0]), dataf, metaf))
+"""
+            % (output, name, len(embeddings), len(embeddings[0]), dataf, metaf)
+        )
     print("Wrote %s" % jsonf)
     if run_server and not web_server.running:
         web_server.start()

+ 14 - 8
python/labours/modes/burndown.py

@@ -25,7 +25,7 @@ def plot_burndown(
     labels: List[int],
     granularity: int,
     sampling: int,
-    resample: str
+    resample: str,
 ) -> None:
     if args.output and args.output.endswith(".json"):
         data = locals().copy()
@@ -54,10 +54,13 @@ def plot_burndown(
     legend = pyplot.legend(loc=legend_loc, fontsize=args.font_size)
     pyplot.ylabel("Lines of code")
     pyplot.xlabel("Time")
-    apply_plot_style(pyplot.gcf(), pyplot.gca(), legend, args.background,
-                     args.font_size, args.size)
-    pyplot.xlim(parse_date(args.start_date, date_range_sampling[0]),
-                parse_date(args.end_date, date_range_sampling[-1]))
+    apply_plot_style(
+        pyplot.gcf(), pyplot.gca(), legend, args.background, args.font_size, args.size
+    )
+    pyplot.xlim(
+        parse_date(args.start_date, date_range_sampling[0]),
+        parse_date(args.end_date, date_range_sampling[-1]),
+    )
     locator = pyplot.gca().xaxis.get_major_locator()
     # set the optimal xticks locator
     if "M" not in resample:
@@ -91,8 +94,9 @@ def plot_burndown(
         labels[endindex].set_text = lambda _: None
         labels[endindex].set_rotation(30)
         labels[endindex].set_ha("right")
-    title = "%s %d x %d (granularity %d, sampling %d)" % \
-        ((name,) + matrix.shape + (granularity, sampling))
+    title = "%s %d x %d (granularity %d, sampling %d)" % (
+        (name,) + matrix.shape + (granularity, sampling)
+    )
     output = args.output
     if output:
         if args.mode == "project" and target == "project":
@@ -110,5 +114,7 @@ def plot_many_burndown(args: Namespace, target: str, header, parts):
     stdout = io.StringIO()
     for name, matrix in tqdm.tqdm(parts):
         with contextlib.redirect_stdout(stdout):
-            plot_burndown(args, target, *load_burndown(header, name, matrix, args.resample))
+            plot_burndown(
+                args, target, *load_burndown(header, name, matrix, args.resample)
+            )
     sys.stdout.write(stdout.getvalue())

+ 91 - 37
python/labours/modes/devs.py

@@ -19,7 +19,7 @@ def show_devs(
     end_date: int,
     people: List[str],
     days: Dict[int, Dict[int, DevDay]],
-    max_people: int = 50
+    max_people: int = 50,
 ) -> None:
     from scipy.signal import convolve, slepian
 
@@ -64,7 +64,9 @@ def show_devs(
     prop_cycle = pyplot.rcParams["axes.prop_cycle"]
     colors = prop_cycle.by_key()["color"]
     fig, axes = pyplot.subplots(final.shape[0], 1)
-    backgrounds = ("#C4FFDB", "#FFD0CD") if args.background == "white" else ("#05401C", "#40110E")
+    backgrounds = (
+        ("#C4FFDB", "#FFD0CD") if args.background == "white" else ("#05401C", "#40110E")
+    )
     max_cluster = numpy.max(clusters)
     for ax, series, cluster, dev_i in zip(axes, final, clusters, route):
         if cluster >= 0:
@@ -79,31 +81,61 @@ def show_devs(
         ax.fill_between(plot_x, series, color=color)
         ax.set_axis_off()
         author = people[dev_i]
-        ax.text(0.03, 0.5, author[:36] + (author[36:] and "..."),
-                horizontalalignment="right", verticalalignment="center",
-                transform=ax.transAxes, fontsize=args.font_size,
-                color="black" if args.background == "white" else "white")
+        ax.text(
+            0.03,
+            0.5,
+            author[:36] + (author[36:] and "..."),
+            horizontalalignment="right",
+            verticalalignment="center",
+            transform=ax.transAxes,
+            fontsize=args.font_size,
+            color="black" if args.background == "white" else "white",
+        )
         ds = devstats[dev_i]
-        stats = "%5d %8s %8s" % (ds[0], _format_number(ds[1] - ds[2]), _format_number(ds[3]))
-        ax.text(0.97, 0.5, stats,
-                horizontalalignment="left", verticalalignment="center",
-                transform=ax.transAxes, fontsize=args.font_size, family="monospace",
-                backgroundcolor=backgrounds[ds[1] <= ds[2]],
-                color="black" if args.background == "white" else "white")
-    axes[0].text(0.97, 1.75, " cmts    delta  changed",
-                 horizontalalignment="left", verticalalignment="center",
-                 transform=axes[0].transAxes, fontsize=args.font_size, family="monospace",
-                 color="black" if args.background == "white" else "white")
+        stats = "%5d %8s %8s" % (
+            ds[0],
+            _format_number(ds[1] - ds[2]),
+            _format_number(ds[3]),
+        )
+        ax.text(
+            0.97,
+            0.5,
+            stats,
+            horizontalalignment="left",
+            verticalalignment="center",
+            transform=ax.transAxes,
+            fontsize=args.font_size,
+            family="monospace",
+            backgroundcolor=backgrounds[ds[1] <= ds[2]],
+            color="black" if args.background == "white" else "white",
+        )
+    axes[0].text(
+        0.97,
+        1.75,
+        " cmts    delta  changed",
+        horizontalalignment="left",
+        verticalalignment="center",
+        transform=axes[0].transAxes,
+        fontsize=args.font_size,
+        family="monospace",
+        color="black" if args.background == "white" else "white",
+    )
     axes[-1].set_axis_on()
     target_num_labels = 12
-    num_months = (end_date.year - start_date.year) * 12 + end_date.month - start_date.month
+    num_months = (
+        (end_date.year - start_date.year) * 12 + end_date.month - start_date.month
+    )
     interval = int(numpy.ceil(num_months / target_num_labels))
     if interval >= 8:
         interval = int(numpy.ceil(num_months / (12 * target_num_labels)))
-        axes[-1].xaxis.set_major_locator(matplotlib.dates.YearLocator(base=max(1, interval // 12)))
+        axes[-1].xaxis.set_major_locator(
+            matplotlib.dates.YearLocator(base=max(1, interval // 12))
+        )
         axes[-1].xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%Y"))
     else:
-        axes[-1].xaxis.set_major_locator(matplotlib.dates.MonthLocator(interval=interval))
+        axes[-1].xaxis.set_major_locator(
+            matplotlib.dates.MonthLocator(interval=interval)
+        )
         axes[-1].xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%Y-%m"))
     for tick in axes[-1].xaxis.get_major_ticks():
         tick.label.set_fontsize(args.font_size)
@@ -122,20 +154,25 @@ def show_devs(
 
 
 def order_commits(
-    chosen_people: Set[str],
-    days: Dict[int, Dict[int, DevDay]],
-    people: List[str]
+    chosen_people: Set[str], days: Dict[int, Dict[int, DevDay]], people: List[str]
 ) -> Tuple[numpy.ndarray, defaultdict, defaultdict, List[int]]:
     from seriate import seriate
+
     try:
         from fastdtw import fastdtw
     except ImportError as e:
-        print("Cannot import fastdtw: %s\nInstall it from https://github.com/slaypni/fastdtw" % e)
+        print(
+            "Cannot import fastdtw: %s\nInstall it from https://github.com/slaypni/fastdtw"
+            % e
+        )
         sys.exit(1)
     # FIXME(vmarkovtsev): remove once https://github.com/slaypni/fastdtw/pull/28 is merged&released
     try:
-        sys.modules["fastdtw.fastdtw"].__norm = lambda p: lambda a, b: numpy.linalg.norm(
-            numpy.atleast_1d(a) - numpy.atleast_1d(b), p)
+        sys.modules[
+            "fastdtw.fastdtw"
+        ].__norm = lambda p: lambda a, b: numpy.linalg.norm(
+            numpy.atleast_1d(a) - numpy.atleast_1d(b), p
+        )
     except KeyError:
         # the native extension does not have this bug
         pass
@@ -160,7 +197,7 @@ def order_commits(
     with tqdm.tqdm() as pb:
         for x, serx in enumerate(series):
             dists[x, x] = 0
-            for y, sery in enumerate(series[x + 1:], start=x + 1):
+            for y, sery in enumerate(series[x + 1 :], start=x + 1):
                 min_day = int(min(serx[0][0], sery[0][0]))
                 max_day = int(max(serx[-1][0], sery[-1][0]))
                 arrx = numpy.zeros(max_day - min_day + 1, dtype=numpy.float32)
@@ -176,15 +213,20 @@ def order_commits(
     return dists, devseries, devstats, route
 
 
-def hdbscan_cluster_routed_series(dists: numpy.ndarray, route: List[int]) -> numpy.ndarray:
+def hdbscan_cluster_routed_series(
+    dists: numpy.ndarray, route: List[int]
+) -> numpy.ndarray:
     try:
         from hdbscan import HDBSCAN
     except ImportError as e:
         print("Cannot import hdbscan: %s" % e)
         sys.exit(1)
 
-    opt_dist_chain = numpy.cumsum(numpy.array(
-        [0] + [dists[route[i], route[i + 1]] for i in range(len(route) - 1)]))
+    opt_dist_chain = numpy.cumsum(
+        numpy.array(
+            [0] + [dists[route[i], route[i + 1]] for i in range(len(route) - 1)]
+        )
+    )
     clusters = HDBSCAN(min_cluster_size=2).fit_predict(opt_dist_chain[:, numpy.newaxis])
     return clusters
 
@@ -196,7 +238,7 @@ def show_devs_efforts(
     end_date: int,
     people: List[str],
     days: Dict[int, Dict[int, DevDay]],
-    max_people: int
+    max_people: int,
 ) -> None:
     from scipy.signal import convolve, slepian
 
@@ -210,15 +252,21 @@ def show_devs_efforts(
         for dev, stats in devs.items():
             efforts_by_dev[dev] += stats.Added + stats.Removed + stats.Changed
     if len(efforts_by_dev) > max_people:
-        chosen = {v for k, v in sorted(
-            ((v, k) for k, v in efforts_by_dev.items()), reverse=True)[:max_people]}
+        chosen = {
+            v
+            for k, v in sorted(
+                ((v, k) for k, v in efforts_by_dev.items()), reverse=True
+            )[:max_people]
+        }
         print("Warning: truncated people to the most active %d" % max_people)
     else:
         chosen = set(efforts_by_dev)
     chosen_efforts = sorted(((efforts_by_dev[k], k) for k in chosen), reverse=True)
     chosen_order = {k: i for i, (_, k) in enumerate(chosen_efforts)}
 
-    efforts = numpy.zeros((len(chosen) + 1, (end_date - start_date).days + 1), dtype=numpy.float32)
+    efforts = numpy.zeros(
+        (len(chosen) + 1, (end_date - start_date).days + 1), dtype=numpy.float32
+    )
     for day, devs in days.items():
         if day < efforts.shape[1]:
             for dev, stats in devs.items():
@@ -229,9 +277,9 @@ def show_devs_efforts(
     window /= window.sum()
     for e in (efforts, efforts_cum):
         for i in range(e.shape[0]):
-            ending = e[i][-len(window) * 2:].copy()
+            ending = e[i][-len(window) * 2 :].copy()
             e[i] = convolve(e[i], window, "same")
-            e[i][-len(ending):] = ending
+            e[i][-len(ending) :] = ending
     matplotlib, pyplot = import_pyplot(args.backend, args.style)
     plot_x = [start_date + timedelta(days=i) for i in range(efforts.shape[1])]
 
@@ -252,8 +300,14 @@ def show_devs_efforts(
             yticks.append(tick[1])
     pyplot.gca().yaxis.set_ticks(yticks)
     legend = pyplot.legend(loc=2, ncol=2, fontsize=args.font_size)
-    apply_plot_style(pyplot.gcf(), pyplot.gca(), legend, args.background,
-                     args.font_size, args.size or "16,10")
+    apply_plot_style(
+        pyplot.gcf(),
+        pyplot.gca(),
+        legend,
+        args.background,
+        args.font_size,
+        args.size or "16,10",
+    )
     if args.mode == "all" and args.output:
         output = get_plot_path(args.output, "efforts")
     else:

+ 41 - 19
python/labours/modes/devs_parallel.py

@@ -14,14 +14,17 @@ def load_devs_parallel(
     ownership: Tuple[List[Any], Dict[Any, Any]],
     couples: Tuple[List[str], csr_matrix],
     devs: Tuple[List[str], Dict[int, Dict[int, DevDay]]],
-    max_people: int
+    max_people: int,
 ):
     from seriate import seriate
+
     try:
         from hdbscan import HDBSCAN
     except ImportError as e:
-        print("Cannot import ortools: %s\nInstall it from "
-              "https://developers.google.com/optimization/install/python/" % e)
+        print(
+            "Cannot import ortools: %s\nInstall it from "
+            "https://developers.google.com/optimization/install/python/" % e
+        )
         sys.exit(1)
 
     people, owned = ownership
@@ -33,8 +36,12 @@ def load_devs_parallel(
     for day, devs in days.items():
         for dev, stats in devs.items():
             commits[people[dev]] += stats.Commits
-    chosen = [k for v, k in sorted(((v, k) for k, v in commits.items()),
-                                   reverse=True)[:max_people]]
+    chosen = [
+        k
+        for v, k in sorted(((v, k) for k, v in commits.items()), reverse=True)[
+            :max_people
+        ]
+    ]
     result = {k: ParallelDevData() for k in chosen}
     for k, v in result.items():
         v.commits_rank = chosen.index(k)
@@ -45,22 +52,31 @@ def load_devs_parallel(
     for day, devs in days.items():
         for dev, stats in devs.items():
             lines[people[dev]] += stats.Added + stats.Removed + stats.Changed
-    lines_index = {k: i for i, (_, k) in enumerate(sorted(
-        ((v, k) for k, v in lines.items() if k in chosen), reverse=True))}
+    lines_index = {
+        k: i
+        for i, (_, k) in enumerate(
+            sorted(((v, k) for k, v in lines.items() if k in chosen), reverse=True)
+        )
+    }
     for k, v in result.items():
         v.lines_rank = lines_index[k]
         v.lines = lines[k]
 
     print("calculating - ownership")
-    owned_index = {k: i for i, (_, k) in enumerate(sorted(
-        ((owned[k][-1].sum(), k) for k in chosen), reverse=True))}
+    owned_index = {
+        k: i
+        for i, (_, k) in enumerate(
+            sorted(((owned[k][-1].sum(), k) for k in chosen), reverse=True)
+        )
+    }
     for k, v in result.items():
         v.ownership_rank = owned_index[k]
         v.ownership = owned[k][-1].sum()
 
     print("calculating - couples")
     embeddings = numpy.genfromtxt(fname="couples_people_data.tsv", delimiter="\t")[
-        [people.index(k) for k in chosen]]
+        [people.index(k) for k in chosen]
+    ]
     embeddings /= numpy.linalg.norm(embeddings, axis=1)[:, None]
     cos = embeddings.dot(embeddings.T)
     cos[cos > 1] = 1  # tiny precision faults
@@ -75,7 +91,9 @@ def load_devs_parallel(
         loss = 0
         for k, v in result.items():
             loss += abs(
-                v.ownership_rank - (couples_order.index(chosen.index(k)) + i) % len(chosen))
+                v.ownership_rank
+                - (couples_order.index(chosen.index(k)) + i) % len(chosen)
+            )
         roll_options.append(loss)
     best_roll = numpy.argmin(roll_options)
     couples_order = list(numpy.roll(couples_order, best_roll))
@@ -118,20 +136,24 @@ def show_devs_parallel(args, name, start_date, end_date, devs):
     # biggest = {k: max(getattr(d, k) for d in devs.values())
     #            for k in ("commits", "lines", "ownership")}
     for k, dev in devs.items():
-        points = numpy.array([
-            (1, dev.commits_rank),
-            (2, dev.lines_rank),
-            (3, dev.ownership_rank),
-            (4, dev.couples_index),
-            (5, dev.commit_coocc_index)],
-            dtype=float)
+        points = numpy.array(
+            [
+                (1, dev.commits_rank),
+                (2, dev.lines_rank),
+                (3, dev.ownership_rank),
+                (4, dev.couples_index),
+                (5, dev.commit_coocc_index),
+            ],
+            dtype=float,
+        )
         points[:, 1] = points[:, 1] / len(devs)
         splines = []
         for i in range(len(points) - 1):
             a, b, c, d = solve_equations(*points[i], *points[i + 1])
             x = numpy.linspace(i + 1, i + 2, 100)
             smooth_points = numpy.array(
-                [x, a * x ** 3 + b * x ** 2 + c * x + d]).T.reshape(-1, 1, 2)
+                [x, a * x ** 3 + b * x ** 2 + c * x + d]
+            ).T.reshape(-1, 1, 2)
             splines.append(smooth_points)
         points = numpy.concatenate(splines)
         segments = numpy.concatenate([points[:-1], points[1:]], axis=1)

+ 3 - 1
python/labours/modes/languages.py

@@ -20,7 +20,9 @@ def show_languages(
         for dev, stats in devs.items():
             for lang, vals in stats.Languages.items():
                 devlangs[dev][lang] += vals
-    devlangs = sorted(devlangs.items(), key=lambda p: -sum(x.sum() for x in p[1].values()))
+    devlangs = sorted(
+        devlangs.items(), key=lambda p: -sum(x.sum() for x in p[1].values())
+    )
     for dev, ls in devlangs:
         print()
         print("#", people[dev])

+ 7 - 3
python/labours/modes/old_vs_new.py

@@ -15,7 +15,7 @@ def show_old_vs_new(
     start_date: int,
     end_date: int,
     people: List[str],
-    days: Dict[int, Dict[int, DevDay]]
+    days: Dict[int, Dict[int, DevDay]],
 ) -> None:
     from scipy.signal import convolve, slepian
 
@@ -36,9 +36,13 @@ def show_old_vs_new(
     matplotlib, pyplot = import_pyplot(args.backend, args.style)
     plot_x = [start_date + timedelta(days=i) for i in range(len(new_lines))]
     pyplot.fill_between(plot_x, new_lines, color="#8DB843", label="Changed new lines")
-    pyplot.fill_between(plot_x, old_lines, color="#E14C35", label="Changed existing lines")
+    pyplot.fill_between(
+        plot_x, old_lines, color="#E14C35", label="Changed existing lines"
+    )
     pyplot.legend(loc=2, fontsize=args.font_size)
-    for tick in chain(pyplot.gca().xaxis.get_major_ticks(), pyplot.gca().yaxis.get_major_ticks()):
+    for tick in chain(
+        pyplot.gca().xaxis.get_major_ticks(), pyplot.gca().yaxis.get_major_ticks()
+    ):
         tick.label.set_fontsize(args.font_size)
     if args.mode == "all" and args.output:
         output = get_plot_path(args.output, "old_vs_new")

+ 7 - 2
python/labours/modes/overwrites.py

@@ -49,8 +49,13 @@ def plot_overwrites_matrix(args, repo, people, matrix):
     ax.set_yticks(numpy.arange(0, matrix.shape[0]))
     ax.set_yticklabels(people, va="center")
     ax.set_xticks(numpy.arange(0.5, matrix.shape[1] + 0.5), minor=True)
-    ax.set_xticklabels(["Unidentified"] + people, rotation=45, ha="left",
-                       va="bottom", rotation_mode="anchor")
+    ax.set_xticklabels(
+        ["Unidentified"] + people,
+        rotation=45,
+        ha="left",
+        va="bottom",
+        rotation_mode="anchor",
+    )
     ax.set_yticks(numpy.arange(0.5, matrix.shape[0] + 0.5), minor=True)
     ax.grid(False)
     ax.grid(which="minor")

+ 18 - 7
python/labours/modes/ownership.py

@@ -9,7 +9,13 @@ from labours.plotting import apply_plot_style, deploy_plot, get_plot_path, impor
 from labours.utils import default_json, floor_datetime, parse_date
 
 
-def load_ownership(header: Tuple[int, int, int, int, float], sequence: List[Any], contents: Dict[Any, Any], max_people: int, order_by_time: bool):
+def load_ownership(
+    header: Tuple[int, int, int, int, float],
+    sequence: List[Any],
+    contents: Dict[Any, Any],
+    max_people: int,
+    order_by_time: bool,
+):
     pandas = import_pandas()
 
     start, last, sampling, _, tick = header
@@ -21,13 +27,15 @@ def load_ownership(header: Tuple[int, int, int, int, float], sequence: List[Any]
         people.append(contents[name].sum(axis=1))
     people = numpy.array(people)
     date_range_sampling = pandas.date_range(
-        start + timedelta(seconds=sampling * tick), periods=people[0].shape[0],
-        freq="%dD" % sampling)
+        start + timedelta(seconds=sampling * tick),
+        periods=people[0].shape[0],
+        freq="%dD" % sampling,
+    )
 
     if people.shape[0] > max_people:
         chosen = numpy.argpartition(-numpy.sum(people, axis=1), max_people)
         others = people[chosen[max_people:]].sum(axis=0)
-        people = people[chosen[:max_people + 1]]
+        people = people[chosen[: max_people + 1]]
         people[max_people] = others
         sequence = [sequence[i] for i in chosen[:max_people]] + ["others"]
         print("Warning: truncated people to the most owning %d" % max_people)
@@ -68,7 +76,9 @@ def plot_ownership(args, repo, names, people, date_range, last):
     polys = pyplot.stackplot(date_range, people, labels=names)
     if names[-1] == "others":
         polys[-1].set_hatch("/")
-    pyplot.xlim(parse_date(args.start_date, date_range[0]), parse_date(args.end_date, last))
+    pyplot.xlim(
+        parse_date(args.start_date, date_range[0]), parse_date(args.end_date, last)
+    )
 
     if args.relative:
         for i in range(people.shape[1]):
@@ -79,8 +89,9 @@ def plot_ownership(args, repo, names, people, date_range, last):
         legend_loc = 2
     ncol = 1 if len(names) < 15 else 2
     legend = pyplot.legend(loc=legend_loc, fontsize=args.font_size, ncol=ncol)
-    apply_plot_style(pyplot.gcf(), pyplot.gca(), legend, args.background,
-                     args.font_size, args.size)
+    apply_plot_style(
+        pyplot.gcf(), pyplot.gca(), legend, args.background, args.font_size, args.size
+    )
     if args.mode == "all" and args.output:
         output = get_plot_path(args.output, "people")
     else:

+ 15 - 5
python/labours/modes/sentiment.py

@@ -14,7 +14,9 @@ def show_sentiment_stats(args, name, resample, start_date, data):
     start_date = datetime.fromtimestamp(start_date)
     data = sorted(data.items())
     mood = numpy.zeros(data[-1][0] + 1, dtype=numpy.float32)
-    timeline = numpy.array([start_date + timedelta(days=i) for i in range(mood.shape[0])])
+    timeline = numpy.array(
+        [start_date + timedelta(days=i) for i in range(mood.shape[0])]
+    )
     for d, val in data:
         mood[d] = (0.5 - val.Value) * 2
     resolution = 32
@@ -35,9 +37,13 @@ def show_sentiment_stats(args, name, resample, start_date, data):
     legend = pyplot.legend(loc=1, fontsize=args.font_size)
     pyplot.ylabel("Comment sentiment")
     pyplot.xlabel("Time")
-    apply_plot_style(pyplot.gcf(), pyplot.gca(), legend, args.background,
-                     args.font_size, args.size)
-    pyplot.xlim(parse_date(args.start_date, timeline[0]), parse_date(args.end_date, timeline[-1]))
+    apply_plot_style(
+        pyplot.gcf(), pyplot.gca(), legend, args.background, args.font_size, args.size
+    )
+    pyplot.xlim(
+        parse_date(args.start_date, timeline[0]),
+        parse_date(args.end_date, timeline[-1]),
+    )
     locator = pyplot.gca().xaxis.get_major_locator()
     # set the optimal xticks locator
     if "M" not in resample:
@@ -74,7 +80,11 @@ def show_sentiment_stats(args, name, resample, start_date, data):
     overall_pos = sum(2 * (0.5 - d[1].Value) for d in data if d[1].Value < 0.5)
     overall_neg = sum(2 * (d[1].Value - 0.5) for d in data if d[1].Value > 0.5)
     title = "%s sentiment +%.1f -%.1f δ=%.1f" % (
-        name, overall_pos, overall_neg, overall_pos - overall_neg)
+        name,
+        overall_pos,
+        overall_neg,
+        overall_pos - overall_neg,
+    )
     if args.mode == "all" and args.output:
         output = get_plot_path(args.output, "sentiment")
     else:

+ 10 - 6
python/labours/objects.py

@@ -1,7 +1,9 @@
 from collections import defaultdict, namedtuple
 
 
-class DevDay(namedtuple("DevDay", ("Commits", "Added", "Removed", "Changed", "Languages"))):
+class DevDay(
+    namedtuple("DevDay", ("Commits", "Added", "Removed", "Changed", "Languages"))
+):
     def add(self, dd: 'DevDay') -> 'DevDay':
         langs = defaultdict(lambda: [0] * 3)
         for key, val in self.Languages.items():
@@ -10,11 +12,13 @@ class DevDay(namedtuple("DevDay", ("Commits", "Added", "Removed", "Changed", "La
         for key, val in dd.Languages.items():
             for i in range(3):
                 langs[key][i] += val[i]
-        return DevDay(Commits=self.Commits + dd.Commits,
-                      Added=self.Added + dd.Added,
-                      Removed=self.Removed + dd.Removed,
-                      Changed=self.Changed + dd.Changed,
-                      Languages=dict(langs))
+        return DevDay(
+            Commits=self.Commits + dd.Commits,
+            Added=self.Added + dd.Added,
+            Removed=self.Removed + dd.Removed,
+            Changed=self.Changed + dd.Changed,
+            Languages=dict(langs),
+        )
 
 
 class ParallelDevData:

+ 2 - 0
python/labours/plotting.py

@@ -3,9 +3,11 @@ import os
 
 def import_pyplot(backend, style):
     import matplotlib
+
     if backend:
         matplotlib.use(backend)
     from matplotlib import pyplot
+
     pyplot.style.use(style)
     print("matplotlib: backend is", matplotlib.get_backend())
     return matplotlib, pyplot

+ 94 - 39
python/labours/readers.py

@@ -2,7 +2,7 @@ from argparse import Namespace
 from importlib import import_module
 import re
 import sys
-from typing import TYPE_CHECKING, Any, Dict, List, Tuple
+from typing import Any, Dict, List, Tuple, TYPE_CHECKING
 
 import numpy
 import yaml
@@ -66,7 +66,9 @@ class YamlReader(Reader):
         try:
             loader = yaml.CLoader
         except AttributeError:
-            print("Warning: failed to import yaml.CLoader, falling back to slow yaml.Loader")
+            print(
+                "Warning: failed to import yaml.CLoader, falling back to slow yaml.Loader"
+            )
             loader = yaml.Loader
         try:
             if file != "-":
@@ -75,8 +77,10 @@ class YamlReader(Reader):
             else:
                 data = yaml.load(sys.stdin, Loader=loader)
         except (UnicodeEncodeError, yaml.reader.ReaderError) as e:
-            print("\nInvalid unicode in the input: %s\nPlease filter it through "
-                  "fix_yaml_unicode.py" % e)
+            print(
+                "\nInvalid unicode in the input: %s\nPlease filter it through "
+                "fix_yaml_unicode.py" % e
+            )
             sys.exit(1)
         if data is None:
             print("\nNo data has been read - has Hercules crashed?")
@@ -98,25 +102,37 @@ class YamlReader(Reader):
         return header["sampling"], header["granularity"], header["tick_size"]
 
     def get_project_burndown(self):
-        return self.data["hercules"]["repository"], \
-            self._parse_burndown_matrix(self.data["Burndown"]["project"]).T
+        return (
+            self.data["hercules"]["repository"],
+            self._parse_burndown_matrix(self.data["Burndown"]["project"]).T,
+        )
 
     def get_files_burndown(self):
-        return [(p[0], self._parse_burndown_matrix(p[1]).T)
-                for p in self.data["Burndown"]["files"].items()]
+        return [
+            (p[0], self._parse_burndown_matrix(p[1]).T)
+            for p in self.data["Burndown"]["files"].items()
+        ]
 
     def get_people_burndown(self):
-        return [(p[0], self._parse_burndown_matrix(p[1]).T)
-                for p in self.data["Burndown"]["people"].items()]
+        return [
+            (p[0], self._parse_burndown_matrix(p[1]).T)
+            for p in self.data["Burndown"]["people"].items()
+        ]
 
     def get_ownership_burndown(self):
-        return self.data["Burndown"]["people_sequence"].copy(), \
-            {p[0]: self._parse_burndown_matrix(p[1])
-             for p in self.data["Burndown"]["people"].items()}
+        return (
+            self.data["Burndown"]["people_sequence"].copy(),
+            {
+                p[0]: self._parse_burndown_matrix(p[1])
+                for p in self.data["Burndown"]["people"].items()
+            },
+        )
 
     def get_people_interaction(self):
-        return self.data["Burndown"]["people_sequence"].copy(), \
-            self._parse_burndown_matrix(self.data["Burndown"]["people_interaction"])
+        return (
+            self.data["Burndown"]["people_sequence"].copy(),
+            self._parse_burndown_matrix(self.data["Burndown"]["people_interaction"]),
+        )
 
     def get_files_coocc(self):
         coocc = self.data["Couples"]["files_coocc"]
@@ -142,10 +158,12 @@ class YamlReader(Reader):
         indices = numpy.array(indices, dtype=numpy.int32)
         data = numpy.array(data, dtype=numpy.int32)
         from scipy.sparse import csr_matrix
+
         return index, csr_matrix((data, indices, indptr), shape=(len(shotness),) * 2)
 
     def get_shotness(self):
         from munch import munchify
+
         obj = munchify(self.data["Shotness"])
         # turn strings into ints
         for item in obj:
@@ -156,25 +174,37 @@ class YamlReader(Reader):
 
     def get_sentiment(self):
         from munch import munchify
-        return munchify({int(key): {
-            "Comments": vals[2].split("|"),
-            "Commits": vals[1],
-            "Value": float(vals[0])
-        } for key, vals in self.data["Sentiment"].items()})
+
+        return munchify(
+            {
+                int(key): {
+                    "Comments": vals[2].split("|"),
+                    "Commits": vals[1],
+                    "Value": float(vals[0]),
+                }
+                for key, vals in self.data["Sentiment"].items()
+            }
+        )
 
     def get_devs(self):
         people = self.data["Devs"]["people"]
-        days = {int(d): {int(dev): DevDay(*(int(x) for x in day[:-1]), day[-1])
-                         for dev, day in devs.items()}
-                for d, devs in self.data["Devs"]["ticks"].items()}
+        days = {
+            int(d): {
+                int(dev): DevDay(*(int(x) for x in day[:-1]), day[-1])
+                for dev, day in devs.items()
+            }
+            for d, devs in self.data["Devs"]["ticks"].items()
+        }
         return people, days
 
     def _parse_burndown_matrix(self, matrix):
-        return numpy.array([numpy.fromstring(line, dtype=int, sep=" ")
-                            for line in matrix.split("\n")])
+        return numpy.array(
+            [numpy.fromstring(line, dtype=int, sep=" ") for line in matrix.split("\n")]
+        )
 
     def _parse_coocc_matrix(self, matrix):
         from scipy.sparse import csr_matrix
+
         data = []
         indices = []
         indptr = [0]
@@ -191,8 +221,10 @@ class ProtobufReader(Reader):
         try:
             from labours.pb_pb2 import AnalysisResults
         except ImportError as e:
-            print("\n\n>>> You need to generate python/hercules/pb/pb_pb2.py - run \"make\"\n",
-                  file=sys.stderr)
+            print(
+                "\n\n>>> You need to generate python/hercules/pb/pb_pb2.py - run \"make\"\n",
+                file=sys.stderr,
+            )
             raise e from None
         self.data = AnalysisResults()
         if file != "-":
@@ -208,7 +240,9 @@ class ProtobufReader(Reader):
             try:
                 mod, name = PB_MESSAGES[key].rsplit(".", 1)
             except KeyError:
-                sys.stderr.write("Warning: there is no registered PB decoder for %s\n" % key)
+                sys.stderr.write(
+                    "Warning: there is no registered PB decoder for %s\n" % key
+                )
                 continue
             cls = getattr(import_module(mod), name)
             self.contents[key] = msg = cls()
@@ -235,7 +269,9 @@ class ProtobufReader(Reader):
         return [self._parse_burndown_matrix(i) for i in self.contents["Burndown"].files]
 
     def get_people_burndown(self) -> List[Any]:
-        return [self._parse_burndown_matrix(i) for i in self.contents["Burndown"].people]
+        return [
+            self._parse_burndown_matrix(i) for i in self.contents["Burndown"].people
+        ]
 
     def get_ownership_burndown(self) -> Tuple[List[Any], Dict[Any, Any]]:
         people = self.get_people_burndown()
@@ -243,8 +279,10 @@ class ProtobufReader(Reader):
 
     def get_people_interaction(self):
         burndown = self.contents["Burndown"]
-        return [i.name for i in burndown.people], \
-            self._parse_sparse_matrix(burndown.people_interaction).toarray()
+        return (
+            [i.name for i in burndown.people],
+            self._parse_sparse_matrix(burndown.people_interaction).toarray(),
+        )
 
     def get_files_coocc(self) -> Tuple[List[str], 'csr_matrix']:
         node = self.contents["Couples"].file_couples
@@ -270,6 +308,7 @@ class ProtobufReader(Reader):
         indices = numpy.array(indices, dtype=numpy.int32)
         data = numpy.array(data, dtype=numpy.int32)
         from scipy.sparse import csr_matrix
+
         return index, csr_matrix((data, indices, indptr), shape=(len(shotness),) * 2)
 
     def get_shotness(self):
@@ -286,15 +325,28 @@ class ProtobufReader(Reader):
 
     def get_devs(self) -> Tuple[List[str], Dict[int, Dict[int, DevDay]]]:
         people = list(self.contents["Devs"].dev_index)
-        days = {d: {dev: DevDay(stats.commits, stats.stats.added, stats.stats.removed,
-                                stats.stats.changed, {k: [v.added, v.removed, v.changed]
-                                                      for k, v in stats.languages.items()})
-                    for dev, stats in day.devs.items()}
-                for d, day in self.contents["Devs"].ticks.items()}
+        days = {
+            d: {
+                dev: DevDay(
+                    stats.commits,
+                    stats.stats.added,
+                    stats.stats.removed,
+                    stats.stats.changed,
+                    {
+                        k: [v.added, v.removed, v.changed]
+                        for k, v in stats.languages.items()
+                    },
+                )
+                for dev, stats in day.devs.items()
+            }
+            for d, day in self.contents["Devs"].ticks.items()
+        }
         return people, days
 
     def _parse_burndown_matrix(self, matrix):
-        dense = numpy.zeros((matrix.number_of_rows, matrix.number_of_columns), dtype=int)
+        dense = numpy.zeros(
+            (matrix.number_of_rows, matrix.number_of_columns), dtype=int
+        )
         for y, row in enumerate(matrix.rows):
             for x, col in enumerate(row.columns):
                 dense[y, x] = col
@@ -302,8 +354,11 @@ class ProtobufReader(Reader):
 
     def _parse_sparse_matrix(self, matrix):
         from scipy.sparse import csr_matrix
-        return csr_matrix((list(matrix.data), list(matrix.indices), list(matrix.indptr)),
-                          shape=(matrix.number_of_rows, matrix.number_of_columns))
+
+        return csr_matrix(
+            (list(matrix.data), list(matrix.indices), list(matrix.indptr)),
+            shape=(matrix.number_of_rows, matrix.number_of_columns),
+        )
 
 
 READERS = {"yaml": YamlReader, "yml": YamlReader, "pb": ProtobufReader}

+ 1 - 0
python/labours/utils.py

@@ -24,6 +24,7 @@ def parse_date(text: None, default: 'Timestamp') -> 'Timestamp':
     if not text:
         return default
     from dateutil.parser import parse
+
     return parse(text)
 
 

+ 4 - 1
python/setup.cfg

@@ -2,7 +2,9 @@
 exclude = labours/pb_pb2.py
 ignore = D,B007,
          # Spurious "unused import" / "redefinition" errors:
-         F401,F811
+         F401,F811,
+         # Black formattings that aren't PEP8 compliant:
+         W503,E203
 import-order-style = appnexus
 inline-quotes = "
 max-line-length = 99
@@ -13,3 +15,4 @@ force_sort_within_sections = true
 line_length = 99
 lines_between_types = 0
 multi_line_output = 0
+order_by_type = false

+ 7 - 5
python/setup.py

@@ -4,12 +4,16 @@ from setuptools import setup
 
 
 try:
-    with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8") as f:
+    with open(
+        os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8"
+    ) as f:
         long_description = f.read()
 except FileNotFoundError:
     long_description = ""
 
-with open(os.path.join(os.path.dirname(__file__), "requirements.in"), encoding="utf-8") as f:
+with open(
+    os.path.join(os.path.dirname(__file__), "requirements.in"), encoding="utf-8"
+) as f:
     requirements = f.readlines()
 
 
@@ -28,9 +32,7 @@ setup(
     keywords=["git", "mloncode", "mining software repositories", "hercules"],
     install_requires=requirements,
     package_data={"labours": ["../LICENSE.md", "../README.md", "../requirements.txt"]},
-    entry_points={
-        "console_scripts": ["labours=labours.__main__:main"],
-    },
+    entry_points={"console_scripts": ["labours=labours.__main__:main"]},
     classifiers=[
         "Development Status :: 5 - Production/Stable",
         "Intended Audience :: Developers",