code-towel 1.0.2__tar.gz → 1.0.3__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.
Files changed (103) hide show
  1. {code_towel-1.0.2/src/code_towel.egg-info → code_towel-1.0.3}/PKG-INFO +11 -1
  2. {code_towel-1.0.2 → code_towel-1.0.3}/README.md +10 -0
  3. {code_towel-1.0.2 → code_towel-1.0.3}/pyproject.toml +1 -1
  4. {code_towel-1.0.2 → code_towel-1.0.3/src/code_towel.egg-info}/PKG-INFO +11 -1
  5. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/cli.py +52 -14
  6. {code_towel-1.0.2 → code_towel-1.0.3}/LICENSE +0 -0
  7. {code_towel-1.0.2 → code_towel-1.0.3}/setup.cfg +0 -0
  8. {code_towel-1.0.2 → code_towel-1.0.3}/src/code_towel.egg-info/SOURCES.txt +0 -0
  9. {code_towel-1.0.2 → code_towel-1.0.3}/src/code_towel.egg-info/dependency_links.txt +0 -0
  10. {code_towel-1.0.2 → code_towel-1.0.3}/src/code_towel.egg-info/entry_points.txt +0 -0
  11. {code_towel-1.0.2 → code_towel-1.0.3}/src/code_towel.egg-info/requires.txt +0 -0
  12. {code_towel-1.0.2 → code_towel-1.0.3}/src/code_towel.egg-info/top_level.txt +0 -0
  13. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/__init__.py +0 -0
  14. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/__init__.py +0 -0
  15. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/assignment_analyzer.py +0 -0
  16. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/ast_normalizer.py +0 -0
  17. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/ast_pretty_printer.py +0 -0
  18. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/binding_detector.py +0 -0
  19. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/block_signature.py +0 -0
  20. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/builtins.py +0 -0
  21. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/exceptions.py +0 -0
  22. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/extractor.py +0 -0
  23. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/models.py +0 -0
  24. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/nominal_unifier.py +0 -0
  25. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/orphan_detector.py +0 -0
  26. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/pipeline.py +0 -0
  27. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/project_layout.py +0 -0
  28. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/refactor_engine.py +0 -0
  29. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/scope_analyzer.py +0 -0
  30. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/unifier.py +0 -0
  31. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/visitor_utils.py +0 -0
  32. {code_towel-1.0.2 → code_towel-1.0.3}/src/towel/unification/visitors.py +0 -0
  33. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_ancestor_insertion.py +0 -0
  34. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_assignment_analyzer_comprehensive.py +0 -0
  35. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_ast_normalizer_comprehensive.py +0 -0
  36. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_ast_pretty_printer.py +0 -0
  37. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_ast_pretty_printer_comprehensive.py +0 -0
  38. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_ast_pretty_printer_more.py +0 -0
  39. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_binding_detector.py +0 -0
  40. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_binding_detector_edge_cases.py +0 -0
  41. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_bindings.py +0 -0
  42. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_bindings_additional.py +0 -0
  43. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_breakers.py +0 -0
  44. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_builtins_comprehensive.py +0 -0
  45. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_comprehensive_coverage.py +0 -0
  46. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_crossfile_integration.py +0 -0
  47. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_crossfile_observational_equivalence.py +0 -0
  48. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_engine_adversarial.py +0 -0
  49. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_exceptions.py +0 -0
  50. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_additional_branches.py +0 -0
  51. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_augassign_and_fstrings.py +0 -0
  52. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_callee_and_multi_return.py +0 -0
  53. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_comprehensive.py +0 -0
  54. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_edge_cases.py +0 -0
  55. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_final_lines.py +0 -0
  56. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_generate_call_variants.py +0 -0
  57. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_more_paths.py +0 -0
  58. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_preamble_and_call_mapping.py +0 -0
  59. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_remaining_branches.py +0 -0
  60. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_return_statements.py +0 -0
  61. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_extractor_substituter_and_helpers.py +0 -0
  62. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_final_coverage_push.py +0 -0
  63. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_free_variable_correspondence.py +0 -0
  64. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_fstrings.py +0 -0
  65. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_helpers.py +0 -0
  66. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_method_level_and_attributes.py +0 -0
  67. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_nominal_unifier.py +0 -0
  68. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_observational_equivalence.py +0 -0
  69. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_orphan_detection.py +0 -0
  70. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_orphan_detector_comprehensive.py +0 -0
  71. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_overlap_filtering.py +0 -0
  72. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_pipeline_api.py +0 -0
  73. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_pipeline_phases.py +0 -0
  74. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_progress_modes.py +0 -0
  75. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_project_layout_and_imports.py +0 -0
  76. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_project_layout_behavior.py +0 -0
  77. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_promotion_and_mangling.py +0 -0
  78. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_refactor_engine_comprehensive.py +0 -0
  79. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_refactor_engine_edge_cases.py +0 -0
  80. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_refactor_engine_more.py +0 -0
  81. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_refactor_engine_targeted_branches.py +0 -0
  82. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_refactoring_engine.py +0 -0
  83. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_regression.py +0 -0
  84. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_regressions_broader.py +0 -0
  85. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_return_values.py +0 -0
  86. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_scope_analyzer.py +0 -0
  87. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_scope_analyzer_comprehensive.py +0 -0
  88. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_signature_prefilter.py +0 -0
  89. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_additional_branches.py +0 -0
  90. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_batch2.py +0 -0
  91. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_batch3.py +0 -0
  92. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_batch4.py +0 -0
  93. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_batch5.py +0 -0
  94. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_bound_variables.py +0 -0
  95. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_comprehensions.py +0 -0
  96. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_comprehensive.py +0 -0
  97. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_core.py +0 -0
  98. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_edge_cases.py +0 -0
  99. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_more_branches.py +0 -0
  100. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_unifier_state_reset.py +0 -0
  101. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_validation_partial_lifetime.py +0 -0
  102. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_variable_capture_bug.py +0 -0
  103. {code_towel-1.0.2 → code_towel-1.0.3}/tests/test_visitor_utils_comprehensive.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-towel
