From acf5077bacacdd5100e52348a6508a7bf55f9275 Mon Sep 17 00:00:00 2001 From: Alvin Wan Date: Wed, 8 Apr 2026 00:04:08 -0700 Subject: [PATCH 1/3] reduce scope analysis overhead --- pymini/pymini.py | 88 +++++++++++++++++------------------------------- 1 file changed, 31 insertions(+), 57 deletions(-) diff --git a/pymini/pymini.py b/pymini/pymini.py index e0726d0..cd0cba8 100644 --- a/pymini/pymini.py +++ b/pymini/pymini.py @@ -106,6 +106,7 @@ def __init__(self): self.bindings = set() self.loads = set() self.external_bindings = set() + self.args = set() def visit_Name(self, node): self.reserved_names.add(node.id) @@ -117,6 +118,7 @@ def visit_Name(self, node): def visit_arg(self, node): self.reserved_names.add(node.arg) self.bindings.add(node.arg) + self.args.add(node.arg) if node.annotation is not None: self.visit(node.annotation) @@ -302,6 +304,7 @@ def __init__( self.class_method_argument_infos = {} self._class_public_member_reference_cache = {} self._module_attribute_reference_cache = {} + self._scope_analysis_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 @@ -676,61 +679,35 @@ def _is_in_function_signature(self, node): current = getattr(current, "parent", None) return False - def _scope_bindings(self, node): - bindings = set() - globals_ = set() - nonlocals_ = set() - args = set() - - class ScopeBindingCollector(ast.NodeVisitor): - def visit_Global(self, inner): - globals_.update(inner.names) - - def visit_Nonlocal(self, inner): - nonlocals_.update(inner.names) - - def visit_arg(self, inner): - args.add(inner.arg) - bindings.add(inner.arg) - - def visit_Name(self, inner): - if isinstance(inner.ctx, ast.Store): - bindings.add(inner.id) - - def visit_FunctionDef(self, inner): - bindings.add(inner.name) - - visit_AsyncFunctionDef = visit_FunctionDef - - def visit_ClassDef(self, inner): - bindings.add(inner.name) - - def visit_Lambda(self, inner): - return None - - def visit_ListComp(self, inner): - return None - - def visit_SetComp(self, inner): - return None - - def visit_DictComp(self, inner): - return None - - def visit_GeneratorExp(self, inner): - return None + def _scope_analysis(self, node): + cache_key = id(node) + cached = self._scope_analysis_cache.get(cache_key) + if cached is not None: + return cached - collector = ScopeBindingCollector() + collector = ScopeLocalNameCollector() args_node = getattr(node, "args", None) if args_node is not None: collector.visit(args_node) for statement in getattr(node, "body", []): - if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): - collector.visit(statement) - continue collector.visit(statement) - bindings.difference_update(globals_ | nonlocals_) - return {"bindings": bindings, "globals": globals_, "args": args} + + cached = { + "reserved_names": frozenset(collector.reserved_names), + "bindings": frozenset(collector.bindings - collector.external_bindings), + "external_bindings": frozenset(collector.external_bindings), + "args": frozenset(collector.args), + } + self._scope_analysis_cache[cache_key] = cached + return cached + + def _scope_bindings(self, node): + analysis = self._scope_analysis(node) + return { + "bindings": set(analysis["bindings"]), + "globals": set(analysis["external_bindings"]), + "args": set(analysis["args"]), + } def _is_preserved_public_global_reference(self, name): if name not in self.public_global_names: @@ -767,13 +744,10 @@ def _is_active_parameter_name(self, name): return False def _local_scope_state(self, node): - collector = ScopeLocalNameCollector() - collector.visit(node.args) - for statement in getattr(node, "body", []): - collector.visit(statement) - reserved_names = set(collector.reserved_names) - local_bindings = collector.bindings - collector.external_bindings - for name in collector.reserved_names - local_bindings: + analysis = self._scope_analysis(node) + reserved_names = set(analysis["reserved_names"]) + local_bindings = analysis["bindings"] + for name in analysis["reserved_names"] - local_bindings: visible_name = self._lookup_visible_identifier(name) if visible_name is not None: reserved_names.add(visible_name) @@ -1991,8 +1965,8 @@ def append_public_aliases(tree, aliases): for node in aliases: inserted = ast.copy_location(node, root) inserted._pymini_generated = True + ast.fix_missing_locations(inserted) root.body.append(inserted) - ast.fix_missing_locations(tree) class Unparser: From 34a1254902ba206735e89a1d24866c69ca5156d2 Mon Sep 17 00:00:00 2001 From: Alvin Wan Date: Wed, 8 Apr 2026 00:43:09 -0700 Subject: [PATCH 2/3] refresh benchmark speed results --- benchmarks/README.md | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/benchmarks/README.md b/benchmarks/README.md index ef603bd..6a1807e 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -97,30 +97,35 @@ Wheel-specific failures: | 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 | +| pyminifier.py | 3.0 ms | 0.8 ms | 2.6 ms | +| pyminify.py | 7.9 ms | 2.0 ms | 6.7 ms | +| click | 855.2 ms | failed | 823.9 ms | +| pytest | 3.258 s | failed | 3.156 s | +| TexSoup | 218.6 ms | failed | 251.1 ms | +| timefhuman | 441.5 ms | failed | 450.6 ms | +| pyminifier | 196.7 ms | 68.5 ms | 191.7 ms | +| rich | 3.226 s | failed | 3.080 s | Speed failures: +- TexSoup + pyminifier: minification fails on `__init__.py` with + `TypeError: 'NoneType' object is not subscriptable`. - 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. +- rich + pyminifier: minification fails on `rich/__init__.py` with + `TypeError: 'NoneType' object is not subscriptable`. +- timefhuman + pyminifier: minification fails on `renderers.py` with + `TypeError: 'NoneType' object is not subscriptable`. 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` -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. +package rows are averages of three fresh-process runs from the same +environment used for the compression comparison. `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. The `click`, `pytest`, and `pyminifier` rows use the checked-in fixtures +under `.bench-repos`; the other package rows use local package checkouts. # Reproduce @@ -138,6 +143,8 @@ The larger package comparisons in this file were run against these checkouts: - `timefhuman` - `pyminifier` - `rich` +- `click` +- `pytest` # Validation From a2de715a772a311f5cbb027158dfca07c4cb7608 Mon Sep 17 00:00:00 2001 From: Alvin Wan Date: Wed, 8 Apr 2026 00:53:10 -0700 Subject: [PATCH 3/3] tighten benchmark failure note --- benchmarks/README.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/benchmarks/README.md b/benchmarks/README.md index 6a1807e..cb270f2 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -106,17 +106,7 @@ Wheel-specific failures: | pyminifier | 196.7 ms | 68.5 ms | 191.7 ms | | rich | 3.226 s | failed | 3.080 s | -Speed failures: - -- TexSoup + pyminifier: minification fails on `__init__.py` with - `TypeError: 'NoneType' object is not subscriptable`. -- 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: minification fails on `rich/__init__.py` with - `TypeError: 'NoneType' object is not subscriptable`. -- timefhuman + pyminifier: minification fails on `renderers.py` with +pyminifier minification fails on `__init__.py` with `TypeError: 'NoneType' object is not subscriptable`. The single-file rows come from [benchmark_speed.py](./benchmark_speed.py). The