code-normalizer-pro 3.0.1__tar.gz → 3.0.2__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 (26) hide show
  1. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/LICENSE +1 -2
  2. code_normalizer_pro-3.0.2/PKG-INFO +24 -0
  3. code_normalizer_pro-3.0.2/README.md +11 -0
  4. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/code_normalizer_pro/__init__.py +4 -5
  5. code_normalizer_pro-3.0.2/code_normalizer_pro/cli.py +26 -0
  6. code_normalizer_pro-3.0.1/code_normalizer_pro/code_normalize_pro.py → code_normalizer_pro-3.0.2/code_normalizer_pro/code_normalizer_pro.py +101 -34
  7. code_normalizer_pro-3.0.2/code_normalizer_pro.egg-info/PKG-INFO +24 -0
  8. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/code_normalizer_pro.egg-info/SOURCES.txt +2 -7
  9. code_normalizer_pro-3.0.2/code_normalizer_pro.egg-info/requires.txt +1 -0
  10. code_normalizer_pro-3.0.2/pyproject.toml +19 -0
  11. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/tests/test_code_normalize_pro.py +50 -6
  12. code_normalizer_pro-3.0.1/PKG-INFO +0 -304
  13. code_normalizer_pro-3.0.1/README.md +0 -277
  14. code_normalizer_pro-3.0.1/code_normalizer_pro/cli.py +0 -42
  15. code_normalizer_pro-3.0.1/code_normalizer_pro.egg-info/PKG-INFO +0 -304
  16. code_normalizer_pro-3.0.1/code_normalizer_pro.egg-info/requires.txt +0 -4
  17. code_normalizer_pro-3.0.1/main.py +0 -16
  18. code_normalizer_pro-3.0.1/pyproject.toml +0 -48
  19. code_normalizer_pro-3.0.1/tests/test_feedback_prioritizer.py +0 -49
  20. code_normalizer_pro-3.0.1/tests/test_launch_metrics.py +0 -42
  21. code_normalizer_pro-3.0.1/tests/test_release_prep.py +0 -41
  22. code_normalizer_pro-3.0.1/tests/test_sales_pipeline_metrics.py +0 -40
  23. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/code_normalizer_pro.egg-info/dependency_links.txt +0 -0
  24. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/code_normalizer_pro.egg-info/entry_points.txt +0 -0
  25. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/code_normalizer_pro.egg-info/top_level.txt +0 -0
  26. {code_normalizer_pro-3.0.1 → code_normalizer_pro-3.0.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026
3
+ Copyright (c) 2026 Michael Rawls Jr.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: code-normalizer-pro
3
+ Version: 3.0.2
4
+ Summary: Production-grade code normalization tool for encoding, newlines, and whitespace hygiene.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/MRJR0101/Code-Normalizer-Pro
7
+ Keywords: code-quality,formatter,normalization,cli,python
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: tqdm
12
+ Dynamic: license-file
13
+
14
+ # code-normalizer-pro
15
+
16
+ A CLI tool that normalizes encoding, line endings, and whitespace across a codebase.
17
+
18
+ ## Install
19
+
20
+ pip install code-normalizer-pro
21
+
22
+ ## Usage
23
+
24
+ code-normalizer-pro . --dry-run -e .py
@@ -0,0 +1,11 @@
1
+ # code-normalizer-pro
2
+
3
+ A CLI tool that normalizes encoding, line endings, and whitespace across a codebase.
4
+
5
+ ## Install
6
+
7
+ pip install code-normalizer-pro
8
+
9
+ ## Usage
10
+
11
+ code-normalizer-pro . --dry-run -e .py
@@ -1,5 +1,4 @@
1
- """Package metadata for code-normalizer-pro."""
2
-
3
- __all__ = ["__version__"]
4
- __version__ = "3.0.1"
5
-
1
+ """Package metadata for code-normalizer-pro."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "3.0.2"
@@ -0,0 +1,26 @@
1
+ """Console entry point for the code-normalizer-pro installed package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import runpy
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def _find_script() -> Path:
11
+ """Return the path to code_normalizer_pro.py."""
12
+ pkg_dir = Path(__file__).resolve().parent
13
+ candidate = pkg_dir / "code_normalizer_pro.py"
14
+ if candidate.is_file():
15
+ return candidate
16
+ raise FileNotFoundError(f"code_normalizer_pro.py not found at: {candidate}")
17
+
18
+
19
+ def main() -> None:
20
+ script = _find_script()
21
+ sys.argv[0] = str(script)
22
+ runpy.run_path(str(script), run_name="__main__")
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
@@ -27,6 +27,7 @@ import os
27
27
  import hashlib
28
28
  import json
29
29
  import shutil
30
+ import tempfile
30
31
  from pathlib import Path
31
32
  from typing import Optional, Tuple, List, Dict, Set
32
33
  from dataclasses import dataclass, asdict
@@ -66,7 +67,7 @@ COMMON_ENCODINGS = [
66
67
  # Multi-language syntax checkers
67
68
  SYNTAX_CHECKERS = {
68
69
  ".py": {
69
- "command": ["python", "-m", "py_compile"],
70
+ "command": [sys.executable, "-m", "py_compile"],
70
71
  "stdin": False,
71
72
  "file_arg": True,
72
73
  },
@@ -215,7 +216,8 @@ class CodeNormalizer:
215
216
  use_cache: bool = True,
216
217
  interactive: bool = False,
217
218
  parallel: bool = False,
218
- max_workers: Optional[int] = None):
219
+ max_workers: Optional[int] = None,
220
+ cache_path: Optional[Path] = None):
219
221
  self.dry_run = dry_run
220
222
  self.verbose = verbose
221
223
  self.in_place = in_place
@@ -224,9 +226,30 @@ class CodeNormalizer:
224
226
  self.interactive = interactive
225
227
  self.parallel = parallel
226
228
  self.max_workers = max_workers or max(1, cpu_count() - 1)
229
+ self.cache_path_override = cache_path
227
230
  self.stats = ProcessStats()
228
231
  self.errors: List[Tuple[Path, str]] = []
229
- self.cache = CacheManager() if use_cache else None
232
+ self.cache = CacheManager(cache_path) if use_cache and cache_path else None
233
+
234
+ def _resolve_cache_path(self, target: Path) -> Path:
235
+ """Place cache beside the project or file being processed."""
236
+ if self.cache_path_override:
237
+ return self.cache_path_override
238
+ base = target if target.is_dir() else target.parent
239
+ return base / CACHE_FILE
240
+
241
+ def _ensure_cache_manager(self, target: Path) -> None:
242
+ """Bind cache storage to the current processing target."""
243
+ if not self.use_cache:
244
+ return
245
+
246
+ desired_path = self._resolve_cache_path(target)
247
+ if self.cache is None or self.cache.cache_path != desired_path:
248
+ self.cache = CacheManager(desired_path)
249
+
250
+ def _should_show_progress(self) -> bool:
251
+ """Avoid tqdm collisions with verbose per-file output."""
252
+ return HAS_TQDM and not self.interactive and not self.verbose
230
253
 
231
254
  def _looks_like_utf16_text(self, data: bytes) -> bool:
232
255
  """Best-effort check for UTF-16 text before binary rejection."""
@@ -320,8 +343,8 @@ class CodeNormalizer:
320
343
 
321
344
  return text, changes
322
345
 
323
- def syntax_check(self, path: Path, language: Optional[str] = None) -> Tuple[bool, str]:
324
- """Run syntax check on file - multi-language support"""
346
+ def _run_syntax_check(self, path: Path, content: Optional[str] = None) -> Tuple[bool, str]:
347
+ """Run a syntax checker against a file or normalized text buffer."""
325
348
  ext = path.suffix.lower()
326
349
 
327
350
  if ext not in SYNTAX_CHECKERS:
@@ -329,10 +352,24 @@ class CodeNormalizer:
329
352
 
330
353
  checker = SYNTAX_CHECKERS[ext]
331
354
  cmd = checker['command'].copy()
355
+ temp_path: Optional[Path] = None
332
356
 
333
357
  try:
334
358
  if checker['file_arg']:
335
- cmd.append(str(path))
359
+ target_path = path
360
+ if content is not None:
361
+ with tempfile.NamedTemporaryFile(
362
+ "w",
363
+ encoding="utf-8",
364
+ newline="\n",
365
+ suffix=path.suffix,
366
+ delete=False,
367
+ ) as tmp:
368
+ tmp.write(content)
369
+ temp_path = Path(tmp.name)
370
+ target_path = temp_path
371
+
372
+ cmd.append(str(target_path))
336
373
  result = subprocess.run(
337
374
  cmd,
338
375
  stdout=subprocess.DEVNULL,
@@ -342,8 +379,9 @@ class CodeNormalizer:
342
379
  )
343
380
  else:
344
381
  # Read file and pass via stdin
345
- with open(path, 'r', encoding='utf-8') as f:
346
- content = f.read()
382
+ if content is None:
383
+ with open(path, 'r', encoding='utf-8') as f:
384
+ content = f.read()
347
385
  result = subprocess.run(
348
386
  cmd,
349
387
  input=content,
@@ -364,6 +402,17 @@ class CodeNormalizer:
364
402
  return False, "Timeout"
365
403
  except Exception as e:
366
404
  return False, str(e)[:100]
405
+ finally:
406
+ if temp_path and temp_path.exists():
407
+ temp_path.unlink(missing_ok=True)
408
+
409
+ def syntax_check(self, path: Path, language: Optional[str] = None) -> Tuple[bool, str]:
410
+ """Run syntax check on file - multi-language support"""
411
+ return self._run_syntax_check(path)
412
+
413
+ def syntax_check_text(self, path: Path, text: str) -> Tuple[bool, str]:
414
+ """Syntax check normalized content without writing it to the real file."""
415
+ return self._run_syntax_check(path, content=text)
367
416
 
368
417
  def create_backup_file(self, path: Path) -> Path:
369
418
  """Create timestamped backup"""
@@ -432,6 +481,8 @@ class CodeNormalizer:
432
481
  self.stats.total_files += 1
433
482
 
434
483
  try:
484
+ self._ensure_cache_manager(path)
485
+
435
486
  # Check cache first (incremental processing)
436
487
  if self.use_cache and self.cache and self.cache.is_cached(path):
437
488
  if self.verbose:
@@ -449,8 +500,11 @@ class CodeNormalizer:
449
500
  # Determine output
450
501
  out_path = self.get_output_path(path, output_path)
451
502
 
452
- # Check if changes needed
453
- if text == normalized:
503
+ needs_encoding_fix = enc != "utf-8"
504
+ needs_content_fix = text != normalized
505
+
506
+ # Check if any normalization work is needed
507
+ if not needs_content_fix and not needs_encoding_fix:
454
508
  if self.verbose:
455
509
  print(f"⊗ SKIP {path.name} - already normalized")
456
510
  self.stats.skipped += 1
@@ -483,6 +537,15 @@ class CodeNormalizer:
483
537
  if changes['final_newline_added']:
484
538
  print(f" Final newline: added")
485
539
 
540
+ if check_syntax:
541
+ ok, reason = self.syntax_check_text(path, normalized)
542
+ status = "✓ OK" if ok else f"✗ {reason}"
543
+ print(f" Syntax: {status}")
544
+ if ok:
545
+ self.stats.syntax_checks_passed += 1
546
+ else:
547
+ self.stats.syntax_checks_failed += 1
548
+
486
549
  self.stats.bytes_removed += changes['bytes_removed']
487
550
  self.stats.processed += 1
488
551
  return True
@@ -545,6 +608,8 @@ class CodeNormalizer:
545
608
  def walk_and_process(self, root: Path, exts: List[str],
546
609
  check_syntax: bool = False) -> None:
547
610
  """Process all files in directory tree"""
611
+ self._ensure_cache_manager(root)
612
+
548
613
  # Collect files
549
614
  files = []
550
615
  for ext in exts:
@@ -592,7 +657,10 @@ class CodeNormalizer:
592
657
 
593
658
  # Confirmation
594
659
  if not self.dry_run and self.in_place and not self.interactive:
595
- response = input(f"\n⚠️ In-place editing will modify {len(files_to_process)} files. Continue? (y/N): ")
660
+ response = input(
661
+ f"\n⚠️ In-place editing will scan {len(files_to_process)} file(s) "
662
+ "and modify only files that need changes. Continue? (y/N): "
663
+ )
596
664
  if response.lower() != 'y':
597
665
  print("Cancelled")
598
666
  return
@@ -610,7 +678,7 @@ class CodeNormalizer:
610
678
 
611
679
  def _process_sequential(self, files: List[Path], check_syntax: bool):
612
680
  """Process files sequentially"""
613
- iterator = tqdm(files, desc="Processing") if HAS_TQDM and not self.interactive else files
681
+ iterator = tqdm(files, desc="Processing") if self._should_show_progress() else files
614
682
 
615
683
  for file_path in iterator:
616
684
  self.process_file(file_path, check_syntax=check_syntax)
@@ -635,7 +703,7 @@ class CodeNormalizer:
635
703
 
636
704
  # Progress tracking
637
705
  iterator = as_completed(futures)
638
- if HAS_TQDM:
706
+ if self._should_show_progress():
639
707
  iterator = tqdm(iterator, total=len(files), desc="Processing")
640
708
 
641
709
  # Collect results
@@ -744,7 +812,7 @@ def install_git_hook(hook_type: str = "pre-commit") -> bool:
744
812
 
745
813
  # Create hook script
746
814
  hook_script = f"""#!/usr/bin/env python3
747
- # Auto-generated by code_normalize_pro.py
815
+ # Auto-generated by code_normalizer_pro.py
748
816
  import subprocess
749
817
  import sys
750
818
  from pathlib import Path
@@ -790,7 +858,7 @@ def main():
790
858
  print("\\n⚠️ Some files need normalization:")
791
859
  for file_path in needs_normalization:
792
860
  print(f" - {{file_path}}")
793
- print("\\nRun: python src/code_normalize_pro.py <file> --in-place")
861
+ print("\\nRun: uv run code-normalizer-pro <file> --in-place")
794
862
  print("Or add --no-verify to skip this check")
795
863
  sys.exit(1)
796
864
 
@@ -814,27 +882,28 @@ if __name__ == "__main__":
814
882
 
815
883
  def main():
816
884
  ap = argparse.ArgumentParser(
885
+ prog="code-normalizer-pro",
817
886
  description="Code Normalizer Pro - Production-grade normalization tool",
818
887
  formatter_class=argparse.RawDescriptionHelpFormatter,
819
888
  epilog="""
820
889
  Examples:
821
890
  # Dry run with parallel processing
822
- python code_normalize_pro.py /path/to/dir --dry-run --parallel
891
+ uv run code-normalizer-pro /path/to/dir --dry-run --parallel
823
892
 
824
893
  # Interactive mode (file-by-file approval)
825
- python code_normalize_pro.py /path/to/dir --interactive
894
+ uv run code-normalizer-pro /path/to/dir --interactive
826
895
 
827
- # In-place with incremental processing (uses cache)
828
- python code_normalize_pro.py /path/to/dir -e .py --in-place --cache
896
+ # In-place normalization (cache is enabled by default)
897
+ uv run code-normalizer-pro /path/to/dir -e .py --in-place
829
898
 
830
- # Multi-language syntax checking
831
- python code_normalize_pro.py /path/to/dir -e .py -e .js -e .go --check
899
+ # Multi-language syntax checking on normalized output
900
+ uv run code-normalizer-pro /path/to/dir -e .py -e .js -e .go --check
832
901
 
833
902
  # Install git pre-commit hook
834
- python code_normalize_pro.py --install-hook
903
+ uv run code-normalizer-pro --install-hook
835
904
 
836
905
  # Parallel processing (all cores)
837
- python code_normalize_pro.py /path/to/dir --parallel --in-place
906
+ uv run code-normalizer-pro /path/to/dir --parallel --in-place
838
907
  """
839
908
  )
840
909
 
@@ -857,7 +926,7 @@ Examples:
857
926
  ap.add_argument(
858
927
  "--check",
859
928
  action="store_true",
860
- help="Run syntax check after normalization"
929
+ help="Run syntax check on normalized output"
861
930
  )
862
931
  ap.add_argument(
863
932
  "--dry-run",
@@ -874,15 +943,16 @@ Examples:
874
943
  action="store_true",
875
944
  help="Don't create backups (dangerous!)"
876
945
  )
877
- ap.add_argument(
946
+ cache_group = ap.add_mutually_exclusive_group()
947
+ cache_group.add_argument(
878
948
  "--cache",
879
949
  action="store_true",
880
- help="Use incremental processing (skip unchanged files)"
950
+ help="Enable incremental processing cache (default)"
881
951
  )
882
- ap.add_argument(
952
+ cache_group.add_argument(
883
953
  "--no-cache",
884
954
  action="store_true",
885
- help="Disable cache (process all files)"
955
+ help="Disable incremental processing cache"
886
956
  )
887
957
  ap.add_argument(
888
958
  "--interactive",
@@ -934,11 +1004,7 @@ Examples:
934
1004
  args.parallel = False
935
1005
 
936
1006
  # Determine cache setting
937
- use_cache = True
938
- if args.no_cache:
939
- use_cache = False
940
- elif args.cache:
941
- use_cache = True
1007
+ use_cache = not args.no_cache
942
1008
 
943
1009
  # Create normalizer
944
1010
  normalizer = CodeNormalizer(
@@ -949,7 +1015,8 @@ Examples:
949
1015
  use_cache=use_cache,
950
1016
  interactive=args.interactive,
951
1017
  parallel=args.parallel,
952
- max_workers=args.workers
1018
+ max_workers=args.workers,
1019
+ cache_path=(args.path / CACHE_FILE) if args.path and args.path.is_dir() else (args.path.parent / CACHE_FILE)
953
1020
  )
954
1021
 
955
1022
  print("="*70)
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: code-normalizer-pro
3
+ Version: 3.0.2
4
+ Summary: Production-grade code normalization tool for encoding, newlines, and whitespace hygiene.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/MRJR0101/Code-Normalizer-Pro
7
+ Keywords: code-quality,formatter,normalization,cli,python
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: tqdm
12
+ Dynamic: license-file
13
+
14
+ # code-normalizer-pro
15
+
16
+ A CLI tool that normalizes encoding, line endings, and whitespace across a codebase.
17
+
18
+ ## Install
19
+
20
+ pip install code-normalizer-pro
21
+
22
+ ## Usage
23
+
24
+ code-normalizer-pro . --dry-run -e .py
@@ -1,18 +1,13 @@
1
1
  LICENSE
2
2
  README.md
3
- main.py
4
3
  pyproject.toml
5
4
  code_normalizer_pro/__init__.py
6
5
  code_normalizer_pro/cli.py
7
- code_normalizer_pro/code_normalize_pro.py
6
+ code_normalizer_pro/code_normalizer_pro.py
8
7
  code_normalizer_pro.egg-info/PKG-INFO
9
8
  code_normalizer_pro.egg-info/SOURCES.txt
10
9
  code_normalizer_pro.egg-info/dependency_links.txt
11
10
  code_normalizer_pro.egg-info/entry_points.txt
12
11
  code_normalizer_pro.egg-info/requires.txt
13
12
  code_normalizer_pro.egg-info/top_level.txt
14
- tests/test_code_normalize_pro.py
15
- tests/test_feedback_prioritizer.py
16
- tests/test_launch_metrics.py
17
- tests/test_release_prep.py
18
- tests/test_sales_pipeline_metrics.py
13
+ tests/test_code_normalize_pro.py
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "code-normalizer-pro"
7
+ version = "3.0.2"
8
+ description = "Production-grade code normalization tool for encoding, newlines, and whitespace hygiene."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = ["tqdm"]
13
+ keywords = ["code-quality", "formatter", "normalization", "cli", "python"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/MRJR0101/Code-Normalizer-Pro"
17
+
18
+ [project.scripts]
19
+ code-normalizer-pro = "code_normalizer_pro.cli:main"
@@ -6,11 +6,7 @@ from pathlib import Path
6
6
 
7
7
 
8
8
  REPO_ROOT = Path(__file__).resolve().parents[1]
9
- SRC_DIR = REPO_ROOT / "src"
10
- if str(SRC_DIR) not in sys.path:
11
- sys.path.insert(0, str(SRC_DIR))
12
-
13
- import code_normalize_pro as cnp
9
+ from code_normalizer_pro import code_normalizer_pro as cnp
14
10
 
15
11
 
16
12
  def test_guess_and_read_accepts_utf16_text(tmp_path: Path) -> None:
@@ -24,6 +20,38 @@ def test_guess_and_read_accepts_utf16_text(tmp_path: Path) -> None:
24
20
  assert "print('hi')" in text
25
21
 
26
22
 
23
+ def test_in_place_rewrites_clean_utf16_to_utf8(tmp_path: Path) -> None:
24
+ sample = tmp_path / "utf16_clean.py"
25
+ sample.write_text("print('hi')\n", encoding="utf-16")
26
+
27
+ normalizer = cnp.CodeNormalizer(
28
+ in_place=True,
29
+ create_backup=False,
30
+ use_cache=False,
31
+ )
32
+
33
+ assert normalizer.process_file(sample) is True
34
+ assert sample.read_text(encoding="utf-8") == "print('hi')\n"
35
+ assert not sample.read_bytes().startswith((b"\xff\xfe", b"\xfe\xff"))
36
+ assert normalizer.stats.processed == 1
37
+ assert normalizer.stats.encoding_changes == 1
38
+
39
+
40
+ def test_dry_run_check_validates_normalized_output(tmp_path: Path, capsys) -> None:
41
+ sample = tmp_path / "needs_fix.py"
42
+ sample.write_bytes(b"print('hi') \r\n")
43
+
44
+ normalizer = cnp.CodeNormalizer(dry_run=True, use_cache=False)
45
+
46
+ assert normalizer.process_file(sample, check_syntax=True) is True
47
+ captured = capsys.readouterr()
48
+
49
+ assert "Would normalize" in captured.out
50
+ assert "Syntax: \u2713 OK" in captured.out
51
+ assert normalizer.stats.processed == 1
52
+ assert normalizer.stats.syntax_checks_passed == 1
53
+
54
+
27
55
  def test_guess_and_read_rejects_binary_with_nuls(tmp_path: Path) -> None:
28
56
  sample = tmp_path / "blob.bin"
29
57
  sample.write_bytes(b"\x89PNG\x00\x01\x02\x03\x04\x00\x00\xff")
@@ -53,7 +81,7 @@ def test_install_git_hook_uses_current_python_and_checks_failures(tmp_path: Path
53
81
  assert "for file_path in files" in hook_text
54
82
  assert 'file_path, "--dry-run"' in hook_text
55
83
  assert "result.returncode != 0" in hook_text
56
- assert "src/code_normalize_pro.py" in hook_text
84
+ assert "code_normalizer_pro.py" in hook_text
57
85
 
58
86
 
59
87
  def test_install_hook_cli_exits_nonzero_outside_git_repo(tmp_path: Path) -> None:
@@ -116,3 +144,19 @@ def test_parallel_cache_hits_are_persisted(tmp_path: Path) -> None:
116
144
  assert second.returncode == 0, second.stderr
117
145
  assert "All discovered files were unchanged and skipped by cache." in second.stdout
118
146
  assert "⊙ Cached hits: 2" in second.stdout
147
+
148
+
149
+ def test_cache_file_scoped_to_target_directory(tmp_path: Path, monkeypatch) -> None:
150
+ sample_dir = tmp_path / "samples"
151
+ sample_dir.mkdir()
152
+ (sample_dir / "a.py").write_text("print('a') \n", encoding="utf-8")
153
+
154
+ elsewhere = tmp_path / "elsewhere"
155
+ elsewhere.mkdir()
156
+ monkeypatch.chdir(elsewhere)
157
+
158
+ normalizer = cnp.CodeNormalizer(use_cache=True)
159
+ normalizer.walk_and_process(sample_dir, [".py"])
160
+
161
+ assert (sample_dir / cnp.CACHE_FILE).exists()
162
+ assert not (elsewhere / cnp.CACHE_FILE).exists()