Skip to content

Commit aa26c76

Browse files
committed
Refactor temporary package directory handling in SubfolderBuildConfig
This commit updates the SubfolderBuildConfig class to create a temporary package directory using the import name directly, rather than a prefixed temporary name. It enhances the logic for checking the existence of the source directory and ensures that the correct pyproject.toml is utilized from subfolders. Additionally, tests are adjusted to verify that the import name is used in package configurations, improving the accuracy of the build process.
1 parent 55e5558 commit aa26c76

File tree

2 files changed

+281
-92
lines changed

2 files changed

+281
-92
lines changed

src/python_package_folder/subfolder_build.py

Lines changed: 144 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ def _create_temp_package_directory(self) -> None:
130130
The package name (with hyphens) is converted to the import name (with underscores).
131131
For example: 'ml-drawing-assistant-data' -> 'ml_drawing_assistant_data'
132132
133-
The temporary directory is created in the project root and contains a copy
134-
of the source directory contents.
133+
The temporary directory is created in the project root with the import name directly.
134+
This way, hatchling will install it with the correct name without needing force-include.
135135
"""
136136
if not self.package_name:
137137
return
@@ -140,20 +140,42 @@ def _create_temp_package_directory(self) -> None:
140140
# PyPI package names use hyphens, but Python import names use underscores
141141
import_name = self.package_name.replace("-", "_")
142142

143-
# Create temporary directory name
144-
temp_dir_name = f".temp_package_{import_name}"
145-
temp_package_dir = self.project_root / temp_dir_name
143+
# Create temporary directory with the import name directly
144+
# This way, hatchling will install it with the correct name
145+
import_name_dir = self.project_root / import_name
146146

147-
# Remove if it already exists (from a previous failed build)
148-
if temp_package_dir.exists():
149-
shutil.rmtree(temp_package_dir)
147+
# Check if the directory already exists and is the correct one
148+
if import_name_dir.exists() and import_name_dir == self._temp_package_dir:
149+
# Directory already exists and is the correct one, no need to recreate
150+
return
151+
152+
# Remove if it already exists (from a previous build)
153+
if import_name_dir.exists():
154+
shutil.rmtree(import_name_dir)
155+
156+
# Copy the entire source directory contents directly to the import name directory
157+
# Check if src_dir exists and is a directory before copying
158+
if not self.src_dir.exists():
159+
print(
160+
f"Warning: Source directory does not exist: {self.src_dir}",
161+
file=sys.stderr,
162+
)
163+
self._temp_package_dir = None
164+
return
165+
166+
if not self.src_dir.is_dir():
167+
print(
168+
f"Warning: Source path is not a directory: {self.src_dir}",
169+
file=sys.stderr,
170+
)
171+
self._temp_package_dir = None
172+
return
150173

151-
# Copy the entire source directory contents to the temporary directory
152174
try:
153-
shutil.copytree(self.src_dir, temp_package_dir)
154-
self._temp_package_dir = temp_package_dir
175+
shutil.copytree(self.src_dir, import_name_dir)
176+
self._temp_package_dir = import_name_dir
155177
print(
156-
f"Created temporary package directory: {temp_package_dir} "
178+
f"Created temporary package directory: {import_name_dir} "
157179
f"(import name: {import_name})"
158180
)
159181
except Exception as e:
@@ -344,6 +366,116 @@ def create_temp_pyproject(self) -> Path | None:
344366
if not self.version:
345367
raise ValueError("Version is required for subfolder builds")
346368

369+
# Check if pyproject.toml exists in subfolder FIRST
370+
# This allows us to handle subfolder pyproject.toml even when parent doesn't exist
371+
# But first ensure src_dir exists
372+
if not self.src_dir.exists() or not self.src_dir.is_dir():
373+
# If src_dir doesn't exist, we can't proceed
374+
print(
375+
f"Warning: Source directory does not exist or is not a directory: {self.src_dir}",
376+
file=sys.stderr,
377+
)
378+
return None
379+
380+
subfolder_pyproject = self.src_dir / "pyproject.toml"
381+
if subfolder_pyproject.exists() and subfolder_pyproject.is_file():
382+
# Read the subfolder pyproject.toml content IMMEDIATELY after checking it exists
383+
# This prevents any issues if the file is affected by subsequent operations
384+
try:
385+
subfolder_content = subfolder_pyproject.read_text(encoding="utf-8")
386+
except (FileNotFoundError, OSError) as e:
387+
# File was deleted or inaccessible between check and read
388+
print(
389+
f"Warning: Could not read subfolder pyproject.toml at {subfolder_pyproject}: {e}. "
390+
"Falling back to creating from parent.",
391+
file=sys.stderr,
392+
)
393+
subfolder_content = None
394+
395+
if subfolder_content is not None:
396+
# Ensure src_dir is a package (has __init__.py) before creating temp directory
397+
# This way the __init__.py will be copied to the temp directory
398+
init_file = self.src_dir / "__init__.py"
399+
if not init_file.exists():
400+
# Create a temporary __init__.py to make it a package
401+
init_file.write_text("# Temporary __init__.py for build\n", encoding="utf-8")
402+
self._temp_init_created = True
403+
else:
404+
self._temp_init_created = False
405+
406+
# Create temporary package directory with correct import name
407+
# This will copy the __init__.py we just created (if any)
408+
self._create_temp_package_directory()
409+
410+
# Determine which directory to use (temp package dir or src_dir)
411+
package_dir = self._temp_package_dir if self._temp_package_dir and self._temp_package_dir.exists() else self.src_dir
412+
# Use the subfolder's pyproject.toml
413+
print(f"Using existing pyproject.toml from subfolder: {subfolder_pyproject}")
414+
self._used_subfolder_pyproject = True
415+
416+
# Store reference to original project root pyproject.toml
417+
original_pyproject = self.project_root / "pyproject.toml"
418+
self.original_pyproject_path = original_pyproject
419+
420+
# Create temporary pyproject.toml file
421+
temp_pyproject_path = self.project_root / "pyproject.toml.temp"
422+
423+
# Adjust packages path to be relative to project root
424+
adjusted_content = self._adjust_subfolder_pyproject_packages_path(subfolder_content)
425+
426+
# Read exclude patterns from root pyproject.toml and inject them (if it exists)
427+
exclude_patterns = []
428+
if original_pyproject.exists():
429+
exclude_patterns = read_exclude_patterns(original_pyproject)
430+
print(
431+
f"INFO: Read exclude patterns from {original_pyproject}: {exclude_patterns}",
432+
file=sys.stderr,
433+
)
434+
else:
435+
print(
436+
f"INFO: No parent pyproject.toml found at {original_pyproject}, skipping exclude patterns",
437+
file=sys.stderr,
438+
)
439+
if exclude_patterns:
440+
adjusted_content = self._inject_exclude_patterns(adjusted_content, exclude_patterns)
441+
442+
# Write adjusted content to temporary file
443+
temp_pyproject_path.write_text(adjusted_content, encoding="utf-8")
444+
self.temp_pyproject = temp_pyproject_path
445+
446+
# Print the temporary pyproject.toml content for debugging
447+
print("\n" + "=" * 80)
448+
print("Temporary pyproject.toml content (from subfolder pyproject.toml):")
449+
print("=" * 80)
450+
print(adjusted_content)
451+
print("=" * 80 + "\n")
452+
453+
# If original pyproject.toml exists, temporarily move it
454+
if original_pyproject.exists():
455+
backup_path = self.project_root / "pyproject.toml.original"
456+
# Remove backup if it already exists (from previous failed test or run)
457+
if backup_path.exists():
458+
backup_path.unlink()
459+
original_pyproject.rename(backup_path)
460+
self.original_pyproject_backup = backup_path
461+
462+
# Move temp file to pyproject.toml for the build
463+
temp_pyproject_path.rename(original_pyproject)
464+
self.temp_pyproject = original_pyproject
465+
466+
# Handle README file
467+
self._handle_readme()
468+
469+
# Exclude files matching exclude patterns
470+
if exclude_patterns:
471+
self._exclude_files_by_patterns(exclude_patterns)
472+
473+
return original_pyproject
474+
475+
# No pyproject.toml in subfolder, create one from parent
476+
self._used_subfolder_pyproject = False
477+
print("No pyproject.toml found in subfolder, creating temporary one from parent")
478+
347479
# Ensure src_dir is a package (has __init__.py) before creating temp directory
348480
# This way the __init__.py will be copied to the temp directory
349481
init_file = self.src_dir / "__init__.py"
@@ -361,71 +493,6 @@ def create_temp_pyproject(self) -> Path | None:
361493
# Determine which directory to use (temp package dir or src_dir)
362494
package_dir = self._temp_package_dir if self._temp_package_dir and self._temp_package_dir.exists() else self.src_dir
363495

364-
# Check if pyproject.toml exists in subfolder
365-
subfolder_pyproject = self.src_dir / "pyproject.toml"
366-
if subfolder_pyproject.exists():
367-
# Use the subfolder's pyproject.toml
368-
print(f"Using existing pyproject.toml from subfolder: {subfolder_pyproject}")
369-
self._used_subfolder_pyproject = True
370-
371-
# Store reference to original project root pyproject.toml
372-
original_pyproject = self.project_root / "pyproject.toml"
373-
self.original_pyproject_path = original_pyproject
374-
375-
# Create temporary pyproject.toml file
376-
temp_pyproject_path = self.project_root / "pyproject.toml.temp"
377-
378-
# Read and adjust the subfolder pyproject.toml
379-
subfolder_content = subfolder_pyproject.read_text(encoding="utf-8")
380-
# Adjust packages path to be relative to project root
381-
adjusted_content = self._adjust_subfolder_pyproject_packages_path(subfolder_content)
382-
383-
# Read exclude patterns from root pyproject.toml and inject them
384-
exclude_patterns = read_exclude_patterns(original_pyproject)
385-
print(
386-
f"INFO: Read exclude patterns from {original_pyproject}: {exclude_patterns}",
387-
file=sys.stderr,
388-
)
389-
if exclude_patterns:
390-
adjusted_content = self._inject_exclude_patterns(adjusted_content, exclude_patterns)
391-
392-
# Write adjusted content to temporary file
393-
temp_pyproject_path.write_text(adjusted_content, encoding="utf-8")
394-
self.temp_pyproject = temp_pyproject_path
395-
396-
# Print the temporary pyproject.toml content for debugging
397-
print("\n" + "=" * 80)
398-
print("Temporary pyproject.toml content (from subfolder pyproject.toml):")
399-
print("=" * 80)
400-
print(adjusted_content)
401-
print("=" * 80 + "\n")
402-
403-
# If original pyproject.toml exists, temporarily move it
404-
if original_pyproject.exists():
405-
backup_path = self.project_root / "pyproject.toml.original"
406-
# Remove backup if it already exists (from previous failed test or run)
407-
if backup_path.exists():
408-
backup_path.unlink()
409-
original_pyproject.rename(backup_path)
410-
self.original_pyproject_backup = backup_path
411-
412-
# Move temp file to pyproject.toml for the build
413-
temp_pyproject_path.rename(original_pyproject)
414-
self.temp_pyproject = original_pyproject
415-
416-
# Handle README file
417-
self._handle_readme()
418-
419-
# Exclude files matching exclude patterns
420-
if exclude_patterns:
421-
self._exclude_files_by_patterns(exclude_patterns)
422-
423-
return original_pyproject
424-
425-
# No pyproject.toml in subfolder, create one from parent
426-
self._used_subfolder_pyproject = False
427-
print("No pyproject.toml found in subfolder, creating temporary one from parent")
428-
429496
# Read the original pyproject.toml
430497
original_pyproject = self.project_root / "pyproject.toml"
431498
if not original_pyproject.exists():

0 commit comments

Comments
 (0)