3
- Version: 1.0.2
3
+ Version: 1.0.3
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: Apache License
@@ -363,6 +363,16 @@ pip install -e ".[dev]"
363
363
  pip install black flake8 mypy coverage
364
364
  ```
365
365
 
366
+ ### Optional: Enhanced Progress Bars
367
+
368
+ For nicer progress bars during analysis, you can optionally install `tqdm`:
369
+
370
+ ```bash
371
+ pip install tqdm
372
+ ```
373
+
374
+ Without `tqdm`, Towel still works perfectly with zero dependencies - it just uses simpler progress indicators.
375
+
366
376
  ## Usage
367
377
 
368
378
  ### Preview Mode (Read-Only)
@@ -132,6 +132,16 @@ pip install -e ".[dev]"
132
132
  pip install black flake8 mypy coverage
133
133
  ```
134
134
 
135
+ ### Optional: Enhanced Progress Bars
136
+
137
+ For nicer progress bars during analysis, you can optionally install `tqdm`:
138
+
139
+ ```bash
140
+ pip install tqdm
141
+ ```
142
+
143
+ Without `tqdm`, Towel still works perfectly with zero dependencies - it just uses simpler progress indicators.
144
+
135
145
  ## Usage
136
146
 
137
147
  ### Preview Mode (Read-Only)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-towel"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "A Python tool that DRYs your code - finds and refactors repeated code using unification-based analysis"
