python-package-folder 1.1.3__py3-none-any.whl → 1.2.0__py3-none-any.whl

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.
@@ -85,15 +85,13 @@ class BuildManager:
85
85
  self.finder = ExternalDependencyFinder(
86
86
  self.project_root, self.src_dir, exclude_patterns=exclude_patterns
87
87
  )
88
+ self.subfolder_config: SubfolderBuildConfig | None = None
88
89
 
89
90
  # Check if it's a valid Python package directory
90
91
  if not any(self.src_dir.glob("*.py")) and not (self.src_dir / "__init__.py").exists():
91
92
  # Allow empty directories for now, but warn
92
93
  pass
93
94
 
94
- self.copied_files: list[Path] = []
95
- self.copied_dirs: list[Path] = []
96
-
97
95
  def find_src_package_dir(self) -> Path | None:
98
96
  """
99
97
  Find the main package directory within src/.
@@ -125,6 +123,21 @@ class BuildManager:
125
123
  # Return the first one or src_dir itself
126
124
  return package_dirs[0] if package_dirs else self.src_dir
127
125
 
126
+ def _is_subfolder_build(self) -> bool:
127
+ """
128
+ Check if we're building a subfolder (not the main src/ directory).
129
+
130
+ Returns:
131
+ True if this is a subfolder build, False otherwise
132
+ """
133
+ # Check if src_dir is not the main src/ directory
134
+ main_src = self.project_root / "src"
135
+ return (
136
+ self.src_dir != main_src
137
+ and self.src_dir != self.project_root
138
+ and self.src_dir.is_relative_to(self.project_root)
139
+ )
140
+
128
141
  def _get_project_name(self) -> str | None:
129
142
  """
130
143
  Get the project name from pyproject.toml.
@@ -155,19 +168,94 @@ class BuildManager:
155
168
 
156
169
  return None
157
170
 
158
- def prepare_build(self) -> list[ExternalDependency]:
171
+ def prepare_build(
172
+ self,
173
+ version: str | None = None,
174
+ package_name: str | None = None,
175
+ dependency_group: str | None = None,
176
+ ) -> list[ExternalDependency]:
159
177
  """
160
178
  Prepare for build by finding and copying external dependencies.
161
179
 
162
- This method:
163
- 1. Finds all Python files in the source directory
164
- 2. Analyzes them for external dependencies
165
- 3. Copies external files/directories into the source directory
166
- 4. Reports any ambiguous imports
180
+ This method automatically detects if you're building a subfolder (not the main src/
181
+ directory) and sets up pyproject.toml appropriately:
182
+ - If pyproject.toml exists in the subfolder, it will be used
183
+ - Otherwise, creates a temporary pyproject.toml with the correct package configuration
184
+ For subfolder builds without their own pyproject.toml, if no version is provided,
185
+ it defaults to "0.0.0" with a warning.
186
+
187
+ Process:
188
+ 1. Detects if this is a subfolder build and sets up pyproject.toml if needed:
189
+ - If pyproject.toml exists in subfolder: uses that file
190
+ - If no pyproject.toml in subfolder: creates temporary one from parent
191
+ 2. Finds all Python files in the source directory
192
+ 3. Analyzes them for external dependencies
193
+ 4. Copies external files/directories into the source directory
194
+ 5. Reports any ambiguous imports
195
+
196
+ Args:
197
+ version: Version for subfolder builds. If building a subfolder and version is None,
198
+ defaults to "0.0.0" with a warning. Only used when creating temporary pyproject.toml
199
+ (ignored if subfolder has its own pyproject.toml). For regular builds, this parameter
200
+ is ignored.
201
+ package_name: Package name for subfolder builds. If None, derived from src_dir name
202
+ (e.g., "empty_drawing_detection" -> "empty-drawing-detection"). Only used when
203
+ creating temporary pyproject.toml (ignored if subfolder has its own pyproject.toml).
204
+ Ignored for regular builds.
205
+ dependency_group: Name of dependency group to copy from parent pyproject.toml.
206
+ Only used for subfolder builds when creating temporary pyproject.toml.
167
207
 
168
208
  Returns:
169
209
  List of ExternalDependency objects that were copied
210
+
211
+ Example:
212
+ ```python
213
+ # Regular build (main src/ directory)
214
+ manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
215
+ deps = manager.prepare_build() # No version needed
216
+
217
+ # Subfolder build (automatic detection)
218
+ manager = BuildManager(
219
+ project_root=Path("."),
220
+ src_dir=Path("src/integration/empty_drawing_detection")
221
+ )
222
+ # Version defaults to "0.0.0" if not provided
223
+ deps = manager.prepare_build(version="1.0.0", package_name="my-package")
224
+ ```
170
225
  """
