python-package-folder 5.0.0__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.
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/PKG-INFO +1 -1
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/coverage.svg +2 -2
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/docs/REFERENCE.md +23 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/docs/USAGE.md +10 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/pyproject.toml +1 -1
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/_hatch_build.py +90 -1
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/python_package_folder.py +9 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/subfolder_build.py +120 -3
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/utils.py +69 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/version_calculator.py +121 -14
- python_package_folder-5.1.0/tests/test_exclude_patterns.py +186 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_version_calculator.py +12 -4
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.copier-answers.yml +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.github/workflows/ci.yml +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.github/workflows/publish.yml +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.gitignore +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/.vscode/settings.json +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/LICENSE +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/MANIFEST.in +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/Makefile +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/README.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/development.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/docs/DEVELOPMENT.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/docs/INSTALLATION.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/docs/PUBLISHING.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/docs/VERSION_RESOLUTION.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/installation.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/publishing.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/__init__.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/finder.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/manager.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/publisher.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/types.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/version.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/conftest.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_build_with_external_deps.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_linting.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_preserve_directory_structure.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_publisher.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_shared_subdirectory_imports.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_spreadsheet_creation_imports.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_subfolder_build.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_third_party_dependencies.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_utils.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_version_manager.py +0 -0
- {python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/tests.py +0 -0
- {python_package_folder-5.0.0 → 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.
|
|
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">
|
|
18
|
-
<text x="81" y="14">
|
|
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:
|
|
@@ -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"]
|
|
@@ -10,6 +10,8 @@ It can be invoked via:
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
13
15
|
import subprocess
|
|
14
16
|
import sys
|
|
15
17
|
from pathlib import Path
|
|
@@ -18,6 +20,13 @@ from .manager import BuildManager
|
|
|
18
20
|
from .utils import find_project_root, find_source_directory
|
|
19
21
|
from .version_calculator import resolve_version
|
|
20
22
|
|
|
23
|
+
# Configure logging for version resolution
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
level=logging.INFO,
|
|
26
|
+
format="%(levelname)s: %(message)s",
|
|
27
|
+
handlers=[logging.StreamHandler(sys.stderr)],
|
|
28
|
+
)
|
|
29
|
+
|
|
21
30
|
|
|
22
31
|
def is_github_actions() -> bool:
|
|
23
32
|
"""Check if running in GitHub Actions."""
|
|
@@ -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,
|
|
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.
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/utils.py
RENAMED
|
@@ -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 []
|
|
@@ -10,12 +10,15 @@ Reference: https://semantic-release.gitbook.io/semantic-release/
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import logging
|
|
13
14
|
import re
|
|
14
15
|
import subprocess
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
import requests
|
|
18
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def query_registry_version(
|
|
21
24
|
package_name: str,
|
|
@@ -38,13 +41,26 @@ def query_registry_version(
|
|
|
38
41
|
|
|
39
42
|
try:
|
|
40
43
|
if repository in ("pypi", "testpypi"):
|
|
41
|
-
|
|
44
|
+
logger.info(f"Querying {repository} for package '{package_name}'")
|
|
45
|
+
version = _query_pypi_version(package_name, repository)
|
|
46
|
+
if version:
|
|
47
|
+
logger.info(f"Found version {version} on {repository}")
|
|
48
|
+
else:
|
|
49
|
+
logger.info(f"Package '{package_name}' not found on {repository} (first release)")
|
|
50
|
+
return version
|
|
42
51
|
elif repository == "azure":
|
|
43
52
|
if not repository_url:
|
|
53
|
+
logger.warning("Azure Artifacts repository URL not provided")
|
|
44
54
|
return None
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
logger.info(f"Querying Azure Artifacts for package '{package_name}' at {repository_url}")
|
|
56
|
+
version = _query_azure_artifacts_version(package_name, repository_url)
|
|
57
|
+
if version:
|
|
58
|
+
logger.info(f"Found version {version} on Azure Artifacts")
|
|
59
|
+
else:
|
|
60
|
+
logger.info(f"Could not retrieve version from Azure Artifacts for '{package_name}' (will fall back to git tags)")
|
|
61
|
+
return version
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.warning(f"Error querying {repository} for package '{package_name}': {e}", exc_info=True)
|
|
48
64
|
return None
|
|
49
65
|
|
|
50
66
|
return None
|
|
@@ -68,6 +84,7 @@ def _query_pypi_version(package_name: str, registry: str) -> str | None:
|
|
|
68
84
|
response = requests.get(url, timeout=10)
|
|
69
85
|
if response.status_code == 404:
|
|
70
86
|
# Package doesn't exist yet (first release)
|
|
87
|
+
logger.debug(f"Package '{package_name}' not found on {registry} (404)")
|
|
71
88
|
return None
|
|
72
89
|
if response.status_code == 200:
|
|
73
90
|
json_data = response.json()
|
|
@@ -80,9 +97,16 @@ def _query_pypi_version(package_name: str, registry: str) -> str | None:
|
|
|
80
97
|
# Sort versions and get latest
|
|
81
98
|
versions = sorted(releases.keys(), key=_parse_version_for_sort)
|
|
82
99
|
version = versions[-1] if versions else None
|
|
100
|
+
if version:
|
|
101
|
+
logger.debug(f"Retrieved version {version} from {registry} for '{package_name}'")
|
|
83
102
|
return version
|
|
103
|
+
logger.warning(f"Unexpected status code {response.status_code} from {registry} for '{package_name}'")
|
|
84
104
|
return None
|
|
85
|
-
except
|
|
105
|
+
except requests.RequestException as e:
|
|
106
|
+
logger.warning(f"Network error querying {registry} for '{package_name}': {e}")
|
|
107
|
+
return None
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning(f"Error parsing response from {registry} for '{package_name}': {e}", exc_info=True)
|
|
86
110
|
return None
|
|
87
111
|
|
|
88
112
|
|
|
@@ -111,17 +135,35 @@ def _query_azure_artifacts_version(
|
|
|
111
135
|
simple_index_url = repository_url.replace("/upload", f"/simple/{package_name}/")
|
|
112
136
|
else:
|
|
113
137
|
simple_index_url = repository_url.rstrip("/") + f"/simple/{package_name}/"
|
|
114
|
-
|
|
138
|
+
logger.debug(f"Constructed Azure Artifacts simple index URL: {simple_index_url}")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning(f"Error constructing Azure Artifacts URL for '{package_name}': {e}")
|
|
115
141
|
return None
|
|
116
142
|
|
|
117
143
|
try:
|
|
118
144
|
response = requests.get(simple_index_url, timeout=5)
|
|
145
|
+
logger.debug(f"Azure Artifacts response status: {response.status_code}")
|
|
146
|
+
|
|
147
|
+
if response.status_code == 401:
|
|
148
|
+
logger.warning(f"Authentication required for Azure Artifacts (401). Package '{package_name}' may require authentication to query.")
|
|
149
|
+
elif response.status_code == 403:
|
|
150
|
+
logger.warning(f"Access forbidden for Azure Artifacts (403). Package '{package_name}' may not be accessible or requires different permissions.")
|
|
151
|
+
elif response.status_code == 404:
|
|
152
|
+
logger.debug(f"Package '{package_name}' not found on Azure Artifacts (404) - first release")
|
|
153
|
+
elif response.status_code != 200:
|
|
154
|
+
logger.warning(f"Unexpected status code {response.status_code} from Azure Artifacts for '{package_name}'")
|
|
155
|
+
|
|
119
156
|
# Azure Artifacts simple index returns HTML, not JSON
|
|
120
157
|
# Parsing HTML is complex and may require authentication
|
|
121
158
|
# For now, we'll return None to fall back to git tags
|
|
122
159
|
# This can be enhanced later with proper HTML parsing or API endpoint discovery
|
|
160
|
+
logger.info(f"Azure Artifacts version query not fully implemented (HTML parsing required). Falling back to git tags.")
|
|
123
161
|
return None
|
|
124
|
-
except
|
|
162
|
+
except requests.RequestException as e:
|
|
163
|
+
logger.warning(f"Network error querying Azure Artifacts for '{package_name}': {e}")
|
|
164
|
+
return None
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.warning(f"Unexpected error querying Azure Artifacts for '{package_name}': {e}", exc_info=True)
|
|
125
167
|
return None
|
|
126
168
|
|
|
127
169
|
|
|
@@ -465,26 +507,91 @@ def resolve_version(
|
|
|
465
507
|
# Step 1: Try to get baseline version from registry
|
|
466
508
|
baseline_version = None
|
|
467
509
|
if repository and package_name:
|
|
510
|
+
logger.info(f"Attempting to query {repository} for baseline version of '{package_name}'")
|
|
468
511
|
baseline_version = query_registry_version(package_name, repository, repository_url)
|
|
469
512
|
|
|
470
513
|
# Step 2: Fallback to git tags if registry query failed
|
|
471
514
|
if not baseline_version:
|
|
515
|
+
logger.info(f"Registry query did not return a version, falling back to git tags")
|
|
516
|
+
if is_subfolder:
|
|
517
|
+
logger.debug(f"Looking for subfolder git tags matching '{package_name}-v*'")
|
|
518
|
+
else:
|
|
519
|
+
logger.debug("Looking for main package git tags matching 'v*'")
|
|
472
520
|
baseline_version = get_latest_git_tag(project_root, package_name, is_subfolder)
|
|
521
|
+
if baseline_version:
|
|
522
|
+
logger.info(f"Found baseline version {baseline_version} from git tags")
|
|
523
|
+
else:
|
|
524
|
+
logger.info("No git tags found")
|
|
473
525
|
|
|
474
|
-
# Step 3: If still no baseline,
|
|
526
|
+
# Step 3: If still no baseline, this is likely the first release
|
|
527
|
+
# Default to 0.0.0 as the starting version (standard semantic-release behavior)
|
|
475
528
|
if not baseline_version:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
529
|
+
logger.info("No baseline version found (no registry version or git tags). Treating as first release (baseline: 0.0.0)")
|
|
530
|
+
baseline_version = "0.0.0"
|
|
531
|
+
# For first release, get all commits (no baseline to compare against)
|
|
532
|
+
# Use HEAD as the reference point to get all commits
|
|
533
|
+
try:
|
|
534
|
+
cmd = ["git", "log", "--format=%B", "HEAD"]
|
|
535
|
+
if subfolder_path:
|
|
536
|
+
# Convert to relative path from project root
|
|
537
|
+
if subfolder_path.is_absolute():
|
|
538
|
+
rel_path = subfolder_path.relative_to(project_root)
|
|
539
|
+
else:
|
|
540
|
+
rel_path = subfolder_path
|
|
541
|
+
cmd.append("--")
|
|
542
|
+
cmd.append(str(rel_path))
|
|
543
|
+
|
|
544
|
+
result = subprocess.run(
|
|
545
|
+
cmd,
|
|
546
|
+
cwd=project_root,
|
|
547
|
+
capture_output=True,
|
|
548
|
+
text=True,
|
|
549
|
+
check=False,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
commits = []
|
|
553
|
+
if result.returncode == 0:
|
|
554
|
+
# Split commits (they're separated by double newlines in --format=%B)
|
|
555
|
+
current_commit = []
|
|
556
|
+
for line in result.stdout.split("\n"):
|
|
557
|
+
if line.strip() == "" and current_commit:
|
|
558
|
+
commit_msg = "\n".join(current_commit).strip()
|
|
559
|
+
if commit_msg:
|
|
560
|
+
commits.append(commit_msg)
|
|
561
|
+
current_commit = []
|
|
562
|
+
else:
|
|
563
|
+
current_commit.append(line)
|
|
564
|
+
|
|
565
|
+
# Don't forget the last commit if there's no trailing newline
|
|
566
|
+
if current_commit:
|
|
567
|
+
commit_msg = "\n".join(current_commit).strip()
|
|
568
|
+
if commit_msg:
|
|
569
|
+
commits.append(commit_msg)
|
|
570
|
+
logger.debug(f"Retrieved {len(commits)} commits for first release")
|
|
571
|
+
except Exception as e:
|
|
572
|
+
logger.warning(f"Error retrieving commits for first release: {e}", exc_info=True)
|
|
573
|
+
commits = []
|
|
574
|
+
else:
|
|
575
|
+
# Step 4: Get commits since baseline
|
|
576
|
+
logger.info(f"Retrieving commits since version {baseline_version}")
|
|
577
|
+
if subfolder_path:
|
|
578
|
+
logger.debug(f"Filtering commits for subfolder path: {subfolder_path}")
|
|
579
|
+
commits = get_commits_since(project_root, baseline_version, subfolder_path, package_name)
|
|
580
|
+
logger.debug(f"Found {len(commits)} commits since {baseline_version}")
|
|
482
581
|
|
|
483
582
|
# Step 5: Calculate next version
|
|
583
|
+
logger.info(f"Calculating next version from baseline {baseline_version} and {len(commits)} commits")
|
|
484
584
|
next_version = calculate_next_version(baseline_version, commits)
|
|
485
585
|
|
|
486
586
|
if next_version:
|
|
587
|
+
logger.info(f"Calculated next version: {next_version}")
|
|
487
588
|
return next_version, None
|
|
488
589
|
else:
|
|
489
590
|
# No relevant commits for version bump
|
|
591
|
+
# For first release (0.0.0), default to 0.1.0 if there are any commits
|
|
592
|
+
if baseline_version == "0.0.0" and commits:
|
|
593
|
+
# Even if commits don't match conventional format, start at 0.1.0 for first release
|
|
594
|
+
logger.info("No conventional commits found, but commits exist. Defaulting to 0.1.0 for first release")
|
|
595
|
+
return "0.1.0", None
|
|
596
|
+
logger.info("No version bump needed (no relevant conventional commits found)")
|
|
490
597
|
return None, None
|
|
@@ -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()
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_version_calculator.py
RENAMED
|
@@ -414,14 +414,22 @@ class TestResolveVersion:
|
|
|
414
414
|
|
|
415
415
|
@patch("python_package_folder.version_calculator.query_registry_version")
|
|
416
416
|
@patch("python_package_folder.version_calculator.get_latest_git_tag")
|
|
417
|
+
@patch("python_package_folder.version_calculator.subprocess.run")
|
|
417
418
|
def test_resolve_version_no_baseline(
|
|
418
419
|
self,
|
|
420
|
+
mock_subprocess: MagicMock,
|
|
419
421
|
mock_git_tag: MagicMock,
|
|
420
422
|
mock_registry: MagicMock,
|
|
421
423
|
) -> None:
|
|
422
|
-
"""Test
|
|
424
|
+
"""Test first release behavior when no baseline version is found."""
|
|
423
425
|
mock_registry.return_value = None
|
|
424
426
|
mock_git_tag.return_value = None
|
|
427
|
+
|
|
428
|
+
# Mock git log for first release with conventional commits
|
|
429
|
+
mock_result = Mock()
|
|
430
|
+
mock_result.returncode = 0
|
|
431
|
+
mock_result.stdout = "feat: initial feature\n\n"
|
|
432
|
+
mock_subprocess.return_value = mock_result
|
|
425
433
|
|
|
426
434
|
version, error = resolve_version(
|
|
427
435
|
Path("/tmp/test"),
|
|
@@ -429,9 +437,9 @@ class TestResolveVersion:
|
|
|
429
437
|
repository="pypi",
|
|
430
438
|
)
|
|
431
439
|
|
|
432
|
-
|
|
433
|
-
assert
|
|
434
|
-
assert
|
|
440
|
+
# For first release with feat commit, should calculate 0.1.0 (0.0.0 + minor)
|
|
441
|
+
assert version == "0.1.0"
|
|
442
|
+
assert error is None
|
|
435
443
|
|
|
436
444
|
@patch("python_package_folder.version_calculator.query_registry_version")
|
|
437
445
|
@patch("python_package_folder.version_calculator.get_commits_since")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/__init__.py
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/__main__.py
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/analyzer.py
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/finder.py
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/manager.py
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/publisher.py
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/py.typed
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/types.py
RENAMED
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/src/python_package_folder/version.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/folder_structure/some_globals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_build_with_external_deps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-5.0.0 → python_package_folder-5.1.0}/tests/test_third_party_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|