code-towel 1.0.2__tar.gz → 1.0.4__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.4}/PKG-INFO +11 -1
  2. {code_towel-1.0.2 → code_towel-1.0.4}/README.md +10 -0
  3. {code_towel-1.0.2 → code_towel-1.0.4}/pyproject.toml +1 -1
  4. {code_towel-1.0.2 → code_towel-1.0.4/src/code_towel.egg-info}/PKG-INFO +11 -1
  5. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/cli.py +53 -13
  6. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/refactor_engine.py +12 -0
  7. {code_towel-1.0.2 → code_towel-1.0.4}/LICENSE +0 -0
  8. {code_towel-1.0.2 → code_towel-1.0.4}/setup.cfg +0 -0
  9. {code_towel-1.0.2 → code_towel-1.0.4}/src/code_towel.egg-info/SOURCES.txt +0 -0
  10. {code_towel-1.0.2 → code_towel-1.0.4}/src/code_towel.egg-info/dependency_links.txt +0 -0
  11. {code_towel-1.0.2 → code_towel-1.0.4}/src/code_towel.egg-info/entry_points.txt +0 -0
  12. {code_towel-1.0.2 → code_towel-1.0.4}/src/code_towel.egg-info/requires.txt +0 -0
  13. {code_towel-1.0.2 → code_towel-1.0.4}/src/code_towel.egg-info/top_level.txt +0 -0
  14. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/__init__.py +0 -0
  15. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/__init__.py +0 -0
  16. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/assignment_analyzer.py +0 -0
  17. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/ast_normalizer.py +0 -0
  18. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/ast_pretty_printer.py +0 -0
  19. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/binding_detector.py +0 -0
  20. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/block_signature.py +0 -0
  21. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/builtins.py +0 -0
  22. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/exceptions.py +0 -0
  23. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/extractor.py +0 -0
  24. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/models.py +0 -0
  25. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/nominal_unifier.py +0 -0
  26. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/orphan_detector.py +0 -0
  27. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/pipeline.py +0 -0
  28. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/project_layout.py +0 -0
  29. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/scope_analyzer.py +0 -0
  30. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/unifier.py +0 -0
  31. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/visitor_utils.py +0 -0
  32. {code_towel-1.0.2 → code_towel-1.0.4}/src/towel/unification/visitors.py +0 -0
  33. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_ancestor_insertion.py +0 -0
  34. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_assignment_analyzer_comprehensive.py +0 -0
  35. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_ast_normalizer_comprehensive.py +0 -0
  36. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_ast_pretty_printer.py +0 -0
  37. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_ast_pretty_printer_comprehensive.py +0 -0
  38. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_ast_pretty_printer_more.py +0 -0
  39. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_binding_detector.py +0 -0
  40. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_binding_detector_edge_cases.py +0 -0
  41. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_bindings.py +0 -0
  42. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_bindings_additional.py +0 -0
  43. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_breakers.py +0 -0
  44. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_builtins_comprehensive.py +0 -0
  45. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_comprehensive_coverage.py +0 -0
  46. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_crossfile_integration.py +0 -0
  47. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_crossfile_observational_equivalence.py +0 -0
  48. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_engine_adversarial.py +0 -0
  49. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_exceptions.py +0 -0
  50. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_additional_branches.py +0 -0
  51. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_augassign_and_fstrings.py +0 -0
  52. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_callee_and_multi_return.py +0 -0
  53. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_comprehensive.py +0 -0
  54. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_edge_cases.py +0 -0
  55. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_final_lines.py +0 -0
  56. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_generate_call_variants.py +0 -0
  57. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_more_paths.py +0 -0
  58. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_preamble_and_call_mapping.py +0 -0
  59. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_remaining_branches.py +0 -0
  60. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_return_statements.py +0 -0
  61. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_extractor_substituter_and_helpers.py +0 -0
  62. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_final_coverage_push.py +0 -0
  63. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_free_variable_correspondence.py +0 -0
  64. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_fstrings.py +0 -0
  65. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_helpers.py +0 -0
  66. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_method_level_and_attributes.py +0 -0
  67. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_nominal_unifier.py +0 -0
  68. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_observational_equivalence.py +0 -0
  69. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_orphan_detection.py +0 -0
  70. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_orphan_detector_comprehensive.py +0 -0
  71. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_overlap_filtering.py +0 -0
  72. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_pipeline_api.py +0 -0
  73. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_pipeline_phases.py +0 -0
  74. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_progress_modes.py +0 -0
  75. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_project_layout_and_imports.py +0 -0
  76. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_project_layout_behavior.py +0 -0
  77. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_promotion_and_mangling.py +0 -0
  78. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_refactor_engine_comprehensive.py +0 -0
  79. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_refactor_engine_edge_cases.py +0 -0
  80. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_refactor_engine_more.py +0 -0
  81. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_refactor_engine_targeted_branches.py +0 -0
  82. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_refactoring_engine.py +0 -0
  83. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_regression.py +0 -0
  84. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_regressions_broader.py +0 -0
  85. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_return_values.py +0 -0
  86. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_scope_analyzer.py +0 -0
  87. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_scope_analyzer_comprehensive.py +0 -0
  88. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_signature_prefilter.py +0 -0
  89. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_additional_branches.py +0 -0
  90. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_batch2.py +0 -0
  91. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_batch3.py +0 -0
  92. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_batch4.py +0 -0
  93. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_batch5.py +0 -0
  94. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_bound_variables.py +0 -0
  95. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_comprehensions.py +0 -0
  96. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_comprehensive.py +0 -0
  97. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_core.py +0 -0
  98. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_edge_cases.py +0 -0
  99. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_more_branches.py +0 -0
  100. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_unifier_state_reset.py +0 -0
  101. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_validation_partial_lifetime.py +0 -0
  102. {code_towel-1.0.2 → code_towel-1.0.4}/tests/test_variable_capture_bug.py +0 -0
  103. {code_towel-1.0.2 → code_towel-1.0.4}/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.4
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.4"
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.4
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,13 +691,19 @@ 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:")
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"}')
702
+ print()
682
703
  print(
683
- 'Example: {"__extracted_func_1": "calculate_total", "__extracted_func_2": "validate_input"}'
704
+ "Paste the JSON response below, then press ENTER followed by Ctrl+D (or Ctrl+Z on Windows):"
684
705
  )