9
9
  authors = [
10
10
  {name = "Eric Allen"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-towel
3
- Version: 1.0.2
3
+ Version: 1.0.3
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: Apache License
@@ -363,6 +363,16 @@ pip install -e ".[dev]"
363
363
  pip install black flake8 mypy coverage
364
364
  ```
365
365
 
366
+ ### Optional: Enhanced Progress Bars
367
+
368
+ For nicer progress bars during analysis, you can optionally install `tqdm`:
369
+
370
+ ```bash
371
+ pip install tqdm
372
+ ```
373
+
374
+ Without `tqdm`, Towel still works perfectly with zero dependencies - it just uses simpler progress indicators.
375
+
366
376
  ## Usage
367
377
 
368
378
  ### Preview Mode (Read-Only)
@@ -622,8 +622,17 @@ def _rename_function_in_directory(
622
622
  old_name: str,
623
623
  new_name: str,
624
624
  dry_run: bool,
625
+ file_filter: Optional[Path] = None,
625
626
  ) -> int:
626
- """Rename a function throughout all Python files in directory."""
627
+ """Rename a function throughout all Python files in directory.
628
+
629
+ Args:
630
+ target: Directory to search in
631
+ old_name: Current function name
632
+ new_name: New function name
633
+ dry_run: If True, don't actually modify files
634
+ file_filter: If provided, only rename in this specific file
635
+ """
627
636
  import re
628
637
 
629
638
  # Pattern to match function definitions and calls
@@ -636,6 +645,10 @@ def _rename_function_in_directory(
636
645
  total_replacements = 0
637
646
 
638
647
  for py_file in target.rglob("*.py"):
648
+ # If file_filter is specified, only process that file
649
+ if file_filter and py_file != file_filter:
650
+ continue
651
+
639
652
  try:
640
653
  content = py_file.read_text()
641
654
  new_content = content
@@ -678,12 +691,16 @@ def _run_interactive_llm_mode(
678
691
  print("=" * 70 + "\n")
679
692
 
680
693
  print("After the LLM provides suggestions, paste the JSON response below.")
681
- print("The response should be a JSON object mapping old names to new names:")
682
- print(
683
- 'Example: {"__extracted_func_1": "calculate_total", "__extracted_func_2": "validate_input"}'
684
- )
694
+ print("The response should be a JSON object mapping old names to new names.")
695
+ print("You can use file-qualified names (e.g., 'path/to/file.py:func_name') to")
696
+ print("rename functions only in specific files, or just the function name to rename")
697
+ print("across all files.")
698
+ print()
699
+ print("Example:")
700
+ print(' {"__extracted_func_1": "calculate_total"}')
701
+ print(' {"src/utils.py:__extracted_func_2": "validate_input"}')
685
702
  print()
686
- print("Paste the JSON response (press Ctrl+D or Ctrl+Z when done):")
703
+ print("Paste the JSON response below, then press ENTER followed by Ctrl+D (or Ctrl+Z on Windows):")
687
704
  print()
688
705
 
689
706
  # Read LLM response from stdin
@@ -732,15 +749,31 @@ def _run_interactive_llm_mode(
732
749
 
733
750
  # Apply renamings
734
751
  total_changes = 0
735
- for old_name, new_name in renames.items():
752
+ for old_name_spec, new_name in renames.items():
736
753
  if not isinstance(new_name, str) or not new_name:
737
- print(f"Warning: Skipping invalid mapping {old_name} -> {new_name}")
754
+ print(f"Warning: Skipping invalid mapping {old_name_spec} -> {new_name}")
738
755
  continue
739
756
 
740
- count = _rename_function_in_directory(target, old_name, new_name, dry_run)
757
+ # Parse file-qualified names (e.g., "path/to/file.py:function_name")
758
+ file_filter = None
759
+ if ":" in old_name_spec:
760
+ file_path_str, old_name = old_name_spec.rsplit(":", 1)
761
+ # Resolve file path relative to target
762
+ file_filter = target / file_path_str
763
+ if not file_filter.exists():
764
+ print(f"Warning: File not found: {file_filter}")
765
+ print(f" Skipping: {old_name_spec} -> {new_name}")
766
+ continue
767
+ else:
768
+ old_name = old_name_spec
769
+
770
+ count = _rename_function_in_directory(target, old_name, new_name, dry_run, file_filter)
741
771
  if count > 0:
742
772
  total_changes += count
743
- print(f" ✓ {old_name} -> {new_name} ({count} replacement(s))")
773
+ if file_filter:
774
+ print(f" ✓ {old_name_spec} -> {new_name} ({count} replacement(s))")
775
+ else:
776
+ print(f" ✓ {old_name} -> {new_name} ({count} replacement(s))")
744
777
 
745
778
  if dry_run:
746
779
  print(f"\n[DRY RUN] Would make {total_changes} change(s)")
@@ -778,13 +811,18 @@ Here are the extracted functions:
778
811
 
779
812
  output_format = """
780
813
 
781
- Please provide your suggestions as a JSON object mapping old names to new names. For example:
814
+ Please provide your suggestions as a JSON object mapping old names to new names.
815
+
816
+ If the same function name appears in multiple files, use file-qualified names (path:function_name)
817
+ to rename them individually. Otherwise, you can use just the function name to rename across all files.
818
+
819
+ For example:
782
820
 
783
821
  ```json
784
822
  {
785
- "__extracted_func_1": "calculate_total_price",
786
- "__extracted_func_2": "validate_email_format",
787
- "__extracted_func_3": "format_date_string"
823
+ "src/utils.py:__extracted_func_1": "calculate_total_price",
824
+ "src/validators.py:__extracted_func_1": "validate_email_format",
825
+ "__extracted_func_2": "format_date_string"
788
826
  }
789
827
  ```
790
828
 
File without changes
File without changes