python-package-folder 1.4.1__py3-none-any.whl → 2.0.4__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.
@@ -268,6 +268,19 @@ class BuildManager:
268
268
  for dep in external_deps:
269
269
  self._copy_dependency(dep)
270
270
 
271
+ # For subfolder builds, extract third-party dependencies and add to pyproject.toml
272
+ if self._is_subfolder_build() and self.subfolder_config:
273
+ # Re-analyze all Python files (including copied dependencies) to find third-party imports
274
+ all_python_files = analyzer.find_all_python_files(self.src_dir)
275
+ third_party_deps = self._extract_third_party_dependencies(all_python_files, analyzer)
276
+ if third_party_deps:
277
+ print(
278
+ f"Found {len(third_party_deps)} third-party dependencies: {', '.join(third_party_deps)}"
279
+ )
280
+ self.subfolder_config.add_third_party_dependencies(third_party_deps)
281
+ else:
282
+ print("No third-party dependencies found in subfolder code")
283
+
271
284
  # Report ambiguous imports
272
285
  self._report_ambiguous_imports(python_files)
273
286
 
@@ -381,6 +394,145 @@ class BuildManager:
381
394
  elif src_item.is_dir():
382
395
  self._copytree_excluding(src_item, dst_item)
383
396
 
397
+ def _get_package_name_from_import(self, module_name: str) -> str | None:
398
+ """
399
+ Get the actual PyPI package name from an import module name.
400
+
401
+ This handles cases where the import name differs from the package name
402
+ (e.g., 'import fitz' from 'pymupdf' package).
403
+
404
+ Args:
405
+ module_name: The module name from the import statement
406
+
407
+ Returns:
408
+ The actual package name, or None if not found
409
+ """
410
+ root_module = module_name.split(".")[0]
411
+ try:
412
+ # Try Python 3.10+ first (has packages_distributions)
413
+ import importlib.metadata as importlib_metadata
414
+
415
+ # Use packages_distributions() if available (Python 3.10+)
416
+ if hasattr(importlib_metadata, "packages_distributions"):
417
+ packages_map = importlib_metadata.packages_distributions()
418
+ # packages_map is a dict mapping module names to list of distribution names
419
+ if root_module in packages_map:
420
+ # Return the first distribution name (usually there's only one)
421
+ dist_names = packages_map[root_module]
422
+ if dist_names:
423
+ return dist_names[0]
424
+
425
+ # Fallback: search all distributions
426
+ for dist in importlib_metadata.distributions():
427
+ try:
428
+ # Check if this distribution provides the module
429
+ # by looking at its files
430
+ files = dist.files or []
431
+ for file in files:
432
+ file_str = str(file)
433
+ # Check if file is the module itself or in a package directory
434
+ if (
435
+ file.suffix == ".py"
436
+ and (file.stem == root_module or file.stem == "__init__")
437
+ ) or (
438
+ "/" in file_str
439
+ and (
440
+ file_str.startswith(f"{root_module}/")
441
+ or file_str.startswith(f"{root_module.replace('_', '-')}/")
442
+ )
443
+ ):
444
+ return dist.metadata["Name"]
445
+ except Exception:
446
+ continue
447
+
448
+ except ImportError:
449
+ try:
450
+ # Fallback for older Python versions
451
+ import importlib_metadata
452
+
453
+ # Search all distributions
454
+ for dist in importlib_metadata.distributions():
455
+ try:
456
+ files = dist.files or []
457
+ for file in files:
458
+ file_str = str(file)
459
+ if (
460
+ file.suffix == ".py"
461
+ and (file.stem == root_module or file.stem == "__init__")
462
+ ) or (
463
+ "/" in file_str
464
+ and (
465
+ file_str.startswith(f"{root_module}/")
466
+ or file_str.startswith(f"{root_module.replace('_', '-')}/")
467
+ )
468
+ ):
469
+ return dist.metadata["Name"]
470
+ except Exception:
471
+ continue
472
+ except ImportError:
473
+ pass
474
+ except Exception:
475
+ pass
476
+
477
+ return None
478
+
479
+ def _extract_third_party_dependencies(
480
+ self, python_files: list[Path], analyzer: ImportAnalyzer
481
+ ) -> list[str]:
482
+ """
483
+ Extract third-party package dependencies from Python files.
484
+
485
+ Analyzes all Python files to find imports classified as "third_party"
486
+ and returns a list of unique package names. Handles cases where the
487
+ import name differs from the package name (e.g., 'fitz' -> 'pymupdf').
488
+
489
+ Args:
490
+ python_files: List of Python file paths to analyze
491
+ analyzer: ImportAnalyzer instance to use for classification
492
+
493
+ Returns:
494
+ List of unique third-party package names (e.g., ["pypdf", "requests", "pymupdf"])
495
+ """
496
+ third_party_packages: set[str] = set()
497
+
498
+ for file_path in python_files:
499
+ imports = analyzer.extract_imports(file_path)
500
+ for imp in imports:
501
+ analyzer.classify_import(imp, self.src_dir)
502
+
503
+ # Extract the root package name (first part of module name)
504
+ root_module = imp.module_name.split(".")[0]
505
+
506
+ # Skip if it's a standard library module
507
+ stdlib_modules = analyzer.get_stdlib_modules()
508
+ if root_module in stdlib_modules:
509
+ continue
510
+
511
+ # If classified as third_party, try to get actual package name
512
+ if imp.classification == "third_party":
513
+ # Try to get the actual package name from metadata
514
+ actual_package = self._get_package_name_from_import(imp.module_name)
515
+ if actual_package:
516
+ third_party_packages.add(actual_package)
517
+ else:
518
+ # Fallback to using the import name
519
+ third_party_packages.add(root_module)
520
+ # If it's ambiguous or unresolved, and not stdlib/local/external,
521
+ # it's likely a third-party package that needs to be declared
522
+ elif imp.classification == "ambiguous" or imp.classification is None:
523
+ # Check if it's not a local or external module
524
+ if not imp.resolved_path:
525
+ # Try to get the actual package name from metadata
526
+ # (might work if package is installed but module path is unusual)
527
+ actual_package = self._get_package_name_from_import(imp.module_name)
528
+ if actual_package:
529
+ third_party_packages.add(actual_package)
530
+ else:
531
+ # Fallback: use import name (will be normalized later)
532
+ third_party_packages.add(root_module)
533
+
534
+ return sorted(list(third_party_packages))
535
+
384
536
  def _report_ambiguous_imports(self, python_files: list[Path]) -> None:
