python-package-folder 2.0.2__tar.gz → 2.0.5__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-2.0.2 → python_package_folder-2.0.5}/PKG-INFO +1 -1
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/manager.py +145 -8
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/subfolder_build.py +26 -4
- python_package_folder-2.0.5/tests/test_third_party_dependencies.py +292 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/.copier-answers.yml +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/.github/workflows/ci.yml +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/.github/workflows/publish.yml +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/.gitignore +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/.vscode/settings.json +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/LICENSE +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/Makefile +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/README.md +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/coverage.svg +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/development.md +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/installation.md +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/publishing.md +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/pyproject.toml +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/__init__.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/finder.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/publisher.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/python_package_folder.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/types.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/utils.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/version.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/conftest.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/subfolder_to_build/README.md +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/utility_folder/some_utility.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/test_build_with_external_deps.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/test_linting.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/test_publisher.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/test_subfolder_build.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/test_utils.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/test_version_manager.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/tests.py +0 -0
- {python_package_folder-2.0.2 → python_package_folder-2.0.5}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-package-folder
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5
|
|
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-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/manager.py
RENAMED
|
@@ -86,6 +86,8 @@ class BuildManager:
|
|
|
86
86
|
self.project_root, self.src_dir, exclude_patterns=exclude_patterns
|
|
87
87
|
)
|
|
88
88
|
self.subfolder_config: SubfolderBuildConfig | None = None
|
|
89
|
+
# Cache for package name lookups (expensive operation)
|
|
90
|
+
self._packages_distributions_cache: dict[str, list[str]] | None = None
|
|
89
91
|
|
|
90
92
|
# Check if it's a valid Python package directory
|
|
91
93
|
if not any(self.src_dir.glob("*.py")) and not (self.src_dir / "__init__.py").exists():
|
|
@@ -271,7 +273,9 @@ class BuildManager:
|
|
|
271
273
|
# For subfolder builds, extract third-party dependencies and add to pyproject.toml
|
|
272
274
|
if self._is_subfolder_build() and self.subfolder_config:
|
|
273
275
|
# Re-analyze all Python files (including copied dependencies) to find third-party imports
|
|
276
|
+
print("Analyzing Python files for third-party dependencies...")
|
|
274
277
|
all_python_files = analyzer.find_all_python_files(self.src_dir)
|
|
278
|
+
print(f"Found {len(all_python_files)} Python files to analyze")
|
|
275
279
|
third_party_deps = self._extract_third_party_dependencies(all_python_files, analyzer)
|
|
276
280
|
if third_party_deps:
|
|
277
281
|
print(
|
|
@@ -394,6 +398,111 @@ class BuildManager:
|
|
|
394
398
|
elif src_item.is_dir():
|
|
395
399
|
self._copytree_excluding(src_item, dst_item)
|
|
396
400
|
|
|
401
|
+
def _get_package_name_from_import(self, module_name: str) -> str | None:
|
|
402
|
+
"""
|
|
403
|
+
Get the actual PyPI package name from an import module name.
|
|
404
|
+
|
|
405
|
+
This handles cases where the import name differs from the package name
|
|
406
|
+
(e.g., 'import fitz' from 'pymupdf' package).
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
module_name: The module name from the import statement
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
The actual package name, or None if not found
|
|
413
|
+
"""
|
|
414
|
+
root_module = module_name.split(".")[0]
|
|
415
|
+
try:
|
|
416
|
+
# Try Python 3.10+ first (has packages_distributions)
|
|
417
|
+
import importlib.metadata as importlib_metadata
|
|
418
|
+
|
|
419
|
+
# Use packages_distributions() if available (Python 3.10+)
|
|
420
|
+
# Cache the result since it's expensive to call
|
|
421
|
+
if hasattr(importlib_metadata, "packages_distributions"):
|
|
422
|
+
if self._packages_distributions_cache is None:
|
|
423
|
+
# Cache the packages_distributions() result
|
|
424
|
+
self._packages_distributions_cache = importlib_metadata.packages_distributions()
|
|
425
|
+
packages_map = self._packages_distributions_cache
|
|
426
|
+
# packages_map is a dict mapping module names to list of distribution names
|
|
427
|
+
if root_module in packages_map:
|
|
428
|
+
# Return the first distribution name (usually there's only one)
|
|
429
|
+
dist_names = packages_map[root_module]
|
|
430
|
+
if dist_names:
|
|
431
|
+
return dist_names[0]
|
|
432
|
+
|
|
433
|
+
# Fallback: search all distributions (this can be slow, so limit search)
|
|
434
|
+
# Only check top-level package matches to speed up search
|
|
435
|
+
dist_count = 0
|
|
436
|
+
max_distributions_to_check = 1000 # Limit to prevent excessive searching
|
|
437
|
+
for dist in importlib_metadata.distributions():
|
|
438
|
+
dist_count += 1
|
|
439
|
+
if dist_count > max_distributions_to_check:
|
|
440
|
+
# Too many distributions, give up to avoid hanging
|
|
441
|
+
break
|
|
442
|
+
try:
|
|
443
|
+
# Check distribution name first (fast check)
|
|
444
|
+
dist_name = dist.metadata.get("Name", "")
|
|
445
|
+
# If distribution name matches or contains the module name, check files
|
|
446
|
+
if dist_name.lower().replace(
|
|
447
|
+
"-", "_"
|
|
448
|
+
) == root_module.lower() or root_module.lower() in dist_name.lower().replace(
|
|
449
|
+
"-", "_"
|
|
450
|
+
):
|
|
451
|
+
# Check if this distribution provides the module by looking at its files
|
|
452
|
+
files = dist.files or []
|
|
453
|
+
# Limit file checking to first 100 files per distribution
|
|
454
|
+
file_count = 0
|
|
455
|
+
for file in files:
|
|
456
|
+
file_count += 1
|
|
457
|
+
if file_count > 100:
|
|
458
|
+
break
|
|
459
|
+
file_str = str(file)
|
|
460
|
+
# Check if file is the module itself or in a package directory
|
|
461
|
+
if (
|
|
462
|
+
file.suffix == ".py"
|
|
463
|
+
and (file.stem == root_module or file.stem == "__init__")
|
|
464
|
+
) or (
|
|
465
|
+
"/" in file_str
|
|
466
|
+
and (
|
|
467
|
+
file_str.startswith(f"{root_module}/")
|
|
468
|
+
or file_str.startswith(f"{root_module.replace('_', '-')}/")
|
|
469
|
+
)
|
|
470
|
+
):
|
|
471
|
+
return dist.metadata["Name"]
|
|
472
|
+
except Exception:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
except ImportError:
|
|
476
|
+
try:
|
|
477
|
+
# Fallback for older Python versions
|
|
478
|
+
import importlib_metadata
|
|
479
|
+
|
|
480
|
+
# Search all distributions
|
|
481
|
+
for dist in importlib_metadata.distributions():
|
|
482
|
+
try:
|
|
483
|
+
files = dist.files or []
|
|
484
|
+
for file in files:
|
|
485
|
+
file_str = str(file)
|
|
486
|
+
if (
|
|
487
|
+
file.suffix == ".py"
|
|
488
|
+
and (file.stem == root_module or file.stem == "__init__")
|
|
489
|
+
) or (
|
|
490
|
+
"/" in file_str
|
|
491
|
+
and (
|
|
492
|
+
file_str.startswith(f"{root_module}/")
|
|
493
|
+
or file_str.startswith(f"{root_module.replace('_', '-')}/")
|
|
494
|
+
)
|
|
495
|
+
):
|
|
496
|
+
return dist.metadata["Name"]
|
|
497
|
+
except Exception:
|
|
498
|
+
continue
|
|
499
|
+
except ImportError:
|
|
500
|
+
pass
|
|
501
|
+
except Exception:
|
|
502
|
+
pass
|
|
503
|
+
|
|
504
|
+
return None
|
|
505
|
+
|
|
397
506
|
def _extract_third_party_dependencies(
|
|
398
507
|
self, python_files: list[Path], analyzer: ImportAnalyzer
|
|
399
508
|
) -> list[str]:
|
|
@@ -401,18 +510,25 @@ class BuildManager:
|
|
|
401
510
|
Extract third-party package dependencies from Python files.
|
|
402
511
|
|
|
403
512
|
Analyzes all Python files to find imports classified as "third_party"
|
|
404
|
-
and returns a list of unique package names.
|
|
513
|
+
and returns a list of unique package names. Handles cases where the
|
|
514
|
+
import name differs from the package name (e.g., 'fitz' -> 'pymupdf').
|
|
405
515
|
|
|
406
516
|
Args:
|
|
407
517
|
python_files: List of Python file paths to analyze
|
|
408
518
|
analyzer: ImportAnalyzer instance to use for classification
|
|
409
519
|
|
|
410
520
|
Returns:
|
|
411
|
-
List of unique third-party package names (e.g., ["pypdf", "requests"])
|
|
521
|
+
List of unique third-party package names (e.g., ["pypdf", "requests", "pymupdf"])
|
|
412
522
|
"""
|
|
413
523
|
third_party_packages: set[str] = set()
|
|
524
|
+
# Cache package name lookups to avoid repeated expensive searches
|
|
525
|
+
package_name_cache: dict[str, str | None] = {}
|
|
526
|
+
|
|
527
|
+
total_files = len(python_files)
|
|
528
|
+
for idx, file_path in enumerate(python_files):
|
|
529
|
+
if idx > 0 and idx % 50 == 0:
|
|
530
|
+
print(f" Analyzing file {idx}/{total_files}...", end="\r", flush=True)
|
|
414
531
|
|
|
415
|
-
for file_path in python_files:
|
|
416
532
|
imports = analyzer.extract_imports(file_path)
|
|
417
533
|
for imp in imports:
|
|
418
534
|
analyzer.classify_import(imp, self.src_dir)
|
|
@@ -425,17 +541,38 @@ class BuildManager:
|
|
|
425
541
|
if root_module in stdlib_modules:
|
|
426
542
|
continue
|
|
427
543
|
|
|
428
|
-
# If classified as third_party,
|
|
544
|
+
# If classified as third_party, try to get actual package name
|
|
429
545
|
if imp.classification == "third_party":
|
|
430
|
-
|
|
546
|
+
# Check cache first
|
|
547
|
+
if root_module not in package_name_cache:
|
|
548
|
+
package_name_cache[root_module] = self._get_package_name_from_import(
|
|
549
|
+
imp.module_name
|
|
550
|
+
)
|
|
551
|
+
actual_package = package_name_cache[root_module]
|
|
552
|
+
if actual_package:
|
|
553
|
+
third_party_packages.add(actual_package)
|
|
554
|
+
else:
|
|
555
|
+
# Fallback to using the import name
|
|
556
|
+
third_party_packages.add(root_module)
|
|
431
557
|
# If it's ambiguous or unresolved, and not stdlib/local/external,
|
|
432
558
|
# it's likely a third-party package that needs to be declared
|
|
433
559
|
elif imp.classification == "ambiguous" or imp.classification is None:
|
|
434
560
|
# Check if it's not a local or external module
|
|
435
561
|
if not imp.resolved_path:
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
562
|
+
# Check cache first
|
|
563
|
+
if root_module not in package_name_cache:
|
|
564
|
+
package_name_cache[root_module] = self._get_package_name_from_import(
|
|
565
|
+
imp.module_name
|
|
566
|
+
)
|
|
567
|
+
actual_package = package_name_cache[root_module]
|
|
568
|
+
if actual_package:
|
|
569
|
+
third_party_packages.add(actual_package)
|
|
570
|
+
else:
|
|
571
|
+
# Fallback: use import name (will be normalized later)
|
|
572
|
+
third_party_packages.add(root_module)
|
|
573
|
+
|
|
574
|
+
if total_files > 50:
|
|
575
|
+
print() # New line after progress indicator
|
|
439
576
|
|
|
440
577
|
return sorted(list(third_party_packages))
|
|
441
578
|
|
|
@@ -590,17 +590,36 @@ class SubfolderBuildConfig:
|
|
|
590
590
|
updated_content = self._add_dependencies_to_pyproject(content, dependencies)
|
|
591
591
|
self.temp_pyproject.write_text(updated_content, encoding="utf-8")
|
|
592
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
|
+
|
|
593
611
|
def _add_dependencies_to_pyproject(self, content: str, dependencies: list[str]) -> str:
|
|
594
612
|
"""
|
|
595
613
|
Add dependencies to pyproject.toml content.
|
|
596
614
|
|
|
597
615
|
Adds the specified dependencies to the [project] section's dependencies list.
|
|
598
616
|
If dependencies already exist, merges them. If no dependencies section exists,
|
|
599
|
-
creates one.
|
|
617
|
+
creates one. Package names are normalized (underscores -> hyphens) to match
|
|
618
|
+
PyPI naming conventions.
|
|
600
619
|
|
|
601
620
|
Args:
|
|
602
621
|
content: Current pyproject.toml content
|
|
603
|
-
dependencies: List of dependency names to add
|
|
622
|
+
dependencies: List of dependency names to add (will be normalized)
|
|
604
623
|
|
|
605
624
|
Returns:
|
|
606
625
|
Updated pyproject.toml content with dependencies added
|
|
@@ -608,6 +627,9 @@ class SubfolderBuildConfig:
|
|
|
608
627
|
if not dependencies:
|
|
609
628
|
return content
|
|
610
629
|
|
|
630
|
+
# Normalize package names (convert underscores to hyphens for PyPI)
|
|
631
|
+
normalized_deps = [self._normalize_package_name(dep) for dep in dependencies]
|
|
632
|
+
|
|
611
633
|
lines = content.split("\n")
|
|
612
634
|
result = []
|
|
613
635
|
in_project = False
|
|
@@ -631,8 +653,8 @@ class SubfolderBuildConfig:
|
|
|
631
653
|
if line.strip().endswith("]"):
|
|
632
654
|
in_dependencies = False
|
|
633
655
|
|
|
634
|
-
# Merge with new dependencies
|
|
635
|
-
all_deps = sorted(existing_deps | set(
|
|
656
|
+
# Merge with new dependencies (normalized)
|
|
657
|
+
all_deps = sorted(existing_deps | set(normalized_deps))
|
|
636
658
|
|
|
637
659
|
# Second pass: build result with dependencies
|
|
638
660
|
in_project = False
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Tests for third-party dependency detection and normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from python_package_folder import BuildManager, ImportAnalyzer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def test_project_with_imports(tmp_path: Path) -> Path:
|
|
15
|
+
"""Create a test project with subfolder containing various imports."""
|
|
16
|
+
project_root = tmp_path / "test_project"
|
|
17
|
+
project_root.mkdir()
|
|
18
|
+
|
|
19
|
+
# Create pyproject.toml
|
|
20
|
+
pyproject_content = """[project]
|
|
21
|
+
name = "test-package"
|
|
22
|
+
version = "0.1.0"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/test_package"]
|
|
26
|
+
"""
|
|
27
|
+
(project_root / "pyproject.toml").write_text(pyproject_content)
|
|
28
|
+
|
|
29
|
+
# Create subfolder with imports
|
|
30
|
+
subfolder = project_root / "subfolder_to_build"
|
|
31
|
+
subfolder.mkdir()
|
|
32
|
+
|
|
33
|
+
# Create a file that imports better_enum (package name: better-enum)
|
|
34
|
+
(subfolder / "better_enum_import.py").write_text(
|
|
35
|
+
"""from better_enum import Enum
|
|
36
|
+
def use_better_enum():
|
|
37
|
+
return Enum
|
|
38
|
+
"""
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Create a file that imports fitz (package name: pymupdf)
|
|
42
|
+
(subfolder / "fitz_import.py").write_text(
|
|
43
|
+
"""import fitz
|
|
44
|
+
def use_fitz():
|
|
45
|
+
return fitz
|
|
46
|
+
"""
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Create a file with standard library import (should be excluded)
|
|
50
|
+
(subfolder / "stdlib_import.py").write_text(
|
|
51
|
+
"""import os
|
|
52
|
+
import sys
|
|
53
|
+
def use_stdlib():
|
|
54
|
+
return os, sys
|
|
55
|
+
"""
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Create a file with local import (should be excluded)
|
|
59
|
+
(subfolder / "local_import.py").write_text(
|
|
60
|
+
"""from better_enum_import import use_better_enum
|
|
61
|
+
def use_local():
|
|
62
|
+
return use_better_enum
|
|
63
|
+
"""
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return project_root
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestThirdPartyDependencyExtraction:
|
|
70
|
+
"""Tests for extracting third-party dependencies from imports."""
|
|
71
|
+
|
|
72
|
+
def test_extract_better_enum_dependency(self, test_project_with_imports: Path) -> None:
|
|
73
|
+
"""Test that better_enum import is detected and normalized to better-enum."""
|
|
74
|
+
project_root = test_project_with_imports
|
|
75
|
+
src_dir = project_root / "subfolder_to_build"
|
|
76
|
+
|
|
77
|
+
manager = BuildManager(project_root, src_dir)
|
|
78
|
+
analyzer = ImportAnalyzer(project_root)
|
|
79
|
+
|
|
80
|
+
# Get all Python files
|
|
81
|
+
python_files = analyzer.find_all_python_files(src_dir)
|
|
82
|
+
|
|
83
|
+
# Mock _get_package_name_from_import to return better-enum for better_enum
|
|
84
|
+
with patch.object(
|
|
85
|
+
manager,
|
|
86
|
+
"_get_package_name_from_import",
|
|
87
|
+
side_effect=lambda name: "better-enum" if name == "better_enum" else None,
|
|
88
|
+
):
|
|
89
|
+
# Extract third-party dependencies
|
|
90
|
+
third_party_deps = manager._extract_third_party_dependencies(python_files, analyzer)
|
|
91
|
+
|
|
92
|
+
# Should include better-enum (normalized from better_enum)
|
|
93
|
+
# If better_enum is classified as third_party or ambiguous, it should be included
|
|
94
|
+
dep_names = {dep.lower().replace("_", "-") for dep in third_party_deps}
|
|
95
|
+
# Check that better-enum is in the list (normalized) or better_enum if not mapped
|
|
96
|
+
assert "better-enum" in dep_names or "better_enum" in third_party_deps
|
|
97
|
+
|
|
98
|
+
def test_extract_fitz_dependency_mapped_to_pymupdf(
|
|
99
|
+
self, test_project_with_imports: Path
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Test that fitz import is mapped to pymupdf package name."""
|
|
102
|
+
project_root = test_project_with_imports
|
|
103
|
+
src_dir = project_root / "subfolder_to_build"
|
|
104
|
+
|
|
105
|
+
manager = BuildManager(project_root, src_dir)
|
|
106
|
+
analyzer = ImportAnalyzer(project_root)
|
|
107
|
+
|
|
108
|
+
# Get all Python files
|
|
109
|
+
python_files = analyzer.find_all_python_files(src_dir)
|
|
110
|
+
|
|
111
|
+
# Mock the _get_package_name_from_import method to return pymupdf for fitz
|
|
112
|
+
with patch.object(
|
|
113
|
+
manager,
|
|
114
|
+
"_get_package_name_from_import",
|
|
115
|
+
side_effect=lambda name: "pymupdf" if name == "fitz" else None,
|
|
116
|
+
):
|
|
117
|
+
third_party_deps = manager._extract_third_party_dependencies(python_files, analyzer)
|
|
118
|
+
|
|
119
|
+
# Should include pymupdf (mapped from fitz) if fitz is classified as third_party
|
|
120
|
+
# Note: This test depends on fitz being classified as third_party
|
|
121
|
+
# If it's not installed, it might be classified as ambiguous
|
|
122
|
+
if "pymupdf" in third_party_deps:
|
|
123
|
+
# Should not include fitz (the import name)
|
|
124
|
+
assert "fitz" not in third_party_deps
|
|
125
|
+
|
|
126
|
+
def test_extract_dependencies_excludes_stdlib(self, test_project_with_imports: Path) -> None:
|
|
127
|
+
"""Test that standard library imports are excluded."""
|
|
128
|
+
project_root = test_project_with_imports
|
|
129
|
+
src_dir = project_root / "subfolder_to_build"
|
|
130
|
+
|
|
131
|
+
manager = BuildManager(project_root, src_dir)
|
|
132
|
+
analyzer = ImportAnalyzer(project_root)
|
|
133
|
+
|
|
134
|
+
# Get all Python files
|
|
135
|
+
python_files = analyzer.find_all_python_files(src_dir)
|
|
136
|
+
|
|
137
|
+
# Extract third-party dependencies
|
|
138
|
+
third_party_deps = manager._extract_third_party_dependencies(python_files, analyzer)
|
|
139
|
+
|
|
140
|
+
# Should not include stdlib modules
|
|
141
|
+
assert "os" not in third_party_deps
|
|
142
|
+
assert "sys" not in third_party_deps
|
|
143
|
+
|
|
144
|
+
def test_extract_dependencies_excludes_local_imports(
|
|
145
|
+
self, test_project_with_imports: Path
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Test that local imports are excluded."""
|
|
148
|
+
project_root = test_project_with_imports
|
|
149
|
+
src_dir = project_root / "subfolder_to_build"
|
|
150
|
+
|
|
151
|
+
manager = BuildManager(project_root, src_dir)
|
|
152
|
+
analyzer = ImportAnalyzer(project_root)
|
|
153
|
+
|
|
154
|
+
# Get all Python files
|
|
155
|
+
python_files = analyzer.find_all_python_files(src_dir)
|
|
156
|
+
|
|
157
|
+
# Extract third-party dependencies
|
|
158
|
+
third_party_deps = manager._extract_third_party_dependencies(python_files, analyzer)
|
|
159
|
+
|
|
160
|
+
# Should not include local module names
|
|
161
|
+
assert "better_enum_import" not in third_party_deps
|
|
162
|
+
|
|
163
|
+
def test_get_package_name_from_import_with_mapping(self, tmp_path: Path) -> None:
|
|
164
|
+
"""Test _get_package_name_from_import with package name mapping."""
|
|
165
|
+
from python_package_folder.manager import BuildManager
|
|
166
|
+
|
|
167
|
+
project_root = tmp_path / "test_project"
|
|
168
|
+
project_root.mkdir()
|
|
169
|
+
src_dir = project_root / "subfolder"
|
|
170
|
+
src_dir.mkdir()
|
|
171
|
+
(src_dir / "test.py").write_text("pass")
|
|
172
|
+
|
|
173
|
+
manager = BuildManager(project_root, src_dir)
|
|
174
|
+
|
|
175
|
+
# Test that the method exists and can be called
|
|
176
|
+
# The actual result depends on what's installed in the environment
|
|
177
|
+
# and how the search through distributions works
|
|
178
|
+
package_name = manager._get_package_name_from_import("fitz")
|
|
179
|
+
# Should return None if pymupdf is not installed, or "pymupdf" if it is
|
|
180
|
+
# The method may return other values if it finds matches in installed packages
|
|
181
|
+
# This is acceptable - the important thing is that the method works
|
|
182
|
+
assert isinstance(package_name, str) or package_name is None
|
|
183
|
+
|
|
184
|
+
def test_get_package_name_fallback_to_import_name(self, tmp_path: Path) -> None:
|
|
185
|
+
"""Test that _get_package_name_from_import can be called."""
|
|
186
|
+
from python_package_folder.manager import BuildManager
|
|
187
|
+
|
|
188
|
+
project_root = tmp_path / "test_project"
|
|
189
|
+
project_root.mkdir()
|
|
190
|
+
src_dir = project_root / "subfolder"
|
|
191
|
+
src_dir.mkdir()
|
|
192
|
+
(src_dir / "test.py").write_text("pass")
|
|
193
|
+
|
|
194
|
+
manager = BuildManager(project_root, src_dir)
|
|
195
|
+
|
|
196
|
+
# Test that the method exists and can be called
|
|
197
|
+
# The actual result depends on what's installed in the environment
|
|
198
|
+
# The search through distributions might find false matches
|
|
199
|
+
package_name = manager._get_package_name_from_import(
|
|
200
|
+
"nonexistent_package_xyz123_very_unlikely_to_exist"
|
|
201
|
+
)
|
|
202
|
+
# The method should return a string (package name) or None
|
|
203
|
+
# False positives are possible when searching through distributions
|
|
204
|
+
assert isinstance(package_name, str) or package_name is None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestThirdPartyDependenciesInSubfolderBuild:
|
|
208
|
+
"""Tests for third-party dependencies in subfolder builds."""
|
|
209
|
+
|
|
210
|
+
def test_subfolder_build_includes_third_party_dependencies(
|
|
211
|
+
self, test_project_with_imports: Path
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Test that subfolder build includes third-party dependencies in pyproject.toml."""
|
|
214
|
+
project_root = test_project_with_imports
|
|
215
|
+
src_dir = project_root / "subfolder_to_build"
|
|
216
|
+
|
|
217
|
+
manager = BuildManager(project_root, src_dir)
|
|
218
|
+
|
|
219
|
+
# Prepare build (this should detect and add third-party dependencies)
|
|
220
|
+
manager.prepare_build(version="1.0.0", package_name="test-subfolder")
|
|
221
|
+
|
|
222
|
+
# Check that subfolder_config was created
|
|
223
|
+
assert manager.subfolder_config is not None
|
|
224
|
+
|
|
225
|
+
# Check that pyproject.toml was created
|
|
226
|
+
pyproject_path = project_root / "pyproject.toml"
|
|
227
|
+
assert pyproject_path.exists()
|
|
228
|
+
|
|
229
|
+
# Read the pyproject.toml content
|
|
230
|
+
content = pyproject_path.read_text()
|
|
231
|
+
|
|
232
|
+
# Should have dependencies section
|
|
233
|
+
assert "dependencies" in content or "[project]" in content
|
|
234
|
+
|
|
235
|
+
# Cleanup
|
|
236
|
+
manager.cleanup()
|
|
237
|
+
|
|
238
|
+
def test_dependencies_normalized_in_pyproject_toml(
|
|
239
|
+
self, test_project_with_imports: Path
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Test that dependencies are normalized (underscores -> hyphens) in pyproject.toml."""
|
|
242
|
+
project_root = test_project_with_imports
|
|
243
|
+
src_dir = project_root / "subfolder_to_build"
|
|
244
|
+
|
|
245
|
+
manager = BuildManager(project_root, src_dir)
|
|
246
|
+
|
|
247
|
+
# Mock the dependency extraction to return better_enum
|
|
248
|
+
with patch.object(
|
|
249
|
+
manager,
|
|
250
|
+
"_extract_third_party_dependencies",
|
|
251
|
+
return_value=["better_enum"],
|
|
252
|
+
):
|
|
253
|
+
manager.prepare_build(version="1.0.0", package_name="test-subfolder")
|
|
254
|
+
|
|
255
|
+
# Check pyproject.toml content
|
|
256
|
+
pyproject_path = project_root / "pyproject.toml"
|
|
257
|
+
content = pyproject_path.read_text()
|
|
258
|
+
|
|
259
|
+
# Should have better-enum (normalized) not better_enum
|
|
260
|
+
assert '"better-enum"' in content or "'better-enum'" in content
|
|
261
|
+
# Should not have better_enum (unnormalized)
|
|
262
|
+
assert '"better_enum"' not in content or (
|
|
263
|
+
'"better_enum"' in content and '"better-enum"' in content
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
manager.cleanup()
|
|
267
|
+
|
|
268
|
+
def test_package_name_mapping_in_pyproject_toml(self, test_project_with_imports: Path) -> None:
|
|
269
|
+
"""Test that import names are mapped to package names in pyproject.toml."""
|
|
270
|
+
project_root = test_project_with_imports
|
|
271
|
+
src_dir = project_root / "subfolder_to_build"
|
|
272
|
+
|
|
273
|
+
manager = BuildManager(project_root, src_dir)
|
|
274
|
+
|
|
275
|
+
# Mock _extract_third_party_dependencies to return pymupdf (mapped from fitz)
|
|
276
|
+
with patch.object(
|
|
277
|
+
manager,
|
|
278
|
+
"_extract_third_party_dependencies",
|
|
279
|
+
return_value=["pymupdf"], # Should be mapped from fitz
|
|
280
|
+
):
|
|
281
|
+
manager.prepare_build(version="1.0.0", package_name="test-subfolder")
|
|
282
|
+
|
|
283
|
+
# Check pyproject.toml content
|
|
284
|
+
pyproject_path = project_root / "pyproject.toml"
|
|
285
|
+
content = pyproject_path.read_text()
|
|
286
|
+
|
|
287
|
+
# Should have pymupdf (the actual package name)
|
|
288
|
+
assert '"pymupdf"' in content or "'pymupdf'" in content
|
|
289
|
+
# Should not have fitz (the import name)
|
|
290
|
+
assert '"fitz"' not in content or ('"fitz"' in content and '"pymupdf"' in content)
|
|
291
|
+
|
|
292
|
+
manager.cleanup()
|
|
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-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/__init__.py
RENAMED
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/__main__.py
RENAMED
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/analyzer.py
RENAMED
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/finder.py
RENAMED
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/publisher.py
RENAMED
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/types.py
RENAMED
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/utils.py
RENAMED
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/src/python_package_folder/version.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/folder_structure/some_globals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-2.0.2 → python_package_folder-2.0.5}/tests/test_build_with_external_deps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|