226
+ # Check if this is a subfolder build and set up config if needed
227
+ if self._is_subfolder_build():
228
+ # For subfolder builds, we need a version
229
+ # If not provided, use a default version
230
+ if not version:
231
+ version = "0.0.0"
232
+ print(
233
+ f"Warning: No version specified for subfolder build. Using default version '{version}'",
234
+ file=sys.stderr,
235
+ )
236
+
237
+ if not package_name:
238
+ # Derive package name from subfolder
239
+ package_name = (
240
+ self.src_dir.name.replace("_", "-").replace(" ", "-").lower().strip("-")
241
+ )
242
+
243
+ print(
244
+ f"Detected subfolder build. Setting up package '{package_name}' version '{version}'..."
245
+ )
246
+ self.subfolder_config = SubfolderBuildConfig(
247
+ project_root=self.project_root,
248
+ src_dir=self.src_dir,
249
+ package_name=package_name,
250
+ version=version,
251
+ dependency_group=dependency_group,
252
+ )
253
+ temp_pyproject = self.subfolder_config.create_temp_pyproject()
254
+ # If temp_pyproject is None, it means no parent pyproject.toml exists
255
+ # This is acceptable for tests or dependency-only operations
256
+ if temp_pyproject is None:
257
+ self.subfolder_config = None
258
+
171
259
  analyzer = ImportAnalyzer(self.project_root)
172
260
 
173
261
  # Find all Python files in src/
@@ -325,10 +413,30 @@ class BuildManager:
325
413
  """
326
414
  Remove all copied files and directories.
327
415
 
328
- This method removes all files and directories that were copied
329
- during prepare_build(). It handles errors gracefully and clears
330
- the internal tracking lists.
416
+ This method removes all files and directories that were copied during prepare_build().
417
+ It also restores the original pyproject.toml if a temporary one was created for a
418
+ subfolder build. It handles errors gracefully and clears the internal tracking lists.
419
+
420
+ This is automatically called by run_build(), but you can call it manually if you
421
+ use prepare_build() directly.
422
+
423
+ Example:
424
+ ```python
425
+ manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
426
+ deps = manager.prepare_build()
427
+ # ... do your build ...
428
+ manager.cleanup() # Restores pyproject.toml and removes copied files
429
+ ```
331
430
  """
431
+ # Restore subfolder config if it was created
432
+ if self.subfolder_config:
433
+ try:
434
+ self.subfolder_config.restore()
435
+ print("Restored original pyproject.toml")
436
+ except Exception as e:
437
+ print(f"Warning: Could not restore pyproject.toml: {e}", file=sys.stderr)
438
+ self.subfolder_config = None
439
+
332
440
  # Remove copied directories first (they may contain files)
333
441
  for dir_path in reversed(self.copied_dirs):
334
442
  if dir_path.exists():
@@ -350,29 +458,59 @@ class BuildManager:
350
458
  self.copied_files.clear()
351
459
  self.copied_dirs.clear()
352
460
 
353
- def run_build(self, build_command: Callable[[], None]) -> None:
461
+ def run_build(
462
+ self,
463
+ build_command: Callable[[], None],
464
+ version: str | None = None,
465
+ package_name: str | None = None,
466
+ dependency_group: str | None = None,
467
+ ) -> None:
354
468
  """
355
469
  Run the build process with dependency management.
356
470
 
357
- This is a convenience method that:
358
- 1. Calls prepare_build() to find and copy dependencies
471
+ This is a convenience method that automatically handles the full build lifecycle:
472
+ 1. Calls prepare_build() to find and copy dependencies (with automatic subfolder detection)
359
473
  2. Executes the provided build_command
360
474
  3. Always calls cleanup() afterward, even if build fails
361
475
 
476
+ For subfolder builds, this method automatically detects the subfolder and creates a
477
+ temporary pyproject.toml with the correct package configuration. The build command
478
+ should be runnable from the project root (e.g., "uv build", "python -m build").
479
+
362
480
  Args:
363
- build_command: Callable that executes the build process
481
+ build_command: Callable that executes the build process. Should run from project root.
482
+ version: Version for subfolder builds. If building a subfolder and version is None,
483
+ defaults to "0.0.0" with a warning. Ignored for regular builds.
484
+ package_name: Package name for subfolder builds. If None, derived from src_dir name.
485
+ Ignored for regular builds.
486
+ dependency_group: Name of dependency group to copy from parent pyproject.toml.
487
+ Only used for subfolder builds.
364
488
 
365
489
  Example:
366
490
  ```python
491
+ from pathlib import Path
492
+ from python_package_folder import BuildManager
493
+ import subprocess
494
+
495
+ # Regular build
496
+ manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
367
497
  def build():
368
498
  subprocess.run(["uv", "build"], check=True)
369
-
370
499
  manager.run_build(build)
500
+
501
+ # Subfolder build (automatic detection)
502
+ manager = BuildManager(
503
+ project_root=Path("."),
504
+ src_dir=Path("src/integration/empty_drawing_detection")
505
+ )
506
+ manager.run_build(build, version="1.0.0")
371
507
  ```
372
508
  """
