From cf118341dde20a9ba27de4e4a2ce3eaf1d5cfb6a Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:02:04 -0600 Subject: [PATCH 1/6] Configure black & ruff with pre-commit --- .pre-commit-config.yaml | 11 +++++++++++ doc/contributing.md | 14 ++++++++++++++ environment.yml | 5 +++++ pyproject.toml | 10 +++++++++- 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2d58682 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: "https://github.com/charliermarsh/ruff-pre-commit" + rev: "v0.0.269" + hooks: + - id: "ruff" + args: ["--fix", "--exit-non-zero-on-fix"] + + - repo: "https://github.com/psf/black" + rev: "23.3.0" + hooks: + - id: "black" diff --git a/doc/contributing.md b/doc/contributing.md index fc5825b..a7de17e 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -21,3 +21,17 @@ rm -rf dist python -m build pip install dist/*.whl # or `dist/*.tar.gz` ``` + + +## Code formatting and linting + +This codebase uses [black](https://black.readthedocs.io/en/stable/) and +[ruff](https://github.com/charliermarsh/ruff) to automatically format and lint the code. + +[`pre-commit`](https://pre-commit.com/) is configured to run them automatically. You can +trigger this manually with `pre-commit run --all-files`. + +Thanks to pre-commit, all commits should be formatted. In cases where formatting needs +to be fixed (e.g. changing config of a linter), a format-only commit should be created, +and then another commit should immediately follow which updates +`.git-blame-ignore-revs`. diff --git a/environment.yml b/environment.yml index 33c0a90..21689cc 100644 --- a/environment.yml +++ b/environment.yml @@ -3,10 +3,15 @@ channels: - "conda-forge" - "nodefaults" dependencies: + # Runtime - "python ~=3.11" - "numpy ~=1.24" - "matplotlib ~=3.7" - "colorspacious ~=1.1" - "scipy ~=1.10" + + # Development + - "pre-commit" + - pip: - "build ~=0.10" diff --git a/pyproject.toml b/pyproject.toml index 1b53c90..e5146ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,12 @@ packages = {find = {}} package-data = {viscm = ["examples/*"]} -# [tool.black] +[tool.black] +target-version = ["py37", "py38", "py39", "py310", "py311"] + +[tool.ruff] +target-version = "py37" +select = ["F", "E", "W", "C90", "I", "UP", "YTT", "B", "A", "C4", "T10", "RUF"] + +[tool.ruff.mccabe] +max-complexity = 11 From 1fec42d0baf90e00d510efd76cb6006fa0c70dc4 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:04:08 -0600 Subject: [PATCH 2/6] Lint and format with `ruff` and `black` --- tests.py | 86 +++-- viscm/__main__.py | 1 + viscm/bezierbuilder.py | 97 +++-- viscm/cli.py | 116 ++++-- viscm/gui.py | 819 +++++++++++++++++++++++------------------ viscm/minimvc.py | 3 +- 6 files changed, 655 insertions(+), 467 deletions(-) diff --git a/tests.py b/tests.py index ec6a02e..ac8d37f 100644 --- a/tests.py +++ b/tests.py @@ -1,13 +1,13 @@ -from viscm.gui import * -from viscm.bezierbuilder import * import numpy as np -import matplotlib as mpl -from matplotlib.backends.qt_compat import QtGui, QtCore -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas -cms = {"viscm/examples/sample_linear.jscm", - "viscm/examples/sample_diverging.jscm", - "viscm/examples/sample_diverging_continuous.jscm"} +from viscm.bezierbuilder import json +from viscm.gui import Colormap, viscm_editor + +cms = { + "viscm/examples/sample_linear.jscm", + "viscm/examples/sample_diverging.jscm", + "viscm/examples/sample_diverging_continuous.jscm", +} def test_editor_loads_native(): @@ -16,8 +16,13 @@ def test_editor_loads_native(): data = json.loads(f.read()) cm = Colormap(None, "CatmulClark", "CAM02-UCS") cm.load(k) - viscm = viscm_editor(uniform_space=cm.uniform_space, cmtype=cm.cmtype, method=cm.method, **cm.params) - assert viscm.name == data["name"] + viscm = viscm_editor( + uniform_space=cm.uniform_space, + cmtype=cm.cmtype, + method=cm.method, + **cm.params, + ) + assert viscm.name == data["name"] extensions = data["extensions"]["https://matplotlib.org/viscm"] xp, yp, fixed = viscm.control_point_model.get_control_points() @@ -26,7 +31,7 @@ def test_editor_loads_native(): assert len(extensions["xp"]) == len(xp) assert len(extensions["yp"]) == len(yp) assert len(xp) == len(yp) - for i in range(len(xp)): + for i in range(len(xp)): assert extensions["xp"][i] == xp[i] assert extensions["yp"][i] == yp[i] assert extensions["min_Jp"] == viscm.min_Jp @@ -35,19 +40,34 @@ def test_editor_loads_native(): assert extensions["cmtype"] == viscm.cmtype colors = data["colors"] - colors = [[int(c[i:i + 2], 16) / 256 for i in range(0, 6, 2)] for c in [colors[i:i + 6] for i in range(0, len(colors), 6)]] + colors = [ + [int(c[i : i + 2], 16) / 256 for i in range(0, 6, 2)] + for c in [colors[i : i + 6] for i in range(0, len(colors), 6)] + ] editor_colors = viscm.cmap_model.get_sRGB(num=256)[0].tolist() for i in range(len(colors)): for z in range(3): assert colors[i][z] == np.rint(editor_colors[i][z] / 256) + +# import matplotlib as mpl +# from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +# from matplotlib.backends.qt_compat import QtCore, QtGui +# # def test_editor_add_point(): # # Testing linear - +# # fig = plt.figure() # figure_canvas = FigureCanvas(fig) -# linear = viscm_editor(min_Jp=40, max_Jp=60, xp=[-10, 10], yp=[0,0], figure=fig, cmtype="linear") - +# linear = viscm_editor( +# min_Jp=40, +# max_Jp=60, +# xp=[-10, 10], +# yp=[0,0], +# figure=fig, +# cmtype="linear", +# ) +# # Jp, ap, bp = linear.cmap_model.get_Jpapbp(3) # eJp, eap, ebp = [40, 50, 60], [-10, 0, 10], [0, 0, 0] # for i in range(3): @@ -61,12 +81,24 @@ def test_editor_loads_native(): # for i in range(3): # for z in range(3): # assert approxeq(rgb[i][z], ergb[i][z]) - + # # Testing adding a point to linear # linear.bezier_builder.mode = "add" -# qtEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, QtCore.QPoint(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.ShiftModifier) -# event = mpl.backend_bases.MouseEvent("button_press_event", figure_canvas, 0, 10, guiEvent=qtEvent) +# qtEvent = QtGui.QMouseEvent( +# QtCore.QEvent.MouseButtonPress, +# QtCore.QPoint(), +# QtCore.Qt.LeftButton, +# QtCore.Qt.LeftButton, +# QtCore.Qt.ShiftModifier, +# ) +# event = mpl.backend_bases.MouseEvent( +# "button_press_event", +# figure_canvas, +# 0, +# 10, +# guiEvent=qtEvent, +# ) # event.xdata = 0 # event.ydata = 10 # event.inaxes = linear.bezier_builder.ax @@ -87,8 +119,20 @@ def test_editor_loads_native(): # # Removing a point from linear # linear.bezier_builder.mode = "remove" -# qtEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, QtCore.QPoint(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.ControlModifier) -# event = mpl.backend_bases.MouseEvent("button_press_event", figure_canvas, 0, 10, guiEvent=qtEvent) +# qtEvent = QtGui.QMouseEvent( +# QtCore.QEvent.MouseButtonPress, +# QtCore.QPoint(), +# QtCore.Qt.LeftButton, +# QtCore.Qt.LeftButton, +# QtCore.Qt.ControlModifier, +# ) +# event = mpl.backend_bases.MouseEvent( +# "button_press_event", +# figure_canvas, +# 0, +# 10, +# guiEvent=qtEvent, +# ) # event.xdata = 0 # event.ydata = 10 # event.inaxes = linear.bezier_builder.ax @@ -102,7 +146,5 @@ def test_editor_loads_native(): # # print(linear.cmap_model.get_Jpapbp(3)) - def approxeq(x, y, err=0.0001): return abs(y - x) < err - diff --git a/viscm/__main__.py b/viscm/__main__.py index f4b3e03..7d19fdb 100644 --- a/viscm/__main__.py +++ b/viscm/__main__.py @@ -4,4 +4,5 @@ # See file LICENSE.txt for license information. from .cli import cli + cli() diff --git a/viscm/bezierbuilder.py b/viscm/bezierbuilder.py index 71c25f0..cbffbeb 100644 --- a/viscm/bezierbuilder.py +++ b/viscm/bezierbuilder.py @@ -1,4 +1,3 @@ -# coding=utf8 # BézierBuilder # # Copyright (c) 2013, Juan Luis Cano Rodríguez @@ -32,17 +31,17 @@ $ python bezier_builder.py """ -from __future__ import division, print_function, absolute_import -import numpy as np from math import factorial -from scipy import signal +import numpy as np +from matplotlib.backends.qt_compat import QtCore from matplotlib.lines import Line2D -from matplotlib.backends.qt_compat import QtGui, QtCore + from .minimvc import Trigger -class ControlPointModel(object): + +class ControlPointModel: def __init__(self, xp, yp, fixed=None): # fixed is either None (if no point is fixed) or and index of a fixed # point @@ -84,22 +83,22 @@ def set_control_points(self, xp, yp, fixed=None): self.trigger.fire() -class ControlPointBuilder(object): +class ControlPointBuilder: def __init__(self, ax, control_point_model): self.ax = ax self.control_point_model = control_point_model self.canvas = self.ax.figure.canvas xp, yp, _ = self.control_point_model.get_control_points() - self.control_polygon = Line2D(xp, yp, - ls="--", c="#666666", marker="x", - mew=2, mec="#204a87") + self.control_polygon = Line2D( + xp, yp, ls="--", c="#666666", marker="x", mew=2, mec="#204a87" + ) self.ax.add_line(self.control_polygon) # Event handler for mouse clicking - self.canvas.mpl_connect('button_press_event', self.on_button_press) - self.canvas.mpl_connect('button_release_event', self.on_button_release) - self.canvas.mpl_connect('motion_notify_event', self.on_motion_notify) + self.canvas.mpl_connect("button_press_event", self.on_button_press) + self.canvas.mpl_connect("button_release_event", self.on_button_release) + self.canvas.mpl_connect("motion_notify_event", self.on_motion_notify) self._index = None # Active vertex @@ -109,7 +108,7 @@ def __init__(self, ax, control_point_model): def on_button_press(self, event): modkey = event.guiEvent.modifiers() - # Ignore clicks outside axes + # Ignore clicks outside axes if event.inaxes != self.ax: return res, ind = self.control_polygon.contains(event) @@ -118,8 +117,7 @@ def on_button_press(self, event): if res and (modkey == QtCore.Qt.ControlModifier or self.mode == "remove"): # Control-click deletes self.control_point_model.remove_point(ind["ind"][0]) - if (modkey == QtCore.Qt.ShiftModifier or self.mode == "add"): - + if modkey == QtCore.Qt.ShiftModifier or self.mode == "add": # Adding a new point. Find the two closest points and insert it in # between them. total_squared_dists = [] @@ -132,10 +130,7 @@ def on_button_press(self, event): total_squared_dists.append(dist) best = np.argmin(total_squared_dists) - self.control_point_model.add_point(best + 1, - event.xdata, - event.ydata) - + self.control_point_model.add_point(best + 1, event.xdata, event.ydata) def on_button_release(self, event): if event.button != 1: @@ -170,7 +165,7 @@ def compute_bezier_points(xp, yp, at, method, grid=256): # arclength(t), and then invert it. t = np.linspace(0, 1, grid) - arclength = compute_arc_length(xp, yp, method, t=t) + arclength = compute_arc_length(xp, yp, method, t=t) arclength /= arclength[-1] # Now (t, arclength) is a lookup table describing the t -> arclength # mapping. Invert it to get at -> t @@ -181,6 +176,7 @@ def compute_bezier_points(xp, yp, at, method, grid=256): return method(list(zip(xp, yp)), at_t).T + def compute_arc_length(xp, yp, method, t=None, grid=256): if t is None: t = np.linspace(0, 1, grid) @@ -194,7 +190,8 @@ def compute_arc_length(xp, yp, method, t=None, grid=256): np.hypot(x_deltas, y_deltas, out=arclength_deltas[1:]) return np.cumsum(arclength_deltas) -class SingleBezierCurveModel(object): + +class SingleBezierCurveModel: def __init__(self, control_point_model, method="CatmulClark"): self.method = eval(method) self.control_point_model = control_point_model @@ -216,7 +213,7 @@ def _refresh(self): # self.canvas.draw() -class TwoBezierCurveModel(object): +class TwoBezierCurveModel: def __init__(self, control_point_model, method="CatmulClark"): self.method = eval(method) self.control_point_model = control_point_model @@ -224,7 +221,6 @@ def __init__(self, control_point_model, method="CatmulClark"): self.bezier_curve = Line2D(x, y) self.trigger = self.control_point_model.trigger self.trigger.add_callback(self._refresh) - def get_bezier_points(self, num=200): return self.get_bezier_points_at(np.linspace(0, 1, num)) @@ -233,15 +229,15 @@ def get_bezier_points_at(self, at, grid=256): at = np.asarray(at) if at.ndim == 0: at = np.array([at]) - - low_mask = (at < 0.5) - high_mask = (at >= 0.5) + + low_mask = at < 0.5 + high_mask = at >= 0.5 xp, yp, fixed = self.control_point_model.get_control_points() assert fixed is not None - low_xp = xp[:fixed + 1] - low_yp = yp[:fixed + 1] + low_xp = xp[: fixed + 1] + low_yp = yp[: fixed + 1] high_xp = xp[fixed:] high_yp = yp[fixed:] @@ -257,13 +253,15 @@ def get_bezier_points_at(self, at, grid=256): low_at = (0.5 - (0.5 - low_at) * sf) * 2 else: high_at = (0.5 + (high_at - 0.5) * sf) * 2 - 1 - low_at = low_at * 2 - - low_points = compute_bezier_points(low_xp, low_yp, - low_at, self.method, grid=grid) - high_points = compute_bezier_points(high_xp, high_yp, - high_at, self.method, grid=grid) - out = np.concatenate([low_points,high_points], 1) + low_at = low_at * 2 + + low_points = compute_bezier_points( + low_xp, low_yp, low_at, self.method, grid=grid + ) + high_points = compute_bezier_points( + high_xp, high_yp, high_at, self.method, grid=grid + ) + out = np.concatenate([low_points, high_points], 1) return out def _refresh(self): @@ -271,7 +269,7 @@ def _refresh(self): self.bezier_curve.set_data(x, y) -class BezierCurveView(object): +class BezierCurveView: def __init__(self, ax, bezier_curve_model): self.ax = ax self.bezier_curve_model = bezier_curve_model @@ -291,19 +289,18 @@ def _refresh(self): # We used to use scipy.special.binom here, -# but reimplementing it ourself lets us avoid pulling in a dependency +# but reimplementing it ourself lets us avoid pulling in a dependency # scipy just for that one function. def binom(n, k): return factorial(n) * 1.0 / (factorial(k) * factorial(n - k)) -def Bernstein(n, k): - """Bernstein polynomial. - """ +def Bernstein(n, k): + """Bernstein polynomial.""" coeff = binom(n, k) def _bpoly(x): - return coeff * x ** k * (1 - x) ** (n - k) + return coeff * x**k * (1 - x) ** (n - k) return _bpoly @@ -318,7 +315,8 @@ def Bezier(points, at): curve = np.zeros((at_flat.shape[0], 2)) for ii in range(N): curve += np.outer(Bernstein(N - 1, ii)(at_flat), points[ii]) - return curve.reshape(at.shape + (2,)) + return curve.reshape((*at.shape, 2)) + def CatmulClark(points, at): points = np.asarray(points) @@ -327,19 +325,10 @@ def CatmulClark(points, at): new_p = np.zeros((2 * len(points), 2)) new_p[0] = points[0] new_p[-1] = points[-1] - new_p[1:-2:2] = 3/4. * points[:-1] + 1/4. * points[1:] - new_p[2:-1:2] = 1/4. * points[:-1] + 3/4. * points[1:] + new_p[1:-2:2] = 3 / 4.0 * points[:-1] + 1 / 4.0 * points[1:] + new_p[2:-1:2] = 1 / 4.0 * points[:-1] + 3 / 4.0 * points[1:] points = new_p xp, yp = zip(*points) xp = np.interp(at, np.linspace(0, 1, len(xp)), xp) yp = np.interp(at, np.linspace(0, 1, len(yp)), yp) return np.asarray(list(zip(xp, yp))) - - - - - - - - - diff --git a/viscm/cli.py b/viscm/cli.py index 246ae46..f7f80cc 100644 --- a/viscm/cli.py +++ b/viscm/cli.py @@ -7,47 +7,78 @@ def cli(): import argparse + argv = sys.argv[1:] parser = argparse.ArgumentParser( prog="python -m viscm", description="A colormap tool.", ) - parser.add_argument("action", metavar="ACTION", - help="'edit' or 'view' (or 'show', same as 'view')", - choices=["edit", "view", "show"], - default="edit", - nargs="?") - parser.add_argument("colormap", metavar="COLORMAP", - default=None, - help="A .json file saved from the editor, a .py file containing" - " a global named `test_cm`, or the name of a matplotlib" - " builtin colormap", - nargs="?") - parser.add_argument("--uniform-space", metavar="SPACE", - default="CAM02-UCS", - dest="uniform_space", - help="The perceptually uniform space to use. Usually " - "you should leave this alone. You can pass 'CIELab' " - "if you're curious how uniform some colormap is in " - "CIELab space. You can pass 'buggy-CAM02-UCS' if " - "you're trying to reproduce the matplotlib colormaps " - "(which turn out to have had a small bug in the " - "assumed sRGB viewing conditions) from their bezier " - "curves.") - parser.add_argument("-t", "--type", type=str, - default="linear", choices=["linear", "diverging", "diverging-continuous"], - help="Choose a colormap type. Supported options are 'linear', 'diverging', and 'diverging-continuous") - parser.add_argument("-m", "--method", type=str, - default="CatmulClark", choices=["Bezier", "CatmulClark"], - help="Choose a spline construction method. 'CatmulClark' is the default, but you may choose the legacy option 'Bezier'") - parser.add_argument("--save", metavar="FILE", - default=None, - help="Immediately save visualization to a file " - "(view-mode only).") - parser.add_argument("--quit", default=False, action="store_true", - help="Quit immediately after starting " - "(useful with --save).") + parser.add_argument( + "action", + metavar="ACTION", + help="'edit' or 'view' (or 'show', same as 'view')", + choices=["edit", "view", "show"], + default="edit", + nargs="?", + ) + parser.add_argument( + "colormap", + metavar="COLORMAP", + default=None, + help="A .json file saved from the editor, a .py file containing" + " a global named `test_cm`, or the name of a matplotlib builtin" + " colormap", + nargs="?", + ) + parser.add_argument( + "--uniform-space", + metavar="SPACE", + default="CAM02-UCS", + dest="uniform_space", + help="The perceptually uniform space to use. Usually " + "you should leave this alone. You can pass 'CIELab' " + "if you're curious how uniform some colormap is in " + "CIELab space. You can pass 'buggy-CAM02-UCS' if " + "you're trying to reproduce the matplotlib colormaps " + "(which turn out to have had a small bug in the " + "assumed sRGB viewing conditions) from their bezier " + "curves.", + ) + parser.add_argument( + "-t", + "--type", + type=str, + default="linear", + choices=["linear", "diverging", "diverging-continuous"], + help=( + "Choose a colormap type. Supported options are 'linear', 'diverging'," + " and 'diverging-continuous" + ), + ) + parser.add_argument( + "-m", + "--method", + type=str, + default="CatmulClark", + choices=["Bezier", "CatmulClark"], + help=( + "Choose a spline construction method. 'CatmulClark' is the default, but" + " you may choose the legacy option 'Bezier'" + ), + ) + parser.add_argument( + "--save", + metavar="FILE", + default=None, + help="Immediately save visualization to a file " "(view-mode only).", + ) + parser.add_argument( + "--quit", + default=False, + action="store_true", + help="Quit immediately after starting " "(useful with --save).", + ) args = parser.parse_args(argv) cm = gui.Colormap(args.type, args.method, args.uniform_space) @@ -56,7 +87,6 @@ def cli(): if args.colormap: cm.load(args.colormap) - # Easter egg! I keep typing 'show' instead of 'view' so accept both if args.action in ("view", "show"): if cm is None: @@ -74,7 +104,13 @@ def cli(): # Hold a reference so it doesn't get GC'ed fig = plt.figure() figureCanvas = gui.FigureCanvas(fig) - v = gui.viscm_editor(figure=fig, uniform_space=cm.uniform_space, cmtype=cm.cmtype, method=cm.method, **cm.params) + v = gui.viscm_editor( + figure=fig, + uniform_space=cm.uniform_space, + cmtype=cm.cmtype, + method=cm.method, + **cm.params, + ) mainwindow = gui.EditorWindow(figureCanvas, v) else: raise RuntimeError("can't happen") @@ -82,8 +118,9 @@ def cli(): if args.quit: sys.exit() - figureCanvas.setSizePolicy(gui.QtWidgets.QSizePolicy.Expanding, - gui.QtWidgets.QSizePolicy.Expanding) + figureCanvas.setSizePolicy( + gui.QtWidgets.QSizePolicy.Expanding, gui.QtWidgets.QSizePolicy.Expanding + ) figureCanvas.updateGeometry() mainwindow.resize(800, 600) @@ -97,6 +134,7 @@ def cli(): # replace it with... the *operating system's* default signal handler, so # instead of a KeyboardInterrupt our process just exits. import signal + signal.signal(signal.SIGINT, signal.SIG_DFL) app.exec_() diff --git a/viscm/gui.py b/viscm/gui.py index 71e44ce..b3a1a2e 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -5,17 +5,17 @@ # Simple script using CIECAM02 and CAM02-UCS to visualize properties of a # matplotlib colormap -from __future__ import division, print_function, absolute_import -import sys -import os.path + import json +import os.path +import sys import numpy as np -#matplotlib.rcParams['backend'] = "QT4AGG" +# matplotlib.rcParams['backend'] = "QT4AGG" # Do this first before any other matplotlib imports, to force matplotlib to # use a Qt backend -from matplotlib.backends.qt_compat import QtWidgets, QtCore, QtGui, _getSaveFileName +from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets, _getSaveFileName try: from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas @@ -26,19 +26,20 @@ from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas import matplotlib +import matplotlib.colors import matplotlib.pyplot as plt import mpl_toolkits.mplot3d -from matplotlib.gridspec import GridSpec -import matplotlib.colors +from colorspacious import ( + CIECAM02Space, + CIECAM02Surround, + cspace_convert, + cspace_converter, +) from matplotlib.colors import ListedColormap +from matplotlib.gridspec import GridSpec -from scipy.interpolate import UnivariateSpline - -from colorspacious import (cspace_converter, cspace_convert, - CIECAM02Space, CIECAM02Surround) from .minimvc import Trigger - # The correct L_A value for the standard sRGB viewing conditions is: # (64 / np.pi) / 5 # Due to an error in our color conversion code, the matplotlib colormaps were @@ -56,43 +57,43 @@ XYZ100_w="D65", Y_b=20, L_A=(64 / np.pi) * 5, # bug: should be / 5 - surround=CIECAM02Surround.AVERAGE) -buggy_CAM02UCS = {"name": "CAM02-UCS", - "ciecam02_space": buggy_sRGB_viewing_conditions, - } + surround=CIECAM02Surround.AVERAGE, +) +buggy_CAM02UCS = { + "name": "CAM02-UCS", + "ciecam02_space": buggy_sRGB_viewing_conditions, +} GREYSCALE_CONVERSION_SPACE = "JCh" _sRGB1_to_JCh = cspace_converter("sRGB1", GREYSCALE_CONVERSION_SPACE) _JCh_to_sRGB1 = cspace_converter(GREYSCALE_CONVERSION_SPACE, "sRGB1") + + def to_greyscale(sRGB1): JCh = _sRGB1_to_JCh(sRGB1) JCh[..., 1] = 0 return np.clip(_JCh_to_sRGB1(JCh), 0, 1) -_deuter50_space = {"name": "sRGB1+CVD", - "cvd_type": "deuteranomaly", - "severity": 50} + +_deuter50_space = {"name": "sRGB1+CVD", "cvd_type": "deuteranomaly", "severity": 50} _deuter50_to_sRGB1 = cspace_converter(_deuter50_space, "sRGB1") -_deuter100_space = {"name": "sRGB1+CVD", - "cvd_type": "deuteranomaly", - "severity": 100} +_deuter100_space = {"name": "sRGB1+CVD", "cvd_type": "deuteranomaly", "severity": 100} _deuter100_to_sRGB1 = cspace_converter(_deuter100_space, "sRGB1") -_prot50_space = {"name": "sRGB1+CVD", - "cvd_type": "protanomaly", - "severity": 50} +_prot50_space = {"name": "sRGB1+CVD", "cvd_type": "protanomaly", "severity": 50} _prot50_to_sRGB1 = cspace_converter(_prot50_space, "sRGB1") -_prot100_space = {"name": "sRGB1+CVD", - "cvd_type": "protanomaly", - "severity": 100} +_prot100_space = {"name": "sRGB1+CVD", "cvd_type": "protanomaly", "severity": 100} _prot100_to_sRGB1 = cspace_converter(_prot100_space, "sRGB1") + def _show_cmap(ax, rgb): ax.imshow(rgb[np.newaxis, ...], aspect="auto") + def _apply_rgb_mat(mat, rgb): return np.clip(np.einsum("...ij,...j->...i", mat, rgb), 0, 1) + # sRGB corners: a' goes from -37.4 to 45 AP_LIM = (-38, 46) # b' goes from -46.5 to 42 @@ -109,6 +110,7 @@ def _setup_Jpapbp_axis(ax): ax.set_ylim(*BP_LIM) ax.set_zlim(*JP_LIM) + # Adapt a matplotlib colormap to a linearly transformed version -- useful for # visualizing how colormaps look given color deficiency. # Kinda a hack, b/c we inherit from Colormap (this is required), but then @@ -119,11 +121,11 @@ def __init__(self, transform, base_cmap): self.base_cmap = base_cmap def __call__(self, *args, **kwargs): - bts = kwargs.pop('bytes', False) + bts = kwargs.pop("bytes", False) fx = self.base_cmap(*args, bytes=False, **kwargs) tfx = self.transform(fx) if bts: - return (tfx * 255).astype('uint8') + return (tfx * 255).astype("uint8") return tfx def set_bad(self, *args, **kwargs): @@ -138,39 +140,39 @@ def set_over(self, *args, **kwargs): def is_gray(self): return False -def _vis_axes(fig): - grid = GridSpec(10, 4, - left=0.02, - right=0.98, - bottom=0.02, - width_ratios=[1] * 4, - height_ratios=[1] * 10) - axes = {'cmap': grid[0, 0], - 'deltas': grid[1:4, 0], - - 'cmap-greyscale': grid[0, 1], - 'lightness-deltas': grid[1:4, 1], - - 'deuteranomaly': grid[4, 0], - 'deuteranopia': grid[5, 0], - 'protanomaly': grid[4, 1], - 'protanopia': grid[5, 1], - - # 'lightness': grid[4:6, 1], - # 'colourfulness': grid[4:6, 2], - # 'hue': grid[4:6, 3], - - 'image0': grid[0:3, 2], - 'image0-cb': grid[0:3, 3], - 'image1': grid[3:6, 2], - 'image1-cb': grid[3:6, 3], - 'image2': grid[6:8, 2:], - 'image2-cb': grid[8:, 2:] - } - axes = dict([(key, fig.add_subplot(value)) - for (key, value) in axes.items()]) - axes['gamut'] = fig.add_subplot(grid[6:, :2], projection='3d') +def _vis_axes(fig): + grid = GridSpec( + 10, + 4, + left=0.02, + right=0.98, + bottom=0.02, + width_ratios=[1] * 4, + height_ratios=[1] * 10, + ) + axes = { + "cmap": grid[0, 0], + "deltas": grid[1:4, 0], + "cmap-greyscale": grid[0, 1], + "lightness-deltas": grid[1:4, 1], + "deuteranomaly": grid[4, 0], + "deuteranopia": grid[5, 0], + "protanomaly": grid[4, 1], + "protanopia": grid[5, 1], + # 'lightness': grid[4:6, 1], + # 'colourfulness': grid[4:6, 2], + # 'hue': grid[4:6, 3], + "image0": grid[0:3, 2], + "image0-cb": grid[0:3, 3], + "image1": grid[3:6, 2], + "image1-cb": grid[3:6, 3], + "image2": grid[6:8, 2:], + "image2-cb": grid[8:, 2:], + } + + axes = {key: fig.add_subplot(value) for (key, value) in axes.items()} + axes["gamut"] = fig.add_subplot(grid[6:, :2], projection="3d") return axes @@ -186,25 +188,35 @@ def lookup_colormap_by_name(name): module_name, object_name = name.split(":", 1) object_path = object_name.split(".") import importlib + cm = importlib.import_module(module_name) for entry in object_path: cm = getattr(cm, entry) return cm - raise ValueError("Can't find colormap {!r}".format(name)) - -class viscm(object): - def __init__(self, cm, figure=None, uniform_space="CAM02-UCS", - name=None, N=256, N_dots=50, show_gamut=False): + raise ValueError(f"Can't find colormap {name!r}") + + +class viscm: + def __init__( + self, + cm, + figure=None, + uniform_space="CAM02-UCS", + name=None, + N=256, + N_dots=50, + show_gamut=False, + ): if isinstance(cm, str): cm = lookup_colormap_by_name(cm) if name is None: name = cm.name - if figure == None: + if figure is None: figure = plt.figure() self._sRGB1_to_uniform = cspace_converter("sRGB1", uniform_space) self.figure = figure - self.figure.suptitle("Colormap evaluation: %s" % (name,), fontsize=24) + self.figure.suptitle(f"Colormap evaluation: {name}", fontsize=24) axes = _vis_axes(self.figure) @@ -229,50 +241,60 @@ def __init__(self, cm, figure=None, uniform_space="CAM02-UCS", x_dots = np.linspace(0, 1, N_dots) RGB_dots = cm(x_dots)[:, :3] - ax = axes['cmap'] + ax = axes["cmap"] _show_cmap(ax, RGB) ax.set_title("The colormap in its glory") ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) def label(ax, s): - ax.text(0.95, 0.05, s, - horizontalalignment="right", - verticalalignment="bottom", - transform=ax.transAxes) + ax.text( + 0.95, + 0.05, + s, + horizontalalignment="right", + verticalalignment="bottom", + transform=ax.transAxes, + ) def title(ax, s): - ax.text(0.98, 0.98, s, - horizontalalignment="right", - verticalalignment="top", - transform=ax.transAxes) + ax.text( + 0.98, + 0.98, + s, + horizontalalignment="right", + verticalalignment="top", + transform=ax.transAxes, + ) Jpapbp = self._sRGB1_to_uniform(RGB) def delta_ymax(values): return max(np.max(values) * 1.1, 0) - ax = axes['deltas'] - local_deltas = np.sqrt( - np.sum((Jpapbp[:-1, :] - Jpapbp[1:, :]) ** 2, axis=-1)) + ax = axes["deltas"] + local_deltas = np.sqrt(np.sum((Jpapbp[:-1, :] - Jpapbp[1:, :]) ** 2, axis=-1)) local_derivs = N * local_deltas ax.plot(x[1:], local_derivs) arclength = np.sum(local_deltas) rmse = np.std(local_derivs) title(ax, "Perceptual derivative") - label(ax, - "Length: %0.1f\nRMS deviation from flat: %0.1f (%0.1f%%)" - % (arclength, rmse, 100 * rmse / arclength)) + label( + ax, + "Length: {:0.1f}\nRMS deviation from flat: {:0.1f} ({:0.1f}%)".format( + arclength, rmse, 100 * rmse / arclength + ), + ) ax.set_ylim(-delta_ymax(-local_derivs), delta_ymax(local_derivs)) ax.get_xaxis().set_visible(False) - ax = axes['cmap-greyscale'] + ax = axes["cmap-greyscale"] _show_cmap(ax, to_greyscale(RGB)) ax.set_title("Black-and-white printed") ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) - ax = axes['lightness-deltas'] + ax = axes["lightness-deltas"] ax.axhline(0, linestyle="--", color="grey") lightness_deltas = np.diff(Jpapbp[:, 0]) lightness_derivs = N * lightness_deltas @@ -281,14 +303,16 @@ def delta_ymax(values): title(ax, "Perceptual lightness derivative") lightness_arclength = np.sum(np.abs(lightness_deltas)) lightness_rmse = np.std(lightness_derivs) - label(ax, - "Length: %0.1f\nRMS deviation from flat: %0.1f (%0.1f%%)" - % (lightness_arclength, - lightness_rmse, - 100 * lightness_rmse / lightness_arclength)) - - ax.set_ylim(-delta_ymax(-lightness_derivs), - delta_ymax(lightness_derivs)) + label( + ax, + "Length: {:0.1f}\nRMS deviation from flat: {:0.1f} ({:0.1f}%)".format( + lightness_arclength, + lightness_rmse, + 100 * lightness_rmse / lightness_arclength, + ), + ) + + ax.set_ylim(-delta_ymax(-lightness_derivs), delta_ymax(lightness_derivs)) ax.get_xaxis().set_visible(False) # ax = axes['lightness'] @@ -311,28 +335,22 @@ def anom(ax, converter, name): ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) - anom(axes['deuteranomaly'], - _deuter50_to_sRGB1, - "Moderate deuteranomaly") - anom(axes['deuteranopia'], - _deuter100_to_sRGB1, - "Complete deuteranopia") - - anom(axes['protanomaly'], - _prot50_to_sRGB1, - "Moderate protanomaly") - anom(axes['protanopia'], - _prot100_to_sRGB1, - "Complete protanopia") - - ax = axes['gamut'] + anom(axes["deuteranomaly"], _deuter50_to_sRGB1, "Moderate deuteranomaly") + anom(axes["deuteranopia"], _deuter100_to_sRGB1, "Complete deuteranopia") + + anom(axes["protanomaly"], _prot50_to_sRGB1, "Moderate protanomaly") + anom(axes["protanopia"], _prot100_to_sRGB1, "Complete protanopia") + + ax = axes["gamut"] ax.plot(Jpapbp[:, 1], Jpapbp[:, 2], Jpapbp[:, 0]) Jpapbp_dots = self._sRGB1_to_uniform(RGB_dots) - ax.scatter(Jpapbp_dots[:, 1], - Jpapbp_dots[:, 2], - Jpapbp_dots[:, 0], - c=RGB_dots[:, :], - s=80) + ax.scatter( + Jpapbp_dots[:, 1], + Jpapbp_dots[:, 2], + Jpapbp_dots[:, 0], + c=RGB_dots[:, :], + s=80, + ) # Draw a wireframe indicating the sRGB gamut self.gamut_patch = sRGB_gamut_patch(uniform_space) @@ -352,8 +370,9 @@ def anom(ax, converter, name): image_args = [] example_dir = os.path.join(os.path.dirname(__file__), "examples") - images.append(np.loadtxt(os.path.join(example_dir, - "st-helens_before-modified.txt.gz")).T) + images.append( + np.loadtxt(os.path.join(example_dir, "st-helens_before-modified.txt.gz")).T + ) image_args.append({}) # Adapted from @@ -376,21 +395,22 @@ def _deuter_transform(RGBA): RGB = RGBA[..., :3] RGB = np.clip(_deuter50_to_sRGB1(RGB), 0, 1) return np.concatenate((RGB, RGBA[..., 3:]), axis=-1) + deuter_cm = TransformedCMap(_deuter_transform, cm) for i, (image, args) in enumerate(zip(images, image_args)): - ax = axes['image%i' % (i,)] + ax = axes["image%i" % (i,)] ax.imshow(image, cmap=cm, **args) ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) - ax_cb = axes['image%i-cb' % (i,)] + ax_cb = axes["image%i-cb" % (i,)] ax_cb.imshow(image, cmap=deuter_cm, **args) ax_cb.get_xaxis().set_visible(False) ax_cb.get_yaxis().set_visible(False) - axes['image0'].set_title("Sample images") - axes['image0-cb'].set_title("Moderate deuter.") + axes["image0"].set_title("Sample images") + axes["image0-cb"].set_title("Moderate deuter.") self.axes = axes def toggle_gamut(self): @@ -410,26 +430,35 @@ def sRGB_gamut_patch(uniform_space, resolution=20): for i in range(resolution): for j in range(resolution): # R quad - sRGB_quads.append([[fixed, i * step, j * step], - [fixed, (i+1) * step, j * step], - [fixed, (i+1) * step, (j+1) * step], - [fixed, i * step, (j+1) * step]]) - sRGB_values.append((fixed, (i + 0.5) * step, (j + 0.5) * step, - 1)) + sRGB_quads.append( + [ + [fixed, i * step, j * step], + [fixed, (i + 1) * step, j * step], + [fixed, (i + 1) * step, (j + 1) * step], + [fixed, i * step, (j + 1) * step], + ] + ) + sRGB_values.append((fixed, (i + 0.5) * step, (j + 0.5) * step, 1)) # G quad - sRGB_quads.append([[i * step, fixed, j * step], - [(i+1) * step, fixed, j * step], - [(i+1) * step, fixed, (j+1) * step], - [i * step, fixed, (j+1) * step]]) - sRGB_values.append(((i + 0.5) * step, fixed, (j + 0.5) * step, - 1)) + sRGB_quads.append( + [ + [i * step, fixed, j * step], + [(i + 1) * step, fixed, j * step], + [(i + 1) * step, fixed, (j + 1) * step], + [i * step, fixed, (j + 1) * step], + ] + ) + sRGB_values.append(((i + 0.5) * step, fixed, (j + 0.5) * step, 1)) # B quad - sRGB_quads.append([[i * step, j * step, fixed], - [(i+1) * step, j * step, fixed], - [(i+1) * step, (j+1) * step, fixed], - [i * step, (j+1) * step, fixed]]) - sRGB_values.append(((i + 0.5) * step, (j + 0.5) * step, fixed, - 1)) + sRGB_quads.append( + [ + [i * step, j * step, fixed], + [(i + 1) * step, j * step, fixed], + [(i + 1) * step, (j + 1) * step, fixed], + [i * step, (j + 1) * step, fixed], + ] + ) + sRGB_values.append(((i + 0.5) * step, (j + 0.5) * step, fixed, 1)) sRGB_quads = np.asarray(sRGB_quads) # work around colorspace transform bugginess in handling high-dim # arrays @@ -437,51 +466,55 @@ def sRGB_gamut_patch(uniform_space, resolution=20): Jpapbp_quads_2d = cspace_convert(sRGB_quads_2d, "sRGB1", uniform_space) Jpapbp_quads = Jpapbp_quads_2d.reshape((-1, 4, 3)) gamut_patch = mpl_toolkits.mplot3d.art3d.Poly3DCollection( - Jpapbp_quads[:, :, [1, 2, 0]]) + Jpapbp_quads[:, :, [1, 2, 0]] + ) gamut_patch.set_facecolor(sRGB_values) gamut_patch.set_edgecolor(sRGB_values) return gamut_patch -def sRGB_gamut_Jp_slice(Jp, uniform_space, - ap_lim=(-50, 50), bp_lim=(-50, 50), resolution=200): - bp_grid, ap_grid = np.mgrid[bp_lim[0] : bp_lim[1] : resolution * 1j, - ap_lim[0] : ap_lim[1] : resolution * 1j] +def sRGB_gamut_Jp_slice( + Jp, uniform_space, ap_lim=(-50, 50), bp_lim=(-50, 50), resolution=200 +): + bp_grid, ap_grid = np.mgrid[ + bp_lim[0] : bp_lim[1] : resolution * 1j, ap_lim[0] : ap_lim[1] : resolution * 1j + ] Jp_grid = Jp * np.ones((resolution, resolution)) - Jpapbp = np.concatenate((Jp_grid[:, :, np.newaxis], - ap_grid[:, :, np.newaxis], - bp_grid[:, :, np.newaxis]), - axis=2) + Jpapbp = np.concatenate( + ( + Jp_grid[:, :, np.newaxis], + ap_grid[:, :, np.newaxis], + bp_grid[:, :, np.newaxis], + ), + axis=2, + ) sRGB = cspace_convert(Jpapbp, uniform_space, "sRGB1") - sRGBA = np.concatenate((sRGB, np.ones(sRGB.shape[:2] + (1,))), - axis=2) + sRGBA = np.concatenate((sRGB, np.ones(sRGB.shape[:2] + (1,))), axis=2) sRGBA[np.any((sRGB < 0) | (sRGB > 1), axis=-1)] = [0, 0, 0, 0] return sRGBA def draw_pure_hue_angles(ax): # Pure hue angles from CIECAM-02 - for color, angle in [("r", 20.14), - ("y", 90.00), - ("g", 164.25), - ("b", 237.53)]: + for color, angle in [("r", 20.14), ("y", 90.00), ("g", 164.25), ("b", 237.53)]: x = np.cos(np.deg2rad(angle)) y = np.sin(np.deg2rad(angle)) ax.plot([0, x * 1000], [0, y * 1000], color + "--") -def draw_sRGB_gamut_Jp_slice(ax, Jp, uniform_space, - ap_lim=(-50, 50), bp_lim=(-50, 50), - **kwargs): - sRGB = sRGB_gamut_Jp_slice(Jp, uniform_space, - ap_lim=ap_lim, bp_lim=bp_lim, **kwargs) - im = ax.imshow(sRGB, aspect="equal", - extent=ap_lim + bp_lim, origin="lower") +def draw_sRGB_gamut_Jp_slice( + ax, Jp, uniform_space, ap_lim=(-50, 50), bp_lim=(-50, 50), **kwargs +): + sRGB = sRGB_gamut_Jp_slice( + Jp, uniform_space, ap_lim=ap_lim, bp_lim=bp_lim, **kwargs + ) + im = ax.imshow(sRGB, aspect="equal", extent=ap_lim + bp_lim, origin="lower") draw_pure_hue_angles(ax) ax.set_xlim(ap_lim) ax.set_ylim(bp_lim) return im + # def sRGB_gamut_J_slice(J, # ap_lim=(-50, 50), bp_lim=(-50, 50), resolution=200): # a_grid, b_grid = np.mgrid[ap_lim[0] : ap_lim[1] : resolution * 1j, @@ -496,21 +529,35 @@ def draw_sRGB_gamut_Jp_slice(ax, Jp, uniform_space, def _viscm_editor_axes(fig): - grid = GridSpec(1, 2, - width_ratios=[9, 1], - height_ratios=[50]) - axes = {'bezier': grid[0, 0], - 'cm': grid[0, 1]} - - axes = dict([(key, fig.add_subplot(value)) - for (key, value) in axes.items()]) + grid = GridSpec(1, 2, width_ratios=[9, 1], height_ratios=[50]) + axes = {"bezier": grid[0, 0], "cm": grid[0, 1]} + + axes = {key: fig.add_subplot(value) for (key, value) in axes.items()} return axes -class viscm_editor(object): - def __init__(self, figure=None, uniform_space="CAM02-UCS", - min_Jp=15, max_Jp=95, xp=None, yp=None, cmtype="linear", filter_k=100, fixed=-1, name="new cm", method="CatmulClark"): - from .bezierbuilder import SingleBezierCurveModel, TwoBezierCurveModel, ControlPointBuilder, ControlPointModel +class viscm_editor: + def __init__( + self, + figure=None, + uniform_space="CAM02-UCS", + min_Jp=15, + max_Jp=95, + xp=None, + yp=None, + cmtype="linear", + filter_k=100, + fixed=-1, + name="new cm", + method="CatmulClark", + ): + from .bezierbuilder import ( + ControlPointBuilder, + ControlPointModel, + SingleBezierCurveModel, + TwoBezierCurveModel, + ) + if figure is None: figure = plt.figure() self.cmtype = cmtype @@ -523,80 +570,98 @@ def __init__(self, figure=None, uniform_space="CAM02-UCS", self.max_Jp = max_Jp self.fixed = fixed if self.cmtype in ["diverging", "diverging-continuous"] and xp is None: - self.fixed = 4; + self.fixed = 4 if xp is None or yp is None: if method == "Bezier": - xp = {"linear":[-2.0591553836234482, 59.377014829142524, - 43.552546744036135, 4.7670857511283202, - -9.5059638942617539], - "diverging":[-9, -15, 43, 30, 0, -20, -30, 20, 1], - "diverging-continuous":[-9, -15, 43, 30, 0, -20, -30, 20, 1], - }[cmtype] - yp = {"linear":[-25.664893617021221, -21.941489361702082, - 38.874113475177353, 20.567375886524871, - 32.047872340425585], - "diverging":[-5, 20, 20, -21, 0, 21, -38, -20, -5], - "diverging-continuous":[-5, 20, 20, -21, 0, 21, -38, -20, -5] - }[cmtype] + xp = { + "linear": [ + -2.0591553836234482, + 59.377014829142524, + 43.552546744036135, + 4.7670857511283202, + -9.5059638942617539, + ], + "diverging": [-9, -15, 43, 30, 0, -20, -30, 20, 1], + "diverging-continuous": [-9, -15, 43, 30, 0, -20, -30, 20, 1], + }[cmtype] + yp = { + "linear": [ + -25.664893617021221, + -21.941489361702082, + 38.874113475177353, + 20.567375886524871, + 32.047872340425585, + ], + "diverging": [-5, 20, 20, -21, 0, 21, -38, -20, -5], + "diverging-continuous": [-5, 20, 20, -21, 0, 21, -38, -20, -5], + }[cmtype] if method == "CatmulClark": - xp = {"linear":[-2, 20,23, 5, -9], - "diverging":[-9, -15, -10, 0, 0, 5, 10, 15, 2], - "diverging-continuous":[-9, -5, -1, 0, 0, 5, 10, 15, 2], - }[cmtype] - yp = {"linear":[-25, -21, 18, 10, 12], - "diverging":[-5, -8, -20, -10, 0, 2, 8, 15, 5], - "diverging-continuous":[-5, -8, -20, -10, 0, 2, 8, 15, 5] - }[cmtype] - xy_lim = {"Bezier" : (-100, 100), - "CatmulClark" : (-50, 50)}[self.method] - - BezierModel, startJp = {"linear":(SingleBezierCurveModel, 0.5), - "diverging":(TwoBezierCurveModel, 0.75), - "diverging-continuous":(TwoBezierCurveModel, 0.5), - }[cmtype] - + xp = { + "linear": [-2, 20, 23, 5, -9], + "diverging": [-9, -15, -10, 0, 0, 5, 10, 15, 2], + "diverging-continuous": [-9, -5, -1, 0, 0, 5, 10, 15, 2], + }[cmtype] + yp = { + "linear": [-25, -21, 18, 10, 12], + "diverging": [-5, -8, -20, -10, 0, 2, 8, 15, 5], + "diverging-continuous": [-5, -8, -20, -10, 0, 2, 8, 15, 5], + }[cmtype] + xy_lim = {"Bezier": (-100, 100), "CatmulClark": (-50, 50)}[self.method] + + BezierModel, startJp = { + "linear": (SingleBezierCurveModel, 0.5), + "diverging": (TwoBezierCurveModel, 0.75), + "diverging-continuous": (TwoBezierCurveModel, 0.5), + }[cmtype] + self.control_point_model = ControlPointModel(xp, yp, fixed=self.fixed) self.bezier_model = BezierModel(self.control_point_model, self.method) - axes['bezier'].add_line(self.bezier_model.bezier_curve) - self.cmap_model = BezierCMapModel(self.bezier_model, - self.min_Jp, - self.max_Jp, - uniform_space, - cmtype=cmtype, - filter_k=filter_k) - + axes["bezier"].add_line(self.bezier_model.bezier_curve) + self.cmap_model = BezierCMapModel( + self.bezier_model, + self.min_Jp, + self.max_Jp, + uniform_space, + cmtype=cmtype, + filter_k=filter_k, + ) self.highlight_point_model = HighlightPointModel(self.cmap_model, startJp) self.highlight_point_model1 = None - self.bezier_builder = ControlPointBuilder(axes['bezier'], - self.control_point_model) + self.bezier_builder = ControlPointBuilder( + axes["bezier"], self.control_point_model + ) - self.bezier_gamut_viewer = GamutViewer2D(axes['bezier'], - self.highlight_point_model, - uniform_space, - ) - - self.bezier_highlight_point_view = HighlightPoint2DView(axes['bezier'], - self.highlight_point_model) + self.bezier_gamut_viewer = GamutViewer2D( + axes["bezier"], + self.highlight_point_model, + uniform_space, + ) + + self.bezier_highlight_point_view = HighlightPoint2DView( + axes["bezier"], self.highlight_point_model + ) if cmtype == "diverging": - self.highlight_point_model1 = HighlightPointModel(self.cmap_model, 1 - startJp) - self.bezier_highlight_point_view1 = HighlightPoint2DView(axes['bezier'], - self.highlight_point_model1) + self.highlight_point_model1 = HighlightPointModel( + self.cmap_model, 1 - startJp + ) + self.bezier_highlight_point_view1 = HighlightPoint2DView( + axes["bezier"], self.highlight_point_model1 + ) # draw_pure_hue_angles(axes['bezier']) - axes['bezier'].set_xlim(*xy_lim) - axes['bezier'].set_ylim(*xy_lim) + axes["bezier"].set_xlim(*xy_lim) + axes["bezier"].set_ylim(*xy_lim) - self.cmap_view = CMapView(axes['cm'], self.cmap_model) + self.cmap_view = CMapView(axes["cm"], self.cmap_model) self.cmap_highlighter = HighlightPointBuilder( - axes['cm'], - self.highlight_point_model, - self.highlight_point_model1) + axes["cm"], self.highlight_point_model, self.highlight_point_model1 + ) self.axes = axes def save_colormap(self, filepath): - with open(filepath, 'w') as f: + with open(filepath, "w") as f: xp, yp, fixed = self.control_point_model.get_control_points() rgb, _ = self.cmap_model.get_sRGB(num=256) hex_blob = "" @@ -609,49 +674,59 @@ def save_colormap(self, filepath): elif self.cmtype == "linear": usage_hints.append("sequential") xp, yp, fixed = self.control_point_model.get_control_points() - extensions = {"min_Jp" : self.min_Jp, - "max_Jp" : self.max_Jp, - "xp" : xp, - "yp" : yp, - "fixed" : fixed, - "filter_k" : self.cmap_model.filter_k, - "cmtype" : self.cmtype, - "uniform_colorspace" : self._uniform_space, - "spline_method" : self.method + extensions = { + "min_Jp": self.min_Jp, + "max_Jp": self.max_Jp, + "xp": xp, + "yp": yp, + "fixed": fixed, + "filter_k": self.cmap_model.filter_k, + "cmtype": self.cmtype, + "uniform_colorspace": self._uniform_space, + "spline_method": self.method, } - json.dump({"content-type": "application/vnd.matplotlib.colormap-v1+json", - "name": self.name, - "license":"http://creativecommons.org/publicdomain/zero/1.0/", - "usage-hints": usage_hints, - "colorspace" : "sRGB", - "domain" : "continuous", - "colors" : hex_blob, - "extensions" : {"https://matplotlib.org/viscm" : extensions} - }, - f, indent=4) + json.dump( + { + "content-type": "application/vnd.matplotlib.colormap-v1+json", + "name": self.name, + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "usage-hints": usage_hints, + "colorspace": "sRGB", + "domain": "continuous", + "colors": hex_blob, + "extensions": {"https://matplotlib.org/viscm": extensions}, + }, + f, + indent=4, + ) print("Saved") def export_py(self, filepath): import textwrap - template = textwrap.dedent(''' + + template = textwrap.dedent( + """ from matplotlib.colors import ListedColormap cm_type = "{type}" cm_data = {array_list} test_cm = ListedColormap(cm_data, name="{name}") - ''') + """ + ) rgb, _ = self.cmap_model.get_sRGB(num=256) - array_list = np.array2string(rgb, max_line_width=78, - prefix='cm_data = ', - separator=',') - with open(filepath, 'w') as f: - f.write(template.format(**dict(array_list=array_list, type=self.cmtype, name=self.name))) - + array_list = np.array2string( + rgb, max_line_width=78, prefix="cm_data = ", separator="," + ) + with open(filepath, "w") as f: + f.write( + template.format( + **{"array_list": array_list, "type": self.cmtype, "name": self.name} + ) + ) def show_viscm(self): - cm = ListedColormap(self.cmap_model.get_sRGB(num=256)[0], - name=self.name) + cm = ListedColormap(self.cmap_model.get_sRGB(num=256)[0], name=self.name) return cm @@ -665,8 +740,10 @@ def _filter_k_update(self, filter_k): self.cmap_model.set_filter_k(filter_k) -class BezierCMapModel(object): - def __init__(self, bezier_model, min_Jp, max_Jp, uniform_space, filter_k=100, cmtype="linear"): +class BezierCMapModel: + def __init__( + self, bezier_model, min_Jp, max_Jp, uniform_space, filter_k=100, cmtype="linear" + ): self.bezier_model = bezier_model self.min_Jp = min_Jp self.max_Jp = max_Jp @@ -691,6 +768,7 @@ def set_filter_k(self, filter_k): def get_Jpapbp_at_point(self, point): from scipy.interpolate import interp1d + Jp, ap, bp = self.get_Jpapbp() Jp, ap, bp = interp1d(np.linspace(0, 1, Jp.size), np.array([Jp, ap, bp]))(point) return Jp, ap, bp @@ -700,6 +778,7 @@ def get_Jpapbp(self, num=200): at = np.linspace(0, 1, num) if self.cmtype == "diverging": from scipy.special import erf + at = 1 + 2 * np.cumsum(erf(self.filter_k * (at - 0.5))) / num Jp = (self.max_Jp - self.min_Jp) * at + self.min_Jp return Jp, ap, bp @@ -713,17 +792,16 @@ def get_sRGB(self, num=200): return sRGB, oog -class CMapView(object): +class CMapView: def __init__(self, ax, cmap_model): self.ax = ax self.cmap_model = cmap_model rgb_display, oog_display = self._drawable_arrays() - self.image = self.ax.imshow(rgb_display, extent=(0, 0.2, 0, 1), - origin="lower") - self.gamut_alert_image = self.ax.imshow(oog_display, - extent=(0.05, 0.15, 0, 1), - origin="lower") + self.image = self.ax.imshow(rgb_display, extent=(0, 0.2, 0, 1), origin="lower") + self.gamut_alert_image = self.ax.imshow( + oog_display, extent=(0.05, 0.15, 0, 1), origin="lower" + ) self.ax.set_xlim(0, 0.2) self.ax.set_ylim(0, 1) self.ax.get_xaxis().set_visible(False) @@ -744,7 +822,7 @@ def _refresh(self): self.gamut_alert_image.set_data(oog_display) -class HighlightPointModel(object): +class HighlightPointModel: def __init__(self, cmap_model, point): self._cmap_model = cmap_model self._point = point @@ -763,7 +841,7 @@ def get_Jpapbp(self): return self._cmap_model.get_Jpapbp_at_point(self._point) -class HighlightPointBuilder(object): +class HighlightPointBuilder: def __init__(self, ax, highlight_point_model_a, highlight_point_model_b): self.ax = ax self.highlight_point_model_b = highlight_point_model_b @@ -772,16 +850,17 @@ def __init__(self, ax, highlight_point_model_a, highlight_point_model_b): self.canvas = self.ax.figure.canvas self._in_drag = False - self.marker_line_a = self.ax.axhline(highlight_point_model_a.get_point(), - linewidth=3, color="r") + self.marker_line_a = self.ax.axhline( + highlight_point_model_a.get_point(), linewidth=3, color="r" + ) if self.highlight_point_model_b: - self.marker_line_b = self.ax.axhline(highlight_point_model_b.get_point(), - linewidth=3, color="r") + self.marker_line_b = self.ax.axhline( + highlight_point_model_b.get_point(), linewidth=3, color="r" + ) self.canvas.mpl_connect("button_press_event", self._on_button_press) self.canvas.mpl_connect("motion_notify_event", self._on_motion) - self.canvas.mpl_connect("button_release_event", - self._on_button_release) + self.canvas.mpl_connect("button_release_event", self._on_button_release) self.highlight_point_model_a.trigger.add_callback(self._refresh) if highlight_point_model_b: @@ -816,17 +895,22 @@ def _refresh(self): self.canvas.draw() -class GamutViewer2D(object): - def __init__(self, ax, highlight_point_model, uniform_space, - ap_lim=(-50, 50), bp_lim=(-50, 50)): +class GamutViewer2D: + def __init__( + self, + ax, + highlight_point_model, + uniform_space, + ap_lim=(-50, 50), + bp_lim=(-50, 50), + ): self.ax = ax self.highlight_point_model = highlight_point_model self.ap_lim = ap_lim self.bp_lim = bp_lim self.uniform_space = uniform_space - self.bgcolors = {"light": (0.9, 0.9, 0.9), - "dark": (0.1, 0.1, 0.1)} + self.bgcolors = {"light": (0.9, 0.9, 0.9), "dark": (0.1, 0.1, 0.1)} # We want some hysteresis, so that there's no point where wiggling the # line back and forth causes background flickering. self.bgcolor_ranges = {"light": (0, 60), "dark": (40, 100)} @@ -834,9 +918,9 @@ def __init__(self, ax, highlight_point_model, uniform_space, self.bg = "light" self.ax.set_facecolor(self.bgcolors[self.bg]) - self.image = self.ax.imshow([[[0, 0, 0]]], aspect="equal", - extent=ap_lim + bp_lim, - origin="lower") + self.image = self.ax.imshow( + [[[0, 0, 0]]], aspect="equal", extent=ap_lim + bp_lim, origin="lower" + ) self.highlight_point_model.trigger.add_callback(self._refresh) @@ -846,12 +930,11 @@ def _refresh(self): if not (low <= Jp <= high): self.bg = self.bg_opposites[self.bg] self.ax.set_facecolor(self.bgcolors[self.bg]) - sRGB = sRGB_gamut_Jp_slice(Jp, self.uniform_space, - self.ap_lim, self.bp_lim) + sRGB = sRGB_gamut_Jp_slice(Jp, self.uniform_space, self.ap_lim, self.bp_lim) self.image.set_data(sRGB) -class HighlightPoint2DView(object): +class HighlightPoint2DView: def __init__(self, ax, highlight_point_model): self.ax = ax self.highlight_point_model = highlight_point_model @@ -871,27 +954,33 @@ def loadpyfile(path): is_native = True cmtype = "linear" method = "Bezier" - ns = {'__name__': '', - '__file__': os.path.basename(path), - } - with open(args.colormap) as f: - code = compile(f.read(), - os.path.basename(args.colormap), - 'exec') + ns = { + "__name__": "", + "__file__": os.path.basename(path), + } + + # FIXME: Should be `args.colormap` should be `path`? + with open(args.colormap) as f: # noqa: F821 + code = compile( + f.read(), + os.path.basename(args.colormap), # noqa: F821 + "exec", + ) exec(code, globals(), ns) - params = ns.get('parameters', {}) + params = ns.get("parameters", {}) if "min_JK" in params: params["min_Jp"] = params.pop("min_JK") params["max_Jp"] = params.pop("max_JK") cmap = ns.get("test_cm", None) return params, cmtype, cmap.name, cmap, is_native, method -class Colormap(object): + +class Colormap: def __init__(self, cmtype, method, uniform_space): self.can_edit = True - self.params = {} - self.cmtype = cmtype + self.params = {} + self.cmtype = cmtype self.method = method self.name = None self.cmap = None @@ -907,15 +996,14 @@ def load(self, path): self.can_edit = True self.cmtype = "linear" self.method = "Bezier" - ns = {'__name__': '', - '__file__': os.path.basename(self.path), - } + ns = { + "__name__": "", + "__file__": os.path.basename(self.path), + } with open(self.path) as f: - code = compile(f.read(), - os.path.basename(self.path), - 'exec') + code = compile(f.read(), os.path.basename(self.path), "exec") exec(code, globals(), ns) - self.params = ns.get('parameters', {}) + self.params = ns.get("parameters", {}) if not self.params: self.can_edit = False if "min_JK" in self.params: @@ -929,30 +1017,59 @@ def load(self, path): data = json.loads(f.read()) self.name = data["name"] colors = data["colors"] - colors = [colors[i:i + 6] for i in range(0, len(colors), 6)] - colors = [[int(c[2 * i:2 * i + 2], 16) / 255 for i in range(3)] for c in colors] + colors = [colors[i : i + 6] for i in range(0, len(colors), 6)] + colors = [ + [int(c[2 * i : 2 * i + 2], 16) / 255 for i in range(3)] + for c in colors + ] self.cmap = matplotlib.colors.ListedColormap(colors, self.name) - if "extensions" in data and "https://matplotlib.org/viscm" in data["extensions"]: + if ( + "extensions" in data + and "https://matplotlib.org/viscm" in data["extensions"] + ): self.can_edit = True - self.params = {k:v for k,v in data["extensions"]["https://matplotlib.org/viscm"].items() - if k in {"xp", "yp", "min_Jp", "max_Jp", "fixed", "filter_k", "uniform_space"}} + self.params = { + k: v + for k, v in data["extensions"][ + "https://matplotlib.org/viscm" + ].items() + if k + in { + "xp", + "yp", + "min_Jp", + "max_Jp", + "fixed", + "filter_k", + "uniform_space", + } + } self.params["name"] = self.name - self.cmtype = data["extensions"]["https://matplotlib.org/viscm"]["cmtype"] - self.method = data["extensions"]["https://matplotlib.org/viscm"]["spline_method"] - self.uniform_space = data["extensions"]["https://matplotlib.org/viscm"]["uniform_colorspace"] + self.cmtype = data["extensions"][ + "https://matplotlib.org/viscm" + ]["cmtype"] + self.method = data["extensions"][ + "https://matplotlib.org/viscm" + ]["spline_method"] + self.uniform_space = data["extensions"][ + "https://matplotlib.org/viscm" + ]["uniform_colorspace"] else: sys.exit("Unsupported filetype") else: self.can_edit = False self.cmap = lookup_colormap_by_name(path) self.name = path - + def about(): - QtWidgets.QMessageBox.about(None, "VISCM", - "Copyright (C) 2015-2016 Nathaniel Smith\n" + - "Copyright (C) 2015-2016 Stéfan van der Walt\n" - "Copyright (C) 2016 Hankun Zhao") + QtWidgets.QMessageBox.about( + None, + "VISCM", + "Copyright (C) 2015-2016 Nathaniel Smith\n" + + "Copyright (C) 2015-2016 Stéfan van der Walt\n" + "Copyright (C) 2016 Hankun Zhao", + ) class ViewerWindow(QtWidgets.QMainWindow): @@ -962,18 +1079,17 @@ def __init__(self, figurecanvas, viscm, cmapname, parent=None): self.main_widget = QtWidgets.QWidget(self) self.cmapname = cmapname - file_menu = QtWidgets.QMenu('&File', self) - file_menu.addAction('&Save', self.save, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) - file_menu.addAction('&Quit', self.fileQuit, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + file_menu = QtWidgets.QMenu("&File", self) + file_menu.addAction("&Save", self.save, QtCore.Qt.CTRL + QtCore.Qt.Key_S) + file_menu.addAction("&Quit", self.fileQuit, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) - options_menu = QtWidgets.QMenu('&Options', self) - options_menu.addAction('&Toggle Gamut', self.toggle_gamut, - QtCore.Qt.CTRL + QtCore.Qt.Key_G) + options_menu = QtWidgets.QMenu("&Options", self) + options_menu.addAction( + "&Toggle Gamut", self.toggle_gamut, QtCore.Qt.CTRL + QtCore.Qt.Key_G + ) - help_menu = QtWidgets.QMenu('&Help', self) - help_menu.addAction('&About', about) + help_menu = QtWidgets.QMenu("&Help", self) + help_menu.addAction("&About", about) self.menuBar().addMenu(file_menu) self.menuBar().addMenu(options_menu) @@ -1003,7 +1119,8 @@ def save(self): fileName, _ = _getSaveFileName( caption="Save file", directory=self.cmapname + ".png", - filter="Image Files (*.png *.jpg *.bmp)") + filter="Image Files (*.png *.jpg *.bmp)", + ) if fileName: self.viscm.save_figure(fileName) @@ -1014,19 +1131,18 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.viscm_editor = viscm_editor - file_menu = QtWidgets.QMenu('&File', self) - file_menu.addAction('&Save', self.save, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) + file_menu = QtWidgets.QMenu("&File", self) + file_menu.addAction("&Save", self.save, QtCore.Qt.CTRL + QtCore.Qt.Key_S) file_menu.addAction("&Export .py", self.export) - file_menu.addAction('&Quit', self.fileQuit, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + file_menu.addAction("&Quit", self.fileQuit, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) - options_menu = QtWidgets.QMenu('&Options', self) - options_menu.addAction('&Load in Viewer', self.loadviewer, - QtCore.Qt.CTRL + QtCore.Qt.Key_V) + options_menu = QtWidgets.QMenu("&Options", self) + options_menu.addAction( + "&Load in Viewer", self.loadviewer, QtCore.Qt.CTRL + QtCore.Qt.Key_V + ) - help_menu = QtWidgets.QMenu('&Help', self) - help_menu.addAction('&About', about) + help_menu = QtWidgets.QMenu("&Help", self) + help_menu.addAction("&About", about) self.menuBar().addMenu(file_menu) self.menuBar().addMenu(options_menu) @@ -1094,8 +1210,7 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.smoothness_slider_num = smoothness_slider_num smoothness_slider_layout = QtWidgets.QHBoxLayout() - smoothness_slider_layout.addWidget( - QtWidgets.QLabel("Transition sharpness")) + smoothness_slider_layout.addWidget(QtWidgets.QLabel("Transition sharpness")) smoothness_slider_layout.addWidget(smoothness_slider) smoothness_slider_layout.addWidget(smoothness_slider_num) @@ -1103,7 +1218,8 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.smoothness_slider = smoothness_slider viscm_editor.cmap_model.filter_k_trigger.add_callback( - self.update_smoothness_slider) + self.update_smoothness_slider + ) self.moveAction = QtWidgets.QAction("Drag points", self) self.moveAction.triggered.connect(self.set_move_mode) @@ -1122,11 +1238,10 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): renameAction = QtWidgets.QAction("Rename colormap", self) renameAction.triggered.connect(self.rename) - saveAction = QtWidgets.QAction('Save', self) + saveAction = QtWidgets.QAction("Save", self) saveAction.triggered.connect(self.save) - - self.toolbar = self.addToolBar('Tools') + self.toolbar = self.addToolBar("Tools") self.toolbar.addAction(self.moveAction) self.toolbar.addAction(self.addAction) self.toolbar.addAction(self.removeAction) @@ -1145,8 +1260,8 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): def rename(self): name, ok = QtWidgets.QInputDialog.getText( - self, "Rename your colormap", "Enter a name", - text=self.viscm_editor.name) + self, "Rename your colormap", "Enter a name", text=self.viscm_editor.name + ) self.viscm_editor.name = name self.setWindowTitle("VISCM Editing : " + self.viscm_editor.name) @@ -1157,9 +1272,8 @@ def smoothness_slider_moved(self): def update_smoothness_slider(self): filter_k = self.viscm_editor.cmap_model.filter_k - self.smoothness_slider_num.setText("{:0.2f}".format(filter_k)) - self.smoothness_slider.setValue( - int(round(np.log10(filter_k) * 1000))) + self.smoothness_slider_num.setText(f"{filter_k:0.2f}") + self.smoothness_slider.setValue(int(round(np.log10(filter_k) * 1000))) def swapjp(self): jp1, jp2 = self.min_slider.value(), self.max_slider.value() @@ -1193,7 +1307,8 @@ def export(self): fileName, _ = _getSaveFileName( caption="Export file", directory=self.viscm_editor.name + ".py", - filter=".py (*.py)") + filter=".py (*.py)", + ) if fileName: self.viscm_editor.export_py(fileName) @@ -1207,7 +1322,8 @@ def save(self): fileName, _ = _getSaveFileName( caption="Save file", directory=self.viscm_editor.name + ".jscm", - filter="JSCM Files (*.jscm)") + filter="JSCM Files (*.jscm)", + ) if fileName: self.viscm_editor.save_colormap(fileName) @@ -1217,8 +1333,9 @@ def loadviewer(self): cm = self.viscm_editor.show_viscm() v = viscm(cm, name=self.viscm_editor.name, figure=newfig) - newcanvas.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + newcanvas.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding + ) newcanvas.updateGeometry() newwindow = ViewerWindow(newcanvas, v, self.viscm_editor.name, parent=self) diff --git a/viscm/minimvc.py b/viscm/minimvc.py index c10ab6f..9174993 100644 --- a/viscm/minimvc.py +++ b/viscm/minimvc.py @@ -3,7 +3,8 @@ # Copyright (C) 2015 Stefan van der Walt # See file LICENSE.txt for license information. -class Trigger(object): + +class Trigger: def __init__(self): self._callbacks = set() From 8aa7bb01440aeca6f8bbcefe0671c28f2ce284c6 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:06:23 -0600 Subject: [PATCH 3/6] Add git-blame-ignore-revs entry for ruff/black application --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..58cc7d8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Applied `ruff` and `black` (gh-64) +1fec42d0baf90e00d510efd76cb6006fa0c70dc4 From c51735ed56ab586bc6458989fcaa4a84bdef4453 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:09:33 -0600 Subject: [PATCH 4/6] Drop Python 3.7 support --- README.rst | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c6c76c7..b55db2d 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ Contact: Nathaniel J. Smith and Stéfan van der Walt Dependencies: - * Python 3.7+ + * Python 3.8+ * `colorspacious `_ * Matplotlib * NumPy diff --git a/pyproject.toml b/pyproject.toml index e5146ae..4871ebb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3", ] -requires-python = "~=3.7" +requires-python = "~=3.8" dependencies = [ "numpy", "matplotlib", @@ -46,10 +46,10 @@ package-data = {viscm = ["examples/*"]} [tool.black] -target-version = ["py37", "py38", "py39", "py310", "py311"] +target-version = ["py38", "py39", "py310", "py311"] [tool.ruff] -target-version = "py37" +target-version = "py38" select = ["F", "E", "W", "C90", "I", "UP", "YTT", "B", "A", "C4", "T10", "RUF"] [tool.ruff.mccabe] From ccf11014cf8e763978dc56a198a67e718ffb9f2f Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:27:37 -0600 Subject: [PATCH 5/6] Run pre-commit checks in CI --- .github/workflows/check.yml | 26 ++++++++++++++++++++++++++ .pre-commit-config.yaml | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 .github/workflows/check.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..4c7a9d5 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,26 @@ +name: "Check with pre-commit" +on: + - "push" + - "pull_request" + +jobs: + + check-with-pre-commit: + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: ["3.11"] + + steps: + - uses: "actions/checkout@v3" + + - name: "Set up Python ${{ matrix.python-version }}" + uses: "actions/setup-python@v4" + with: + python-version: "${{ matrix.python-version }}" + + - name: "Install pre-commit" + run: "pip install pre-commit" + + - name: "Run checks with pre-commit" + run: "pre-commit run --all-files --show-diff-on-failure --color always" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d58682..cbacefd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,8 @@ repos: rev: "v0.0.269" hooks: - id: "ruff" + # NOTE: "--exit-non-zero-on-fix" is important for CI to function + # correctly! args: ["--fix", "--exit-non-zero-on-fix"] - repo: "https://github.com/psf/black" From 2c63ce5f2eb679e13d1284c5d64857ddb9d1b4c6 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:47:03 -0600 Subject: [PATCH 6/6] Add example commits for our formatting best practice --- doc/contributing.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/contributing.md b/doc/contributing.md index a7de17e..45cc0df 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -34,4 +34,6 @@ trigger this manually with `pre-commit run --all-files`. Thanks to pre-commit, all commits should be formatted. In cases where formatting needs to be fixed (e.g. changing config of a linter), a format-only commit should be created, and then another commit should immediately follow which updates -`.git-blame-ignore-revs`. +`.git-blame-ignore-revs`. For example: +[1fec42d](https://github.com/matplotlib/viscm/pull/64/commits/1fec42d0baf90e00d510efd76cb6006fa0c70dc4), +[8aa7bb0](https://github.com/matplotlib/viscm/pull/64/commits/8aa7bb01440aeca6f8bbcefe0671c28f2ce284c6).