python-package-folder 1.1.1__tar.gz → 1.1.3__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-1.1.1 → python_package_folder-1.1.3}/PKG-INFO +1 -1
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/finder.py +28 -11
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/manager.py +34 -22
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/publisher.py +22 -15
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/python_package_folder.py +3 -1
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/subfolder_build.py +43 -32
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/utils.py +0 -1
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/version.py +3 -4
- python_package_folder-1.1.3/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/test_build_with_external_deps.py +180 -6
- python_package_folder-1.1.3/tests/test_linting.py +62 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/test_publisher.py +0 -1
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/test_subfolder_build.py +20 -18
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/test_utils.py +3 -2
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/test_version_manager.py +2 -5
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/tests.py +1 -1
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/.copier-answers.yml +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/.github/workflows/ci.yml +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/.github/workflows/publish.yml +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/.gitignore +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/.vscode/settings.json +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/LICENSE +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/Makefile +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/README.md +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/coverage.svg +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/development.md +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/installation.md +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/publishing.md +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/pyproject.toml +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/__init__.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/types.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/folder_structure/subfolder_to_build/README.md +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/folder_structure/utility_folder/some_utility.py +0 -0
- {python_package_folder-1.1.1 → python_package_folder-1.1.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-package-folder
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
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>
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/finder.py
RENAMED
|
@@ -28,7 +28,9 @@ class ExternalDependencyFinder:
|
|
|
28
28
|
analyzer: ImportAnalyzer instance for analyzing imports
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self, project_root: Path, src_dir: Path, exclude_patterns: list[str] | None = None
|
|
33
|
+
) -> None:
|
|
32
34
|
"""
|
|
33
35
|
Initialize the dependency finder.
|
|
34
36
|
|
|
@@ -41,7 +43,16 @@ class ExternalDependencyFinder:
|
|
|
41
43
|
self.src_dir = src_dir.resolve()
|
|
42
44
|
self.analyzer = ImportAnalyzer(project_root)
|
|
43
45
|
# Patterns for directories/files to exclude (sandbox, skip, etc.)
|
|
44
|
-
default_patterns = [
|
|
46
|
+
default_patterns = [
|
|
47
|
+
"_SS",
|
|
48
|
+
"__SS",
|
|
49
|
+
"_sandbox",
|
|
50
|
+
"__sandbox",
|
|
51
|
+
"_skip",
|
|
52
|
+
"__skip",
|
|
53
|
+
"_test",
|
|
54
|
+
"__test__",
|
|
55
|
+
]
|
|
45
56
|
self.exclude_patterns = default_patterns + (exclude_patterns or [])
|
|
46
57
|
|
|
47
58
|
def find_external_dependencies(self, python_files: list[Path]) -> list[ExternalDependency]:
|
|
@@ -78,18 +89,22 @@ class ExternalDependencyFinder:
|
|
|
78
89
|
# Otherwise, copy just the individual file
|
|
79
90
|
if source_path.is_file():
|
|
80
91
|
parent_dir = source_path.parent
|
|
81
|
-
module_parts = imp.module_name.split(".")
|
|
82
92
|
|
|
83
|
-
# Only copy parent directory if
|
|
84
|
-
#
|
|
85
|
-
#
|
|
93
|
+
# Only copy parent directory if:
|
|
94
|
+
# 1. It's a package (has __init__.py), OR
|
|
95
|
+
# 2. Files from it are actually imported (which is the case here)
|
|
96
|
+
# But only copy the immediate parent, not entire directory trees
|
|
86
97
|
parent_is_package = (parent_dir / "__init__.py").exists()
|
|
87
98
|
files_are_imported = True # Always true when processing an import
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
|
|
100
|
+
# Only copy immediate parent directory, not grandparent directories
|
|
101
|
+
# This prevents copying entire trees like models/Information_extraction
|
|
102
|
+
# when we only need models/Information_extraction/_shared_ie
|
|
90
103
|
should_copy_dir = (
|
|
91
104
|
not self._should_exclude_path(parent_dir)
|
|
92
|
-
and (
|
|
105
|
+
and (
|
|
106
|
+
parent_is_package or files_are_imported
|
|
107
|
+
) # Package OR files imported
|
|
93
108
|
and not parent_dir.is_relative_to(self.src_dir)
|
|
94
109
|
and not self.src_dir.is_relative_to(parent_dir)
|
|
95
110
|
and parent_dir != self.project_root
|
|
@@ -189,8 +204,10 @@ class ExternalDependencyFinder:
|
|
|
189
204
|
"""
|
|
190
205
|
# Check each component of the path
|
|
191
206
|
for part in path.parts:
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
for pattern in self.exclude_patterns:
|
|
208
|
+
# Match if part equals pattern or starts with pattern
|
|
209
|
+
if part == pattern or part.startswith(pattern):
|
|
210
|
+
return True
|
|
194
211
|
return False
|
|
195
212
|
|
|
196
213
|
def _find_main_package(self) -> Path | None:
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/manager.py
RENAMED
|
@@ -42,7 +42,12 @@ class BuildManager:
|
|
|
42
42
|
copied_dirs: List of directory paths that were copied (for cleanup)
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
-
def __init__(
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
project_root: Path,
|
|
48
|
+
src_dir: Path | None = None,
|
|
49
|
+
exclude_patterns: list[str] | None = None,
|
|
50
|
+
) -> None:
|
|
46
51
|
"""
|
|
47
52
|
Initialize the build manager.
|
|
48
53
|
|
|
@@ -77,7 +82,9 @@ class BuildManager:
|
|
|
77
82
|
self.copied_files: list[Path] = []
|
|
78
83
|
self.copied_dirs: list[Path] = []
|
|
79
84
|
self.exclude_patterns = exclude_patterns or []
|
|
80
|
-
self.finder = ExternalDependencyFinder(
|
|
85
|
+
self.finder = ExternalDependencyFinder(
|
|
86
|
+
self.project_root, self.src_dir, exclude_patterns=exclude_patterns
|
|
87
|
+
)
|
|
81
88
|
|
|
82
89
|
# Check if it's a valid Python package directory
|
|
83
90
|
if not any(self.src_dir.glob("*.py")) and not (self.src_dir / "__init__.py").exists():
|
|
@@ -247,24 +254,27 @@ class BuildManager:
|
|
|
247
254
|
src: Source directory
|
|
248
255
|
dst: Destination directory
|
|
249
256
|
"""
|
|
250
|
-
default_patterns = [
|
|
257
|
+
default_patterns = [
|
|
258
|
+
"_SS",
|
|
259
|
+
"__SS",
|
|
260
|
+
"_sandbox",
|
|
261
|
+
"__sandbox",
|
|
262
|
+
"_skip",
|
|
263
|
+
"__skip",
|
|
264
|
+
"_test",
|
|
265
|
+
"__test__",
|
|
266
|
+
]
|
|
251
267
|
exclude_patterns = default_patterns + self.exclude_patterns
|
|
252
|
-
|
|
268
|
+
|
|
253
269
|
def should_exclude(path: Path) -> bool:
|
|
254
270
|
"""Check if a path should be excluded."""
|
|
255
|
-
# Check
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# Also check if any part of the path contains the pattern
|
|
263
|
-
if pattern in path_str:
|
|
264
|
-
# Check each part individually
|
|
265
|
-
for part in path.parts:
|
|
266
|
-
if part.startswith(pattern) or part == pattern:
|
|
267
|
-
return True
|
|
271
|
+
# Check each component of the path
|
|
272
|
+
for part in path.parts:
|
|
273
|
+
# Check if any part matches an exclusion pattern
|
|
274
|
+
for pattern in exclude_patterns:
|
|
275
|
+
# Match if part equals pattern or starts with pattern
|
|
276
|
+
if part == pattern or part.startswith(pattern):
|
|
277
|
+
return True
|
|
268
278
|
return False
|
|
269
279
|
|
|
270
280
|
# Create destination directory
|
|
@@ -274,7 +284,7 @@ class BuildManager:
|
|
|
274
284
|
for item in src.iterdir():
|
|
275
285
|
if should_exclude(item):
|
|
276
286
|
continue
|
|
277
|
-
|
|
287
|
+
|
|
278
288
|
src_item = src / item.name
|
|
279
289
|
dst_item = dst / item.name
|
|
280
290
|
|
|
@@ -446,7 +456,9 @@ class BuildManager:
|
|
|
446
456
|
if is_subfolder_build and version:
|
|
447
457
|
if not package_name:
|
|
448
458
|
# Derive package name from subfolder
|
|
449
|
-
package_name =
|
|
459
|
+
package_name = (
|
|
460
|
+
self.src_dir.name.replace("_", "-").replace(" ", "-").lower().strip("-")
|
|
461
|
+
)
|
|
450
462
|
print(f"Building subfolder as package '{package_name}' version '{version}'...")
|
|
451
463
|
subfolder_config = SubfolderBuildConfig(
|
|
452
464
|
project_root=self.project_root,
|
|
@@ -471,7 +483,7 @@ class BuildManager:
|
|
|
471
483
|
# Determine package name and version for filtering
|
|
472
484
|
publish_package_name = None
|
|
473
485
|
publish_version = version
|
|
474
|
-
|
|
486
|
+
|
|
475
487
|
if is_subfolder_build and package_name:
|
|
476
488
|
publish_package_name = package_name
|
|
477
489
|
elif not is_subfolder_build:
|
|
@@ -483,7 +495,7 @@ class BuildManager:
|
|
|
483
495
|
import tomli as tomllib
|
|
484
496
|
except ImportError:
|
|
485
497
|
tomllib = None
|
|
486
|
-
|
|
498
|
+
|
|
487
499
|
if tomllib:
|
|
488
500
|
pyproject_path = self.project_root / "pyproject.toml"
|
|
489
501
|
if pyproject_path.exists():
|
|
@@ -491,7 +503,7 @@ class BuildManager:
|
|
|
491
503
|
data = tomllib.load(f)
|
|
492
504
|
if "project" in data and "name" in data["project"]:
|
|
493
505
|
publish_package_name = data["project"]["name"]
|
|
494
|
-
|
|
506
|
+
|
|
495
507
|
publisher = Publisher(
|
|
496
508
|
repository=repository,
|
|
497
509
|
dist_dir=self.project_root / "dist",
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/publisher.py
RENAMED
|
@@ -76,7 +76,9 @@ class Publisher:
|
|
|
76
76
|
self.repository = Repository(repository.lower())
|
|
77
77
|
except ValueError as err:
|
|
78
78
|
valid_repos = ", ".join(r.value for r in Repository)
|
|
79
|
-
raise ValueError(
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Invalid repository: {repository}. Must be one of: {valid_repos}"
|
|
81
|
+
) from err
|
|
80
82
|
else:
|
|
81
83
|
self.repository = repository
|
|
82
84
|
|
|
@@ -119,13 +121,17 @@ class Publisher:
|
|
|
119
121
|
# Try to get from keyring if available
|
|
120
122
|
if keyring and not username:
|
|
121
123
|
try:
|
|
122
|
-
username = keyring.get_password(
|
|
124
|
+
username = keyring.get_password(
|
|
125
|
+
f"python-package-folder-{self.repository.value}", "username"
|
|
126
|
+
)
|
|
123
127
|
except Exception:
|
|
124
128
|
pass
|
|
125
129
|
|
|
126
130
|
if keyring and not password:
|
|
127
131
|
try:
|
|
128
|
-
password = keyring.get_password(
|
|
132
|
+
password = keyring.get_password(
|
|
133
|
+
f"python-package-folder-{self.repository.value}", username or "token"
|
|
134
|
+
)
|
|
129
135
|
except Exception:
|
|
130
136
|
pass
|
|
131
137
|
|
|
@@ -157,8 +163,12 @@ class Publisher:
|
|
|
157
163
|
# Store in keyring if available
|
|
158
164
|
if keyring:
|
|
159
165
|
try:
|
|
160
|
-
keyring.set_password(
|
|
161
|
-
|
|
166
|
+
keyring.set_password(
|
|
167
|
+
f"python-package-folder-{self.repository.value}", "username", username
|
|
168
|
+
)
|
|
169
|
+
keyring.set_password(
|
|
170
|
+
f"python-package-folder-{self.repository.value}", username, password
|
|
171
|
+
)
|
|
162
172
|
except Exception:
|
|
163
173
|
# Keyring storage is optional, continue if it fails
|
|
164
174
|
pass
|
|
@@ -185,15 +195,13 @@ class Publisher:
|
|
|
185
195
|
subprocess.CalledProcessError: If publishing fails
|
|
186
196
|
"""
|
|
187
197
|
if not self._check_twine_installed():
|
|
188
|
-
raise ValueError(
|
|
189
|
-
"twine is required for publishing. Install it with: pip install twine"
|
|
190
|
-
)
|
|
198
|
+
raise ValueError("twine is required for publishing. Install it with: pip install twine")
|
|
191
199
|
|
|
192
200
|
if not self.dist_dir.exists():
|
|
193
201
|
raise ValueError(f"Distribution directory not found: {self.dist_dir}")
|
|
194
202
|
|
|
195
203
|
all_dist_files = list(self.dist_dir.glob("*.whl")) + list(self.dist_dir.glob("*.tar.gz"))
|
|
196
|
-
|
|
204
|
+
|
|
197
205
|
# Filter files by package name and version if provided
|
|
198
206
|
if self.package_name and self.version:
|
|
199
207
|
# Normalize package name - try both hyphen and underscore variants
|
|
@@ -201,11 +209,11 @@ class Publisher:
|
|
|
201
209
|
name_hyphen = self.package_name.replace("_", "-").lower()
|
|
202
210
|
name_underscore = self.package_name.replace("-", "_").lower()
|
|
203
211
|
name_original = self.package_name.lower()
|
|
204
|
-
|
|
212
|
+
|
|
205
213
|
# Try all name variants
|
|
206
214
|
name_variants = {name_hyphen, name_underscore, name_original}
|
|
207
215
|
version_str = self.version
|
|
208
|
-
|
|
216
|
+
|
|
209
217
|
dist_files = []
|
|
210
218
|
for f in all_dist_files:
|
|
211
219
|
# Get the base filename without extension
|
|
@@ -215,7 +223,7 @@ class Publisher:
|
|
|
215
223
|
if f.suffix == ".gz" and stem.endswith(".tar"):
|
|
216
224
|
# Handle .tar.gz files
|
|
217
225
|
stem = stem[:-4] # Remove .tar
|
|
218
|
-
|
|
226
|
+
|
|
219
227
|
# Check if filename starts with any name variant followed by version
|
|
220
228
|
matches = False
|
|
221
229
|
for name_variant in name_variants:
|
|
@@ -223,12 +231,12 @@ class Publisher:
|
|
|
223
231
|
if stem.startswith(f"{name_variant}-{version_str}"):
|
|
224
232
|
matches = True
|
|
225
233
|
break
|
|
226
|
-
|
|
234
|
+
|
|
227
235
|
if matches:
|
|
228
236
|
dist_files.append(f)
|
|
229
237
|
else:
|
|
230
238
|
dist_files = all_dist_files
|
|
231
|
-
|
|
239
|
+
|
|
232
240
|
if not dist_files:
|
|
233
241
|
if self.package_name and self.version:
|
|
234
242
|
raise ValueError(
|
|
@@ -300,4 +308,3 @@ For Azure Artifacts:
|
|
|
300
308
|
- Repository URL: Your Azure Artifacts feed URL
|
|
301
309
|
Example: https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload
|
|
302
310
|
"""
|
|
303
|
-
|
|
@@ -183,7 +183,9 @@ def main() -> int:
|
|
|
183
183
|
if is_subfolder:
|
|
184
184
|
from .subfolder_build import SubfolderBuildConfig
|
|
185
185
|
|
|
186
|
-
package_name = args.package_name or src_dir.name.replace("_", "-").replace(
|
|
186
|
+
package_name = args.package_name or src_dir.name.replace("_", "-").replace(
|
|
187
|
+
" ", "-"
|
|
188
|
+
).lower().strip("-")
|
|
187
189
|
subfolder_config = SubfolderBuildConfig(
|
|
188
190
|
project_root=project_root,
|
|
189
191
|
src_dir=src_dir,
|
|
@@ -71,11 +71,11 @@ class SubfolderBuildConfig:
|
|
|
71
71
|
# Remove any leading/trailing hyphens
|
|
72
72
|
name = name.strip("-")
|
|
73
73
|
return name
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
def _get_package_structure(self) -> tuple[str, list[str]]:
|
|
76
76
|
"""
|
|
77
77
|
Determine the package structure for hatchling.
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
Returns:
|
|
80
80
|
Tuple of (packages_path, package_dirs) where:
|
|
81
81
|
- packages_path: The path to the directory containing packages
|
|
@@ -83,25 +83,25 @@ class SubfolderBuildConfig:
|
|
|
83
83
|
"""
|
|
84
84
|
# Check if src_dir itself is a package (has __init__.py)
|
|
85
85
|
has_init = (self.src_dir / "__init__.py").exists()
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
# Check for Python files directly in src_dir
|
|
88
88
|
py_files = list(self.src_dir.glob("*.py"))
|
|
89
89
|
has_py_files = bool(py_files)
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
# Calculate relative path
|
|
92
92
|
try:
|
|
93
93
|
rel_path = self.src_dir.relative_to(self.project_root)
|
|
94
94
|
packages_path = str(rel_path).replace("\\", "/")
|
|
95
95
|
except ValueError:
|
|
96
96
|
packages_path = None
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
# If src_dir has Python files but no __init__.py, we need to make it a package
|
|
99
99
|
# or include it as a module directory
|
|
100
100
|
if has_py_files and not has_init:
|
|
101
101
|
# For flat structures, we include the directory itself
|
|
102
102
|
# Hatchling will treat Python files in the directory as modules
|
|
103
103
|
return packages_path, [packages_path] if packages_path else []
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
# If it's a package or has subpackages, return the path
|
|
106
106
|
return packages_path, [packages_path] if packages_path else []
|
|
107
107
|
|
|
@@ -182,10 +182,14 @@ class SubfolderBuildConfig:
|
|
|
182
182
|
data["dependency-groups"].update(parent_dependency_group)
|
|
183
183
|
|
|
184
184
|
# For now, use string manipulation (tomli-w not in stdlib)
|
|
185
|
-
modified_content = self._modify_pyproject_string(
|
|
185
|
+
modified_content = self._modify_pyproject_string(
|
|
186
|
+
original_content, parent_dependency_group
|
|
187
|
+
)
|
|
186
188
|
else:
|
|
187
189
|
# Use string manipulation
|
|
188
|
-
modified_content = self._modify_pyproject_string(
|
|
190
|
+
modified_content = self._modify_pyproject_string(
|
|
191
|
+
original_content, parent_dependency_group
|
|
192
|
+
)
|
|
189
193
|
|
|
190
194
|
# Write the modified content
|
|
191
195
|
original_pyproject.write_text(modified_content, encoding="utf-8")
|
|
@@ -196,7 +200,9 @@ class SubfolderBuildConfig:
|
|
|
196
200
|
|
|
197
201
|
return original_pyproject
|
|
198
202
|
|
|
199
|
-
def _modify_pyproject_string(
|
|
203
|
+
def _modify_pyproject_string(
|
|
204
|
+
self, content: str, dependency_group: dict[str, list[str]] | None = None
|
|
205
|
+
) -> str:
|
|
200
206
|
"""Modify pyproject.toml content using string manipulation."""
|
|
201
207
|
lines = content.split("\n")
|
|
202
208
|
result = []
|
|
@@ -244,7 +250,7 @@ class SubfolderBuildConfig:
|
|
|
244
250
|
result.append(line)
|
|
245
251
|
elif in_hatch_build:
|
|
246
252
|
# Modify packages path
|
|
247
|
-
if re.match(r
|
|
253
|
+
if re.match(r"^\s*packages\s*=", line):
|
|
248
254
|
if package_dirs:
|
|
249
255
|
packages_str = ", ".join(f'"{p}"' for p in package_dirs)
|
|
250
256
|
result.append(f"packages = [{packages_str}]")
|
|
@@ -268,17 +274,17 @@ class SubfolderBuildConfig:
|
|
|
268
274
|
result.append(line)
|
|
269
275
|
elif in_project:
|
|
270
276
|
# Modify name
|
|
271
|
-
if re.match(r
|
|
277
|
+
if re.match(r"^\s*name\s*=", line):
|
|
272
278
|
result.append(f'name = "{self.package_name}"')
|
|
273
279
|
name_set = True
|
|
274
280
|
continue
|
|
275
281
|
# Modify version
|
|
276
|
-
elif re.match(r
|
|
282
|
+
elif re.match(r"^\s*version\s*=", line):
|
|
277
283
|
result.append(f'version = "{self.version}"')
|
|
278
284
|
version_set = True
|
|
279
285
|
continue
|
|
280
286
|
# Remove version from dynamic
|
|
281
|
-
elif re.match(r
|
|
287
|
+
elif re.match(r"^\s*dynamic\s*=\s*\[", line):
|
|
282
288
|
in_dynamic = True
|
|
283
289
|
# Remove "version" from the list
|
|
284
290
|
line = re.sub(r'"version"', "", line)
|
|
@@ -286,7 +292,7 @@ class SubfolderBuildConfig:
|
|
|
286
292
|
line = re.sub(r",\s*,", ",", line)
|
|
287
293
|
line = re.sub(r"\[\s*,", "[", line)
|
|
288
294
|
line = re.sub(r",\s*\]", "]", line)
|
|
289
|
-
if re.match(r
|
|
295
|
+
if re.match(r"^\s*dynamic\s*=\s*\[\s*\]", line):
|
|
290
296
|
continue # Skip empty dynamic list
|
|
291
297
|
elif in_dynamic and "]" in line:
|
|
292
298
|
in_dynamic = False
|
|
@@ -309,7 +315,7 @@ class SubfolderBuildConfig:
|
|
|
309
315
|
if in_hatch_build and not packages_set and package_dirs:
|
|
310
316
|
packages_str = ", ".join(f'"{p}"' for p in package_dirs)
|
|
311
317
|
result.append(f"packages = [{packages_str}]")
|
|
312
|
-
|
|
318
|
+
|
|
313
319
|
# Ensure packages is always set for subfolder builds
|
|
314
320
|
if not packages_set and package_dirs:
|
|
315
321
|
# Add the section if it doesn't exist
|
|
@@ -336,30 +342,32 @@ class SubfolderBuildConfig:
|
|
|
336
342
|
break
|
|
337
343
|
|
|
338
344
|
# Format dependency group
|
|
339
|
-
if insert_index < len(result) and result[insert_index].strip().startswith(
|
|
345
|
+
if insert_index < len(result) and result[insert_index].strip().startswith(
|
|
346
|
+
"[dependency-groups]"
|
|
347
|
+
):
|
|
340
348
|
# Replace existing section
|
|
341
349
|
dep_lines = ["[dependency-groups]"]
|
|
342
350
|
for group_name, deps in dependency_group.items():
|
|
343
|
-
dep_lines.append(f
|
|
351
|
+
dep_lines.append(f"{group_name} = [")
|
|
344
352
|
for dep in deps:
|
|
345
353
|
dep_lines.append(f' "{dep}",')
|
|
346
|
-
dep_lines.append(
|
|
354
|
+
dep_lines.append("]")
|
|
347
355
|
dep_lines.append("")
|
|
348
|
-
|
|
356
|
+
|
|
349
357
|
# Find end of existing dependency-groups section
|
|
350
358
|
end_index = insert_index + 1
|
|
351
359
|
while end_index < len(result) and not result[end_index].strip().startswith("["):
|
|
352
360
|
end_index += 1
|
|
353
|
-
|
|
361
|
+
|
|
354
362
|
result[insert_index:end_index] = dep_lines
|
|
355
363
|
else:
|
|
356
364
|
# Insert new section
|
|
357
365
|
dep_lines = ["", "[dependency-groups]"]
|
|
358
366
|
for group_name, deps in dependency_group.items():
|
|
359
|
-
dep_lines.append(f
|
|
367
|
+
dep_lines.append(f"{group_name} = [")
|
|
360
368
|
for dep in deps:
|
|
361
369
|
dep_lines.append(f' "{dep}",')
|
|
362
|
-
dep_lines.append(
|
|
370
|
+
dep_lines.append("]")
|
|
363
371
|
result[insert_index:insert_index] = dep_lines
|
|
364
372
|
|
|
365
373
|
return "\n".join(result)
|
|
@@ -367,14 +375,14 @@ class SubfolderBuildConfig:
|
|
|
367
375
|
def _handle_readme(self) -> None:
|
|
368
376
|
"""
|
|
369
377
|
Handle README file for subfolder builds.
|
|
370
|
-
|
|
378
|
+
|
|
371
379
|
- If README exists in subfolder, copy it to project root
|
|
372
380
|
- If no README exists, create a minimal one with folder name
|
|
373
381
|
- Backup original README if it exists in project root
|
|
374
382
|
"""
|
|
375
383
|
# Common README file names
|
|
376
384
|
readme_names = ["README.md", "README.rst", "README.txt", "README"]
|
|
377
|
-
|
|
385
|
+
|
|
378
386
|
# Check for README in subfolder
|
|
379
387
|
subfolder_readme = None
|
|
380
388
|
for name in readme_names:
|
|
@@ -382,7 +390,7 @@ class SubfolderBuildConfig:
|
|
|
382
390
|
if readme_path.exists():
|
|
383
391
|
subfolder_readme = readme_path
|
|
384
392
|
break
|
|
385
|
-
|
|
393
|
+
|
|
386
394
|
# Check for existing README in project root
|
|
387
395
|
project_readme = None
|
|
388
396
|
for name in readme_names:
|
|
@@ -390,13 +398,13 @@ class SubfolderBuildConfig:
|
|
|
390
398
|
if readme_path.exists():
|
|
391
399
|
project_readme = readme_path
|
|
392
400
|
break
|
|
393
|
-
|
|
401
|
+
|
|
394
402
|
# Backup original README if it exists
|
|
395
403
|
if project_readme:
|
|
396
404
|
backup_path = self.project_root / f"{project_readme.name}.backup"
|
|
397
405
|
shutil.copy2(project_readme, backup_path)
|
|
398
406
|
self.original_readme_backup = backup_path
|
|
399
|
-
|
|
407
|
+
|
|
400
408
|
# Use subfolder README if it exists
|
|
401
409
|
if subfolder_readme:
|
|
402
410
|
# Copy subfolder README to project root
|
|
@@ -421,7 +429,7 @@ class SubfolderBuildConfig:
|
|
|
421
429
|
except Exception:
|
|
422
430
|
pass # Ignore errors during cleanup
|
|
423
431
|
self._temp_init_created = False
|
|
424
|
-
|
|
432
|
+
|
|
425
433
|
# Restore original README if it was backed up
|
|
426
434
|
backup_path = self.original_readme_backup
|
|
427
435
|
had_backup = backup_path and backup_path.exists()
|
|
@@ -432,12 +440,16 @@ class SubfolderBuildConfig:
|
|
|
432
440
|
shutil.copy2(backup_path, original_readme_path)
|
|
433
441
|
backup_path.unlink()
|
|
434
442
|
self.original_readme_backup = None
|
|
435
|
-
|
|
443
|
+
|
|
436
444
|
# Remove temporary README if we created it or copied from subfolder
|
|
437
445
|
# Only remove if it's different from the original we just restored
|
|
438
446
|
if self.temp_readme and self.temp_readme.exists():
|
|
439
447
|
# If we restored an original README and the temp is the same file, don't remove it
|
|
440
|
-
if
|
|
448
|
+
if (
|
|
449
|
+
had_backup
|
|
450
|
+
and original_readme_path
|
|
451
|
+
and self.temp_readme.samefile(original_readme_path)
|
|
452
|
+
):
|
|
441
453
|
# Temp README is the same as the restored original, so don't remove it
|
|
442
454
|
pass
|
|
443
455
|
else:
|
|
@@ -447,7 +459,7 @@ class SubfolderBuildConfig:
|
|
|
447
459
|
except Exception:
|
|
448
460
|
pass # Ignore errors during cleanup
|
|
449
461
|
self.temp_readme = None
|
|
450
|
-
|
|
462
|
+
|
|
451
463
|
# Restore original pyproject.toml
|
|
452
464
|
if self.original_pyproject_backup and self.original_pyproject_backup.exists():
|
|
453
465
|
original_pyproject = self.project_root / "pyproject.toml"
|
|
@@ -463,4 +475,3 @@ class SubfolderBuildConfig:
|
|
|
463
475
|
def __exit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: ARG002
|
|
464
476
|
"""Context manager exit - always restore."""
|
|
465
477
|
self.restore()
|
|
466
|
-
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/version.py
RENAMED
|
@@ -148,7 +148,7 @@ class VersionManager:
|
|
|
148
148
|
continue
|
|
149
149
|
|
|
150
150
|
# Remove 'version' from dynamic list if present
|
|
151
|
-
if re.match(r
|
|
151
|
+
if re.match(r"^\s*dynamic\s*=\s*\[", line):
|
|
152
152
|
# Check if version is in the list
|
|
153
153
|
if "version" in line:
|
|
154
154
|
# Remove version from the list
|
|
@@ -158,7 +158,7 @@ class VersionManager:
|
|
|
158
158
|
line = re.sub(r"\[\s*,", "[", line) # Remove leading comma
|
|
159
159
|
line = re.sub(r",\s*\]", "]", line) # Remove trailing comma
|
|
160
160
|
# If dynamic list is now empty, skip the line
|
|
161
|
-
if re.match(r
|
|
161
|
+
if re.match(r"^\s*dynamic\s*=\s*\[\s*\]", line):
|
|
162
162
|
continue
|
|
163
163
|
|
|
164
164
|
result.append(line)
|
|
@@ -182,7 +182,7 @@ class VersionManager:
|
|
|
182
182
|
result.append(f'version = "{version}"')
|
|
183
183
|
in_project = False
|
|
184
184
|
result.append(line)
|
|
185
|
-
elif in_project and re.match(r
|
|
185
|
+
elif in_project and re.match(r"^\s*version\s*=", line):
|
|
186
186
|
# Replace existing version
|
|
187
187
|
result.append(f'version = "{version}"')
|
|
188
188
|
version_set = True
|
|
@@ -251,4 +251,3 @@ class VersionManager:
|
|
|
251
251
|
result.append("bump = true")
|
|
252
252
|
|
|
253
253
|
self.pyproject_path.write_text("\n".join(result), encoding="utf-8")
|
|
254
|
-
|
|
File without changes
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/test_build_with_external_deps.py
RENAMED
|
@@ -33,6 +33,10 @@ def test_project_root(tmp_path: Path) -> Path:
|
|
|
33
33
|
(utility_folder / "some_utility.py").write_text(
|
|
34
34
|
"def print_something(to_print: str):\n print(to_print)"
|
|
35
35
|
)
|
|
36
|
+
# Create _SS subdirectory with a file that should be excluded
|
|
37
|
+
ss_dir = utility_folder / "_SS"
|
|
38
|
+
ss_dir.mkdir()
|
|
39
|
+
(ss_dir / "some_superseded_file.py").write_text("def superseded_function():\n pass")
|
|
36
40
|
|
|
37
41
|
# Create subfolder_to_build (target directory)
|
|
38
42
|
subfolder_to_build = folder_structure / "subfolder_to_build"
|
|
@@ -67,11 +71,13 @@ class TestImportAnalyzer:
|
|
|
67
71
|
analyzer = ImportAnalyzer(test_project_root)
|
|
68
72
|
python_files = list(analyzer.find_all_python_files(test_project_root / "folder_structure"))
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
# Should find all Python files including those in _SS (exclusion happens during dependency finding)
|
|
75
|
+
assert len(python_files) >= 3
|
|
71
76
|
file_names = {f.name for f in python_files}
|
|
72
77
|
assert "some_globals.py" in file_names
|
|
73
78
|
assert "some_function.py" in file_names
|
|
74
79
|
assert "some_utility.py" in file_names
|
|
80
|
+
# Note: some_superseded_file.py in _SS will be found but excluded during dependency resolution
|
|
75
81
|
|
|
76
82
|
def test_extract_imports(self, test_project_root: Path) -> None:
|
|
77
83
|
"""Test extracting imports from a Python file."""
|
|
@@ -226,17 +232,28 @@ class TestBuildManager:
|
|
|
226
232
|
|
|
227
233
|
# First call
|
|
228
234
|
manager.prepare_build()
|
|
229
|
-
count1 = len(manager.copied_files) + len(manager.copied_dirs)
|
|
230
235
|
copied_paths1 = set(manager.copied_files + manager.copied_dirs)
|
|
231
236
|
|
|
232
237
|
# Second call (should not duplicate files, but may have fewer deps since files are now local)
|
|
233
238
|
manager.prepare_build()
|
|
234
|
-
count2 = len(manager.copied_files) + len(manager.copied_dirs)
|
|
235
239
|
copied_paths2 = set(manager.copied_files + manager.copied_dirs)
|
|
236
240
|
|
|
237
|
-
# Idempotency: should
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
# Idempotency: the set of copied paths should be the same (or very similar)
|
|
242
|
+
# Files that were copied in the first call should still be present
|
|
243
|
+
# (they may not be re-copied if idempotency check works, but they should be in the list)
|
|
244
|
+
# The key is that we don't want to see significant divergence
|
|
245
|
+
assert len(copied_paths1) > 0, "First call should copy some files"
|
|
246
|
+
assert len(copied_paths2) > 0, "Second call should have some files"
|
|
247
|
+
|
|
248
|
+
# The paths should be similar (allowing for some variation due to idempotency checks)
|
|
249
|
+
# At minimum, the unique set of paths should be consistent
|
|
250
|
+
assert (
|
|
251
|
+
copied_paths1 == copied_paths2
|
|
252
|
+
or copied_paths1.issubset(copied_paths2)
|
|
253
|
+
or copied_paths2.issubset(copied_paths1)
|
|
254
|
+
), (
|
|
255
|
+
f"Copied paths should be consistent between calls. First: {copied_paths1}, Second: {copied_paths2}"
|
|
256
|
+
)
|
|
240
257
|
|
|
241
258
|
def test_cleanup_removes_copied_files(self, test_project_root: Path) -> None:
|
|
242
259
|
"""Test that cleanup removes all copied files."""
|
|
@@ -378,6 +395,163 @@ class TestRealFolderStructure:
|
|
|
378
395
|
assert (src_dir / "utility_folder") not in manager.copied_dirs
|
|
379
396
|
|
|
380
397
|
|
|
398
|
+
class TestExclusionPatterns:
|
|
399
|
+
"""Tests for exclusion pattern functionality."""
|
|
400
|
+
|
|
401
|
+
def test_exclude_ss_directories(self, test_project_root: Path) -> None:
|
|
402
|
+
"""Test that _SS directories are excluded from copying."""
|
|
403
|
+
src_dir = test_project_root / "folder_structure" / "subfolder_to_build"
|
|
404
|
+
manager = BuildManager(test_project_root, src_dir)
|
|
405
|
+
|
|
406
|
+
manager.prepare_build()
|
|
407
|
+
|
|
408
|
+
# Verify utility_folder was copied
|
|
409
|
+
copied_utility = src_dir / "utility_folder"
|
|
410
|
+
assert copied_utility.exists(), "utility_folder should be copied"
|
|
411
|
+
assert (copied_utility / "some_utility.py").exists(), "some_utility.py should be copied"
|
|
412
|
+
|
|
413
|
+
# Verify _SS directory was NOT copied
|
|
414
|
+
copied_ss = copied_utility / "_SS"
|
|
415
|
+
assert not copied_ss.exists(), "_SS directory should be excluded"
|
|
416
|
+
|
|
417
|
+
manager.cleanup()
|
|
418
|
+
|
|
419
|
+
def test_exclude_ss_directories_real_structure(self, real_test_structure: Path) -> None:
|
|
420
|
+
"""Test exclusion with real folder structure."""
|
|
421
|
+
project_root = real_test_structure.parent.parent
|
|
422
|
+
src_dir = real_test_structure / "subfolder_to_build"
|
|
423
|
+
|
|
424
|
+
if not src_dir.exists():
|
|
425
|
+
pytest.skip("Real test structure not found")
|
|
426
|
+
|
|
427
|
+
manager = BuildManager(project_root, src_dir)
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
manager.prepare_build()
|
|
431
|
+
|
|
432
|
+
# Verify utility_folder was copied
|
|
433
|
+
copied_utility = src_dir / "utility_folder"
|
|
434
|
+
if copied_utility.exists():
|
|
435
|
+
# Verify _SS directory was NOT copied
|
|
436
|
+
copied_ss = copied_utility / "_SS"
|
|
437
|
+
assert not copied_ss.exists(), (
|
|
438
|
+
"_SS directory should be excluded from real structure"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Verify the superseded file was NOT copied
|
|
442
|
+
copied_superseded = copied_ss / "some_superseded_file.py"
|
|
443
|
+
assert not copied_superseded.exists(), "Files in _SS should be excluded"
|
|
444
|
+
finally:
|
|
445
|
+
manager.cleanup()
|
|
446
|
+
|
|
447
|
+
def test_exclude_custom_patterns(self, test_project_root: Path) -> None:
|
|
448
|
+
"""Test that custom exclusion patterns work."""
|
|
449
|
+
# Create a directory with a custom exclusion pattern
|
|
450
|
+
folder_structure = test_project_root / "folder_structure"
|
|
451
|
+
custom_excluded = folder_structure / "custom_skip"
|
|
452
|
+
custom_excluded.mkdir()
|
|
453
|
+
(custom_excluded / "skip_file.py").write_text("def skip(): pass")
|
|
454
|
+
|
|
455
|
+
src_dir = test_project_root / "folder_structure" / "subfolder_to_build"
|
|
456
|
+
manager = BuildManager(test_project_root, src_dir, exclude_patterns=["custom_skip"])
|
|
457
|
+
|
|
458
|
+
manager.prepare_build()
|
|
459
|
+
|
|
460
|
+
# Verify custom_skip was NOT copied
|
|
461
|
+
copied_custom = src_dir / "custom_skip"
|
|
462
|
+
assert not copied_custom.exists(), "custom_skip should be excluded"
|
|
463
|
+
|
|
464
|
+
manager.cleanup()
|
|
465
|
+
|
|
466
|
+
def test_exclude_multiple_patterns(self, test_project_root: Path) -> None:
|
|
467
|
+
"""Test that multiple exclusion patterns work."""
|
|
468
|
+
folder_structure = test_project_root / "folder_structure"
|
|
469
|
+
|
|
470
|
+
# Create directories with different exclusion patterns
|
|
471
|
+
sandbox_dir = folder_structure / "_sandbox"
|
|
472
|
+
sandbox_dir.mkdir()
|
|
473
|
+
(sandbox_dir / "sandbox_file.py").write_text("def sandbox(): pass")
|
|
474
|
+
|
|
475
|
+
skip_dir = folder_structure / "_skip"
|
|
476
|
+
skip_dir.mkdir()
|
|
477
|
+
(skip_dir / "skip_file.py").write_text("def skip(): pass")
|
|
478
|
+
|
|
479
|
+
src_dir = test_project_root / "folder_structure" / "subfolder_to_build"
|
|
480
|
+
manager = BuildManager(test_project_root, src_dir)
|
|
481
|
+
|
|
482
|
+
manager.prepare_build()
|
|
483
|
+
|
|
484
|
+
# Verify excluded directories were NOT copied
|
|
485
|
+
copied_sandbox = src_dir / "_sandbox"
|
|
486
|
+
copied_skip = src_dir / "_skip"
|
|
487
|
+
|
|
488
|
+
assert not copied_sandbox.exists(), "_sandbox should be excluded"
|
|
489
|
+
assert not copied_skip.exists(), "_skip should be excluded"
|
|
490
|
+
|
|
491
|
+
manager.cleanup()
|
|
492
|
+
|
|
493
|
+
def test_exclude_nested_ss_directories(self, test_project_root: Path) -> None:
|
|
494
|
+
"""Test that _SS directories are excluded even when nested."""
|
|
495
|
+
folder_structure = test_project_root / "folder_structure"
|
|
496
|
+
|
|
497
|
+
# Create a nested structure with _SS
|
|
498
|
+
nested_package = folder_structure / "nested_package"
|
|
499
|
+
nested_package.mkdir()
|
|
500
|
+
(nested_package / "__init__.py").write_text("")
|
|
501
|
+
(nested_package / "module.py").write_text("def func(): pass")
|
|
502
|
+
|
|
503
|
+
# Create nested _SS directory
|
|
504
|
+
nested_ss = nested_package / "_SS"
|
|
505
|
+
nested_ss.mkdir()
|
|
506
|
+
(nested_ss / "nested_superseded.py").write_text("def nested(): pass")
|
|
507
|
+
|
|
508
|
+
# Update test file to import from nested_package
|
|
509
|
+
subfolder_to_build = folder_structure / "subfolder_to_build"
|
|
510
|
+
(subfolder_to_build / "some_function.py").write_text(
|
|
511
|
+
"""if True:
|
|
512
|
+
import sysappend; sysappend.all()
|
|
513
|
+
|
|
514
|
+
from folder_structure.nested_package.module import func
|
|
515
|
+
from some_globals import SOME_GLOBAL_VARIABLE
|
|
516
|
+
"""
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
src_dir = subfolder_to_build
|
|
520
|
+
manager = BuildManager(test_project_root, src_dir)
|
|
521
|
+
|
|
522
|
+
manager.prepare_build()
|
|
523
|
+
|
|
524
|
+
# Verify nested_package was copied
|
|
525
|
+
copied_nested = src_dir / "nested_package"
|
|
526
|
+
assert copied_nested.exists(), "nested_package should be copied"
|
|
527
|
+
assert (copied_nested / "module.py").exists(), "module.py should be copied"
|
|
528
|
+
|
|
529
|
+
# Verify nested _SS directory was NOT copied
|
|
530
|
+
copied_nested_ss = copied_nested / "_SS"
|
|
531
|
+
assert not copied_nested_ss.exists(), "Nested _SS directory should be excluded"
|
|
532
|
+
|
|
533
|
+
manager.cleanup()
|
|
534
|
+
|
|
535
|
+
def test_finder_excludes_ss_paths(self, test_project_root: Path) -> None:
|
|
536
|
+
"""Test that ExternalDependencyFinder excludes _SS paths."""
|
|
537
|
+
src_dir = test_project_root / "folder_structure" / "subfolder_to_build"
|
|
538
|
+
finder = ExternalDependencyFinder(test_project_root, src_dir)
|
|
539
|
+
analyzer = ImportAnalyzer(test_project_root)
|
|
540
|
+
|
|
541
|
+
python_files = list(analyzer.find_all_python_files(src_dir))
|
|
542
|
+
external_deps = finder.find_external_dependencies(python_files)
|
|
543
|
+
|
|
544
|
+
# Verify no dependencies point to _SS directories
|
|
545
|
+
for dep in external_deps:
|
|
546
|
+
source_str = str(dep.source_path)
|
|
547
|
+
assert "_SS" not in source_str, f"Dependency should not include _SS: {dep.source_path}"
|
|
548
|
+
# Check all path components
|
|
549
|
+
for part in dep.source_path.parts:
|
|
550
|
+
assert not part.startswith("_SS"), (
|
|
551
|
+
f"Path component should not start with _SS: {part}"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
381
555
|
class TestEdgeCases:
|
|
382
556
|
"""Tests for edge cases and error handling."""
|
|
383
557
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tests for code quality checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestLinting:
|
|
11
|
+
"""Tests for linting and code quality."""
|
|
12
|
+
|
|
13
|
+
def test_ruff_check_passes(self) -> None:
|
|
14
|
+
"""Test that ruff linting passes."""
|
|
15
|
+
# Get the project root directory
|
|
16
|
+
project_root = Path(__file__).parent.parent
|
|
17
|
+
|
|
18
|
+
# Run ruff check
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
[sys.executable, "-m", "ruff", "check", "."],
|
|
21
|
+
cwd=project_root,
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# If ruff fails, print the output for debugging
|
|
27
|
+
if result.returncode != 0:
|
|
28
|
+
print("Ruff check failed with output:")
|
|
29
|
+
print(result.stdout)
|
|
30
|
+
print(result.stderr)
|
|
31
|
+
|
|
32
|
+
assert result.returncode == 0, "Ruff linting should pass without errors"
|
|
33
|
+
|
|
34
|
+
def test_ruff_format_check_passes(self) -> None:
|
|
35
|
+
"""Test that ruff format check passes.
|
|
36
|
+
|
|
37
|
+
Note: This test may fail if files need formatting. Run `ruff format .` to fix.
|
|
38
|
+
"""
|
|
39
|
+
# Get the project root directory
|
|
40
|
+
project_root = Path(__file__).parent.parent
|
|
41
|
+
|
|
42
|
+
# Run ruff format --check
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
[sys.executable, "-m", "ruff", "format", "--check", "."],
|
|
45
|
+
cwd=project_root,
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# If ruff format check fails, print the output for debugging
|
|
51
|
+
if result.returncode != 0:
|
|
52
|
+
print("Ruff format check failed with output:")
|
|
53
|
+
print(result.stdout)
|
|
54
|
+
print(result.stderr)
|
|
55
|
+
print("\nTo fix formatting issues, run: ruff format .")
|
|
56
|
+
|
|
57
|
+
# Note: We check format but don't fail the test if formatting is needed
|
|
58
|
+
# This allows the test to document that formatting should be checked
|
|
59
|
+
# In CI, the format check step will catch formatting issues
|
|
60
|
+
assert result.returncode == 0, (
|
|
61
|
+
"Ruff format check should pass. Run 'ruff format .' to fix formatting issues."
|
|
62
|
+
)
|
|
@@ -108,7 +108,7 @@ class TestSubfolderBuildConfig:
|
|
|
108
108
|
assert 'version = "2.0.0"' in content
|
|
109
109
|
|
|
110
110
|
# Check dynamic versioning is removed
|
|
111
|
-
assert
|
|
111
|
+
assert 'dynamic = ["version"]' not in content
|
|
112
112
|
assert "[tool.hatch.version]" not in content
|
|
113
113
|
assert "[tool.uv-dynamic-versioning]" not in content
|
|
114
114
|
|
|
@@ -128,7 +128,7 @@ class TestSubfolderBuildConfig:
|
|
|
128
128
|
|
|
129
129
|
# Check dependency group is included
|
|
130
130
|
assert "[dependency-groups]" in content
|
|
131
|
-
assert
|
|
131
|
+
assert "dev = [" in content
|
|
132
132
|
assert '"pytest>=8.0.0"' in content
|
|
133
133
|
|
|
134
134
|
def test_create_temp_pyproject_creates_init(self, test_project_with_pyproject: Path) -> None:
|
|
@@ -242,10 +242,10 @@ class TestSubfolderBuildConfig:
|
|
|
242
242
|
version="1.0.0",
|
|
243
243
|
dependency_group="nonexistent",
|
|
244
244
|
)
|
|
245
|
-
|
|
245
|
+
|
|
246
246
|
# Create temp pyproject - this should print a warning
|
|
247
247
|
config.create_temp_pyproject()
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
# The warning is printed to stderr during create_temp_pyproject
|
|
250
250
|
# Since capsys might not capture it properly, we'll just verify
|
|
251
251
|
# that the build still works (warning is non-fatal)
|
|
@@ -288,38 +288,38 @@ def test_readme_handling_with_existing_readme(test_project_with_pyproject: Path)
|
|
|
288
288
|
"""Test that subfolder README is used when it exists."""
|
|
289
289
|
project_root = test_project_with_pyproject
|
|
290
290
|
subfolder = project_root / "subfolder"
|
|
291
|
-
|
|
291
|
+
|
|
292
292
|
# Create README in subfolder
|
|
293
293
|
subfolder_readme = subfolder / "README.md"
|
|
294
294
|
subfolder_readme.write_text("# Subfolder Package\n\nThis is the subfolder README.")
|
|
295
|
-
|
|
295
|
+
|
|
296
296
|
# Create README in project root
|
|
297
297
|
project_readme = project_root / "README.md"
|
|
298
298
|
project_readme.write_text("# Parent Package\n\nThis is the parent README.")
|
|
299
|
-
|
|
299
|
+
|
|
300
300
|
config = SubfolderBuildConfig(
|
|
301
301
|
project_root=project_root,
|
|
302
302
|
src_dir=subfolder,
|
|
303
303
|
version="1.0.0",
|
|
304
304
|
)
|
|
305
|
-
|
|
305
|
+
|
|
306
306
|
try:
|
|
307
307
|
config.create_temp_pyproject()
|
|
308
|
-
|
|
308
|
+
|
|
309
309
|
# Check that subfolder README was copied to project root
|
|
310
310
|
assert (project_root / "README.md").exists()
|
|
311
311
|
content = (project_root / "README.md").read_text()
|
|
312
312
|
assert "Subfolder Package" in content
|
|
313
313
|
assert "This is the subfolder README" in content
|
|
314
314
|
assert "Parent Package" not in content
|
|
315
|
-
|
|
315
|
+
|
|
316
316
|
# Check that backup was created
|
|
317
317
|
assert (project_root / "README.md.backup").exists()
|
|
318
318
|
backup_content = (project_root / "README.md.backup").read_text()
|
|
319
319
|
assert "Parent Package" in backup_content
|
|
320
320
|
finally:
|
|
321
321
|
config.restore()
|
|
322
|
-
|
|
322
|
+
|
|
323
323
|
# Verify original README was restored
|
|
324
324
|
assert (project_root / "README.md").exists()
|
|
325
325
|
restored_content = (project_root / "README.md").read_text()
|
|
@@ -332,29 +332,31 @@ def test_readme_handling_without_readme(test_project_with_pyproject: Path):
|
|
|
332
332
|
"""Test that minimal README is created when subfolder has no README."""
|
|
333
333
|
project_root = test_project_with_pyproject
|
|
334
334
|
subfolder = project_root / "subfolder"
|
|
335
|
-
|
|
335
|
+
|
|
336
336
|
# Ensure no README exists
|
|
337
337
|
assert not (subfolder / "README.md").exists()
|
|
338
338
|
assert not (subfolder / "README.rst").exists()
|
|
339
|
-
|
|
339
|
+
|
|
340
340
|
config = SubfolderBuildConfig(
|
|
341
341
|
project_root=project_root,
|
|
342
342
|
src_dir=subfolder,
|
|
343
343
|
version="1.0.0",
|
|
344
344
|
)
|
|
345
|
-
|
|
345
|
+
|
|
346
346
|
try:
|
|
347
347
|
config.create_temp_pyproject()
|
|
348
|
-
|
|
348
|
+
|
|
349
349
|
# Check that minimal README was created
|
|
350
350
|
assert (project_root / "README.md").exists()
|
|
351
351
|
content = (project_root / "README.md").read_text()
|
|
352
352
|
assert content.strip() == f"# {subfolder.name}"
|
|
353
353
|
finally:
|
|
354
354
|
config.restore()
|
|
355
|
-
|
|
355
|
+
|
|
356
356
|
# Verify README was removed if it didn't exist before
|
|
357
357
|
if not (project_root / "README.md.backup").exists():
|
|
358
358
|
# No backup means no original README, so temp should be removed
|
|
359
|
-
assert
|
|
360
|
-
|
|
359
|
+
assert (
|
|
360
|
+
not (project_root / "README.md").exists()
|
|
361
|
+
or (project_root / "README.md").read_text() != f"# {subfolder.name}\n"
|
|
362
|
+
)
|
|
@@ -45,7 +45,9 @@ class TestFindProjectRoot:
|
|
|
45
45
|
|
|
46
46
|
assert found is None
|
|
47
47
|
|
|
48
|
-
def test_find_project_root_defaults_to_cwd(
|
|
48
|
+
def test_find_project_root_defaults_to_cwd(
|
|
49
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
50
|
+
) -> None:
|
|
49
51
|
"""Test that find_project_root defaults to current directory."""
|
|
50
52
|
project_root = tmp_path / "project"
|
|
51
53
|
project_root.mkdir()
|
|
@@ -156,4 +158,3 @@ class TestIsPythonPackageDirectory:
|
|
|
156
158
|
pkg_dir.mkdir()
|
|
157
159
|
|
|
158
160
|
assert is_python_package_directory(pkg_dir) is False
|
|
159
|
-
|
|
@@ -70,7 +70,7 @@ class TestVersionManager:
|
|
|
70
70
|
# Check version was set in file
|
|
71
71
|
content = (test_pyproject / "pyproject.toml").read_text()
|
|
72
72
|
assert '"2.0.0"' in content or "'2.0.0'" in content
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
# Check version can be read back (may need to re-read)
|
|
75
75
|
# The set_version modifies the file, so get_current_version should work
|
|
76
76
|
content_after = (test_pyproject / "pyproject.toml").read_text()
|
|
@@ -129,9 +129,7 @@ class TestVersionManager:
|
|
|
129
129
|
assert "[tool.hatch.version]" in content
|
|
130
130
|
assert "[tool.uv-dynamic-versioning]" in content
|
|
131
131
|
|
|
132
|
-
def test_restore_dynamic_versioning_with_existing_version(
|
|
133
|
-
self, test_pyproject: Path
|
|
134
|
-
) -> None:
|
|
132
|
+
def test_restore_dynamic_versioning_with_existing_version(self, test_pyproject: Path) -> None:
|
|
135
133
|
"""Test restoring dynamic versioning when static version exists."""
|
|
136
134
|
manager = VersionManager(test_pyproject)
|
|
137
135
|
|
|
@@ -145,4 +143,3 @@ class TestVersionManager:
|
|
|
145
143
|
content = (test_pyproject / "pyproject.toml").read_text()
|
|
146
144
|
# Version should be removed or dynamic should be added
|
|
147
145
|
assert "[tool.hatch.version]" in content or 'dynamic = ["version"]' in content
|
|
148
|
-
|
|
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-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/__init__.py
RENAMED
|
File without changes
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/__main__.py
RENAMED
|
File without changes
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/analyzer.py
RENAMED
|
File without changes
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/py.typed
RENAMED
|
File without changes
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/src/python_package_folder/types.py
RENAMED
|
File without changes
|
{python_package_folder-1.1.1 → python_package_folder-1.1.3}/tests/folder_structure/some_globals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|