373
509
  try:
374
510
  print("Analyzing project for external dependencies...")
375
- external_deps = self.prepare_build()
511
+ external_deps = self.prepare_build(
512
+ version=version, package_name=package_name, dependency_group=dependency_group
513
+ )
376
514
 
377
515
  if external_deps:
378
516
  print(f"\nFound {len(external_deps)} external dependencies")
@@ -444,49 +582,37 @@ class BuildManager:
444
582
 
445
583
  version_manager = None
446
584
  original_version = None
447
- subfolder_config = None
448
-
449
- # Check if we're building a subfolder (not the main src/ directory)
450
- is_subfolder_build = not self.src_dir.is_relative_to(self.project_root / "src") or (
451
- self.src_dir != self.project_root / "src" and self.src_dir != self.project_root
452
- )
453
585
 
454
586
  try:
455
- # For subfolder builds, create a temporary pyproject.toml
456
- if is_subfolder_build and version:
457
- if not package_name:
458
- # Derive package name from subfolder
459
- package_name = (
460
- self.src_dir.name.replace("_", "-").replace(" ", "-").lower().strip("-")
461
- )
462
- print(f"Building subfolder as package '{package_name}' version '{version}'...")
463
- subfolder_config = SubfolderBuildConfig(
464
- project_root=self.project_root,
465
- src_dir=self.src_dir,
466
- package_name=package_name,
467
- version=version,
468
- dependency_group=dependency_group,
469
- )
470
- subfolder_config.create_temp_pyproject()
471
- elif version:
472
- # Regular build with version override
587
+ # For non-subfolder builds with version, use VersionManager
588
+ if version and not self._is_subfolder_build():
473
589
  version_manager = VersionManager(self.project_root)
474
590
  original_version = version_manager.get_current_version()
475
591
  print(f"Setting version to {version}...")
476
592
  version_manager.set_version(version)
477
593
 
478
- # Build the package
479
- self.run_build(build_command)
594
+ # Build the package (prepare_build will handle subfolder config if needed)
595
+ self.run_build(
596
+ build_command,
597
+ version=version,
598
+ package_name=package_name,
599
+ dependency_group=dependency_group,
600
+ )
480
601
 
481
602
  # Publish if repository is specified
482
603
  if repository:
483
604
  # Determine package name and version for filtering
484
605
  publish_package_name = None
485
606
  publish_version = version
486
-
487
- if is_subfolder_build and package_name:
488
- publish_package_name = package_name
489
- elif not is_subfolder_build:
607
+ is_subfolder_build = self._is_subfolder_build()
608
+
609
+ if is_subfolder_build:
610
+ # Get package name from subfolder_config if it was created, otherwise use provided
611
+ if self.subfolder_config:
612
+ publish_package_name = self.subfolder_config.package_name
613
+ elif package_name:
614
+ publish_package_name = package_name
615
+ else:
490
616
  # For regular builds, get package name from pyproject.toml
491
617
  try:
492
618
  import tomllib
@@ -515,15 +641,7 @@ class BuildManager:
515
641
  )
516
642
  publisher.publish(skip_existing=skip_existing)
517
643
  finally:
518
- # Restore subfolder config if used
519
- if subfolder_config and restore_versioning:
520
- try:
521
- subfolder_config.restore()
522
- print("Restored original pyproject.toml")
523
- except Exception as e:
524
- print(f"Warning: Could not restore pyproject.toml: {e}", file=sys.stderr)
525
-
526
- # Restore versioning if needed
644
+ # Restore versioning if needed (subfolder config is handled by cleanup)
527
645
  if version_manager and restore_versioning:
