python-package-folder 5.1.1__tar.gz → 5.1.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 (61) hide show
  1. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/PKG-INFO +1 -1
  2. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/pyproject.toml +1 -3
  3. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/subfolder_build.py +153 -166
  4. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_subfolder_build.py +3 -3
  5. python_package_folder-5.1.1/src/python_package_folder/_hatch_build.py +0 -173
  6. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.copier-answers.yml +0 -0
  7. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  8. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  9. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.cursor/rules/general.mdc +0 -0
  10. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.cursor/rules/python.mdc +0 -0
  11. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.github/workflows/ci.yml +0 -0
  12. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.github/workflows/publish.yml +0 -0
  13. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.gitignore +0 -0
  14. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/.vscode/settings.json +0 -0
  15. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/LICENSE +0 -0
  16. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/MANIFEST.in +0 -0
  17. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/Makefile +0 -0
  18. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/README.md +0 -0
  19. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/coverage.svg +0 -0
  20. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/development.md +0 -0
  21. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/docs/DEVELOPMENT.md +0 -0
  22. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/docs/INSTALLATION.md +0 -0
  23. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/docs/PUBLISHING.md +0 -0
  24. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/docs/REFERENCE.md +0 -0
  25. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/docs/USAGE.md +0 -0
  26. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/docs/VERSION_RESOLUTION.md +0 -0
  27. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/installation.md +0 -0
  28. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/publishing.md +0 -0
  29. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/__init__.py +0 -0
  30. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/__main__.py +0 -0
  31. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/analyzer.py +0 -0
  32. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/finder.py +0 -0
  33. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/manager.py +0 -0
  34. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/publisher.py +0 -0
  35. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/py.typed +0 -0
  36. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/python_package_folder.py +0 -0
  37. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/types.py +0 -0
  38. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/utils.py +0 -0
  39. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/version.py +0 -0
  40. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/src/python_package_folder/version_calculator.py +0 -0
  41. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/conftest.py +0 -0
  42. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/folder_structure/some_globals.py +0 -0
  43. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  44. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  45. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  46. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  47. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  48. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  49. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_build_with_external_deps.py +0 -0
  50. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_exclude_patterns.py +0 -0
  51. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_linting.py +0 -0
  52. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_preserve_directory_structure.py +0 -0
  53. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_publisher.py +0 -0
  54. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_shared_subdirectory_imports.py +0 -0
  55. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_spreadsheet_creation_imports.py +0 -0
  56. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_utils.py +0 -0
  58. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_version_calculator.py +0 -0
  59. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/test_version_manager.py +0 -0
  60. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/tests/tests.py +0 -0
  61. {python_package_folder-5.1.1 → python_package_folder-5.1.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.1.1
3
+ Version: 5.1.2
4
4
  Summary: Python package to automatically package and build a folder, fetching all relevant dependencies.
5
5
  Project-URL: Repository, https://github.com/alelom/python-package-folder
6
6
  Author-email: Alessio Lombardi <work@alelom.com>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "5.1.1"
46
+ version = "5.1.2"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -154,7 +154,5 @@ testpaths = [
154
154
  "src",
155
155
  "tests",
156
156
  ]
157
- # Ignore build hooks - they're not test files and require hatchling which isn't in test deps
158
- ignore = ["_hatch_build.py"]
159
157
  norecursedirs = []
160
158
  filterwarnings = []
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import re
11
11
  import shutil
12
12
  import sys
13
+ import tempfile
13
14
  from pathlib import Path
14
15
  from typing import TYPE_CHECKING
15
16
 
@@ -75,6 +76,8 @@ class SubfolderBuildConfig:
75
76
  self.temp_readme: Path | None = None
76
77
  self.original_readme_backup: Path | None = None
77
78
  self._used_subfolder_pyproject = False
79
+ self._excluded_files: list[tuple[Path, Path]] = [] # List of (original_path, temp_path) tuples
80
+ self._exclude_temp_dir: Path | None = None
78
81
 
79
82
  def _derive_package_name(self) -> str:
80
83
  """Derive package name from source directory name."""
@@ -205,80 +208,13 @@ class SubfolderBuildConfig:
205
208
  has_build_system = any(line.strip().startswith("[build-system]") for line in result)
206
209
  if not has_build_system:
207
210
  # Insert build-system at the very beginning of the file
208
- # Include python-package-folder in requires so the build hook is available
209
211
  build_system_lines = [
210
212
  "[build-system]",
211
- 'requires = ["hatchling", "python-package-folder"]',
213
+ 'requires = ["hatchling"]',
212
214
  'build-backend = "hatchling.build"',
213
215
  "",
214
216
  ]
215
217
  result = build_system_lines + result
216
- else:
217
- # Ensure python-package-folder is in requires for build hook availability
218
- in_build_system = False
219
- for i, line in enumerate(result):
220
- if line.strip().startswith("[build-system]"):
221
- in_build_system = True
222
- elif in_build_system and line.strip().startswith("requires"):
223
- # Check if python-package-folder is already in requires
224
- if "python-package-folder" not in line:
225
- # Add python-package-folder to requires
226
- if "]" in line:
227
- # Single-line requires
228
- result[i] = line.rstrip().rstrip("]") + ', "python-package-folder"]'
229
- else:
230
- # Multi-line requires, find the closing bracket
231
- for j in range(i + 1, len(result)):
232
- if "]" in result[j]:
233
- # Insert before closing
234
- # Determine indentation from the previous line (last item in list)
235
- if j > i + 1:
236
- # Look at the previous line to get indentation
237
- prev_line = result[j - 1]
238
- # Extract leading whitespace from previous line
239
- indent = len(prev_line) - len(prev_line.lstrip())
240
- indent_str = " " * indent
241
- else:
242
- # Fallback: use standard 4-space indent
243
- indent_str = " "
244
- result.insert(j, f'{indent_str}"python-package-folder",')
245
- break
246
- break
247
- elif in_build_system and line.strip().startswith("[") and not line.strip().startswith("[build-system"):
248
- # End of build-system section
249
- break
250
-
251
- # Register the build hook to enable exclude patterns
252
- build_hook_registered = any(
253
- "[tool.hatch.build.hooks.custom]" in line for line in result
254
- )
255
- if not build_hook_registered:
256
- # Add build hook registration after [tool.hatch.build.targets.wheel] section
257
- hook_insert_index = len(result)
258
- for i, line in enumerate(result):
259
- if line.strip().startswith("[tool.hatch.build.targets.wheel]"):
260
- # Find the end of this section
261
- for j in range(i + 1, len(result)):
262
- if result[j].strip().startswith("[") and not result[j].strip().startswith(
263
- "[tool.hatch.build.targets"
264
- ):
265
- hook_insert_index = j
266
- break
267
- if hook_insert_index == len(result):
268
- # Insert after packages line if section continues
269
- for j in range(i + 1, len(result)):
270
- if result[j].strip().startswith("["):
271
- hook_insert_index = j
272
- break
273
- break
274
-
275
- # Insert build hook registration
276
- hook_lines = [
277
- "",
278
- "[tool.hatch.build.hooks.custom]",
279
- 'path = "python_package_folder._hatch_build:CustomBuildHook"',
280
- ]
281
- result[hook_insert_index:hook_insert_index] = hook_lines
282
218
 
283
219
  # Ensure hatch build section exists if packages path is needed
284
220
  if not packages_set and correct_packages_path:
@@ -390,6 +326,10 @@ class SubfolderBuildConfig:
390
326
  # Handle README file
391
327
  self._handle_readme()
392
328
 
329
+ # Exclude files matching exclude patterns
330
+ if exclude_patterns:
331
+ self._exclude_files_by_patterns(exclude_patterns)
332
+
393
333
  return original_pyproject
394
334
 
395
335
  # No pyproject.toml in subfolder, create one from parent
@@ -497,6 +437,10 @@ class SubfolderBuildConfig:
497
437
  # Handle README file
498
438
  self._handle_readme()
499
439
 
440
+ # Exclude files matching exclude patterns
441
+ if exclude_patterns:
442
+ self._exclude_files_by_patterns(exclude_patterns)
443
+
500
444
  return original_pyproject
501
445
 
502
446
  def _modify_pyproject_string(
@@ -637,54 +581,13 @@ class SubfolderBuildConfig:
637
581
  has_build_system = any(line.strip().startswith("[build-system]") for line in result)
638
582
  if not has_build_system:
639
583
  # Insert build-system at the very beginning of the file
640
- # Include python-package-folder in requires so the build hook is available
641
584
  build_system_lines = [
642
585
  "[build-system]",
643
- 'requires = ["hatchling", "python-package-folder"]',
586
+ 'requires = ["hatchling"]',
644
587
  'build-backend = "hatchling.build"',
645
588
  "",
646
589
  ]
647
590
  result = build_system_lines + result
648
- else:
649
- # Ensure python-package-folder is in requires for build hook availability
650
- in_build_system = False
651
- requires_modified = False
652
- for i, line in enumerate(result):
653
- if line.strip().startswith("[build-system]"):
654
- in_build_system = True
655
- elif in_build_system and line.strip().startswith("requires"):
656
- # Check if python-package-folder is already in requires
657
- if "python-package-folder" not in line:
658
- # Add python-package-folder to requires
659
- if "]" in line:
660
- # Single-line requires (may have closing bracket on same line)
661
- if line.strip().endswith("]"):
662
- result[i] = line.rstrip().rstrip("]") + ', "python-package-folder"]'
663
- else:
664
- # Closing bracket might be on same line but not at end
665
- result[i] = line.rstrip().rstrip("]") + ', "python-package-folder"]'
666
- else:
667
- # Multi-line requires, find the closing bracket
668
- for j in range(i + 1, len(result)):
669
- if "]" in result[j]:
670
- # Insert before closing
671
- # Determine indentation from the previous line (last item in list)
672
- if j > i + 1:
673
- # Look at the previous line to get indentation
674
- prev_line = result[j - 1]
675
- # Extract leading whitespace from previous line
676
- indent = len(prev_line) - len(prev_line.lstrip())
677
- indent_str = " " * indent
678
- else:
679
- # Fallback: use standard 4-space indent
680
- indent_str = " "
681
- result.insert(j, f'{indent_str}"python-package-folder",')
682
- break
683
- requires_modified = True
684
- break
685
- elif in_build_system and line.strip().startswith("[") and not line.strip().startswith("[build-system"):
686
- # End of build-system section
687
- break
688
591
 
689
592
  # Ensure packages is always set for subfolder builds
690
593
  if not packages_set and package_dirs:
@@ -695,65 +598,42 @@ class SubfolderBuildConfig:
695
598
  packages_str = ", ".join(f'"{p}"' for p in package_dirs)
696
599
  result.append(f"packages = [{packages_str}]")
697
600
 
698
- # Register the build hook to enable exclude patterns
699
- # Always register the build hook if exclude patterns are present, or if we want to support them
700
- # Check if build hook is already registered
701
- build_hook_registered = any(
702
- "[tool.hatch.build.hooks.custom]" in line for line in result
703
- )
704
-
705
- if not build_hook_registered:
706
- # Add build hook registration after [tool.hatch.build.targets.wheel] section
707
- hook_insert_index = len(result)
708
- wheel_section_found = False
709
- for i, line in enumerate(result):
710
- if line.strip().startswith("[tool.hatch.build.targets.wheel]"):
711
- wheel_section_found = True
712
- # Find the end of this section
713
- for j in range(i + 1, len(result)):
714
- if result[j].strip().startswith("[") and not result[j].strip().startswith(
715
- "[tool.hatch.build.targets"
716
- ):
717
- hook_insert_index = j
718
- break
719
- if hook_insert_index == len(result):
720
- # Insert after packages line if section continues
721
- for j in range(i + 1, len(result)):
722
- if result[j].strip().startswith("["):
723
- hook_insert_index = j
724
- break
725
- break
726
-
727
- # If wheel section not found, insert before sdist section or at end
728
- if not wheel_section_found:
729
- for i, line in enumerate(result):
730
- if line.strip().startswith("[tool.hatch.build.targets.sdist]"):
731
- hook_insert_index = i
732
- break
733
-
734
- # Insert build hook registration
735
- hook_lines = [
736
- "",
737
- "[tool.hatch.build.hooks.custom]",
738
- 'path = "python_package_folder._hatch_build:CustomBuildHook"',
739
- ]
740
- result[hook_insert_index:hook_insert_index] = hook_lines
741
-
742
601
  # Use only-include for source distributions to ensure only the subfolder is included
743
602
  # This prevents including files from the project root
744
603
  if package_dirs:
745
- result.append("")
746
- result.append("[tool.hatch.build.targets.sdist]")
747
- # Include only the subfolder directory and necessary files
748
- only_include_paths = [package_dirs[0]]
749
- # Also include pyproject.toml and README if they exist
750
- only_include_paths.append("pyproject.toml")
751
- only_include_paths.append("README.md")
752
- only_include_paths.append("README.rst")
753
- only_include_paths.append("README.txt")
754
- only_include_paths.append("README")
755
- only_include_str = ", ".join(f'"{p}"' for p in only_include_paths)
756
- result.append(f"only-include = [{only_include_str}]")
604
+ # Check if sdist section already exists
605
+ sdist_section_exists = any(
606
+ line.strip().startswith("[tool.hatch.build.targets.sdist]")
607
+ for line in result
608
+ )
609
+ # Check if only-include is already set in the sdist section
610
+ only_include_set = False
611
+ if sdist_section_exists:
612
+ in_sdist_section = False
613
+ for line in result:
614
+ if line.strip().startswith("[tool.hatch.build.targets.sdist]"):
615
+ in_sdist_section = True
616
+ elif line.strip().startswith("[") and in_sdist_section:
617
+ in_sdist_section = False
618
+ elif in_sdist_section and re.match(r"^\s*only-include\s*=", line):
619
+ only_include_set = True
620
+ break
621
+
622
+ if not sdist_section_exists or not only_include_set:
623
+ if not sdist_section_exists:
624
+ result.append("")
625
+ result.append("[tool.hatch.build.targets.sdist]")
626
+ # Include only the subfolder directory and necessary files
627
+ only_include_paths = [package_dirs[0]]
628
+ # Also include pyproject.toml and README if they exist
629
+ only_include_paths.append("pyproject.toml")
630
+ only_include_paths.append("README.md")
631
+ only_include_paths.append("README.rst")
632
+ only_include_paths.append("README.txt")
633
+ only_include_paths.append("README")
634
+ only_include_str = ", ".join(f'"{p}"' for p in only_include_paths)
635
+ if not only_include_set:
636
+ result.append(f"only-include = [{only_include_str}]")
757
637
 
758
638
  # Add dependency group if specified
759
639
  if dependency_group:
@@ -1085,6 +965,110 @@ class SubfolderBuildConfig:
1085
965
  target_readme.write_text(readme_content, encoding="utf-8")
1086
966
  self.temp_readme = target_readme
1087
967
 
968
+ def _exclude_files_by_patterns(self, exclude_patterns: list[str]) -> None:
969
+ """
970
+ Temporarily move files matching exclude patterns out of the source directory.
971
+
972
+ Files are moved to a temporary directory and will be restored in restore().
973
+ This ensures excluded files are not included in the build.
974
+
975
+ Args:
976
+ exclude_patterns: List of regex patterns to match against path components
977
+ """
978
+ if not exclude_patterns:
979
+ return
980
+
981
+ # Compile regex patterns for efficiency
982
+ compiled_patterns = [re.compile(pattern) for pattern in exclude_patterns]
983
+
984
+ def should_exclude(path: Path) -> bool:
985
+ """Check if a path should be excluded based on patterns."""
986
+ # Check each component of the path
987
+ for part in path.parts:
988
+ # Check if any part matches any pattern
989
+ for pattern in compiled_patterns:
990
+ if pattern.search(part):
991
+ return True
992
+ return False
993
+
994
+ # Create temporary directory for excluded files
995
+ if self._exclude_temp_dir is None:
996
+ self._exclude_temp_dir = Path(tempfile.mkdtemp(prefix="python-package-folder-excluded-"))
997
+
998
+ # Find all files and directories in src_dir that match exclude patterns
999
+ excluded_items: list[Path] = []
1000
+
1001
+ # Walk through src_dir and find matching items
1002
+ # Sort by depth (shallow first) so we can skip children of excluded directories
1003
+ all_items = sorted(self.src_dir.rglob("*"), key=lambda p: len(p.parts))
1004
+
1005
+ for item in all_items:
1006
+ # Skip if already excluded (parent was excluded)
1007
+ if any(
1008
+ excluded.is_dir() and (item == excluded or item.is_relative_to(excluded))
1009
+ for excluded in excluded_items
1010
+ ):
1011
+ continue
1012
+
1013
+ # Check if this item should be excluded
1014
+ try:
1015
+ rel_path = item.relative_to(self.src_dir)
1016
+ if should_exclude(rel_path):
1017
+ excluded_items.append(item)
1018
+ except ValueError:
1019
+ # Path is not relative to src_dir, skip
1020
+ continue
1021
+
1022
+ # Move excluded items to temporary directory
1023
+ for item in excluded_items:
1024
+ try:
1025
+ # Calculate relative path from src_dir
1026
+ rel_path = item.relative_to(self.src_dir)
1027
+ # Create corresponding path in temp directory
1028
+ temp_path = self._exclude_temp_dir / rel_path
1029
+ # Create parent directories if needed
1030
+ temp_path.parent.mkdir(parents=True, exist_ok=True)
1031
+
1032
+ # Move the item
1033
+ if item.exists():
1034
+ shutil.move(str(item), str(temp_path))
1035
+ self._excluded_files.append((item, temp_path))
1036
+ print(f"Excluded {rel_path} from build", file=sys.stderr)
1037
+ except Exception as e:
1038
+ print(
1039
+ f"Warning: Could not exclude {item}: {e}",
1040
+ file=sys.stderr,
1041
+ )
1042
+
1043
+ def _restore_excluded_files(self) -> None:
1044
+ """Restore files that were excluded by _exclude_files_by_patterns."""
1045
+ # Restore files in reverse order (to handle nested directories correctly)
1046
+ for original_path, temp_path in reversed(self._excluded_files):
1047
+ try:
1048
+ if temp_path.exists():
1049
+ # Ensure parent directory exists
1050
+ original_path.parent.mkdir(parents=True, exist_ok=True)
1051
+ # Move back
1052
+ shutil.move(str(temp_path), str(original_path))
1053
+ except Exception as e:
1054
+ print(
1055
+ f"Warning: Could not restore excluded file {original_path}: {e}",
1056
+ file=sys.stderr,
1057
+ )
1058
+
1059
+ # Clean up temporary directory
1060
+ if self._exclude_temp_dir and self._exclude_temp_dir.exists():
1061
+ try:
1062
+ shutil.rmtree(self._exclude_temp_dir)
1063
+ except Exception as e:
1064
+ print(
1065
+ f"Warning: Could not remove temporary exclude directory {self._exclude_temp_dir}: {e}",
1066
+ file=sys.stderr,
1067
+ )
1068
+
1069
+ self._excluded_files.clear()
1070
+ self._exclude_temp_dir = None
1071
+
1088
1072
  def restore(self) -> None:
1089
1073
  """
1090
1074
  Restore the original pyproject.toml and remove temporary __init__.py if created.
@@ -1094,6 +1078,9 @@ class SubfolderBuildConfig:
1094
1078
  This method removes the temporary pyproject.toml and restores the original from
1095
1079
  the backup location, ensuring the original file is never modified.
1096
1080
  """
1081
+ # Restore excluded files first
1082
+ self._restore_excluded_files()
1083
+
1097
1084
  # Remove temporary __init__.py if we created it
1098
1085
  if self._temp_init_created:
1099
1086
  init_file = self.src_dir / "__init__.py"
@@ -661,7 +661,7 @@ class TestSubfolderBuildTemporaryPyprojectCreation:
661
661
 
662
662
  # Verify build-system section is added (required for hatchling)
663
663
  assert "[build-system]" in content
664
- assert 'requires = ["hatchling", "python-package-folder"]' in content
664
+ assert 'requires = ["hatchling"]' in content
665
665
  assert 'build-backend = "hatchling.build"' in content
666
666
  assert "[tool.hatch.version]" not in content
667
667
  assert "[tool.uv-dynamic-versioning]" not in content
@@ -894,7 +894,7 @@ description = "Subfolder package"
894
894
 
895
895
  # Verify build-system section uses hatchling, not setuptools
896
896
  assert "[build-system]" in content
897
- assert 'requires = ["hatchling", "python-package-folder"]' in content
897
+ assert 'requires = ["hatchling"]' in content
898
898
  assert 'build-backend = "hatchling.build"' in content
899
899
  assert "setuptools" not in content or 'build-backend = "setuptools' not in content
900
900
 
@@ -929,7 +929,7 @@ description = "Subfolder package"
929
929
 
930
930
  # Verify build-system section is added
931
931
  assert "[build-system]" in content
932
- assert 'requires = ["hatchling", "python-package-folder"]' in content
932
+ assert 'requires = ["hatchling"]' in content
933
933
  assert 'build-backend = "hatchling.build"' in content
934
934
 
935
935
  config.restore()
@@ -1,173 +0,0 @@
1
- """
2
- Hatch build hook to automatically include all files from the scripts directory.
3
-
4
- This hook ensures all non-Python files in the scripts directory are included
5
- in the wheel without creating duplicates, and automatically includes any new
6
- files added to the directory without requiring manual configuration updates.
7
-
8
- Also filters files based on exclude patterns from pyproject.toml.
9
- """
10
-
11
- import re
12
- import sys
13
- from pathlib import Path
14
- from typing import Any
15
-
16
- from hatchling.builders.hooks.plugin.interface import BuildHookInterface
17
-
18
- from .utils import read_exclude_patterns
19
-
20
-
21
- class CustomBuildHook(BuildHookInterface):
22
- """Build hook to include all files from the scripts directory and filter exclude patterns."""
23
-
24
- def initialize(self, version: str, build_data: dict[str, Any]) -> None:
25
- """Initialize the build hook and add scripts directory files."""
26
- # Debug: Print to stderr so it shows in build output
27
- print(f"[DEBUG] Build hook called. Root: {self.root}", file=sys.stderr)
28
-
29
- # Read exclude patterns from pyproject.toml
30
- pyproject_path = Path(self.root) / "pyproject.toml"
31
- exclude_patterns = read_exclude_patterns(pyproject_path)
32
-
33
- if exclude_patterns:
34
- print(f"[DEBUG] Found {len(exclude_patterns)} exclude pattern(s): {exclude_patterns}", file=sys.stderr)
35
- # Filter build_data entries based on exclude patterns
36
- self._filter_build_data(build_data, exclude_patterns)
37
-
38
- # Try multiple possible locations for the scripts directory
39
- # 1. Source layout: src/python_package_folder/scripts
40
- # 2. Sdist layout: python_package_folder/scripts (after extraction)
41
- # 3. Alternative sdist layout: scripts/ (if extracted differently)
42
- possible_scripts_dirs = [
43
- Path(self.root) / "src" / "python_package_folder" / "scripts",
44
- Path(self.root) / "python_package_folder" / "scripts",
45
- Path(self.root) / "scripts",
46
- ]
47
-
48
- scripts_dir = None
49
- for possible_dir in possible_scripts_dirs:
50
- if possible_dir.exists() and possible_dir.is_dir():
51
- scripts_dir = possible_dir
52
- print(f"[DEBUG] Found scripts dir at: {scripts_dir}", file=sys.stderr)
53
- break
54
-
55
- if scripts_dir is None:
56
- print(f"[DEBUG] Scripts directory not found. Tried: {[str(d) for d in possible_scripts_dirs]}", file=sys.stderr)
57
- return
58
-
59
- # If scripts directory exists, include all files from it
60
- if scripts_dir.exists() and scripts_dir.is_dir():
61
- # Add all files from scripts directory to force-include
62
- # This ensures they're included in the wheel at the correct location
63
- for script_file in scripts_dir.iterdir():
64
- if script_file.is_file():
65
- # Calculate relative paths from project root
66
- try:
67
- source_path = script_file.relative_to(self.root)
68
- except ValueError:
69
- # If relative_to fails, try to construct path manually
70
- # This can happen with sdist layouts
71
- if "python_package_folder" in str(script_file):
72
- # Extract the part after python_package_folder
73
- parts = script_file.parts
74
- try:
75
- idx = parts.index("python_package_folder")
76
- source_path = Path(*parts[idx:])
77
- except (ValueError, IndexError):
78
- # Fallback: use the filename
79
- source_path = Path("python_package_folder") / "scripts" / script_file.name
80
- else:
81
- source_path = Path("python_package_folder") / "scripts" / script_file.name
82
-
83
- # Target path inside the wheel package (always the same)
84
- target_path = f"python_package_folder/scripts/{script_file.name}"
85
-
86
- print(f"[DEBUG] Adding {source_path} -> {target_path}", file=sys.stderr)
87
-
88
- # Add to force-include (hatchling will handle this)
89
- # We need to add it to build_data['force_include']
90
- if "force_include" not in build_data:
91
- build_data["force_include"] = {}
92
- build_data["force_include"][str(source_path)] = target_path
93
-
94
- print(f"[DEBUG] force_include now has {len(build_data.get('force_include', {}))} entries", file=sys.stderr)
95
-
96
- def _filter_build_data(self, build_data: dict[str, Any], exclude_patterns: list[str]) -> None:
97
- """
98
- Filter build_data entries based on exclude patterns.
99
-
100
- Removes files/directories that match any of the exclude patterns from
101
- build_data. Patterns are matched against any path component using regex.
102
-
103
- Args:
104
- build_data: Hatchling build data dictionary
105
- exclude_patterns: List of regex patterns to match against paths
106
- """
107
- # Compile regex patterns for efficiency
108
- compiled_patterns = [re.compile(pattern) for pattern in exclude_patterns]
109
-
110
- def should_exclude(path_str: str) -> bool:
111
- """Check if a path should be excluded based on patterns."""
112
- # Check each component of the path
113
- path = Path(path_str)
114
- for part in path.parts:
115
- # Check if any part matches any pattern
116
- for pattern in compiled_patterns:
117
- if pattern.search(part):
118
- return True
119
- return False
120
-
121
- # Filter force_include entries
122
- if "force_include" in build_data and isinstance(build_data["force_include"], dict):
123
- original_count = len(build_data["force_include"])
124
- filtered = {
125
- source: target
126
- for source, target in build_data["force_include"].items()
127
- if not should_exclude(source) and not should_exclude(target)
128
- }
129
- build_data["force_include"] = filtered
130
- excluded_count = original_count - len(filtered)
131
- if excluded_count > 0:
132
- print(
133
- f"[DEBUG] Excluded {excluded_count} file(s) from force_include based on exclude patterns",
134
- file=sys.stderr,
135
- )
136
-
137
- # Filter other file collections that might exist
138
- # Hatchling may store files in different keys depending on the build target
139
- for key in ["shared_data", "artifacts"]:
140
- if key in build_data and isinstance(build_data[key], dict):
141
- original_count = len(build_data[key])
142
- filtered = {
143
- source: target
144
- for source, target in build_data[key].items()
145
- if not should_exclude(source) and not should_exclude(target)
146
- }
147
- build_data[key] = filtered
148
- excluded_count = original_count - len(filtered)
149
- if excluded_count > 0:
150
- print(
151
- f"[DEBUG] Excluded {excluded_count} file(s) from {key} based on exclude patterns",
152
- file=sys.stderr,
153
- )
154
-
155
- # Filter files list if it exists (for sdist)
156
- if "files" in build_data and isinstance(build_data["files"], list):
157
- original_count = len(build_data["files"])
158
- filtered = [
159
- file_entry
160
- for file_entry in build_data["files"]
161
- if not should_exclude(str(file_entry))
162
- ]
163
- build_data["files"] = filtered
164
- excluded_count = original_count - len(filtered)
165
- if excluded_count > 0:
166
- print(
167
- f"[DEBUG] Excluded {excluded_count} file(s) from files list based on exclude patterns",
168
- file=sys.stderr,
169
- )
170
-
171
-
172
- # Export the hook class (hatchling might need this)
173
- __all__ = ["CustomBuildHook"]