code-towel 1.414__tar.gz → 1.618__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_towel-1.414 → code_towel-1.618}/CHANGELOG.md +26 -0
- {code_towel-1.414 → code_towel-1.618}/CONTRIBUTING.md +1 -1
- {code_towel-1.414/src/code_towel.egg-info → code_towel-1.618}/PKG-INFO +131 -15
- {code_towel-1.414 → code_towel-1.618}/README.md +130 -14
- code_towel-1.618/SECURITY.md +19 -0
- {code_towel-1.414 → code_towel-1.618}/docs/ARCHITECTURE.md +23 -13
- {code_towel-1.414 → code_towel-1.618}/docs/KNOWN_LIMITATIONS.md +11 -5
- code_towel-1.618/docs/QUICKSTART.md +66 -0
- {code_towel-1.414 → code_towel-1.618}/docs/RELEASING.md +5 -5
- {code_towel-1.414 → code_towel-1.618}/docs/USAGE_GUIDE.md +34 -27
- code_towel-1.618/docs/proposals/reuse-existing-function.md +117 -0
- {code_towel-1.414 → code_towel-1.618}/pyproject.toml +1 -4
- {code_towel-1.414 → code_towel-1.618}/scripts/add_copyright_headers.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/scripts/ecosystem_check.py +41 -7
- {code_towel-1.414 → code_towel-1.618}/scripts/promote_dry_helpers.py +1 -1
- {code_towel-1.414 → code_towel-1.618/src/code_towel.egg-info}/PKG-INFO +131 -15
- {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/SOURCES.txt +7 -18
- code_towel-1.618/src/code_towel.egg-info/entry_points.txt +2 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/__init__.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/src/towel/changes.py +8 -7
- {code_towel-1.414 → code_towel-1.618}/src/towel/cli.py +140 -73
- {code_towel-1.414 → code_towel-1.618}/src/towel/renaming.py +16 -7
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/__init__.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/assignment_analyzer.py +3 -21
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/binding_detector.py +4 -8
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/block_signature.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/builtins.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/definite_assignment.py +10 -25
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/exceptions.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/extractor.py +24 -20
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/instantiation.py +10 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/models.py +9 -5
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/nominal_unifier.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/orphan_detector.py +1 -1
- code_towel-1.618/src/towel/unification/parameters.py +41 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/pipeline.py +9 -59
- code_towel-1.618/src/towel/unification/progress.py +43 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/project_layout.py +100 -12
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/refactor_engine.py +709 -602
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/scope_analyzer.py +8 -26
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/semantic_safety.py +76 -12
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/unifier.py +19 -24
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/visitors.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/test_examples/README.md +49 -19
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/README.md +7 -14
- {code_towel-1.414 → code_towel-1.618}/tests/OBSERVATIONAL_EQUIVALENCE.md +37 -80
- code_towel-1.618/tests/README.md +84 -0
- {code_towel-1.414 → code_towel-1.618}/tests/automatic_equivalence_tester.py +1 -4
- {code_towel-1.414 → code_towel-1.618}/tests/generate_baseline.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_adversarial_renaming.py +28 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_adversarial_semantics.py +6 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_ancestor_insertion.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_assignment_analyzer_comprehensive.py +1 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_backend_layouts.py +60 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_binding_detector.py +1 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_binding_detector_edge_cases.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_bindings.py +7 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_breakers.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_builtins_comprehensive.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_candidate_index.py +1 -13
- {code_towel-1.414 → code_towel-1.618}/tests/test_cli_integration.py +28 -20
- {code_towel-1.414 → code_towel-1.618}/tests/test_comprehensive_coverage.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_crossfile_observational_equivalence.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_engine_adversarial.py +3 -3
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_additional_branches.py +2 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_comprehensive.py +55 -19
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_edge_cases.py +15 -16
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_return_statements.py +2 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_final_coverage_push.py +0 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_free_variable_correspondence.py +2 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_helper_inventory.py +44 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_helpers.py +0 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_nominal_unifier.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_observational_equivalence.py +2 -4
- {code_towel-1.414 → code_towel-1.618}/tests/test_orphan_detector_comprehensive.py +1 -1
- code_towel-1.618/tests/test_out_of_place_cycle_regression.py +117 -0
- code_towel-1.618/tests/test_parameters.py +29 -0
- code_towel-1.618/tests/test_project_layout_and_imports.py +263 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_project_layout_behavior.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_promotion_and_mangling.py +36 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_comprehensive.py +8 -7
- {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_edge_cases.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_more.py +0 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_targeted_branches.py +5 -9
- {code_towel-1.414 → code_towel-1.618}/tests/test_refactoring_engine.py +0 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_regression.py +1 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_scope_analyzer_comprehensive.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_semantic_safety_regressions.py +51 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_signature_prefilter.py +1 -1
- code_towel-1.618/tests/test_trivial_helper_filter.py +55 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_additional_branches.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_bound_variables.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_comprehensive.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_edge_cases.py +1 -1
- {code_towel-1.414 → code_towel-1.618}/tests/test_validation_partial_lifetime.py +2 -2
- {code_towel-1.414 → code_towel-1.618}/tests/test_variable_capture_bug.py +2 -2
- {code_towel-1.414 → code_towel-1.618}/uv.lock +1 -1
- code_towel-1.414/SECURITY.md +0 -13
- code_towel-1.414/docs/BUG_HANDOFF_2025-12-03.md +0 -278
- code_towel-1.414/docs/CROSS_FILE_REFACTORING.md +0 -233
- code_towel-1.414/docs/JUSTFILE_REFERENCE.md +0 -361
- code_towel-1.414/docs/README_DIRECTORY_USAGE.md +0 -265
- code_towel-1.414/docs/TESTING_AUDIT.md +0 -281
- code_towel-1.414/docs/TROUBLESHOOTING.md +0 -284
- code_towel-1.414/docs/UNIFICATION_IMPLEMENTATION.md +0 -257
- code_towel-1.414/scripts/dry +0 -7
- code_towel-1.414/scripts/preview +0 -162
- code_towel-1.414/src/code_towel.egg-info/entry_points.txt +0 -4
- code_towel-1.414/src/towel/unification/ast_normalizer.py +0 -237
- code_towel-1.414/src/towel/unification/ast_pretty_printer.py +0 -133
- code_towel-1.414/src/towel/unification/visitor_utils.py +0 -83
- code_towel-1.414/tests/README.md +0 -87
- code_towel-1.414/tests/test_ast_normalizer_comprehensive.py +0 -644
- code_towel-1.414/tests/test_ast_pretty_printer.py +0 -77
- code_towel-1.414/tests/test_ast_pretty_printer_comprehensive.py +0 -550
- code_towel-1.414/tests/test_ast_pretty_printer_more.py +0 -26
- code_towel-1.414/tests/test_legacy_normalizers.py +0 -69
- code_towel-1.414/tests/test_project_layout_and_imports.py +0 -145
- code_towel-1.414/tests/test_visitor_utils_comprehensive.py +0 -363
- {code_towel-1.414 → code_towel-1.618}/.flake8 +0 -0
- {code_towel-1.414 → code_towel-1.618}/.pre-commit-config.yaml +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/README.md +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/binding_constructs_comprehensive.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/bindings_comprehensions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/bindings_for_loops.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/closure_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/complex_expressions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/control_flow_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/edge_cases_stress_test.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/example1_simple.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/example2_classes.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/example3_file1.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/example3_file2.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/example4_complex.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/exception_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/fstrings_constants.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/functional_patterns.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/global_nonlocal_examples.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/hygienic_naming.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/method_chains.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/nested_structures.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/real_world_patterns.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/referential_transparency.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/return_values.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/scoping_edge_cases.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/side_effects_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/syntactic_coverage_comprehensive.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/.templates/tricky_edge_cases_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/CODE_OF_CONDUCT.md +0 -0
- {code_towel-1.414 → code_towel-1.618}/LICENSE +0 -0
- {code_towel-1.414 → code_towel-1.618}/MANIFEST.in +0 -0
- {code_towel-1.414 → code_towel-1.618}/docs/ADVERSARIAL_REVIEW.md +0 -0
- {code_towel-1.414 → code_towel-1.618}/docs/OPEN_SOURCE_AUDIT.md +0 -0
- {code_towel-1.414 → code_towel-1.618}/docs/PRODUCTION_READINESS.md +0 -0
- {code_towel-1.414 → code_towel-1.618}/docs/RELEASE_LOG.md +0 -0
- {code_towel-1.414 → code_towel-1.618}/justfile +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/bench_refactor.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/debug_pair.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/ecosystem/manifest.toml +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/report_net_benefit_diff.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/run_crossfile_coverage.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/set_version.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/trace_proposals.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/scripts/verify-examples +0 -0
- {code_towel-1.414 → code_towel-1.618}/setup.cfg +0 -0
- {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/dependency_links.txt +0 -0
- {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/requires.txt +0 -0
- {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/top_level.txt +0 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/filesystem.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/py.typed +0 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/structural_memo.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/src/towel/unification/thunk_inlining.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/binding_constructs_comprehensive.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/bindings_comprehensions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/bindings_for_loops.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/closure_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/complex_expressions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/control_flow_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/edge_cases_stress_test.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/example1_simple.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/example2_classes.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/example3_file1.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/example3_file2.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/example4_complex.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/exception_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/fstrings_constants.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/functional_patterns.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/global_nonlocal_examples.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/hygienic_naming.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/method_chains.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/nested_structures.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/real_world_patterns.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/referential_transparency.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/return_values.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/scoping_edge_cases.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/side_effects_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/syntactic_coverage_comprehensive.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples/tricky_edge_cases_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/multi_level/api/checkout.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/multi_level/core/services/payment.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/multi_level/utils/validators.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/nested_structure/src/data_processor.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/simple_crossfile/admin_service.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/simple_crossfile/user_service.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/multi_level/api/checkout.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/multi_level/core/services/payment.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/multi_level/utils/validators.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/nested_structure/src/data_processor.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/simple_crossfile/admin_service.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/simple_crossfile/user_service.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/binding_constructs_comprehensive.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/bindings_comprehensions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/bindings_for_loops.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/closure_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/complex_expressions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/control_flow_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/edge_cases_stress_test.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example1_simple.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example2_classes.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example3_file1.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example3_file2.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example4_complex.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/exception_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/fstrings_constants.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/functional_patterns.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/global_nonlocal_examples.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/hygienic_naming.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/method_chains.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/nested_structures.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/real_world_patterns.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/referential_transparency.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/return_values.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/scoping_edge_cases.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/side_effects_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/syntactic_coverage_comprehensive.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/tricky_edge_cases_adversarial.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/conftest.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/crossfile_equivalence_tester.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/edge_case_values.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/equivalence_targets.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h01_side_effect_order.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h02_conditional_eval.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h03_loop_reeval.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h04_closure_freevar.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h05_cond_return_plus_retvar.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h06_del_in_block.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h07_except_unbind.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h08_late_binding_closure.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h10_short_circuit.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h13_dunder_file.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h26_match_capture.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h38_closure_after_block.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h39_augassign_after_block.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h51_semicolons.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h52_del_after_block.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h53_nested_func_reads_retvar.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h54_attr_param_property.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h55_walrus_used_after.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h56_global_write.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h57_exception_in_param.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h58_nested_def_freevar_reassigned.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h59_retvar_conditional_unbound.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h60_finally_return.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h61_str_method_param.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r01_property_thunk_order.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r02_cluster_attrs.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r07_attr_two_positions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r100_clustered_site_with_live_binding.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r101_same_named_method_forces_module_helper.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r11_same_function_pair.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r14_starred_param.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r22_fstring_attr.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r26_attr_store_param.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r27_del_subscript.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r32_method_callee.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r38_name_two_positions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r39_cluster_alpha.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r40_class_hierarchy.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r41_conditional_call_param.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r42_loop_call_param.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r43_opaque_decorator_receiver.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r44_cluster_nested_overlap.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r45_async_no_await.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r46_nonlocal_counter.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r47_lru_cache_method.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r50_helper_name_collision.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r53_try_else_binding.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r54_with_binding_used_after.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r55_chained_assign.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r57_star_unpack.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r58_escaping_closure.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r60_local_import.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r61_same_function_clean.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r62_recursion.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r63_except_var_used_in_handler.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r66_conditional_callee.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r69_annassign.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r72_global_promoted.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r73_slice_vs_index.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r74_starred_vs_plain.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r75_prior_helper_param_names.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r76_return_order.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r77_mixed_return_and_variables.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r78_walrus_in_parameter.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r79_list_display_in_loop.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r80_inlined_leading_thunk.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r81_conditionally_bound_free_variable.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r82_local_classes_same_name.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r83_tab_indented_class.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r84_warn_stacklevel.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r85_conditionally_bound_parameter.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r86_annotated_assignment_live.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r87_nested_function_in_method.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r88_elif_branch.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r89_conditionally_bound_return.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r90_cluster_across_classes.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r91_class_body_helper.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r92_parameterless_class_body_helper.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r93_import_name_differs.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r94_import_binds_live_name.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r95_type_checking_annotation.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r96_unpacked_targets_live.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r97_helper_in_function_with_outside_site.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r98_same_named_methods_nested_helpers.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r99_partial_return_branches.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/pkg/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/pkg/a.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/pkg/b.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/run.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/pkg/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/pkg/a.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/pkg/b.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/run.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/pkg/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/pkg/a.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/pkg/b.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/run.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/a.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/b.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/util1.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/util2.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/run.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/pkg/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/pkg/a.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/pkg/b.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/run.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/a.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/b.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/c.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/d.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/e.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/run.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/base.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/introspect.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/revoke.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/__init__.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/access.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/base.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/request.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/run.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/run_tests.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_analysis_sessions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_bindings_additional.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_change_transactions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_cli_output_safety.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_copy_preservation.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_crossfile_integration.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_definite_assignment.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_engine_failure_visibility.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_equivalence_harness_regressions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_equivalence_target_selection.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_exceptions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_augassign_and_fstrings.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_callee_and_multi_return.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_final_lines.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_generate_call_variants.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_more_paths.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_preamble_and_call_mapping.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_remaining_branches.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_substituter_and_helpers.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_frame_sensitivity_scan.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_fstrings.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_hostile_battery.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_hostile_crossfile_battery.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_import_bindings.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_import_resolution.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_instantiation_check.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_method_level_and_attributes.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_method_receiver_arguments.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_orphan_detection.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_overlap_filtering.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_parent_watchdog.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_pipeline_api.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_pipeline_phases.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_progress_modes.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_regressions_broader.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_release_regressions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_rename_parameters_and_methods.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_return_values.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_scope_analyzer.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_semantic_guards_extended.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_structural_memo.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_thunk_inlining.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch2.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch3.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch4.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch5.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_comprehensions.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_core.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_more_branches.py +0 -0
- {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_state_reset.py +0 -0
|
@@ -7,6 +7,32 @@ ecosystem evidence behind each claim. The format follows
|
|
|
7
7
|
[Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
|
|
8
8
|
[Semantic Versioning](https://semver.org/).
|
|
9
9
|
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [1.618] — 2026-09-16
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Cross-file helpers are imported relatively by default (`from .module import
|
|
16
|
+
helper`), which stays valid when an out-of-place output is adopted into its
|
|
17
|
+
real location. An absolute import is used only when a packaging marker
|
|
18
|
+
(`pyproject.toml`/`setup.*`) anchors the module name, so the previous
|
|
19
|
+
behavior is preserved for in-place refactoring of a packaged project.
|
|
20
|
+
- Trivial forwarding helpers are no longer proposed: a block whose helper body
|
|
21
|
+
would be a single `raise`, a `return` of one call, or a bare call adds
|
|
22
|
+
indirection without sharing logic. Construct the engine with
|
|
23
|
+
`skip_trivial_helpers=False` to restore the old behavior.
|
|
24
|
+
- A cross-file helper is placed in a module that does not close an import cycle.
|
|
25
|
+
When the shared block spans modules, the helper is hosted in one the others
|
|
26
|
+
already import rather than adding a back-edge; the extraction is declined only
|
|
27
|
+
when no placement is safe (a genuine pre-existing cycle). This replaces the
|
|
28
|
+
earlier behavior of hosting the helper in the first module and declining
|
|
29
|
+
whenever that would cycle.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- The rename tool no longer refuses a module merely because it contains a local
|
|
33
|
+
variable or parameter named `vars`, `globals`, `locals`, `eval`, or `exec`;
|
|
34
|
+
it flags only a genuine reference to the builtin.
|
|
35
|
+
|
|
10
36
|
## [1.414] — 2026-09-15
|
|
11
37
|
|
|
12
38
|
First release prepared under the open-source audit. Beta: the engineering and
|
|
@@ -172,4 +172,4 @@ Thank you for contributing to Towel!
|
|
|
172
172
|
|
|
173
173
|
For reproducible tool versions, use `uv sync --frozen --extra dev` and the commands in README.md. CI runs the full tests and an unconditional 85% coverage gate for each supported Python version. `just release VERSION` prepares local distributions only; publication requires maintainer review of the current audit and policy decisions.
|
|
174
174
|
|
|
175
|
-
Release maintainers should follow [docs/RELEASING.md](docs/RELEASING.md).
|
|
175
|
+
Release maintainers should follow [docs/RELEASING.md](docs/RELEASING.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-towel
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.618
|
|
4
4
|
Summary: A Python tool that DRYs your code - finds and refactors repeated code using unification-based analysis
|
|
5
5
|
Author: Eric Allen
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -36,19 +36,85 @@ Dynamic: license-file
|
|
|
36
36
|
|
|
37
37
|
Towel finds repeated Python code using unification and proposes helper-function extractions.
|
|
38
38
|
|
|
39
|
+
> **Install from PyPI as [`code-towel`](https://pypi.org/project/code-towel/)** (the command is `towel`):
|
|
40
|
+
> `pip install code-towel`
|
|
41
|
+
> Do **not** install `towel`: `pip install towel` and `uvx towel` fetch a different, unrelated project.
|
|
42
|
+
|
|
39
43
|
**Release status: 1.414 (beta).** Every accepted proposal is verified by instantiating the helper with each call's arguments and comparing the result with the block it replaces; arguments that could have observable effects or fresh identity are evaluated inside the helper at their original position. A standing ecosystem check refactors 91 public projects and runs each one's own test suite before and after: 75 pass identically, 13 produce no proposal, and 3 differ only in documented frame-sensitive or source-observing ways (see below). Refactoring is still a change to your code: preview first, review the diff, and run your tests. [Known limitations](docs/KNOWN_LIMITATIONS.md) lists what is verified, what is rejected, and what remains outside the model; [the readiness report](docs/PRODUCTION_READINESS.md) records the evidence.
|
|
40
44
|
|
|
45
|
+
**New here?** The [Quick start](docs/QUICKSTART.md) gets you from install to a reviewed refactoring in four steps.
|
|
46
|
+
|
|
47
|
+
## What it does
|
|
48
|
+
|
|
49
|
+
Towel finds code that is repeated across your functions and pulls each group of
|
|
50
|
+
duplicates into one shared helper, rewriting the copies as calls to it. It works
|
|
51
|
+
by *anti-unification*: it computes the least-general generalization of the
|
|
52
|
+
matching blocks, so the parts that are the same become the helper's body and the
|
|
53
|
+
parts that differ become its parameters.
|
|
54
|
+
|
|
55
|
+
Given two functions that share a block:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
def order_summary(order):
|
|
59
|
+
items = [i for i in order.items if i.in_stock]
|
|
60
|
+
subtotal = sum(i.price for i in items)
|
|
61
|
+
total = round(subtotal * 1.08, 2)
|
|
62
|
+
return f"Order {order.id}: ${total}"
|
|
63
|
+
|
|
64
|
+
def quote_summary(quote):
|
|
65
|
+
items = [i for i in quote.items if i.in_stock]
|
|
66
|
+
subtotal = sum(i.price for i in items)
|
|
67
|
+
total = round(subtotal * 1.08, 2)
|
|
68
|
+
return f"Quote {quote.id}: ${total}"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Towel proposes the shared block as a helper (emitted with a placeholder name you
|
|
72
|
+
rename afterward) and rewrites both functions to call it:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
def __extracted_func_0(record):
|
|
76
|
+
items = [i for i in record.items if i.in_stock]
|
|
77
|
+
subtotal = sum(i.price for i in items)
|
|
78
|
+
total = round(subtotal * 1.08, 2)
|
|
79
|
+
return total
|
|
80
|
+
|
|
81
|
+
def order_summary(order):
|
|
82
|
+
total = __extracted_func_0(order)
|
|
83
|
+
return f"Order {order.id}: ${total}"
|
|
84
|
+
|
|
85
|
+
def quote_summary(quote):
|
|
86
|
+
total = __extracted_func_0(quote)
|
|
87
|
+
return f"Quote {quote.id}: ${total}"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
What makes Towel different from a search-and-replace is that it is conservative
|
|
91
|
+
and verified. It proposes an extraction only when it can prove the result runs
|
|
92
|
+
the same as the original — instantiating the helper with each call's arguments
|
|
93
|
+
and comparing against the block it replaced — and it refuses cases it cannot
|
|
94
|
+
establish rather than guess. It skips trivial extractions that would add
|
|
95
|
+
indirection without sharing real logic, wraps arguments that must not be
|
|
96
|
+
evaluated eagerly in zero-argument `lambda`s (see
|
|
97
|
+
[below](#why-some-arguments-are-wrapped-in-lambda)), and leaves naming to you.
|
|
98
|
+
Nothing is written without your say-so: the workflow is preview, refactor into a
|
|
99
|
+
copy, review the diff, and run your tests.
|
|
100
|
+
|
|
41
101
|
## Install
|
|
42
102
|
|
|
43
|
-
The
|
|
103
|
+
The PyPI package is **`code-towel`**; installing it gives you the **`towel`** command.
|
|
44
104
|
|
|
45
105
|
```bash
|
|
46
|
-
|
|
106
|
+
pip install code-towel
|
|
47
107
|
towel --version
|
|
48
108
|
towel --help
|
|
49
109
|
```
|
|
50
110
|
|
|
51
|
-
|
|
111
|
+
Install `code-towel`, not `towel`: the name `towel` on PyPI is a different, unrelated project, so `pip install towel` and `uvx towel` will not install this tool. To run it with `uvx` without installing, name the package explicitly:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
uvx --from code-towel towel --help
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The runtime uses only the standard library; optional `tqdm` provides progress bars. Platform, CPU, memory, and disk requirements are in [Requirements](#requirements) below.
|
|
52
118
|
|
|
53
119
|
## Requirements
|
|
54
120
|
|
|
@@ -79,7 +145,8 @@ The two largest projects in the ecosystem check, networkx and Sphinx, are the sl
|
|
|
79
145
|
## Use
|
|
80
146
|
|
|
81
147
|
```bash
|
|
82
|
-
# Read-only analysis
|
|
148
|
+
# Read-only analysis: lists each opportunity with the extracted helper and,
|
|
149
|
+
# per call site, the original block (-) next to the generated call (+)
|
|
83
150
|
towel preview path/to/project
|
|
84
151
|
|
|
85
152
|
# Write to a new output directory
|
|
@@ -98,7 +165,7 @@ To roll back an interrupted batch, use `towel recover /path/to/.towel-transactio
|
|
|
98
165
|
|
|
99
166
|
For detailed conservative rejection reasons, set `DEBUG_PROPOSAL_REJECTIONS=1` when running preview.
|
|
100
167
|
|
|
101
|
-
|
|
168
|
+
Run `towel dry --help` for import-layout, iteration, and progress options.
|
|
102
169
|
|
|
103
170
|
## What is analyzed
|
|
104
171
|
|
|
@@ -106,10 +173,51 @@ The pipeline parses modules, analyzes scopes, collects functions and classes, co
|
|
|
106
173
|
|
|
107
174
|
Differing sub-expressions become helper parameters. Names, literals, and tuples of those are passed eagerly; any other expression is passed as a zero-argument thunk and evaluated inside the helper where the original expression stood, so evaluation order, count, and conditionality are preserved. Expressions that read names bound inside the block are lambda-lifted with those names as arguments. A thunk the helper would evaluate first, once, and unconditionally is passed eagerly instead, since nothing can observe the difference. Before a proposal is offered, the helper is instantiated with each call's arguments and must reproduce the original block up to renamed binders.
|
|
108
175
|
|
|
109
|
-
The refactoring pipeline preserves the original Python operators.
|
|
176
|
+
The refactoring pipeline preserves the original Python operators. Generator/suspension operations and frame-sensitive calls such as `locals()` are conservatively rejected. Nested blocks that bind names used outside the block are rejected until full control-flow liveness is supported. Static local import cycles and cross-module global declarations are rejected. This reduces the number of proposals rather than claiming an unsupported transformation is safe.
|
|
110
177
|
|
|
111
178
|
Dynamic imports, reflection, arbitrary callbacks, runtime rebinding, metaclasses, and external side effects limit what can be established statically. Each engine owns a bounded analysis session with content checks and isolated AST snapshots. The test import-isolation harness and an individual engine instance require sequential use. Candidates involving detected namespace rebinding, frame inspection, or comprehension assignment expressions are rejected; opaque external reflection and rebinding remain outside the supported model.
|
|
112
179
|
|
|
180
|
+
## Why some arguments are wrapped in `lambda`
|
|
181
|
+
|
|
182
|
+
When the differing sub-expression between two duplicates is more than a plain value, Towel passes it as a zero-argument lambda (a *thunk*) and calls it inside the helper at the exact place the original expression stood. In the output that looks like this:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
notify(lambda: welcome_email(user))
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
To a casual reader the lambda looks redundant. It is not. Passing the expression directly would evaluate it once, unconditionally, at the call site, and that changes behavior whenever the original evaluated it conditionally, more than once, or not at all. Consider two blocks that differ only in one call (illustrative):
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
# before
|
|
192
|
+
if user.active:
|
|
193
|
+
send(welcome_email(user))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Pass that call eagerly and it runs for every user, active or not:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
# WRONG: welcome_email(user) runs unconditionally, with its side effects, and
|
|
200
|
+
# can raise, even when user.active is False
|
|
201
|
+
def notify(message, user):
|
|
202
|
+
if user.active:
|
|
203
|
+
send(message)
|
|
204
|
+
|
|
205
|
+
notify(welcome_email(user), user)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The thunk defers it to the spot the original code ran it, preserving the condition:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# correct
|
|
212
|
+
def notify(build_message, user):
|
|
213
|
+
if user.active:
|
|
214
|
+
send(build_message())
|
|
215
|
+
|
|
216
|
+
notify(lambda: welcome_email(user), user)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The same reasoning covers an expression inside a loop (the thunk runs once per iteration, as the original did) or one that might raise. Towel keeps the wrapper only where it can matter: an expression the helper would evaluate first, once, and unconditionally is passed directly, because then nothing can observe the difference. So a `lambda:` in the output is a deliberate marker that Towel is preserving that argument's timing, count, or conditionality; its absence means eager evaluation was proven equivalent.
|
|
220
|
+
|
|
113
221
|
## Naming the helpers with an LLM
|
|
114
222
|
|
|
115
223
|
Towel deliberately generates meaningless names, `__extracted_func_3` and `__param_0`, and leaves the naming to a separate, review-first step. Extraction is a verified mechanical transformation; choosing a good name is a judgment call, so the two are kept apart. The intended workflow hands the naming to a coding assistant, because the assistant reads far better names out of the call sites than any heuristic, and you review its choices before they touch the code.
|
|
@@ -131,6 +239,8 @@ towel rename-helpers path/to/cleaned --rename-file renames.json
|
|
|
131
239
|
|
|
132
240
|
The JSON inventory gives the assistant what it needs to name well: every helper with its scope, source, and call sites, and for each parameter its evaluation kind (`value`, `thunk`, `lifted`, `receiver`) and the actual argument expressions passed at every call site. A parameter that always receives `user.email` and `account.email` should become `email`, and the argument expressions are how the assistant sees that.
|
|
133
241
|
|
|
242
|
+
Each helper also carries a `changes` list: for every call site, the exact original block it replaced (`before`) next to the generated call (`after`). Seeing what the code did before extraction is what lets an assistant finish good names, write a docstring, and infer parameter and return types. This comes from a small `.towel-helpers.json` that `towel dry` writes next to its output; it is only for the naming step and is safe to delete afterward.
|
|
243
|
+
|
|
134
244
|
Each inventory entry carries the exact mapping key to use as a rename target: `"path.py:helper"` renames a module-level helper together with its importers, `"helper"` renames a unique class-level helper together with every attribute reference, and `"path.py:helper.__param_0"` renames a parameter within the helper's scope. The rename is applied as one atomic batch with scope and importer checks; a name collision, a mangled name, or a dynamic reference aborts the whole batch and reports the reason, so a bad suggestion changes nothing. `--dry-run` reports the same JSON without writing.
|
|
135
245
|
|
|
136
246
|
The shared `towel-rename` skill (in the agent-skills repository) walks an assistant through the whole loop: extract, review the diff, name, apply, and re-test. An interactive prompt mode is also available for naming by hand, and it calls no LLM service.
|
|
@@ -160,15 +270,21 @@ Behavioral tests compare sampled return values and types, exceptions, output, an
|
|
|
160
270
|
|
|
161
271
|
## Documentation
|
|
162
272
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- [
|
|
166
|
-
- [Known limitations](docs/KNOWN_LIMITATIONS.md)
|
|
167
|
-
- [
|
|
168
|
-
|
|
169
|
-
|
|
273
|
+
Start here:
|
|
274
|
+
|
|
275
|
+
- [Quick start](docs/QUICKSTART.md) — install and refactor in four steps
|
|
276
|
+
- [Known limitations](docs/KNOWN_LIMITATIONS.md) — what is verified, what is rejected, and what is outside the model
|
|
277
|
+
- [Python API guide](docs/USAGE_GUIDE.md) — using `UnificationRefactorEngine` directly
|
|
278
|
+
|
|
279
|
+
How it works and why to trust it:
|
|
280
|
+
|
|
281
|
+
- [Architecture](docs/ARCHITECTURE.md) — the pipeline, the algorithms, and their references
|
|
282
|
+
- [Production readiness](docs/PRODUCTION_READINESS.md) — the ecosystem evidence behind the claims
|
|
283
|
+
- [Adversarial review](docs/ADVERSARIAL_REVIEW.md) — defects found and repaired
|
|
284
|
+
|
|
285
|
+
Project:
|
|
170
286
|
|
|
171
|
-
|
|
287
|
+
- [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md) · [Releasing](docs/RELEASING.md)
|
|
172
288
|
|
|
173
289
|
## License
|
|
174
290
|
|
|
@@ -2,19 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
Towel finds repeated Python code using unification and proposes helper-function extractions.
|
|
4
4
|
|
|
5
|
+
> **Install from PyPI as [`code-towel`](https://pypi.org/project/code-towel/)** (the command is `towel`):
|
|
6
|
+
> `pip install code-towel`
|
|
7
|
+
> Do **not** install `towel`: `pip install towel` and `uvx towel` fetch a different, unrelated project.
|
|
8
|
+
|
|
5
9
|
**Release status: 1.414 (beta).** Every accepted proposal is verified by instantiating the helper with each call's arguments and comparing the result with the block it replaces; arguments that could have observable effects or fresh identity are evaluated inside the helper at their original position. A standing ecosystem check refactors 91 public projects and runs each one's own test suite before and after: 75 pass identically, 13 produce no proposal, and 3 differ only in documented frame-sensitive or source-observing ways (see below). Refactoring is still a change to your code: preview first, review the diff, and run your tests. [Known limitations](docs/KNOWN_LIMITATIONS.md) lists what is verified, what is rejected, and what remains outside the model; [the readiness report](docs/PRODUCTION_READINESS.md) records the evidence.
|
|
6
10
|
|
|
11
|
+
**New here?** The [Quick start](docs/QUICKSTART.md) gets you from install to a reviewed refactoring in four steps.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
Towel finds code that is repeated across your functions and pulls each group of
|
|
16
|
+
duplicates into one shared helper, rewriting the copies as calls to it. It works
|
|
17
|
+
by *anti-unification*: it computes the least-general generalization of the
|
|
18
|
+
matching blocks, so the parts that are the same become the helper's body and the
|
|
19
|
+
parts that differ become its parameters.
|
|
20
|
+
|
|
21
|
+
Given two functions that share a block:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
def order_summary(order):
|
|
25
|
+
items = [i for i in order.items if i.in_stock]
|
|
26
|
+
subtotal = sum(i.price for i in items)
|
|
27
|
+
total = round(subtotal * 1.08, 2)
|
|
28
|
+
return f"Order {order.id}: ${total}"
|
|
29
|
+
|
|
30
|
+
def quote_summary(quote):
|
|
31
|
+
items = [i for i in quote.items if i.in_stock]
|
|
32
|
+
subtotal = sum(i.price for i in items)
|
|
33
|
+
total = round(subtotal * 1.08, 2)
|
|
34
|
+
return f"Quote {quote.id}: ${total}"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Towel proposes the shared block as a helper (emitted with a placeholder name you
|
|
38
|
+
rename afterward) and rewrites both functions to call it:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
def __extracted_func_0(record):
|
|
42
|
+
items = [i for i in record.items if i.in_stock]
|
|
43
|
+
subtotal = sum(i.price for i in items)
|
|
44
|
+
total = round(subtotal * 1.08, 2)
|
|
45
|
+
return total
|
|
46
|
+
|
|
47
|
+
def order_summary(order):
|
|
48
|
+
total = __extracted_func_0(order)
|
|
49
|
+
return f"Order {order.id}: ${total}"
|
|
50
|
+
|
|
51
|
+
def quote_summary(quote):
|
|
52
|
+
total = __extracted_func_0(quote)
|
|
53
|
+
return f"Quote {quote.id}: ${total}"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
What makes Towel different from a search-and-replace is that it is conservative
|
|
57
|
+
and verified. It proposes an extraction only when it can prove the result runs
|
|
58
|
+
the same as the original — instantiating the helper with each call's arguments
|
|
59
|
+
and comparing against the block it replaced — and it refuses cases it cannot
|
|
60
|
+
establish rather than guess. It skips trivial extractions that would add
|
|
61
|
+
indirection without sharing real logic, wraps arguments that must not be
|
|
62
|
+
evaluated eagerly in zero-argument `lambda`s (see
|
|
63
|
+
[below](#why-some-arguments-are-wrapped-in-lambda)), and leaves naming to you.
|
|
64
|
+
Nothing is written without your say-so: the workflow is preview, refactor into a
|
|
65
|
+
copy, review the diff, and run your tests.
|
|
66
|
+
|
|
7
67
|
## Install
|
|
8
68
|
|
|
9
|
-
The
|
|
69
|
+
The PyPI package is **`code-towel`**; installing it gives you the **`towel`** command.
|
|
10
70
|
|
|
11
71
|
```bash
|
|
12
|
-
|
|
72
|
+
pip install code-towel
|
|
13
73
|
towel --version
|
|
14
74
|
towel --help
|
|
15
75
|
```
|
|
16
76
|
|
|
17
|
-
|
|
77
|
+
Install `code-towel`, not `towel`: the name `towel` on PyPI is a different, unrelated project, so `pip install towel` and `uvx towel` will not install this tool. To run it with `uvx` without installing, name the package explicitly:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uvx --from code-towel towel --help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The runtime uses only the standard library; optional `tqdm` provides progress bars. Platform, CPU, memory, and disk requirements are in [Requirements](#requirements) below.
|
|
18
84
|
|
|
19
85
|
## Requirements
|
|
20
86
|
|
|
@@ -45,7 +111,8 @@ The two largest projects in the ecosystem check, networkx and Sphinx, are the sl
|
|
|
45
111
|
## Use
|
|
46
112
|
|
|
47
113
|
```bash
|
|
48
|
-
# Read-only analysis
|
|
114
|
+
# Read-only analysis: lists each opportunity with the extracted helper and,
|
|
115
|
+
# per call site, the original block (-) next to the generated call (+)
|
|
49
116
|
towel preview path/to/project
|
|
50
117
|
|
|
51
118
|
# Write to a new output directory
|
|
@@ -64,7 +131,7 @@ To roll back an interrupted batch, use `towel recover /path/to/.towel-transactio
|
|
|
64
131
|
|
|
65
132
|
For detailed conservative rejection reasons, set `DEBUG_PROPOSAL_REJECTIONS=1` when running preview.
|
|
66
133
|
|
|
67
|
-
|
|
134
|
+
Run `towel dry --help` for import-layout, iteration, and progress options.
|
|
68
135
|
|
|
69
136
|
## What is analyzed
|
|
70
137
|
|
|
@@ -72,10 +139,51 @@ The pipeline parses modules, analyzes scopes, collects functions and classes, co
|
|
|
72
139
|
|
|
73
140
|
Differing sub-expressions become helper parameters. Names, literals, and tuples of those are passed eagerly; any other expression is passed as a zero-argument thunk and evaluated inside the helper where the original expression stood, so evaluation order, count, and conditionality are preserved. Expressions that read names bound inside the block are lambda-lifted with those names as arguments. A thunk the helper would evaluate first, once, and unconditionally is passed eagerly instead, since nothing can observe the difference. Before a proposal is offered, the helper is instantiated with each call's arguments and must reproduce the original block up to renamed binders.
|
|
74
141
|
|
|
75
|
-
The refactoring pipeline preserves the original Python operators.
|
|
142
|
+
The refactoring pipeline preserves the original Python operators. Generator/suspension operations and frame-sensitive calls such as `locals()` are conservatively rejected. Nested blocks that bind names used outside the block are rejected until full control-flow liveness is supported. Static local import cycles and cross-module global declarations are rejected. This reduces the number of proposals rather than claiming an unsupported transformation is safe.
|
|
76
143
|
|
|
77
144
|
Dynamic imports, reflection, arbitrary callbacks, runtime rebinding, metaclasses, and external side effects limit what can be established statically. Each engine owns a bounded analysis session with content checks and isolated AST snapshots. The test import-isolation harness and an individual engine instance require sequential use. Candidates involving detected namespace rebinding, frame inspection, or comprehension assignment expressions are rejected; opaque external reflection and rebinding remain outside the supported model.
|
|
78
145
|
|
|
146
|
+
## Why some arguments are wrapped in `lambda`
|
|
147
|
+
|
|
148
|
+
When the differing sub-expression between two duplicates is more than a plain value, Towel passes it as a zero-argument lambda (a *thunk*) and calls it inside the helper at the exact place the original expression stood. In the output that looks like this:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
notify(lambda: welcome_email(user))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
To a casual reader the lambda looks redundant. It is not. Passing the expression directly would evaluate it once, unconditionally, at the call site, and that changes behavior whenever the original evaluated it conditionally, more than once, or not at all. Consider two blocks that differ only in one call (illustrative):
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# before
|
|
158
|
+
if user.active:
|
|
159
|
+
send(welcome_email(user))
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Pass that call eagerly and it runs for every user, active or not:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# WRONG: welcome_email(user) runs unconditionally, with its side effects, and
|
|
166
|
+
# can raise, even when user.active is False
|
|
167
|
+
def notify(message, user):
|
|
168
|
+
if user.active:
|
|
169
|
+
send(message)
|
|
170
|
+
|
|
171
|
+
notify(welcome_email(user), user)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The thunk defers it to the spot the original code ran it, preserving the condition:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# correct
|
|
178
|
+
def notify(build_message, user):
|
|
179
|
+
if user.active:
|
|
180
|
+
send(build_message())
|
|
181
|
+
|
|
182
|
+
notify(lambda: welcome_email(user), user)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The same reasoning covers an expression inside a loop (the thunk runs once per iteration, as the original did) or one that might raise. Towel keeps the wrapper only where it can matter: an expression the helper would evaluate first, once, and unconditionally is passed directly, because then nothing can observe the difference. So a `lambda:` in the output is a deliberate marker that Towel is preserving that argument's timing, count, or conditionality; its absence means eager evaluation was proven equivalent.
|
|
186
|
+
|
|
79
187
|
## Naming the helpers with an LLM
|
|
80
188
|
|
|
81
189
|
Towel deliberately generates meaningless names, `__extracted_func_3` and `__param_0`, and leaves the naming to a separate, review-first step. Extraction is a verified mechanical transformation; choosing a good name is a judgment call, so the two are kept apart. The intended workflow hands the naming to a coding assistant, because the assistant reads far better names out of the call sites than any heuristic, and you review its choices before they touch the code.
|
|
@@ -97,6 +205,8 @@ towel rename-helpers path/to/cleaned --rename-file renames.json
|
|
|
97
205
|
|
|
98
206
|
The JSON inventory gives the assistant what it needs to name well: every helper with its scope, source, and call sites, and for each parameter its evaluation kind (`value`, `thunk`, `lifted`, `receiver`) and the actual argument expressions passed at every call site. A parameter that always receives `user.email` and `account.email` should become `email`, and the argument expressions are how the assistant sees that.
|
|
99
207
|
|
|
208
|
+
Each helper also carries a `changes` list: for every call site, the exact original block it replaced (`before`) next to the generated call (`after`). Seeing what the code did before extraction is what lets an assistant finish good names, write a docstring, and infer parameter and return types. This comes from a small `.towel-helpers.json` that `towel dry` writes next to its output; it is only for the naming step and is safe to delete afterward.
|
|
209
|
+
|
|
100
210
|
Each inventory entry carries the exact mapping key to use as a rename target: `"path.py:helper"` renames a module-level helper together with its importers, `"helper"` renames a unique class-level helper together with every attribute reference, and `"path.py:helper.__param_0"` renames a parameter within the helper's scope. The rename is applied as one atomic batch with scope and importer checks; a name collision, a mangled name, or a dynamic reference aborts the whole batch and reports the reason, so a bad suggestion changes nothing. `--dry-run` reports the same JSON without writing.
|
|
101
211
|
|
|
102
212
|
The shared `towel-rename` skill (in the agent-skills repository) walks an assistant through the whole loop: extract, review the diff, name, apply, and re-test. An interactive prompt mode is also available for naming by hand, and it calls no LLM service.
|
|
@@ -126,15 +236,21 @@ Behavioral tests compare sampled return values and types, exceptions, output, an
|
|
|
126
236
|
|
|
127
237
|
## Documentation
|
|
128
238
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- [
|
|
132
|
-
- [Known limitations](docs/KNOWN_LIMITATIONS.md)
|
|
133
|
-
- [
|
|
134
|
-
|
|
135
|
-
|
|
239
|
+
Start here:
|
|
240
|
+
|
|
241
|
+
- [Quick start](docs/QUICKSTART.md) — install and refactor in four steps
|
|
242
|
+
- [Known limitations](docs/KNOWN_LIMITATIONS.md) — what is verified, what is rejected, and what is outside the model
|
|
243
|
+
- [Python API guide](docs/USAGE_GUIDE.md) — using `UnificationRefactorEngine` directly
|
|
244
|
+
|
|
245
|
+
How it works and why to trust it:
|
|
246
|
+
|
|
247
|
+
- [Architecture](docs/ARCHITECTURE.md) — the pipeline, the algorithms, and their references
|
|
248
|
+
- [Production readiness](docs/PRODUCTION_READINESS.md) — the ecosystem evidence behind the claims
|
|
249
|
+
- [Adversarial review](docs/ADVERSARIAL_REVIEW.md) — defects found and repaired
|
|
250
|
+
|
|
251
|
+
Project:
|
|
136
252
|
|
|
137
|
-
|
|
253
|
+
- [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md) · [Releasing](docs/RELEASING.md)
|
|
138
254
|
|
|
139
255
|
## License
|
|
140
256
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
Towel rewrites source code. Review the generated diff and run the affected project's own tests before adopting it; see [docs/KNOWN_LIMITATIONS.md](docs/KNOWN_LIMITATIONS.md) for what is verified and what is not.
|
|
4
|
+
|
|
5
|
+
## Supported versions
|
|
6
|
+
|
|
7
|
+
Security fixes are provided for the latest released version, currently **1.414**. Earlier versions are not supported; the PyPI releases `1.0.0`–`1.0.4` are yanked for broken import handling and are not a recommended installation target. Upgrade to the latest release rather than relying on a fix to an older one.
|
|
8
|
+
|
|
9
|
+
## Reporting a vulnerability
|
|
10
|
+
|
|
11
|
+
Report suspected vulnerabilities privately through GitHub's **"Report a vulnerability"** button on this repository's **Security** tab (Security → Advisories → Report a vulnerability). That opens a private security advisory visible only to the maintainer. Please do not open a public issue for a security report, and do not include credentials or confidential source code in a report.
|
|
12
|
+
|
|
13
|
+
This is a solo-maintained project with no formal response-time commitment. Reports are handled on a best-effort basis; you will receive an acknowledgment when a report is triaged. If a report leads to a fix, it ships in a new release, and the affected versions are noted in the advisory and the [changelog](CHANGELOG.md).
|
|
14
|
+
|
|
15
|
+
## Execution and source safety
|
|
16
|
+
|
|
17
|
+
Towel's analysis parses Python syntax without executing the analyzed project. The behavioral test harness executes fixtures and must be used only on trusted code. The rename assistant prints a prompt containing source excerpts for the user to transfer manually; consider the destination before sharing confidential code.
|
|
18
|
+
|
|
19
|
+
Directory refactoring excludes symlinked Python files and refuses overlapping input/output trees. Generated files are compiled before writing. Writes use staged byte plans, per-file atomic replacement, rollback, and an interruption-recovery journal. Apply and recovery require exclusive write access; an unrelated writer can race snapshot checks. A batch is not globally atomic to readers, and a sequence of extractions is not a single atomic operation. Compilation does not prove behavior preservation. Static analysis cannot fully model reflection, dynamic imports, arbitrary callbacks, or external effects. Keep the original version under source control and review every generated diff.
|
|
@@ -42,7 +42,10 @@ fixed-point loop (below).
|
|
|
42
42
|
blocks that unify with the accepted template and can share the helper.
|
|
43
43
|
9. **Materialize.** `extractor.py` renders the helper and the call sites,
|
|
44
44
|
compiles the generated Python to confirm it parses, and detects overlapping
|
|
45
|
-
replacements.
|
|
45
|
+
replacements. A proposal whose rendered helper body is a single forwarding
|
|
46
|
+
statement — a lone `raise`, a `return` of one call, or a bare call — is
|
|
47
|
+
dropped here: it would only add indirection. Construct the engine with
|
|
48
|
+
`skip_trivial_helpers=False` to keep such helpers.
|
|
46
49
|
10. **Apply.** `changes.py` turns accepted proposals into an immutable byte
|
|
47
50
|
plan and applies it transactionally (see *Application and recovery*).
|
|
48
51
|
|
|
@@ -172,8 +175,23 @@ helper can be imported correctly. It reads the build backend from
|
|
|
172
175
|
`pyproject.toml` and derives import roots for setuptools (including
|
|
173
176
|
`package-dir` mappings and the classic `src` layout), Hatch (wheel
|
|
174
177
|
`packages`/`sources`), Flit, Poetry (`packages` with `from`), and pdm
|
|
175
|
-
(`package-dir`).
|
|
176
|
-
the
|
|
178
|
+
(`package-dir`). An unrecognized backend falls back to conventional inference:
|
|
179
|
+
a package or module named after the distribution, in the project root or under
|
|
180
|
+
`src`. Only a layout that cannot be resolved either way raises rather than
|
|
181
|
+
guessing; the ecosystem check reports those as `UNSUPPORTED`.
|
|
182
|
+
|
|
183
|
+
For a helper shared between two files in the same package, `refactor_engine.py`
|
|
184
|
+
generates a relative import (`from .module import helper`, or a deeper
|
|
185
|
+
`..sub.module`). A relative import encodes only the intrinsic same-package
|
|
186
|
+
relationship, so it stays valid wherever the code lands — in particular when an
|
|
187
|
+
out-of-place output is adopted into its real location, the documented workflow —
|
|
188
|
+
and it matches the intra-package style the code already uses. An absolute import
|
|
189
|
+
is used only when the discovered layout is anchored by a real packaging marker
|
|
190
|
+
(`ProjectLayout.metadata_root`), so the absolute name survives relocation; a flat
|
|
191
|
+
module with no package, where a relative import would not resolve, keeps a bare
|
|
192
|
+
absolute name.
|
|
193
|
+
|
|
194
|
+
`semantic_safety.py`'s
|
|
177
195
|
`would_create_import_cycle` follows static imports through local modules,
|
|
178
196
|
caching each module's import edges by path, mtime, and size, and rejects a
|
|
179
197
|
helper placement that would close a cycle.
|
|
@@ -273,16 +291,6 @@ mapping as one atomic batch with scope and importer checks; a collision, a
|
|
|
273
291
|
mangled name, or a dynamic reference aborts the whole batch and reports why, so
|
|
274
292
|
a bad suggestion changes nothing. See the README for the end-to-end workflow.
|
|
275
293
|
|
|
276
|
-
## Legacy normalization utilities
|
|
277
|
-
|
|
278
|
-
[`ast_normalizer.py`](../src/towel/unification/ast_normalizer.py) is an
|
|
279
|
-
importable compatibility module that emits `DeprecationWarning` when its
|
|
280
|
-
transformers are constructed. Its assignment-to-augmented-assignment and
|
|
281
|
-
arithmetic rewrites are **not used by the production pipeline** and are unsafe
|
|
282
|
-
for arbitrary Python (`x = x + y` and `x += y` differ in aliasing and operator
|
|
283
|
-
dispatch; reordering changes evaluation order and overloaded behavior). Do not
|
|
284
|
-
reintroduce them into analysis or use them as an equivalence oracle.
|
|
285
|
-
|
|
286
294
|
## Verification and evidence
|
|
287
295
|
|
|
288
296
|
The transformation's safety rests on the per-proposal instantiation invariant.
|
|
@@ -341,6 +349,8 @@ but the ideas and their names are from the literature.
|
|
|
341
349
|
| Liveness and orphans | `definite_assignment.py`, `orphan_detector.py` |
|
|
342
350
|
| Safety guards, import cycles, pre-scan | `semantic_safety.py` |
|
|
343
351
|
| Helper and call-site rendering | `extractor.py`, `thunk_inlining.py` |
|
|
352
|
+
| Parameter enumeration | `parameters.py` |
|
|
353
|
+
| Progress reporting | `progress.py` |
|
|
344
354
|
| Structural memoization | `structural_memo.py` |
|
|
345
355
|
| Cross-file layout | `project_layout.py` |
|
|
346
356
|
| Data model | `models.py` |
|
|
@@ -127,6 +127,10 @@ uncertainty. Common reasons a real duplicate is not extracted:
|
|
|
127
127
|
|
|
128
128
|
- A differing sub-expression is a slice, a starred item, or a whole f-string;
|
|
129
129
|
these are container syntax rather than values.
|
|
130
|
+
- The extracted helper body would be a single forwarding statement — a lone
|
|
131
|
+
`raise`, a `return` of one call, or a bare call. Such a helper shares no
|
|
132
|
+
logic, only a name, so it is skipped by default; construct the engine with
|
|
133
|
+
`skip_trivial_helpers=False` to keep it.
|
|
130
134
|
- The block deletes, rebinds, or declares a name the caller keeps using.
|
|
131
135
|
- A nested function or lambda shares a rebound name with the block.
|
|
132
136
|
- The helper would need more than the configured maximum parameters.
|
|
@@ -143,11 +147,13 @@ uncertainty. Common reasons a real duplicate is not extracted:
|
|
|
143
147
|
would have to be rendered inside the preceding branch's `else`; the
|
|
144
148
|
`elif`'s own body and further branches remain candidates. This gives up a
|
|
145
149
|
valid extraction when the preceding branch always exits (tabulate).
|
|
146
|
-
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
- setuptools, Hatch, Flit, Poetry, and pdm layouts are read from their own
|
|
151
|
+
configuration. Any other build backend (for example ``flit_scm``) falls back
|
|
152
|
+
to conventional inference: a package or module named after the distribution,
|
|
153
|
+
in the project root or under ``src``. A project whose layout cannot be
|
|
154
|
+
resolved either way is refused for directory mode, and the ecosystem check
|
|
155
|
+
reports it as `UNSUPPORTED`. Poetry ``packages`` entries with ``to`` or glob
|
|
156
|
+
patterns, and a pdm ``package-dir`` pattern, are refused likewise.
|
|
151
157
|
|
|
152
158
|
Set `DEBUG_PROPOSAL_REJECTIONS=1` to print the reason for each rejected pair.
|
|
153
159
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Quick start
|
|
2
|
+
|
|
3
|
+
Towel finds repeated Python code and extracts each group of duplicates into one
|
|
4
|
+
helper function, rewriting the duplicates as calls. It verifies every extraction
|
|
5
|
+
and leaves the naming to you. Refactoring changes your code, so the workflow is:
|
|
6
|
+
preview, apply to a copy, review the diff, and run your tests.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install code-towel
|
|
12
|
+
towel --version
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The PyPI package is `code-towel` and the command is `towel`. Do not install
|
|
16
|
+
`towel`: that name belongs to a different, unrelated project, so `pip install
|
|
17
|
+
towel` and `uvx towel` will not get this tool. Python 3.11–3.13 on macOS or
|
|
18
|
+
Linux, no runtime dependencies.
|
|
19
|
+
|
|
20
|
+
## 1. See what it would change (read-only)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
towel preview path/to/project
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For each opportunity, `preview` prints the extracted helper and, per call site, the original block (`-`) next to the generated call (`+`), so you can see exactly what would change before applying anything.
|
|
27
|
+
|
|
28
|
+
## 2. Refactor into a fresh copy
|
|
29
|
+
|
|
30
|
+
Never refactor in place on your first run — write to a new directory and diff it.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
towel dry path/to/project path/to/cleaned --non-interactive
|
|
34
|
+
diff -ru path/to/project path/to/cleaned | less
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The output compiles and, for every proposal, the generated helper is verified to
|
|
38
|
+
reproduce the exact code it replaced. Helpers get placeholder names like
|
|
39
|
+
`__extracted_func_3`.
|
|
40
|
+
|
|
41
|
+
## 3. Give the helpers real names (optional, LLM-assisted)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
towel rename-helpers path/to/cleaned --list --json > helpers.json
|
|
45
|
+
# Have a coding assistant read helpers.json and write renames.json,
|
|
46
|
+
# then apply the batch (it aborts whole if any name is unsafe):
|
|
47
|
+
towel rename-helpers path/to/cleaned --rename-file renames.json --dry-run
|
|
48
|
+
towel rename-helpers path/to/cleaned --rename-file renames.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
See the [README](../README.md#naming-the-helpers-with-an-llm) for the full
|
|
52
|
+
naming workflow.
|
|
53
|
+
|
|
54
|
+
## 4. Review and test
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# adopt the cleaned copy however you version-control it, then:
|
|
58
|
+
pytest # or your project's own test command
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Next
|
|
62
|
+
|
|
63
|
+
- [README](../README.md) — full CLI usage, requirements, and timing
|
|
64
|
+
- [Known limitations](KNOWN_LIMITATIONS.md) — what is verified, rejected, and outside the model
|
|
65
|
+
- [Python API guide](USAGE_GUIDE.md) — using `UnificationRefactorEngine` directly
|
|
66
|
+
- [Architecture](ARCHITECTURE.md) — how it works
|