528
646
  try:
529
647
  if original_version:
@@ -29,8 +29,10 @@ class SubfolderBuildConfig:
29
29
  """
30
30
  Manages temporary build configuration for subfolder builds.
31
31
 
32
- When building a subfolder as a separate package, this class creates
33
- a temporary pyproject.toml with the appropriate package name and version.
32
+ When building a subfolder as a separate package, this class:
33
+ - Uses the subfolder's pyproject.toml if it exists
34
+ - Otherwise creates a temporary pyproject.toml with the appropriate package name and version
35
+ - Handles README files similarly (uses subfolder README if present)
34
36
  """
35
37
 
36
38
  def __init__(
@@ -47,9 +49,12 @@ class SubfolderBuildConfig:
47
49
  Args:
48
50
  project_root: Root directory containing the main pyproject.toml
49
51
  src_dir: Source directory being built (subfolder)
50
- package_name: Name for the subfolder package (default: derived from src_dir name)
51
- version: Version for the subfolder package (required if building subfolder)
52
- dependency_group: Name of dependency group to copy from parent pyproject.toml
52
+ package_name: Name for the subfolder package (default: derived from src_dir name).
53
+ Only used if subfolder doesn't have its own pyproject.toml.
54
+ version: Version for the subfolder package (required if building subfolder).
55
+ Only used if subfolder doesn't have its own pyproject.toml.
56
+ dependency_group: Name of dependency group to copy from parent pyproject.toml.
57
+ Only used if subfolder doesn't have its own pyproject.toml.
53
58
  """
54
59
  self.project_root = project_root.resolve()
55
60
  self.src_dir = src_dir.resolve()
@@ -61,6 +66,7 @@ class SubfolderBuildConfig:
61
66
  self._temp_init_created = False
62
67
  self.temp_readme: Path | None = None
63
68
  self.original_readme_backup: Path | None = None
69
+ self._used_subfolder_pyproject = False
64
70
 
65
71
  def _derive_package_name(self) -> str:
66
72
  """Derive package name from source directory name."""
@@ -105,15 +111,17 @@ class SubfolderBuildConfig:
105
111
  # If it's a package or has subpackages, return the path
106
112
  return packages_path, [packages_path] if packages_path else []
107
113
 
108
- def create_temp_pyproject(self) -> Path:
114
+ def create_temp_pyproject(self) -> Path | None:
109
115
  """
110
116
  Create a temporary pyproject.toml for the subfolder build.
111
117
 
112
- This creates a pyproject.toml in the project root that overrides
113
- the package name and version for building the subfolder.
118
+ If a pyproject.toml exists in the subfolder, it will be used instead of creating
119
+ a new one. Otherwise, creates a pyproject.toml in the project root based on the
120
+ parent pyproject.toml with the appropriate package name and version.
114
121
 
115
122
  Returns:
116
- Path to the temporary pyproject.toml file
123
+ Path to the pyproject.toml file (either from subfolder or created temporary),
124
+ or None if no parent pyproject.toml exists (in which case subfolder config is skipped)
117
125
  """
118
126
  if not self.version:
119
127
  raise ValueError("Version is required for subfolder builds")
@@ -127,10 +135,47 @@ class SubfolderBuildConfig:
127
135
  else:
128
136
  self._temp_init_created = False
129
137
 
138
+ # Check if pyproject.toml exists in subfolder
139
+ subfolder_pyproject = self.src_dir / "pyproject.toml"
140
+ if subfolder_pyproject.exists():
141
+ # Use the subfolder's pyproject.toml
142
+ print(f"Using existing pyproject.toml from subfolder: {subfolder_pyproject}")
143
+ self._used_subfolder_pyproject = True
144
+
145
+ # Backup the original project root pyproject.toml if it exists
146
+ original_pyproject = self.project_root / "pyproject.toml"
147
+ if original_pyproject.exists():
148
+ backup_path = self.project_root / "pyproject.toml.backup"
149
+ shutil.copy2(original_pyproject, backup_path)
150
+ self.original_pyproject_backup = backup_path
151
+
152
+ # Copy subfolder pyproject.toml to project root
153
+ shutil.copy2(subfolder_pyproject, original_pyproject)
154
+ self.temp_pyproject = original_pyproject
155
+
156
+ # Handle README file
157
+ self._handle_readme()
158
+
159
+ return original_pyproject
160
+
161
+ # No pyproject.toml in subfolder, create one from parent
162
+ self._used_subfolder_pyproject = False
163
+ print("No pyproject.toml found in subfolder, creating temporary one from parent")
164
+
130
165
  # Read the original pyproject.toml
131
166
  original_pyproject = self.project_root / "pyproject.toml"
132
167
  if not original_pyproject.exists():
133
- raise FileNotFoundError(f"pyproject.toml not found: {original_pyproject}")
168
+ # If no parent pyproject.toml exists, we can't create a temporary one
169
+ # This is acceptable for tests or cases where only dependency copying is needed
170
+ print(
171
+ f"Warning: No pyproject.toml found in project root ({original_pyproject}). "
172
+ "Skipping subfolder build configuration. Only dependency copying will be performed.",
173
+ file=sys.stderr,
174
+ )
175
+ # Still handle README file
176
+ self._handle_readme()
177
+ # Return None to indicate no pyproject.toml was created
178
+ return None
134
179
 
135
180
  original_content = original_pyproject.read_text(encoding="utf-8")
136
181
 
@@ -419,7 +464,13 @@ class SubfolderBuildConfig:
419
464
  self.temp_readme = target_readme
420
465
 
421
466
  def restore(self) -> None:
422
- """Restore the original pyproject.toml and remove temporary __init__.py if created."""
467
+ """
468
+ Restore the original pyproject.toml and remove temporary __init__.py if created.
469
+
470
+ If a subfolder pyproject.toml was used, it restores the original project root
471
+ pyproject.toml from backup. If a temporary pyproject.toml was created, it
472
+ restores the original as well.
473
+ """
423
474
  # Remove temporary __init__.py if we created it
424
475
  if self._temp_init_created:
425
476
  init_file = self.src_dir / "__init__.py"
@@ -460,13 +511,18 @@ class SubfolderBuildConfig:
460
511
  pass # Ignore errors during cleanup
461
512
  self.temp_readme = None
462
513
 
463
- # Restore original pyproject.toml
464
- if self.original_pyproject_backup and self.original_pyproject_backup.exists():
514
+ # Restore original pyproject.toml (only if we created/used one)
515
+ if (
516
+ self.temp_pyproject
517
+ and self.original_pyproject_backup
518
+ and self.original_pyproject_backup.exists()
519
+ ):
465
520
  original_pyproject = self.project_root / "pyproject.toml"
466
521
  shutil.copy2(self.original_pyproject_backup, original_pyproject)
467
522
  self.original_pyproject_backup.unlink()
468
523
  self.original_pyproject_backup = None
469
524
  self.temp_pyproject = None
525
+ self._used_subfolder_pyproject = False
470
526
 
471
527
  def __enter__(self) -> Self:
472
528
  """Context manager entry."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 1.1.3
3
+ Version: 1.2.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>
@@ -125,9 +125,11 @@ This package will automatically:
125
125
 
126
126
  ## Features
127
127
 
128
- - **Subfolder Build Support**: Build subfolders as separate packages with automatic project root detection
128
+ - **Subfolder Build Support**: Build subfolders as separate packages with automatic detection and configuration
129
+ - **Automatic subfolder detection**: Detects when building a subfolder (not the main `src/` directory)
129
130
  - Creates any needed file for publishing automatically, cleaning up if not originally in the subfolder after the build/publish process. E.g. copies external dependencies into the source directory before build and cleans them up afterward; temporary `__init__.py` creation for non-package subfolders; uses subfolder README if present, otherwise creates minimal README
130
131
  - Automatic package name derivation from subfolder name
132
+ - Automatic temporary `pyproject.toml` creation with correct package structure
131
133
  - Dependency group selection: specify which dependency group from parent `pyproject.toml` to include.
132
134
 
133
135
  - **Smart Import Classification and analysis**:
@@ -268,16 +270,20 @@ cd my_project/subfolder_to_build
268
270
  python-package-folder --version "1.0.0" --publish pypi
269
271
  ```
270
272
 
271
- When building from a subdirectory, you **must** specify `--version` because subfolders are built as separate packages with their own version.
273
+ The tool **automatically detects** when you're building a subfolder (any directory that's not the main `src/` directory) and sets up the appropriate build configuration.
272
274
 
273
275
  The tool automatically:
276
+ - **Detects subfolder builds**: Automatically identifies when building from a subdirectory
274
277
  - Finds the project root by looking for `pyproject.toml` in parent directories
275
278
  - Uses the current directory as the source directory if it contains Python files
276
279
  - Falls back to `project_root/src` if the current directory isn't suitable
277
- - For subfolder builds: creates a temporary `pyproject.toml` with:
278
- - Package name derived from the subfolder name (or use `--package-name` to override)
279
- - Version from `--version` argument
280
- - Proper package path configuration for hatchling
280
+ - **For subfolder builds**: Handles `pyproject.toml` configuration:
281
+ - **If `pyproject.toml` exists in subfolder**: Uses that file (copies it to project root temporarily)
282
+ - **If no `pyproject.toml` in subfolder**: Creates a temporary `pyproject.toml` with:
283
+ - Package name derived from the subfolder name (e.g., `empty_drawing_detection` → `empty-drawing-detection`)
284
+ - Version from `--version` argument (defaults to `0.0.0` with a warning if not provided)
285
+ - Proper package path configuration for hatchling
286
+ - Dependency groups from parent `pyproject.toml` if specified
281
287
  - Creates temporary `__init__.py` files if needed to make subfolders valid Python packages
282
288
  - **README handling for subfolder builds**:
283
289
  - If a README file (README.md, README.rst, README.txt, or README) exists in the subfolder, it will be used instead of the parent README
@@ -285,6 +291,8 @@ The tool automatically:
285
291
  - Restores the original `pyproject.toml` after build (unless `--no-restore-versioning` is used)
286
292
  - Cleans up temporary `__init__.py` files after build
287
293
 
294
+ **Note**: While version is not strictly required (defaults to `0.0.0`), it's recommended to specify `--version` for subfolder builds to ensure proper versioning.
295
+
288
296
  **Subfolder Build Example:**
289
297
  ```bash
290
298
  # Build a subfolder as a separate package
@@ -293,6 +301,11 @@ python-package-folder --version "0.1.0" --package-name "my-subfolder-package" --
293
301
 
294
302
  # Build with a specific dependency group from parent pyproject.toml
295
303
  python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
304
+
305
+ # If subfolder has its own pyproject.toml, it will be used automatically
306
+ # (package-name and version arguments are ignored in this case)
307
+ cd src/integration/my_package # assuming my_package/pyproject.toml exists
308
+ python-package-folder --publish pypi
296
309
  ```
297
310
 
298
311
  **Dependency Groups**: When building a subfolder, you can specify a dependency group from the parent `pyproject.toml` to include in the subfolder's build configuration. This allows subfolders to inherit specific dependencies from the parent project:
@@ -308,6 +321,8 @@ The specified dependency group will be copied from the parent `pyproject.toml`'s
308
321
 
309
322
  You can also use the package programmatically:
310
323
 
324
+ ### Basic Usage
325
+
311
326
  ```python
312
327
  from pathlib import Path
313
328
  from python_package_folder import BuildManager
@@ -328,11 +343,11 @@ for dep in external_deps:
328
343
  # Run your build process here
329
344
  # ...
330
345
 
331
- # Cleanup copied files
346
+ # Cleanup copied files (also restores pyproject.toml if subfolder build)
332
347
  manager.cleanup()
333
348
  ```
334
349
 
335
- Or use the convenience method:
350
+ ### Using the Convenience Method
336
351
 
337
352
  ```python
338
353
  from pathlib import Path
@@ -348,6 +363,55 @@ def build_command():
348
363
  manager.run_build(build_command)
349
364
  ```
350
365
 
366
+ ### Subfolder Builds (Automatic Detection)
367
+
368
+ The tool automatically detects when you're building a subfolder and sets up the appropriate configuration:
369
+
370
+ ```python
371
+ from pathlib import Path
372
+ from python_package_folder import BuildManager
373
+ import subprocess
374
+
375
+ # Building a subfolder - automatic detection!
376
+ manager = BuildManager(
377
+ project_root=Path("."),
378
+ src_dir=Path("src/integration/empty_drawing_detection")
379
+ )
380
+
381
+ def build_command():
382
+ subprocess.run(["uv", "build"], check=True)
383
+
384
+ # prepare_build() automatically:
385
+ # - Detects this is a subfolder build
386
+ # - If pyproject.toml exists in subfolder: uses that file
387
+ # - If no pyproject.toml in subfolder: creates temporary one with package name "empty-drawing-detection"
388
+ # - Uses version "0.0.0" (or pass version="1.0.0" to override) if creating temporary pyproject.toml
389
+ external_deps = manager.prepare_build(version="1.0.0")
390
+
391
+ # Run build - uses the pyproject.toml (either from subfolder or temporary)
392
+ build_command()
393
+
394
+ # Cleanup restores original pyproject.toml and removes copied files
395
+ manager.cleanup()
396
+ ```
397
+
398
+ **Note**: If the subfolder has its own `pyproject.toml`, it will be used automatically. The `version` and `package_name` parameters are only used when creating a temporary `pyproject.toml` from the parent configuration.
399
+
400
+ Or use the convenience method:
401
+
402
+ ```python
403
+ manager = BuildManager(
404
+ project_root=Path("."),
405
+ src_dir=Path("src/integration/empty_drawing_detection")
406
+ )
407
+
408
+ def build_command():
409
+ subprocess.run(["uv", "build"], check=True)
410
+
411
+ # All handled automatically: subfolder detection, pyproject.toml setup, build, cleanup
412
+ manager.run_build(build_command, version="1.0.0", package_name="my-custom-name")
413
+ ```
414
+
351
415
  ## Working with sysappend
352
416
 
353
417
  This package works well with projects using [sysappend](https://pypi.org/project/sysappend/) for flexible import management. When you have imports like:
@@ -393,20 +457,28 @@ The `--version` option:
393
457
 
394
458
  ### Subfolder Versioning
395
459
 
396
- When building from a subdirectory (not the main `src/` directory), you **must** specify `--version`:
460
+ When building from a subdirectory (not the main `src/` directory), the tool automatically detects the subfolder and sets up the build configuration:
397
461
 
398
462
  ```bash
399
- # Build a subfolder as a separate package
463
+ # Build a subfolder as a separate package (version recommended but not required)
400
464
  cd my_project/subfolder_to_build
401
465
  python-package-folder --version "1.0.0" --publish pypi
402
466
 
403
467
  # With custom package name
404
468
  python-package-folder --version "1.0.0" --package-name "my-custom-name" --publish pypi
469
+
470
+ # Version defaults to "0.0.0" if not specified (with a warning)
471
+ python-package-folder --publish pypi
405
472
  ```
406
473
 
407
474
  For subfolder builds:
408
- - **Version is required**: The tool will error if `--version` is not provided
409
- - **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`)
475
+ - **Automatic detection**: The tool automatically detects subfolder builds
476
+ - **pyproject.toml handling**:
477
+ - If `pyproject.toml` exists in subfolder: Uses that file (copied to project root temporarily)
478
+ - If no `pyproject.toml` in subfolder: Creates temporary one with correct package structure
479
+ - **Version**: Recommended but not required when creating temporary pyproject.toml. If not provided, defaults to `0.0.0` with a warning. Ignored if subfolder has its own `pyproject.toml`.
480
+ - **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`). Only used when creating temporary pyproject.toml.
481
+ - **Restoration**: Original `pyproject.toml` is restored after build
410
482
  - **Temporary configuration**: Creates a temporary `pyproject.toml` with:
411
483
  - Custom package name (from `--package-name` or derived)
412
484
  - Specified version
@@ -715,7 +787,8 @@ version_manager.restore_dynamic_versioning()
715
787
 
716
788
  ### SubfolderBuildConfig
717
789
 
718
- Manages temporary build configuration for subfolder builds.
790
+ Manages temporary build configuration for subfolder builds. If a `pyproject.toml` exists
791
+ in the subfolder, it will be used instead of creating a new one.
719
792
 
720
793
  ```python
