python-package-folder 5.0.1__tar.gz → 5.1.0__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.0.1 → python_package_folder-5.1.0}/PKG-INFO +1 -1
  2. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/coverage.svg +2 -2
  3. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/docs/REFERENCE.md +23 -0
  4. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/docs/USAGE.md +10 -0
  5. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/pyproject.toml +1 -1
  6. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/_hatch_build.py +90 -1
  7. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/subfolder_build.py +120 -3
  8. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/utils.py +69 -0
  9. python_package_folder-5.1.0/tests/test_exclude_patterns.py +186 -0
  10. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.copier-answers.yml +0 -0
  11. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  12. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  13. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.cursor/rules/general.mdc +0 -0
  14. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.cursor/rules/python.mdc +0 -0
  15. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.github/workflows/ci.yml +0 -0
  16. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.github/workflows/publish.yml +0 -0
  17. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.gitignore +0 -0
  18. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/.vscode/settings.json +0 -0
  19. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/LICENSE +0 -0
  20. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/MANIFEST.in +0 -0
  21. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/Makefile +0 -0
  22. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/README.md +0 -0
  23. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/development.md +0 -0
  24. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/docs/DEVELOPMENT.md +0 -0
  25. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/docs/INSTALLATION.md +0 -0
  26. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/docs/PUBLISHING.md +0 -0
  27. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/docs/VERSION_RESOLUTION.md +0 -0
  28. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/installation.md +0 -0
  29. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/publishing.md +0 -0
  30. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/__init__.py +0 -0
  31. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/__main__.py +0 -0
  32. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/analyzer.py +0 -0
  33. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/finder.py +0 -0
  34. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/manager.py +0 -0
  35. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/publisher.py +0 -0
  36. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/py.typed +0 -0
  37. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/python_package_folder.py +0 -0
  38. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/types.py +0 -0
  39. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/version.py +0 -0
  40. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/src/python_package_folder/version_calculator.py +0 -0
  41. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/conftest.py +0 -0
  42. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/folder_structure/some_globals.py +0 -0
  43. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  44. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  45. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  46. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  47. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  48. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  49. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_build_with_external_deps.py +0 -0
  50. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_linting.py +0 -0
  51. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_publisher.py +0 -0
  53. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_subfolder_build.py +0 -0
  56. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_utils.py +0 -0
  58. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_version_calculator.py +0 -0
  59. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/test_version_manager.py +0 -0
  60. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/tests/tests.py +0 -0
  61. {python_package_folder-5.0.1 → python_package_folder-5.1.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.0.1
3
+ Version: 5.1.0
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>
@@ -14,7 +14,7 @@
14
14
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
15
15
  <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
16
16
  <text x="31.5" y="14">coverage</text>
17
- <text x="81" y="15" fill="#010101" fill-opacity=".3">68%</text>
18
- <text x="81" y="14">68%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">65%</text>
18
+ <text x="81" y="14">65%</text>
19
19
  </g>
20
20
  </svg>
@@ -1,5 +1,28 @@
1
1
  # API Reference
2
2
 
3
+ ## Configuration
4
+
5
+ ### Exclude Patterns
6
+
7
+ You can configure exclude patterns in `pyproject.toml` to prevent folders and files from being included in published packages (wheel/sdist). Patterns are matched using regex against any path component (directory or file name).
8
+
9
+ ```toml
10
+ [tool.python-package-folder]
11
+ exclude-patterns = ["_SS", "__SS", ".*_test.*", "sandbox"]
12
+ ```
13
+
14
+ **Pattern Matching:**
15
+ - Patterns are regex strings that match against any path component
16
+ - If any component in a path matches any pattern, the entire path is excluded
17
+ - Examples:
18
+ - `"_SS"` - Matches any path component containing `_SS` (e.g., `data_storage/_SS/...`)
19
+ - `".*_test.*"` - Matches any path component containing `_test` (e.g., `my_test_file.py`, `test_data/`)
20
+ - `"sandbox"` - Matches any path component containing `sandbox`
21
+
22
+ **Subfolder Builds:**
23
+ - Exclude patterns from the root `pyproject.toml` are automatically applied to subfolder builds
24
+ - Patterns are injected into the temporary `pyproject.toml` created for subfolder builds
25
+
3
26
  ## Command Line Options
4
27
 
5
28
  ```
@@ -46,6 +46,7 @@
46
46
  - Subfolder-specific package name (derived or custom)
47
47
  - Specified version
48
48
  - Correct package path for hatchling
49
+ - Exclude patterns from root `pyproject.toml` (if configured)
49
50
  6. **Build Execution**: Runs build command with all dependencies in place
50
51
  7. **Cleanup**: Restores original `pyproject.toml` and removes temporary `__init__.py`
51
52
 
@@ -114,6 +115,15 @@ python-package-folder --version "1.0.0" --dependency-group "dev" --publish pypi
114
115
 
115
116
  The specified dependency group will be copied from the parent `pyproject.toml`'s `[dependency-groups]` section into the temporary `pyproject.toml` used for the subfolder build.
116
117
 
118
+ **Exclude Patterns**: You can configure exclude patterns in the root `pyproject.toml` to prevent certain folders/files from being included in published packages:
119
+
120
+ ```toml
121
+ [tool.python-package-folder]
122
+ exclude-patterns = ["_SS", "__SS", ".*_test.*"]
123
+ ```
124
+
125
+ These patterns are automatically applied to both main package builds and subfolder builds. Patterns use regex matching against any path component (directory or file name). See the [API Reference](REFERENCE.md#exclude-patterns) for more details.
126
+
117
127
  ## Python API Usage
118
128
 
119
129
  You can also use the package programmatically:
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "5.0.1"
46
+ version = "5.1.0"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -4,23 +4,37 @@ Hatch build hook to automatically include all files from the scripts directory.
4
4
  This hook ensures all non-Python files in the scripts directory are included
5
5
  in the wheel without creating duplicates, and automatically includes any new
6
6
  files added to the directory without requiring manual configuration updates.
7
+
8
+ Also filters files based on exclude patterns from pyproject.toml.
7
9
  """
8
10
 
11
+ import re
9
12
  import sys
10
13
  from pathlib import Path
11
14
  from typing import Any
12
15
 
13
16
  from hatchling.builders.hooks.plugin.interface import BuildHookInterface
14
17
 
18
+ from .utils import read_exclude_patterns
19
+
15
20
 
16
21
  class CustomBuildHook(BuildHookInterface):
17
- """Build hook to include all files from the scripts directory."""
22
+ """Build hook to include all files from the scripts directory and filter exclude patterns."""
18
23
 
19
24
  def initialize(self, version: str, build_data: dict[str, Any]) -> None:
20
25
  """Initialize the build hook and add scripts directory files."""
21
26
  # Debug: Print to stderr so it shows in build output
22
27
  print(f"[DEBUG] Build hook called. Root: {self.root}", file=sys.stderr)
23
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
+
24
38
  # Try multiple possible locations for the scripts directory
25
39
  # 1. Source layout: src/python_package_folder/scripts
26
40
  # 2. Sdist layout: python_package_folder/scripts (after extraction)
@@ -79,6 +93,81 @@ class CustomBuildHook(BuildHookInterface):
79
93
 
80
94
  print(f"[DEBUG] force_include now has {len(build_data.get('force_include', {}))} entries", file=sys.stderr)
81
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
+
82
171
 
83
172
  # Export the hook class (hatchling might need this)
84
173
  __all__ = ["CustomBuildHook"]
@@ -24,6 +24,8 @@ except ImportError:
24
24
  except ImportError:
25
25
  tomllib = None
26
26
 
27
+ from .utils import read_exclude_patterns
28
+
27
29
 
28
30
  class SubfolderBuildConfig:
29
31
  """
@@ -288,6 +290,11 @@ class SubfolderBuildConfig:
288
290
  subfolder_content = subfolder_pyproject.read_text(encoding="utf-8")
289
291
  # Adjust packages path to be relative to project root
290
292
  adjusted_content = self._adjust_subfolder_pyproject_packages_path(subfolder_content)
293
+
294
+ # Read exclude patterns from root pyproject.toml and inject them
295
+ exclude_patterns = read_exclude_patterns(original_pyproject)
296
+ if exclude_patterns:
297
+ adjusted_content = self._inject_exclude_patterns(adjusted_content, exclude_patterns)
291
298
 
292
299
  # Write adjusted content to temporary file
293
300
  temp_pyproject_path.write_text(adjusted_content, encoding="utf-8")
@@ -373,6 +380,9 @@ class SubfolderBuildConfig:
373
380
  file=sys.stderr,
374
381
  )
375
382
 
383
+ # Read exclude patterns from root pyproject.toml
384
+ exclude_patterns = read_exclude_patterns(original_pyproject)
385
+
376
386
  if data:
377
387
  # Modify using parsed data
378
388
  if "project" in data:
@@ -394,12 +404,12 @@ class SubfolderBuildConfig:
394
404
 
395
405
  # For now, use string manipulation (tomli-w not in stdlib)
396
406
  modified_content = self._modify_pyproject_string(
397
- original_content, parent_dependency_group
407
+ original_content, parent_dependency_group, exclude_patterns
398
408
  )
399
409
  else:
400
410
  # Use string manipulation
401
411
  modified_content = self._modify_pyproject_string(
402
- original_content, parent_dependency_group
412
+ original_content, parent_dependency_group, exclude_patterns
403
413
  )
404
414
 
405
415
  # Write the modified content to a temporary file
@@ -423,7 +433,10 @@ class SubfolderBuildConfig:
423
433
  return original_pyproject
424
434
 
425
435
  def _modify_pyproject_string(
426
- self, content: str, dependency_group: dict[str, list[str]] | None = None
436
+ self,
437
+ content: str,
438
+ dependency_group: dict[str, list[str]] | None = None,
439
+ exclude_patterns: list[str] | None = None,
427
440
  ) -> str:
428
441
  """Modify pyproject.toml content using string manipulation."""
429
442
  lines = content.split("\n")
@@ -635,8 +648,112 @@ class SubfolderBuildConfig:
635
648
  dep_lines.append("]")
636
649
  result[insert_index:insert_index] = dep_lines
637
650
 
651
+ # Add exclude patterns if specified
652
+ if exclude_patterns:
653
+ # Find where to insert [tool.python-package-folder] section
654
+ # Usually after [dependency-groups] or at the end
655
+ insert_index = len(result)
656
+ tool_section_exists = False
657
+ for i, line in enumerate(result):
658
+ if line.strip() == "[tool.python-package-folder]":
659
+ tool_section_exists = True
660
+ insert_index = i
661
+ break
662
+ elif line.strip().startswith("[tool.") and i > 0:
663
+ # Insert before other tool sections
664
+ insert_index = i
665
+ break
666
+
667
+ # Format exclude patterns
668
+ patterns_str = ", ".join(f'"{p}"' for p in exclude_patterns)
669
+ exclude_lines = [
670
+ "",
671
+ "[tool.python-package-folder]",
672
+ f'exclude-patterns = [{patterns_str}]',
673
+ ]
674
+
675
+ if tool_section_exists:
676
+ # Replace or update existing section
677
+ end_index = insert_index + 1
678
+ while end_index < len(result) and not result[end_index].strip().startswith("["):
679
+ end_index += 1
680
+ # Check if exclude-patterns already exists
681
+ has_exclude_patterns = any(
682
+ "exclude-patterns" in result[i] for i in range(insert_index, end_index)
683
+ )
684
+ if has_exclude_patterns:
685
+ # Update existing exclude-patterns line
686
+ for i in range(insert_index, end_index):
687
+ if "exclude-patterns" in result[i]:
688
+ result[i] = f'exclude-patterns = [{patterns_str}]'
689
+ break
690
+ else:
691
+ # Add exclude-patterns to existing section
692
+ result.insert(end_index - 1, f'exclude-patterns = [{patterns_str}]')
693
+ else:
694
+ # Insert new section
695
+ result[insert_index:insert_index] = exclude_lines
696
+
638
697
  return "\n".join(result)
639
698
 
699
+ def _inject_exclude_patterns(self, content: str, exclude_patterns: list[str]) -> str:
700
+ """
701
+ Inject exclude patterns into pyproject.toml content.
702
+
703
+ Adds or updates [tool.python-package-folder] exclude-patterns section.
704
+
705
+ Args:
706
+ content: pyproject.toml content
707
+ exclude_patterns: List of exclude patterns to inject
708
+
709
+ Returns:
710
+ Modified pyproject.toml content with exclude patterns
711
+ """
712
+ if not exclude_patterns:
713
+ return content
714
+
715
+ lines = content.split("\n")
716
+ result = []
717
+ tool_section_exists = False
718
+ tool_section_index = -1
719
+ tool_section_end = -1
720
+
721
+ # Find [tool.python-package-folder] section
722
+ for i, line in enumerate(lines):
723
+ if line.strip() == "[tool.python-package-folder]":
724
+ tool_section_exists = True
725
+ tool_section_index = i
726
+ # Find end of section
727
+ for j in range(i + 1, len(lines)):
728
+ if lines[j].strip().startswith("["):
729
+ tool_section_end = j
730
+ break
731
+ if tool_section_end == -1:
732
+ tool_section_end = len(lines)
733
+ break
734
+
735
+ if tool_section_exists:
736
+ # Update existing section
737
+ patterns_str = ", ".join(f'"{p}"' for p in exclude_patterns)
738
+ has_exclude_patterns = False
739
+ for i in range(tool_section_index + 1, tool_section_end):
740
+ if "exclude-patterns" in lines[i]:
741
+ # Update existing line
742
+ lines[i] = f'exclude-patterns = [{patterns_str}]'
743
+ has_exclude_patterns = True
744
+ break
745
+ if not has_exclude_patterns:
746
+ # Add exclude-patterns to existing section
747
+ lines.insert(tool_section_end, f'exclude-patterns = [{patterns_str}]')
748
+ return "\n".join(lines)
749
+ else:
750
+ # Add new section at the end
751
+ patterns_str = ", ".join(f'"{p}"' for p in exclude_patterns)
752
+ lines.append("")
753
+ lines.append("[tool.python-package-folder]")
754
+ lines.append(f'exclude-patterns = [{patterns_str}]')
755
+ return "\n".join(lines)
756
+
640
757
  def add_third_party_dependencies(self, dependencies: list[str]) -> None:
641
758
  """
642
759
  Add third-party dependencies to the temporary pyproject.toml.
@@ -6,6 +6,14 @@ from __future__ import annotations
6
6
 
7
7
  from pathlib import Path
8
8
 
9
+ try:
10
+ import tomllib
11
+ except ImportError:
12
+ try:
13
+ import tomli as tomllib
14
+ except ImportError:
15
+ tomllib = None
16
+
9
17
 
10
18
  def find_project_root(start_path: Path | None = None) -> Path | None:
11
19
  """
@@ -106,3 +114,64 @@ def is_python_package_directory(path: Path) -> bool:
106
114
  return True
107
115
 
108
116
  return False
117
+
118
+
119
+ def read_exclude_patterns(pyproject_path: Path) -> list[str]:
120
+ """
121
+ Read exclude patterns from pyproject.toml.
122
+
123
+ Reads the exclude-patterns configuration from [tool.python-package-folder] section.
124
+ Returns an empty list if the section or option doesn't exist.
125
+
126
+ Args:
127
+ pyproject_path: Path to the pyproject.toml file
128
+
129
+ Returns:
130
+ List of exclude patterns (regex strings), or empty list if not found
131
+ """
132
+ if not pyproject_path.exists():
133
+ return []
134
+
135
+ try:
136
+ if tomllib:
137
+ content = pyproject_path.read_text(encoding="utf-8")
138
+ data = tomllib.loads(content)
139
+ tool_section = data.get("tool", {})
140
+ tool_config = tool_section.get("python-package-folder", {})
141
+ # TOML keys with hyphens are preserved as-is
142
+ patterns = tool_config.get("exclude-patterns", [])
143
+ if isinstance(patterns, list):
144
+ return [str(p) for p in patterns]
145
+ return []
146
+ else:
147
+ # Fallback: simple string parsing
148
+ content = pyproject_path.read_text(encoding="utf-8")
149
+ in_section = False
150
+ patterns = []
151
+ for line in content.split("\n"):
152
+ stripped = line.strip()
153
+ if stripped == "[tool.python-package-folder]":
154
+ in_section = True
155
+ continue
156
+ elif stripped.startswith("[") and in_section:
157
+ # Moved to a different section
158
+ break
159
+ elif in_section and stripped.startswith("exclude-patterns"):
160
+ # Parse the array
161
+ if "=" in stripped:
162
+ value = stripped.split("=", 1)[1].strip()
163
+ # Simple parsing for array of strings
164
+ if value.startswith("[") and value.endswith("]"):
165
+ # Remove brackets and split
166
+ items = value[1:-1].split(",")
167
+ for item in items:
168
+ item = item.strip().strip('"').strip("'")
169
+ if item:
170
+ patterns.append(item)
171
+ break
172
+ return patterns
173
+ except Exception:
174
+ # If parsing fails, return empty list
175
+ return []
176
+
177
+ return []
@@ -0,0 +1,186 @@
1
+ """Tests for exclude patterns functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from python_package_folder import BuildManager, SubfolderBuildConfig
11
+ from python_package_folder.utils import read_exclude_patterns
12
+
13
+
14
+ @pytest.fixture
15
+ def test_project_with_exclude_patterns(tmp_path: Path) -> Path:
16
+ """Create a test project with exclude patterns in pyproject.toml."""
17
+ project_root = tmp_path / "test_project"
18
+ project_root.mkdir()
19
+
20
+ # Create pyproject.toml with exclude patterns
21
+ pyproject_content = """[project]
22
+ name = "test-package"
23
+ version = "0.1.0"
24
+
25
+ [build-system]
26
+ requires = ["hatchling"]
27
+ build-backend = "hatchling.build"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/test_package"]
31
+
32
+ [tool.python-package-folder]
33
+ exclude-patterns = ["_SS", ".*_test.*", "sandbox"]
34
+ """
35
+ (project_root / "pyproject.toml").write_text(pyproject_content)
36
+
37
+ # Create source directory structure
38
+ src_dir = project_root / "src" / "test_package"
39
+ src_dir.mkdir(parents=True)
40
+
41
+ # Create regular files
42
+ (src_dir / "__init__.py").write_text("")
43
+ (src_dir / "module.py").write_text("def func(): pass")
44
+
45
+ # Create files that should be excluded
46
+ (src_dir / "module_test.py").write_text("# test file")
47
+ (src_dir / "test_data.py").write_text("# test data")
48
+
49
+ # Create directories that should be excluded
50
+ ss_dir = src_dir / "data_storage" / "_SS"
51
+ ss_dir.mkdir(parents=True)
52
+ (ss_dir / "file.py").write_text("# SS file")
53
+
54
+ sandbox_dir = src_dir / "sandbox"
55
+ sandbox_dir.mkdir()
56
+ (sandbox_dir / "file.py").write_text("# sandbox file")
57
+
58
+ # Create nested excluded directory
59
+ nested_ss = src_dir / "nested" / "_SS_nested"
60
+ nested_ss.mkdir(parents=True)
61
+ (nested_ss / "file.py").write_text("# nested SS file")
62
+
63
+ return project_root
64
+
65
+
66
+ @pytest.fixture
67
+ def test_subfolder_project(tmp_path: Path) -> Path:
68
+ """Create a test project with subfolder and exclude patterns."""
69
+ project_root = tmp_path / "test_project"
70
+ project_root.mkdir()
71
+
72
+ # Create pyproject.toml with exclude patterns
73
+ pyproject_content = """[project]
74
+ name = "test-package"
75
+ version = "0.1.0"
76
+
77
+ [build-system]
78
+ requires = ["hatchling"]
79
+ build-backend = "hatchling.build"
80
+
81
+ [tool.hatch.build.targets.wheel]
82
+ packages = ["src/test_package"]
83
+
84
+ [tool.python-package-folder]
85
+ exclude-patterns = ["_SS", ".*_test.*"]
86
+ """
87
+ (project_root / "pyproject.toml").write_text(pyproject_content)
88
+
89
+ # Create subfolder
90
+ subfolder = project_root / "subfolder"
91
+ subfolder.mkdir()
92
+ (subfolder / "__init__.py").write_text("")
93
+ (subfolder / "module.py").write_text("def func(): pass")
94
+
95
+ # Create files that should be excluded
96
+ (subfolder / "module_test.py").write_text("# test file")
97
+ ss_dir = subfolder / "_SS"
98
+ ss_dir.mkdir()
99
+ (ss_dir / "file.py").write_text("# SS file")
100
+
101
+ return project_root
102
+
103
+
104
+ class TestReadExcludePatterns:
105
+ """Tests for reading exclude patterns from pyproject.toml."""
106
+
107
+ def test_read_exclude_patterns_exists(self, test_project_with_exclude_patterns: Path) -> None:
108
+ """Test reading exclude patterns when they exist."""
109
+ pyproject_path = test_project_with_exclude_patterns / "pyproject.toml"
110
+ patterns = read_exclude_patterns(pyproject_path)
111
+
112
+ assert len(patterns) == 3
113
+ assert "_SS" in patterns
114
+ assert ".*_test.*" in patterns
115
+ assert "sandbox" in patterns
116
+
117
+ def test_read_exclude_patterns_not_exists(self, tmp_path: Path) -> None:
118
+ """Test reading exclude patterns when section doesn't exist."""
119
+ pyproject_path = tmp_path / "pyproject.toml"
120
+ pyproject_path.write_text("[project]\nname = 'test'")
121
+
122
+ patterns = read_exclude_patterns(pyproject_path)
123
+ assert patterns == []
124
+
125
+ def test_read_exclude_patterns_file_not_exists(self, tmp_path: Path) -> None:
126
+ """Test reading exclude patterns when file doesn't exist."""
127
+ pyproject_path = tmp_path / "nonexistent.toml"
128
+ patterns = read_exclude_patterns(pyproject_path)
129
+ assert patterns == []
130
+
131
+
132
+ class TestExcludePatternsInBuild:
133
+ """Tests for exclude patterns in build process."""
134
+
135
+ def test_exclude_patterns_in_temp_pyproject(
136
+ self, test_project_with_exclude_patterns: Path
137
+ ) -> None:
138
+ """Test that exclude patterns are injected into temporary pyproject.toml."""
139
+ project_root = test_project_with_exclude_patterns
140
+ src_dir = project_root / "src" / "test_package"
141
+
142
+ # This is detected as a subfolder build
143
+ config = SubfolderBuildConfig(
144
+ project_root=project_root,
145
+ src_dir=src_dir,
146
+ package_name="test-package",
147
+ version="1.0.0",
148
+ )
149
+
150
+ temp_pyproject = config.create_temp_pyproject()
151
+ assert temp_pyproject is not None
152
+
153
+ # Check that exclude patterns are in the temporary pyproject.toml
154
+ content = temp_pyproject.read_text()
155
+ assert "[tool.python-package-folder]" in content
156
+ assert "exclude-patterns" in content
157
+ assert "_SS" in content
158
+ assert ".*_test.*" in content
159
+ assert "sandbox" in content
160
+
161
+ config.restore()
162
+
163
+ def test_exclude_patterns_subfolder_build(self, test_subfolder_project: Path) -> None:
164
+ """Test that exclude patterns from root are applied to subfolder builds."""
165
+ project_root = test_subfolder_project
166
+ subfolder = project_root / "subfolder"
167
+
168
+ config = SubfolderBuildConfig(
169
+ project_root=project_root,
170
+ src_dir=subfolder,
171
+ package_name="subfolder",
172
+ version="1.0.0",
173
+ )
174
+
175
+ # Create temporary pyproject.toml
176
+ temp_pyproject = config.create_temp_pyproject()
177
+ assert temp_pyproject is not None
178
+
179
+ # Check that exclude patterns are in the temporary pyproject.toml
180
+ content = temp_pyproject.read_text()
181
+ assert "[tool.python-package-folder]" in content
182
+ assert "exclude-patterns" in content
183
+ assert "_SS" in content
184
+ assert ".*_test.*" in content
185
+
186
+ config.restore()