385
537
  """
386
538
  Report any ambiguous imports that couldn't be resolved.
@@ -254,6 +254,9 @@ class SubfolderBuildConfig:
254
254
  # If original pyproject.toml exists, temporarily move it
255
255
  if original_pyproject.exists():
256
256
  backup_path = self.project_root / "pyproject.toml.original"
257
+ # Remove backup if it already exists (from previous failed test or run)
258
+ if backup_path.exists():
259
+ backup_path.unlink()
257
260
  original_pyproject.rename(backup_path)
258
261
  self.original_pyproject_backup = backup_path
259
262
 
@@ -292,6 +295,9 @@ class SubfolderBuildConfig:
292
295
 
293
296
  # Temporarily move original to backup location
294
297
  backup_path = self.project_root / "pyproject.toml.original"
298
+ # Remove backup if it already exists (from previous failed test or run)
299
+ if backup_path.exists():
300
+ backup_path.unlink()
295
301
  original_pyproject.rename(backup_path)
296
302
  self.original_pyproject_backup = backup_path
297
303
 
@@ -559,6 +565,139 @@ class SubfolderBuildConfig:
559
565
 
560
566
  return "\n".join(result)
561
567
 
568
+ def add_third_party_dependencies(self, dependencies: list[str]) -> None:
569
+ """
570
+ Add third-party dependencies to the temporary pyproject.toml.
571
+
572
+ This method updates the pyproject.toml file that was created for the subfolder
573
+ build by adding the specified dependencies to the [project.dependencies] section.
574
+
575
+ Args:
576
+ dependencies: List of third-party package names to add (e.g., ["pypdf", "requests"])
577
+ """
578
+ if not self.temp_pyproject or not self.temp_pyproject.exists():
579
+ print(
580
+ f"Warning: Cannot add third-party dependencies - pyproject.toml not found at {self.temp_pyproject}",
581
+ file=sys.stderr,
582
+ )
583
+ return
584
+
585
+ if not dependencies:
586
+ return
587
+
588
+ print(f"Adding third-party dependencies to pyproject.toml: {', '.join(dependencies)}")
589
+ content = self.temp_pyproject.read_text(encoding="utf-8")
590
+ updated_content = self._add_dependencies_to_pyproject(content, dependencies)
591
+ self.temp_pyproject.write_text(updated_content, encoding="utf-8")
592
+
593
+ def _normalize_package_name(self, package_name: str) -> str:
594
+ """
595
+ Normalize package name for PyPI.
596
+
597
+ Converts underscores to hyphens, as PyPI package names typically use hyphens
598
+ while Python import names use underscores (e.g., 'better_enum' -> 'better-enum').
599
+
600
+ Args:
601
+ package_name: Package name from import statement
602
+
603
+ Returns:
604
+ Normalized package name for PyPI
605
+ """
606
+ # Convert underscores to hyphens for PyPI package names
607
+ # This handles the common case where import names use underscores
608
+ # but PyPI package names use hyphens
609
+ return package_name.replace("_", "-")
610
+
611
+ def _add_dependencies_to_pyproject(self, content: str, dependencies: list[str]) -> str:
612
+ """
613
+ Add dependencies to pyproject.toml content.
614
+
615
+ Adds the specified dependencies to the [project] section's dependencies list.
616
+ If dependencies already exist, merges them. If no dependencies section exists,
617
+ creates one. Package names are normalized (underscores -> hyphens) to match
618
+ PyPI naming conventions.
619
+
620
+ Args:
621
+ content: Current pyproject.toml content
622
+ dependencies: List of dependency names to add (will be normalized)
623
+
624
+ Returns:
625
+ Updated pyproject.toml content with dependencies added
626
+ """
627
+ if not dependencies:
628
+ return content
629
+
630
+ # Normalize package names (convert underscores to hyphens for PyPI)
631
+ normalized_deps = [self._normalize_package_name(dep) for dep in dependencies]
632
+
633
+ lines = content.split("\n")
634
+ result = []
635
+ in_project = False
636
+ in_dependencies = False
637
+ dependencies_added = False
638
+ existing_deps: set[str] = set()
639
+
640
+ # First pass: find existing dependencies
641
+ for line in lines:
642
+ if line.strip().startswith("[project]"):
643
+ in_project = True
644
+ elif line.strip().startswith("[") and in_project:
645
+ in_project = False
646
+ elif in_project and re.match(r"^\s*dependencies\s*=\s*\[", line):
647
+ in_dependencies = True
648
+ elif in_dependencies:
649
+ # Extract existing dependency names
650
+ dep_match = re.search(r'["\']([^"\']+)["\']', line)
651
+ if dep_match:
652
+ existing_deps.add(dep_match.group(1))
653
+ if line.strip().endswith("]"):
654
+ in_dependencies = False
655
+
656
+ # Merge with new dependencies (normalized)
657
+ all_deps = sorted(existing_deps | set(normalized_deps))
658
+
659
+ # Second pass: build result with dependencies
660
+ in_project = False
661
+ in_dependencies = False
662
+ for line in lines:
663
+ if line.strip().startswith("[project]"):
664
+ in_project = True
665
+ result.append(line)
666
+ elif line.strip().startswith("[") and in_project:
667
+ # End of [project] section, add dependencies if not already present
668
+ if not dependencies_added:
669
+ result.append("dependencies = [")
670
+ for dep in all_deps:
671
+ result.append(f' "{dep}",')
672
+ result.append("]")
673
+ result.append("")
674
+ in_project = False
675
+ result.append(line)
676
+ elif in_project and re.match(r"^\s*dependencies\s*=\s*\[", line):
677
+ # Replace existing dependencies section
678
+ result.append("dependencies = [")
679
+ for dep in all_deps:
680
+ result.append(f' "{dep}",')
681
+ result.append("]")
682
+ dependencies_added = True
683
+ in_dependencies = True
684
+ elif in_dependencies:
685
+ # Skip lines in existing dependencies section (already replaced)
686
+ if line.strip().endswith("]"):
687
+ in_dependencies = False
688
+ else:
689
+ result.append(line)
690
+
691
+ # If [project] section exists but no dependencies were added, add them
692
+ if in_project and not dependencies_added:
693
+ result.append("dependencies = [")
694
+ for dep in all_deps:
695
+ result.append(f' "{dep}",')
696
+ result.append("]")
697
+ result.append("")
698
+
699
+ return "\n".join(result)
700
+
562
701
  def _handle_readme(self) -> None:
563
702
  """