721
794
  from python_package_folder import SubfolderBuildConfig
@@ -724,11 +797,11 @@ from pathlib import Path
724
797
  config = SubfolderBuildConfig(
725
798
  project_root=Path("."),
726
799
  src_dir=Path("subfolder"),
727
- package_name="my-subfolder",
728
- version="1.0.0"
800
+ package_name="my-subfolder", # Only used if subfolder has no pyproject.toml
801
+ version="1.0.0" # Only used if subfolder has no pyproject.toml
729
802
  )
730
803
 
731
- # Create temporary pyproject.toml
804
+ # Create temporary pyproject.toml (or use subfolder's if it exists)
732
805
  config.create_temp_pyproject()
733
806
 
734
807
  # ... build process ...
@@ -738,13 +811,13 @@ config.restore()
738
811
  ```
739
812
 
740
813
  **Methods:**
741
- - `create_temp_pyproject() -> Path`: Create temporary `pyproject.toml` with subfolder-specific configuration
814
+ - `create_temp_pyproject() -> Path`: Use subfolder's `pyproject.toml` if it exists, otherwise create temporary `pyproject.toml` with subfolder-specific configuration
742
815
  - `restore() -> None`: Restore original `pyproject.toml` and clean up temporary files
743
816
 
744
- **Note**: This class automatically creates `__init__.py` files if needed to make subfolders valid Python packages. It also handles README files:
745
- - If a README exists in the subfolder, it will be used instead of the parent README
746
- - If no README exists in the subfolder, a minimal README with just the folder name will be created
747
- - The original parent README is backed up and restored after the build completes
817
+ **Note**: This class automatically:
818
+ - **pyproject.toml handling**: If a `pyproject.toml` exists in the subfolder, it will be used (copied to project root temporarily). Otherwise, creates a temporary one from the parent configuration.
819
+ - **README handling**: If a README exists in the subfolder, it will be used instead of the parent README. If no README exists in the subfolder, a minimal README with just the folder name will be created. The original parent README is backed up and restored after the build completes.
820
+ - **Package initialization**: Creates `__init__.py` files if needed to make subfolders valid Python packages.
748
821
 
749
822
 
750
823
  ## Development
@@ -2,16 +2,16 @@ python_package_folder/__init__.py,sha256=DQt-uldOEKfh0MUqCvKdeNKOnpuOvpb7blYvXMy
2
2
  python_package_folder/__main__.py,sha256=a-__-VLhYw-J7S7CsHdhtEvQr3RiAZxiYDvKhKTgMX4,291
3
3
  python_package_folder/analyzer.py,sha256=Iw5bdg9NahO57L3CZgGYbhU-m2mh0DpQQ-xqIINUfic,10976
4
4
  python_package_folder/finder.py,sha256=_LvJ9xBVKv41UK5sbwbNyKmuYjAOqUbzvZhK7NCYQF8,9130
5
- python_package_folder/manager.py,sha256=VZ2e6ydQUPlDnve-PlqGfa86XksSCCqFDZ46GDwZr2A,20549
5
+ python_package_folder/manager.py,sha256=AlzEqI7q0Q2mVmc_HjIEBmomlT053qqLBMZZ52X7IDQ,26360
6
6
  python_package_folder/publisher.py,sha256=1xa6PuduOXNVTTp4IrJcx4qOskugX2fTeJ9QsLDLuUM,11535
7
7
  python_package_folder/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  python_package_folder/python_package_folder.py,sha256=3ECayVwjXQAnujr2e21xvsuwetcqgRF5Zawt3qgtI9M,8779
9
- python_package_folder/subfolder_build.py,sha256=NIlQRMjBQvBJdxYpwYyOcQWqijPKmgRogsRyY9rElkk,19567
9
+ python_package_folder/subfolder_build.py,sha256=BBiuszOVYUHX3aC4bbvg5_39Wh286e006lhTALU1fqQ,22358
10
10
  python_package_folder/types.py,sha256=3yeSRR5p_3PDKEAaehW_RJ7NwJHexOIeA08bGaT1iSY,2368
11
11
  python_package_folder/utils.py,sha256=hoCNRiTHe_-zjAFztxyqjNEMKiEl7XCkSDl36iaMcjM,2966
12
12
  python_package_folder/version.py,sha256=kIDP6S9trEfs9gj7lBYGxrWm4RPssRla24UtlO9Jkh4,9111
13
- python_package_folder-1.1.3.dist-info/METADATA,sha256=7dD2K5qmYNEBJS8m6i-I5_lrZeAm_jab21rULqHp2mw,28134
14
- python_package_folder-1.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- python_package_folder-1.1.3.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
16
- python_package_folder-1.1.3.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
17
- python_package_folder-1.1.3.dist-info/RECORD,,
13
+ python_package_folder-1.2.0.dist-info/METADATA,sha256=n6EfjNT-NT8RowwHjbdGUhHQm5v5pDS1oFSKnEfJJNU,32164
14
+ python_package_folder-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ python_package_folder-1.2.0.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
16
+ python_package_folder-1.2.0.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
17
+ python_package_folder-1.2.0.dist-info/RECORD,,