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.
- python_package_folder/manager.py +152 -0
- python_package_folder/subfolder_build.py +139 -0
- {python_package_folder-1.4.1.dist-info → python_package_folder-2.0.4.dist-info}/METADATA +1 -1
- {python_package_folder-1.4.1.dist-info → python_package_folder-2.0.4.dist-info}/RECORD +7 -7
- {python_package_folder-1.4.1.dist-info → python_package_folder-2.0.4.dist-info}/WHEEL +0 -0
- {python_package_folder-1.4.1.dist-info → python_package_folder-2.0.4.dist-info}/entry_points.txt +0 -0
- {python_package_folder-1.4.1.dist-info → python_package_folder-2.0.4.dist-info}/licenses/LICENSE +0 -0
python_package_folder/manager.py
CHANGED
|
@@ -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:
|
|
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=
|
|
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=
|
|
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-
|
|
14
|
-
python_package_folder-
|
|
15
|
-
python_package_folder-
|
|
16
|
-
python_package_folder-
|
|
17
|
-
python_package_folder-
|
|
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,,
|
|
File without changes
|
{python_package_folder-1.4.1.dist-info → python_package_folder-2.0.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{python_package_folder-1.4.1.dist-info → python_package_folder-2.0.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|