From 33675188fd471c4463efc1b1bb808fdb7a9515df Mon Sep 17 00:00:00 2001
From: Alvin Wan
Date: Tue, 7 Apr 2026 23:16:55 -0700
Subject: [PATCH 1/4] Speed up package minification
---
pymini/pymini.py | 74 +++++++++++++++++++++++++++++++++++-------------
1 file changed, 55 insertions(+), 19 deletions(-)
diff --git a/pymini/pymini.py b/pymini/pymini.py
index a474314..e0726d0 100644
--- a/pymini/pymini.py
+++ b/pymini/pymini.py
@@ -1,6 +1,7 @@
import ast
import copy
import keyword
+from collections import Counter
from typing import Dict, List, Optional, Set
from .utils import variable_name_generator
@@ -299,6 +300,8 @@ def __init__(
self.class_member_mappings = {}
self.callable_argument_infos = {}
self.class_method_argument_infos = {}
+ self._class_public_member_reference_cache = {}
+ self._module_attribute_reference_cache = {}
self.modules = set(modules) # don't alias variables imported from these modules
self.keep_global_variables = keep_global_variables
self.rename_arguments = rename_arguments
@@ -416,28 +419,61 @@ def _public_class_reference_count(self, node, old_name):
count += 1
return count
- def _public_member_reference_count(self, class_node, class_name, member_name):
- count = 1
+ def _class_public_member_references(self, class_node):
+ cache_key = id(class_node)
+ cached = self._class_public_member_reference_cache.get(cache_key)
+ if cached is not None:
+ return cached
+
+ name_loads = Counter()
+ attribute_loads_by_base = {}
for current in ast.walk(class_node):
- if isinstance(current, ast.Name) and isinstance(current.ctx, ast.Load) and current.id == member_name:
- count += 1
- elif (
- isinstance(current, ast.Attribute)
- and current.attr == member_name
- and isinstance(current.value, ast.Name)
- and current.value.id in {"self", "cls", class_name}
- ):
- count += 1
+ if isinstance(current, ast.Name) and isinstance(current.ctx, ast.Load):
+ name_loads[current.id] += 1
+ elif isinstance(current, ast.Attribute) and isinstance(current.value, ast.Name):
+ base_name = current.value.id
+ base_counts = attribute_loads_by_base.get(base_name)
+ if base_counts is None:
+ base_counts = Counter()
+ attribute_loads_by_base[base_name] = base_counts
+ base_counts[current.attr] += 1
+
+ cached = {
+ "name_loads": name_loads,
+ "attribute_loads_by_base": attribute_loads_by_base,
+ }
+ self._class_public_member_reference_cache[cache_key] = cached
+ return cached
+
+ def _module_attribute_references(self, module):
+ cache_key = id(module)
+ cached = self._module_attribute_reference_cache.get(cache_key)
+ if cached is not None:
+ return cached
+
+ attribute_loads_by_base = {}
+ for current in ast.walk(module):
+ if not isinstance(current, ast.Attribute) or not isinstance(current.value, ast.Name):
+ continue
+ base_name = current.value.id
+ base_counts = attribute_loads_by_base.get(base_name)
+ if base_counts is None:
+ base_counts = Counter()
+ attribute_loads_by_base[base_name] = base_counts
+ base_counts[current.attr] += 1
+
+ self._module_attribute_reference_cache[cache_key] = attribute_loads_by_base
+ return attribute_loads_by_base
+
+ def _public_member_reference_count(self, class_node, class_name, member_name):
+ references = self._class_public_member_references(class_node)
+ count = 1 + references["name_loads"].get(member_name, 0)
+ attribute_loads_by_base = references["attribute_loads_by_base"]
+ for base_name in {"self", "cls", class_name}:
+ count += attribute_loads_by_base.get(base_name, {}).get(member_name, 0)
module = self._containing_module(class_node)
if module is not None:
- for current in ast.walk(module):
- if (
- isinstance(current, ast.Attribute)
- and current.attr == member_name
- and isinstance(current.value, ast.Name)
- and current.value.id == class_name
- ):
- count += 1
+ count += self._module_attribute_references(module).get(class_name, {}).get(member_name, 0)
return count
def _public_global_reference_count(self, node, old_name):
From a60536b014a49b90ab2c084de977f15bf52b9648 Mon Sep 17 00:00:00 2001
From: Alvin Wan
Date: Tue, 7 Apr 2026 23:21:56 -0700
Subject: [PATCH 2/4] Document package speed baselines
---
benchmarks/README.md | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/benchmarks/README.md b/benchmarks/README.md
index e391878..81a13fb 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -95,6 +95,23 @@ Wheel-specific failures:
## Speed
+### Package Mode vs `origin/main`
+
+One-shot package minification timings on the checked-in fixtures under
+`.bench-repos`, using the same aggressive package settings as the compression
+results (`--rename-modules --rename-global-variables --rename-arguments`):
+
+| Package | `origin/main` | this branch | speedup |
+| --- | ---: | ---: | ---: |
+| click | 9.290 s | 3.529 s | 2.63x |
+| pytest | 27.858 s | 15.592 s | 1.79x |
+
+These are package-mode API timings measured with `.venv/bin/python` on the same
+machine, comparing the current branch against a detached `origin/main`
+worktree.
+
+### Tool Comparison
+
| Input | pymini | pyminifier | python-minifier |
| --- | ---: | ---: | ---: |
| pyminifier.py | 11.8 ms | 1.7 ms | 7.5 ms |
@@ -110,7 +127,9 @@ Speed failures:
The single-file rows come from [benchmark_speed.py](./benchmark_speed.py). The
package rows are one-shot package minification timings from the same
-environment used for the compression comparison.
+environment used for the compression comparison. The `click` and `pytest`
+baseline rows above are branch-vs-`origin/main` measurements on the checked-in
+fixtures, rather than external-tool comparisons.
# Reproduce
From 25c9b2b0643f98692229138de6967be6d425f8df Mon Sep 17 00:00:00 2001
From: Alvin Wan
Date: Tue, 7 Apr 2026 23:27:57 -0700
Subject: [PATCH 3/4] Fold speed baselines into table
---
benchmarks/README.md | 40 +++++++++++++---------------------------
1 file changed, 13 insertions(+), 27 deletions(-)
diff --git a/benchmarks/README.md b/benchmarks/README.md
index 81a13fb..9340eb6 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -95,31 +95,16 @@ Wheel-specific failures:
## Speed
-### Package Mode vs `origin/main`
-
-One-shot package minification timings on the checked-in fixtures under
-`.bench-repos`, using the same aggressive package settings as the compression
-results (`--rename-modules --rename-global-variables --rename-arguments`):
-
-| Package | `origin/main` | this branch | speedup |
-| --- | ---: | ---: | ---: |
-| click | 9.290 s | 3.529 s | 2.63x |
-| pytest | 27.858 s | 15.592 s | 1.79x |
-
-These are package-mode API timings measured with `.venv/bin/python` on the same
-machine, comparing the current branch against a detached `origin/main`
-worktree.
-
-### Tool Comparison
-
-| Input | pymini | pyminifier | python-minifier |
-| --- | ---: | ---: | ---: |
-| pyminifier.py | 11.8 ms | 1.7 ms | 7.5 ms |
-| pyminify.py | 25.3 ms | 4.4 ms | 24.2 ms |
-| TexSoup | 124.9 ms | 52.2 ms | 117.2 ms |
-| timefhuman | 352.0 ms | 71.0 ms | 266.0 ms |
-| pyminifier | 137.1 ms | 35.6 ms | 114.8 ms |
-| rich | 3286.6 ms | failed | 1838.7 ms |
+| Input | pymini | pymini (`origin/main`) | pyminifier | python-minifier |
+| --- | ---: | ---: | ---: | ---: |
+| pyminifier.py | 11.8 ms | - | 1.7 ms | 7.5 ms |
+| pyminify.py | 25.3 ms | - | 4.4 ms | 24.2 ms |
+| click | 3.529 s | 9.290 s | - | - |
+| pytest | 15.592 s | 27.858 s | - | - |
+| TexSoup | 124.9 ms | - | 52.2 ms | 117.2 ms |
+| timefhuman | 352.0 ms | - | 71.0 ms | 266.0 ms |
+| pyminifier | 137.1 ms | - | 35.6 ms | 114.8 ms |
+| rich | 3286.6 ms | - | failed | 1838.7 ms |
Speed failures:
@@ -128,8 +113,9 @@ Speed failures:
The single-file rows come from [benchmark_speed.py](./benchmark_speed.py). The
package rows are one-shot package minification timings from the same
environment used for the compression comparison. The `click` and `pytest`
-baseline rows above are branch-vs-`origin/main` measurements on the checked-in
-fixtures, rather than external-tool comparisons.
+baseline column values are branch-vs-`origin/main` package-mode API timings on
+the checked-in fixtures under `.bench-repos`, measured with `.venv/bin/python`
+using `--rename-modules --rename-global-variables --rename-arguments`.
# Reproduce
From 9105854f3e200a23360f75230b27d39c68a87860 Mon Sep 17 00:00:00 2001
From: Alvin Wan
Date: Tue, 7 Apr 2026 23:36:47 -0700
Subject: [PATCH 4/4] Update click and pytest speed baselines
---
benchmarks/README.md | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/benchmarks/README.md b/benchmarks/README.md
index 9340eb6..ef603bd 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -95,27 +95,32 @@ Wheel-specific failures:
## Speed
-| Input | pymini | pymini (`origin/main`) | pyminifier | python-minifier |
-| --- | ---: | ---: | ---: | ---: |
-| pyminifier.py | 11.8 ms | - | 1.7 ms | 7.5 ms |
-| pyminify.py | 25.3 ms | - | 4.4 ms | 24.2 ms |
-| click | 3.529 s | 9.290 s | - | - |
-| pytest | 15.592 s | 27.858 s | - | - |
-| TexSoup | 124.9 ms | - | 52.2 ms | 117.2 ms |
-| timefhuman | 352.0 ms | - | 71.0 ms | 266.0 ms |
-| pyminifier | 137.1 ms | - | 35.6 ms | 114.8 ms |
-| rich | 3286.6 ms | - | failed | 1838.7 ms |
+| Input | pymini | pyminifier | python-minifier |
+| --- | ---: | ---: | ---: |
+| pyminifier.py | 11.8 ms | 1.7 ms | 7.5 ms |
+| pyminify.py | 25.3 ms | 4.4 ms | 24.2 ms |
+| click | 3.529 s | failed | 914.4 ms |
+| pytest | 15.592 s | failed | 4.567 s |
+| TexSoup | 124.9 ms | 52.2 ms | 117.2 ms |
+| timefhuman | 352.0 ms | 71.0 ms | 266.0 ms |
+| pyminifier | 137.1 ms | 35.6 ms | 114.8 ms |
+| rich | 3.287 s | failed | 1.839 s |
Speed failures:
+- click + pyminifier: minification fails on `click/__init__.py` with
+ `TypeError: 'NoneType' object is not subscriptable`.
+- pytest + pyminifier: minification fails on `_pytest/_argcomplete.py` with
+ `TypeError: 'NoneType' object is not subscriptable`.
- rich + pyminifier: the same minification failure prevents a timing result.
The single-file rows come from [benchmark_speed.py](./benchmark_speed.py). The
package rows are one-shot package minification timings from the same
environment used for the compression comparison. The `click` and `pytest`
-baseline column values are branch-vs-`origin/main` package-mode API timings on
-the checked-in fixtures under `.bench-repos`, measured with `.venv/bin/python`
-using `--rename-modules --rename-global-variables --rename-arguments`.
+rows were measured on the checked-in fixtures under `.bench-repos`; `pymini`
+used package mode with `--rename-modules --rename-global-variables
+--rename-arguments`, while the baseline tools minified each file independently
+in the preserved package tree.
# Reproduce