685
706
  print()
686
- print("Paste the JSON response (press Ctrl+D or Ctrl+Z when done):")
687
- print()
688
707
 
689
708
  # Read LLM response from stdin
690
709
  try:
@@ -732,15 +751,31 @@ def _run_interactive_llm_mode(
732
751
 
733
752
  # Apply renamings
734
753
  total_changes = 0
735
- for old_name, new_name in renames.items():
754
+ for old_name_spec, new_name in renames.items():
736
755
  if not isinstance(new_name, str) or not new_name:
737
- print(f"Warning: Skipping invalid mapping {old_name} -> {new_name}")
756
+ print(f"Warning: Skipping invalid mapping {old_name_spec} -> {new_name}")
738
757
  continue
739
758
 
740
- count = _rename_function_in_directory(target, old_name, new_name, dry_run)
759
+ # Parse file-qualified names (e.g., "path/to/file.py:function_name")
760
+ file_filter = None
761
+ if ":" in old_name_spec:
762
+ file_path_str, old_name = old_name_spec.rsplit(":", 1)
763
+ # Resolve file path relative to target
764
+ file_filter = target / file_path_str
765
+ if not file_filter.exists():
766
+ print(f"Warning: File not found: {file_filter}")
767
+ print(f" Skipping: {old_name_spec} -> {new_name}")
768
+ continue
769
+ else:
770
+ old_name = old_name_spec
771
+
772
+ count = _rename_function_in_directory(target, old_name, new_name, dry_run, file_filter)
741
773
  if count > 0:
742
774
  total_changes += count
743
- print(f" ✓ {old_name} -> {new_name} ({count} replacement(s))")
775
+ if file_filter:
776
+ print(f" ✓ {old_name_spec} -> {new_name} ({count} replacement(s))")
777
+ else:
778
+ print(f" ✓ {old_name} -> {new_name} ({count} replacement(s))")
744
779
 
745
780
  if dry_run:
746
781
  print(f"\n[DRY RUN] Would make {total_changes} change(s)")
@@ -778,13 +813,18 @@ Here are the extracted functions:
778
813
 
779
814
  output_format = """
780
815
 
781
- Please provide your suggestions as a JSON object mapping old names to new names. For example:
816
+ Please provide your suggestions as a JSON object mapping old names to new names.
817
+
818
+ If the same function name appears in multiple files, use file-qualified names (path:function_name)
819
+ to rename them individually. Otherwise, you can use just the function name to rename across all files.
820
+
821
+ For example:
782
822
 
783
823
  ```json
784
824
  {
785
- "__extracted_func_1": "calculate_total_price",
786
- "__extracted_func_2": "validate_email_format",
787
- "__extracted_func_3": "format_date_string"
825
+ "src/utils.py:__extracted_func_1": "calculate_total_price",
826
+ "src/validators.py:__extracted_func_1": "validate_email_format",
827
+ "__extracted_func_2": "format_date_string"
788
828
  }
789
829
  ```
790
830
 
@@ -538,6 +538,7 @@ class UnificationRefactorEngine:
538
538
  docstring_char: Optional[str] = None
539
539
  last_import_line = 0
540
540
  after_docstring = 0
541
+ in_multiline_import = False
541
542
 
542
543
  for i, line in enumerate(lines):
543
544
  stripped = line.strip()
@@ -557,8 +558,19 @@ class UnificationRefactorEngine:
557
558
  after_docstring = i + 1
558
559
  continue
559
560
 
561
+ # Check for start of multi-line import (has opening paren but no closing paren)
560
562
  if stripped.startswith("import ") or stripped.startswith("from "):
561
563
  last_import_line = i + 1
564
+ # Check if this is a multi-line import
565
+ if "(" in line and ")" not in line:
566
+ in_multiline_import = True
567
+ continue
568
+
569
+ # Inside a multi-line import - continue until we see closing paren
570
+ if in_multiline_import:
571
+ last_import_line = i + 1
572
+ if ")" in line:
573
+ in_multiline_import = False
562
574
  continue
563
575
 
564
576
  if last_import_line > 0 and stripped and not stripped.startswith("#"):
File without changes
File without changes