564
703
  Handle README file for subfolder builds.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 1.4.1
3
+ Version: 2.0.4
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>
@@ -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=w7hc2oyOoPK7tvlwcJDXnB3eiJsuGZc4BkOpTfZP7Vo,12257
4
4
  python_package_folder/finder.py,sha256=_LvJ9xBVKv41UK5sbwbNyKmuYjAOqUbzvZhK7NCYQF8,9130
5
- python_package_folder/manager.py,sha256=cQRBLdru8bSovenR5hXFRKmjrBdTmjl0owJdqbXyHjg,30989
5
+ python_package_folder/manager.py,sha256=C3atdBMZqZTFnDMuKSTtb7q2gL90AHo7Wu7u3l8nLLI,38085
6
6
  python_package_folder/publisher.py,sha256=TSjdOvxvnWLbJCnduTK_xZBRfvsrq9kpEH-sfebeWkU,13507
7
7
  python_package_folder/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  python_package_folder/python_package_folder.py,sha256=RPsqRcIy_LjzzTHdp4qdtFJ4-4xhtR_0YLIC0RlUxFo,8841
9
- python_package_folder/subfolder_build.py,sha256=JRctqCaaax76sI7qvjRxR3M9Otz27_tUVYKCCN-Gjyw,29535
9
+ python_package_folder/subfolder_build.py,sha256=LMiJ4Ck6PW1x3hmiSK-9fDH4Q6RrHqot4drx4lise3E,35310
10
10
  python_package_folder/types.py,sha256=3yeSRR5p_3PDKEAaehW_RJ7NwJHexOIeA08bGaT1iSY,2368
11
11
  python_package_folder/utils.py,sha256=lIkWsFKeAYAJ9TDUM99T4pUBHJVbUvCdUgkWQN-LUho,3111
12
12
  python_package_folder/version.py,sha256=kIDP6S9trEfs9gj7lBYGxrWm4RPssRla24UtlO9Jkh4,9111
13
- python_package_folder-1.4.1.dist-info/METADATA,sha256=E6egWuU3bFmFJeM8s74ra7If-P69qNeGNS6Qnz8YRyc,33282
14
- python_package_folder-1.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- python_package_folder-1.4.1.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
16
- python_package_folder-1.4.1.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
17
- python_package_folder-1.4.1.dist-info/RECORD,,
13
+ python_package_folder-2.0.4.dist-info/METADATA,sha256=4PIl6MNJWiM4Laz4Rs36zLr5O9T34BnqN9RjWDmR_VQ,33282
14
+ python_package_folder-2.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ python_package_folder-2.0.4.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
16
+ python_package_folder-2.0.4.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
17
+ python_package_folder-2.0.4.dist-info/RECORD,,