From b3804776e7893b8296877ff12a359c0f2cf75f7f Mon Sep 17 00:00:00 2001 From: "randomizedcoder dave.seddon.ca@gmail.com" Date: Fri, 18 Sep 2026 11:14:52 -0700 Subject: [PATCH] refactor(util): modern type hints and drop vestigial py2 __future__ imports These util/ scripts already run under python3 (all have a python3 shebang) but still carried Python-2-straddle idioms and had no type annotations. This modernizes them: - Remove `from __future__ import division, print_function` (both are the default in Python 3, so these lines were no-ops). ruff reports them as UP010; the count over these files goes from 14 to 0. - Add PEP 585 / PEP 604 type annotations to every function: built-in generics (list[...], dict[...], tuple[...]) and X | None unions rather than typing.List / Optional. Where a function returns a matplotlib Axes or a dict_keys view, typing.Any is used. A single `from __future__ import annotations` is added to each file that gains annotations, so the hints are lazy strings with zero runtime cost and work on older 3.x too. - avg.py: correct the stale `#!/usr/bin/env python` shebang to python3 to match the rest of util/. Also fixes three genuine Python-2 leftovers in diff_metrics.py that would raise NameError under python3 on their code paths (they are not exercised by the common path, which is why they survived): `long(...)` -> `int(...)` (x2) and `printf(...)` -> `print(...)`. diff_metrics.py now runs end-to-end (verified the changed-metric path, including its %lu format, which is valid in python3). Scope: the two large scripts, tthoma.py (14774 lines) and cperf.py (1984 lines), are intentionally left for follow-up PRs to keep this reviewable. strip_decl.py already had neither a __future__ line nor functions, so it is unchanged. Verification: all 18 changed files byte-compile; ruff UP010 is clean (14->0) and no legacy typing constructs are introduced (UP006/UP007/ UP035/UP045 clean); annotation names resolve (F821 clean apart from the py2 bugs above, which are fixed); several scripts smoke-tested via stdin. Co-Authored-By: Claude Opus 4.8 --- util/avg.py | 3 +-- util/diff_metrics.py | 12 ++++++------ util/diff_rtts.py | 8 ++++---- util/metrics.py | 7 ++++--- util/plot.py | 21 ++++++++++++--------- util/plot_tthoma.py | 5 +++-- util/rpcid.py | 10 +++++----- util/service.py | 11 ++++++----- util/smi.py | 1 - util/strip.py | 13 +++++++------ util/tput.py | 1 - util/ttgrep.py | 5 +++-- util/ttmerge.py | 5 +++-- util/ttoffset.py | 1 - util/ttprint.py | 1 - util/ttrange.py | 1 - util/ttsum.py | 4 +++- util/ttsyslog.py | 1 - 18 files changed, 57 insertions(+), 53 deletions(-) diff --git a/util/avg.py b/util/avg.py index c3126986..145fa2ad 100755 --- a/util/avg.py +++ b/util/avg.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python3 """ Reads lines and extracts the first floating-point number to appear on @@ -6,7 +6,6 @@ Usage: avg.py [file] """ -from __future__ import division, print_function from glob import glob from optparse import OptionParser import math diff --git a/util/diff_metrics.py b/util/diff_metrics.py index 863598a4..a2b47530 100755 --- a/util/diff_metrics.py +++ b/util/diff_metrics.py @@ -12,7 +12,7 @@ diff_metrics file1 file2 """ -from __future__ import division, print_function +from __future__ import annotations from glob import glob from optparse import OptionParser import math @@ -25,7 +25,7 @@ # metric names, values are metric values. metrics = {} -def scan_first(name): +def scan_first(name: str) -> None: """ Scan the metrics file given by 'name' and record its metrics. """ @@ -37,10 +37,10 @@ def scan_first(name): if not match: print("Didn't match: %s\n" % (line)) continue - metrics[match.group(1)] = long(match.group(2)) + metrics[match.group(1)] = int(match.group(2)) f.close() -def scan_second(name): +def scan_second(name: str) -> None: """ Scan the metrics file given by 'name', compare its metrics to those that have been recorded, and print an output line with @@ -55,7 +55,7 @@ def scan_second(name): print("Didn't match: %s\n" % (line)) continue name = match.group(1) - value = long(match.group(2)) + value = int(match.group(2)) comment = match.group(3) if not name in metrics: print("No metric for %s\n" % (name)) @@ -68,7 +68,7 @@ def scan_second(name): f.close() if len(sys.argv) != 3: - printf("Usage: %s file file2\n" % sys.argv[0]) + print("Usage: %s file file2\n" % sys.argv[0]) exit(1) scan_first(sys.argv[1]) diff --git a/util/diff_rtts.py b/util/diff_rtts.py index 69227b43..184f38c4 100755 --- a/util/diff_rtts.py +++ b/util/diff_rtts.py @@ -9,7 +9,7 @@ Usage: diff_rtts.py file1 file2 """ -from __future__ import division, print_function +from __future__ import annotations from glob import glob from operator import itemgetter from optparse import OptionParser @@ -19,7 +19,7 @@ import string import sys -def read_rtts(file): +def read_rtts(file: str) -> list[list[float]]: """ Read a .rtts file and returns a list of (length, slowdown) pairs. @@ -44,7 +44,7 @@ def read_rtts(file): f.close() return slowdowns -def avg_slowdown(slowdowns): +def avg_slowdown(slowdowns: list[list[float]]) -> float: """ Return average slowdown from a list of (length, slowdown) pairs. @@ -55,7 +55,7 @@ def avg_slowdown(slowdowns): sum += item[1] return sum/len(slowdowns) -def deciles(slowdowns): +def deciles(slowdowns: list[list[float]]) -> tuple[list[float], list[float], list[float], list[float], list[float], list[float]]: """ Given a list of (length, slowdown) pairs, divide into 10 groups by length, then returns 6 lists (each with one entry per decile), diff --git a/util/metrics.py b/util/metrics.py index 4641e121..21ac510b 100755 --- a/util/metrics.py +++ b/util/metrics.py @@ -13,7 +13,8 @@ what has changed. File defaults to ~/.homa_metrics. """ -from __future__ import division, print_function +from __future__ import annotations +from typing import TextIO from glob import glob from optparse import OptionParser import math @@ -42,7 +43,7 @@ # Read in metrics, parse the results for internal use, and, optionally # copy the raw metrics to an output file. Also reinitialize symbols -def read_metrics(metrics_file, out): +def read_metrics(metrics_file: str, out: TextIO | None) -> list[dict[str, int]]: """ Read metrics from the file whose name is "metrics_file" and generate a data structure in the format described above for "prev". In @@ -80,7 +81,7 @@ def read_metrics(metrics_file, out): f.close() return metrics -def scale_number(number): +def scale_number(number: float) -> str: """ Return a string describing a number, but with a "K", "M", or "G" suffix to keep the number small and readable diff --git a/util/plot.py b/util/plot.py index d2f05612..83ba6214 100755 --- a/util/plot.py +++ b/util/plot.py @@ -5,6 +5,9 @@ # This file provides a library of functions for generating plots. +from __future__ import annotations +from typing import Any + import matplotlib import matplotlib.pyplot as plt import os @@ -38,7 +41,7 @@ # in that column. file_data = {} -def __read_file(file): +def __read_file(file: str) -> None: """ Read a file and add its contents to the file_data variable. If the file has already been read, then this function does nothing. @@ -83,7 +86,7 @@ def __read_file(file): f.close() file_data[file] = columns -def get_column(file, column): +def get_column(file: str, column: str) -> list[float | str]: """ Return a list containing the values of a given column in a given file. @@ -96,7 +99,7 @@ def get_column(file, column): raise Exception('Column %s doesn\'t exist in %s' % (column, name)) return file_data[file][column] -def get_column_names(file): +def get_column_names(file: str) -> Any: """ Returns a list containing the names of all of the columns in file. """ @@ -104,7 +107,7 @@ def get_column_names(file): __read_file(file) return file_data[file].keys() -def get_numbers(file): +def get_numbers(file: str) -> list[int]: """ Scans all of the column names in file for numbers and returns a sorted list of all the unique numbers found. @@ -117,7 +120,7 @@ def get_numbers(file): numbers.add(int(match.group(1))) return sorted(list(numbers)) -def max_value(file, columns): +def max_value(file: str, columns: list[str]) -> float | str | None: """ Returns the largest value in a set of columns. @@ -131,7 +134,7 @@ def max_value(file, columns): overall_max = col_max return overall_max -def node_name(file): +def node_name(file: str) -> str: """ Given the name of a trace file, return a shorter name that can be used (e.g. in titles) to identify the node represented by the file. @@ -142,8 +145,8 @@ def node_name(file): name = name[i+1:] return name -def start_plot(max_x, max_y, title="", x_label="", y_label="", size=10, - figsize=[6,4]): +def start_plot(max_x: float, max_y: float, title: str = "", x_label: str = "", + y_label: str = "", size: int = 10, figsize: list[int] = [6,4]) -> Any: """ Create a basic pyplot graph without plotting any data. Returns the Axes object for the plot. @@ -169,7 +172,7 @@ def start_plot(max_x, max_y, title="", x_label="", y_label="", size=10, ax.set_ylabel(y_label, size=size) return ax -def plot_colors(file): +def plot_colors(file: str) -> None: """ Generates a test plot that shows the standard colors defined above. diff --git a/util/plot_tthoma.py b/util/plot_tthoma.py index 3b193875..01d2fb29 100755 --- a/util/plot_tthoma.py +++ b/util/plot_tthoma.py @@ -6,6 +6,7 @@ # This file provides a collection of functions that plot data generated # by tthoma.py. Invoke with the --help option for more information. +from __future__ import annotations from glob import glob from optparse import OptionParser import math @@ -19,7 +20,7 @@ import plot -def backlog(data_file, plot_file): +def backlog(data_file: str, plot_file: str) -> None: """ Generates a plot of network backlog data produced by the "net" analyzer of tthoma.py. @@ -51,7 +52,7 @@ def backlog(data_file, plot_file): plt.savefig(plot_file) -def colors(plot_file): +def colors(plot_file: str) -> None: """ Generates a plot displaying standard colors. diff --git a/util/rpcid.py b/util/rpcid.py index 099e3355..5e519165 100755 --- a/util/rpcid.py +++ b/util/rpcid.py @@ -22,7 +22,7 @@ about the RPC described by that line. """ -from __future__ import division, print_function +from __future__ import annotations from glob import glob from optparse import OptionParser import math @@ -54,7 +54,7 @@ rpcs_analyzed = 0 -def track_nic_queue(line, time): +def track_nic_queue(line: str, time: float) -> None: """ Update info about the NIC queue length to reflect the transmission of a new packet (or several packets if there is TSO) @@ -83,7 +83,7 @@ def track_nic_queue(line, time): # print("NIC queue: bytes %d, usecs %.3f, time %.3f empty_time %.3f" % # (bytes, usecs, time, nic_empty_time)) -def add_stat(name, value): +def add_stat(name: str, value: float) -> None: """ Record a statistic with a given name and value (either appends to an existing list in stats or starts a new one). @@ -95,13 +95,13 @@ def add_stat(name, value): else: stats[name].append(value) -def avg_stat(name): +def avg_stat(name: str) -> float: global stats if not name in stats: return 0.0 return sum(stats[name]) / len(stats[name]) -def analyze_rpc(id, client_num, server_num): +def analyze_rpc(id: str, client_num: int | str, server_num: str) -> None: """ Analyze the client and server timetraces for a given RPC and output a latency breakdown for the RPC. diff --git a/util/service.py b/util/service.py index f8bb4cc3..75ec3f1f 100755 --- a/util/service.py +++ b/util/service.py @@ -10,7 +10,8 @@ The existing timetrace is in tt_file (or stdin in tt_file is omitted). """ -from __future__ import division, print_function +from __future__ import annotations +from typing import Any from glob import glob from optparse import OptionParser import math @@ -73,7 +74,7 @@ max = 0 max_id = "" -def average(dict, key): +def average(dict: list[dict[str, Any]], key: str) -> float: sum = 0.0 if len(dict) == 0: return 0.0 @@ -84,7 +85,7 @@ def average(dict, key): sum += record[key] return sum/len(dict) -def largest(dict, key): +def largest(dict: list[dict[str, Any]], key: str) -> dict[str, Any] | float | None: max = None if len(dict) == 0: return 0.0 @@ -93,7 +94,7 @@ def largest(dict, key): max = record return max -def smallest(dict, key): +def smallest(dict: list[dict[str, Any]], key: str) -> dict[str, Any] | float | None: min = None if len(dict) == 0: return 0.0 @@ -102,7 +103,7 @@ def smallest(dict, key): min = record return min -def collect(dict, key): +def collect(dict: list[dict[str, Any]], key: str) -> list[float]: result = [] for record in dict: if key in record: diff --git a/util/smi.py b/util/smi.py index f5df0933..57260d9b 100755 --- a/util/smi.py +++ b/util/smi.py @@ -8,7 +8,6 @@ The existing timetrace is in tt_file (or stdin in tt_file is omitted). """ -from __future__ import division, print_function from glob import glob from optparse import OptionParser import math diff --git a/util/strip.py b/util/strip.py index 4d2b4226..5ccb39ea 100755 --- a/util/strip.py +++ b/util/strip.py @@ -62,6 +62,7 @@ #endif /* See strip.py */ """ +from __future__ import annotations from collections import defaultdict from glob import glob from optparse import OptionParser @@ -74,7 +75,7 @@ exit_code = 0 -def remove_close(line): +def remove_close(line: str) -> str: """ Given a line of text containing a '}', remove the '}' and any following white space. If there is no '}', returns the original line. @@ -87,7 +88,7 @@ def remove_close(line): break return line[0:i] + line [j:] -def remove_open(line): +def remove_open(line: str) -> str: """ Given a line of text containing a '{', remove the '{' and any preceding white space. If there is no '{', returns the original line. @@ -101,7 +102,7 @@ def remove_open(line): break return line[0:j+1] + line [i+1:] -def leading_space(line): +def leading_space(line: str) -> int: """ Return the number of characters of leading space in a line (a tab counts as 8 spaces). @@ -117,7 +118,7 @@ def leading_space(line): break return count -def last_non_blank(s): +def last_non_blank(s: str) -> str | None: """ Return the last non-blank character in s, or None if there is no non-blank character in s. @@ -127,7 +128,7 @@ def last_non_blank(s): return s2[-1] return None -def blank_next_ok(line): +def blank_next_ok(line: str) -> bool: """ Given a line, return True if it is OK for this line to be followed by a blank line. False means that if the next line to be output is blank, @@ -140,7 +141,7 @@ def blank_next_ok(line): return False return True -def scan(file): +def scan(file: str) -> list[str]: """ Read a file, remove information that shouldn't appear in the Linux kernel version, and return an array of lines representing the stripped file. diff --git a/util/tput.py b/util/tput.py index 41bd27fa..4d215ef1 100755 --- a/util/tput.py +++ b/util/tput.py @@ -7,7 +7,6 @@ The existing timetrace is in tt_file (or stdin in tt_file is omitted). """ -from __future__ import division, print_function from glob import glob from optparse import OptionParser import math diff --git a/util/ttgrep.py b/util/ttgrep.py index 72084f6d..ceaec37a 100755 --- a/util/ttgrep.py +++ b/util/ttgrep.py @@ -11,7 +11,8 @@ Usage: ttgrep.py [--rebase] regex [file] """ -from __future__ import division, print_function +from __future__ import annotations +from typing import TextIO from glob import glob from optparse import OptionParser import math @@ -22,7 +23,7 @@ rebase = False -def scan(f, pattern): +def scan(f: TextIO, pattern: str) -> None: """ Scan the log file given by 'f' (handle for an open file) and output all-time trace records that match pattern. diff --git a/util/ttmerge.py b/util/ttmerge.py index 1e069b44..0020060e 100755 --- a/util/ttmerge.py +++ b/util/ttmerge.py @@ -9,7 +9,8 @@ Usage: ttmerge.py file file file ... """ -from __future__ import division, print_function +from __future__ import annotations +from typing import Any from glob import glob import math from optparse import OptionParser @@ -39,7 +40,7 @@ # ticks to microseconds. ghz = 0.0 -def next_line(info): +def next_line(info: dict[str, Any]) -> None: """ Read information from a file. The info argument is one of the entries in files. diff --git a/util/ttoffset.py b/util/ttoffset.py index c0ae0d1a..540787a6 100755 --- a/util/ttoffset.py +++ b/util/ttoffset.py @@ -13,7 +13,6 @@ added to each timestamp. """ -from __future__ import division, print_function from glob import glob from optparse import OptionParser import math diff --git a/util/ttprint.py b/util/ttprint.py index b3dfc6b0..993da496 100755 --- a/util/ttprint.py +++ b/util/ttprint.py @@ -9,7 +9,6 @@ with times in microseconds instead of clock cycles. """ -from __future__ import division, print_function from glob import glob from optparse import OptionParser import math diff --git a/util/ttrange.py b/util/ttrange.py index d1c888d2..ac858d06 100755 --- a/util/ttrange.py +++ b/util/ttrange.py @@ -12,7 +12,6 @@ timestamps fall between start_time and end_time, inclusive. """ -from __future__ import division, print_function from glob import glob from optparse import OptionParser import math diff --git a/util/ttsum.py b/util/ttsum.py index 960a28b4..2b5654e5 100755 --- a/util/ttsum.py +++ b/util/ttsum.py @@ -8,6 +8,8 @@ information. Use the --help option to print usage information. """ +from __future__ import annotations +from typing import TextIO from collections import defaultdict from glob import glob from optparse import OptionParser @@ -64,7 +66,7 @@ # event. eventCounts = defaultdict(lambda: defaultdict(lambda: 0)) -def scan(f, startingEvent): +def scan(f: TextIO, startingEvent: str | None) -> None: """ Scan the log file given by 'f' (handle for an open file) and collect information from time trace records as described by the arguments. diff --git a/util/ttsyslog.py b/util/ttsyslog.py index 9f27187b..3f096e6c 100755 --- a/util/ttsyslog.py +++ b/util/ttsyslog.py @@ -16,7 +16,6 @@ timetrace records are output to file file2. """ -from __future__ import division, print_function from glob import glob from optparse import OptionParser import math