python-package-folder 1.4.0__tar.gz → 2.0.2__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.
Files changed (45) hide show
  1. python_package_folder-2.0.2/.github/workflows/publish.yml +104 -0
  2. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/PKG-INFO +1 -1
  3. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/coverage.svg +2 -2
  4. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/pyproject.toml +1 -1
  5. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/manager.py +75 -0
  6. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/publisher.py +14 -0
  7. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/subfolder_build.py +117 -0
  8. python_package_folder-2.0.2/tests/conftest.py +96 -0
  9. python_package_folder-2.0.2/tests/folder_structure/subfolder_to_build/__init__.py +1 -0
  10. python_package_folder-2.0.2/tests/folder_structure/subfolder_to_build/some_globals.py +1 -0
  11. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/test_build_with_external_deps.py +85 -5
  12. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/test_subfolder_build.py +66 -0
  13. python_package_folder-1.4.0/.github/workflows/publish.yml +0 -44
  14. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/.copier-answers.yml +0 -0
  15. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/.cursor/rules/general.mdc +0 -0
  16. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/.cursor/rules/python.mdc +0 -0
  17. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/.github/workflows/ci.yml +0 -0
  18. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/.gitignore +0 -0
  19. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/.vscode/settings.json +0 -0
  20. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/LICENSE +0 -0
  21. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/Makefile +0 -0
  22. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/README.md +0 -0
  23. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/development.md +0 -0
  24. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/installation.md +0 -0
  25. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/publishing.md +0 -0
  26. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/__init__.py +0 -0
  27. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/__main__.py +0 -0
  28. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/analyzer.py +0 -0
  29. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/finder.py +0 -0
  30. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/py.typed +0 -0
  31. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/python_package_folder.py +0 -0
  32. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/types.py +0 -0
  33. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/utils.py +0 -0
  34. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/src/python_package_folder/version.py +0 -0
  35. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/folder_structure/some_globals.py +0 -0
  36. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  37. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  38. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  39. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  40. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/test_linting.py +0 -0
  41. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/test_publisher.py +0 -0
  42. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/test_utils.py +0 -0
  43. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/test_version_manager.py +0 -0
  44. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/tests/tests.py +0 -0
  45. {python_package_folder-1.4.0 → python_package_folder-2.0.2}/uv.lock +0 -0
@@ -0,0 +1,104 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch: # Enable manual trigger.
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write # Mandatory for OIDC.
13
+ contents: read
14
+ steps:
15
+ - name: Checkout (official GitHub action)
16
+ uses: actions/checkout@v4
17
+ with:
18
+ # Important for versioning plugins:
19
+ fetch-depth: 0
20
+
21
+ - name: Install uv (official Astral action)
22
+ uses: astral-sh/setup-uv@v5
23
+ with:
24
+ version: "0.9.5"
25
+ enable-cache: true
26
+ python-version: "3.12"
27
+
28
+ - name: Set up Python (using uv)
29
+ run: uv python install
30
+
31
+ - name: Install all dependencies
32
+ run: uv sync --all-extras
33
+
34
+ - name: Run tests
35
+ run: uv run pytest
36
+
37
+ - name: Verify pyproject.toml is correct
38
+ run: |
39
+ # Verify pyproject.toml has the correct package name
40
+ if ! grep -q 'name = "python-package-folder"' pyproject.toml; then
41
+ echo "Error: pyproject.toml does not have correct package name 'python-package-folder'"
42
+ echo "Current content:"
43
+ grep '^name =' pyproject.toml || echo "No name field found"
44
+ exit 1
45
+ fi
46
+ # Verify packages points to the correct location
47
+ if ! grep -q 'packages = \["src/python_package_folder"\]' pyproject.toml; then
48
+ echo "Error: pyproject.toml does not have correct packages configuration"
49
+ echo "Current packages:"
50
+ grep -A 1 '\[tool.hatch.build.targets.wheel\]' pyproject.toml || echo "No packages found"
51
+ exit 1
52
+ fi
53
+ echo "✓ pyproject.toml is correct"
54
+
55
+ - name: Clean dist directory
56
+ run: |
57
+ # Remove any existing distribution files to prevent publishing test artifacts
58
+ rm -rf dist/
59
+ echo "✓ Cleaned dist/ directory"
60
+
61
+ - name: Build package
62
+ run: uv build
63
+
64
+ - name: Verify distribution files
65
+ run: |
66
+ # List all distribution files that will be published
67
+ echo "Distribution files to be published:"
68
+ ls -la dist/ || (echo "No dist/ directory found" && exit 1)
69
+
70
+ # Check for unexpected files (exclude .gitignore and expected package files)
71
+ UNEXPECTED_FILES=0
72
+ for file in dist/*.whl dist/*.tar.gz; do
73
+ if [ -f "$file" ]; then
74
+ filename=$(basename "$file")
75
+ # Check if file starts with python_package_folder (package directory name)
76
+ if [[ ! "$filename" =~ ^python_package_folder- ]]; then
77
+ echo "Error: Found unexpected distribution file: $filename"
78
+ UNEXPECTED_FILES=$((UNEXPECTED_FILES + 1))
79
+ fi
80
+ fi
81
+ done
82
+
83
+ if [ $UNEXPECTED_FILES -gt 0 ]; then
84
+ echo "Error: Found $UNEXPECTED_FILES unexpected distribution file(s)"
85
+ echo "Files in dist/:"
86
+ ls -la dist/
87
+ exit 1
88
+ fi
89
+
90
+ # Verify at least one expected file exists
91
+ if ! ls dist/python_package_folder-*.whl dist/python_package_folder-*.tar.gz 2>/dev/null | grep -q .; then
92
+ echo "Error: No python_package_folder distribution files found"
93
+ echo "Files in dist/:"
94
+ ls -la dist/
95
+ exit 1
96
+ fi
97
+
98
+ echo "✓ Only python_package_folder distribution files found"
99
+
100
+ - name: Publish to PyPI
101
+ run: uv publish --trusted-publishing always
102
+ # Although uv is newer and faster, the "official" publishing option is the one from PyPA,
103
+ # which uses twine. If desired, replace `uv publish` with:
104
+ # uses: pypa/gh-action-pypi-publish@release/v1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 1.4.0
3
+ Version: 2.0.2
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>
@@ -14,7 +14,7 @@
14
14
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
15
15
  <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
16
16
  <text x="31.5" y="14">coverage</text>
17
- <text x="81" y="15" fill="#010101" fill-opacity=".3">67%</text>
18
- <text x="81" y="14">67%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">68%</text>
18
+ <text x="81" y="14">68%</text>
19
19
  </g>
20
20
  </svg>
@@ -172,4 +172,4 @@ testpaths = [
172
172
  "tests",
173
173
  ]
174
174
  norecursedirs = []
175
- filterwarnings = []
175
+ filterwarnings = []
@@ -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,51 @@ class BuildManager:
381
394
  elif src_item.is_dir():
382
395
  self._copytree_excluding(src_item, dst_item)
383
396
 
397
+ def _extract_third_party_dependencies(
398
+ self, python_files: list[Path], analyzer: ImportAnalyzer
399
+ ) -> list[str]:
400
+ """
401
+ Extract third-party package dependencies from Python files.
402
+
403
+ Analyzes all Python files to find imports classified as "third_party"
404
+ and returns a list of unique package names.
405
+
406
+ Args:
407
+ python_files: List of Python file paths to analyze
408
+ analyzer: ImportAnalyzer instance to use for classification
409
+
410
+ Returns:
411
+ List of unique third-party package names (e.g., ["pypdf", "requests"])
412
+ """
413
+ third_party_packages: set[str] = set()
414
+
415
+ for file_path in python_files:
416
+ imports = analyzer.extract_imports(file_path)
417
+ for imp in imports:
418
+ analyzer.classify_import(imp, self.src_dir)
419
+
420
+ # Extract the root package name (first part of module name)
421
+ root_module = imp.module_name.split(".")[0]
422
+
423
+ # Skip if it's a standard library module
424
+ stdlib_modules = analyzer.get_stdlib_modules()
425
+ if root_module in stdlib_modules:
426
+ continue
427
+
428
+ # If classified as third_party, add it
429
+ if imp.classification == "third_party":
430
+ third_party_packages.add(root_module)
431
+ # If it's ambiguous or unresolved, and not stdlib/local/external,
432
+ # it's likely a third-party package that needs to be declared
433
+ elif imp.classification == "ambiguous" or imp.classification is None:
434
+ # Check if it's not a local or external module
435
+ if not imp.resolved_path:
436
+ # This is likely a third-party package that's not installed
437
+ # in the build environment but needs to be declared
438
+ third_party_packages.add(root_module)
439
+
440
+ return sorted(list(third_party_packages))
441
+
384
442
  def _report_ambiguous_imports(self, python_files: list[Path]) -> None:
385
443
  """
386
444
  Report any ambiguous imports that couldn't be resolved.
@@ -700,6 +758,11 @@ class BuildManager:
700
758
  publish_package_name = self.subfolder_config.package_name
701
759
  elif package_name:
702
760
  publish_package_name = package_name
761
+ else:
762
+ # Last resort: derive from src_dir name
763
+ publish_package_name = (
764
+ self.src_dir.name.replace("_", "-").replace(" ", "-").lower().strip("-")
765
+ )
703
766
  else:
704
767
  # For regular builds, get package name from pyproject.toml
705
768
  try:
@@ -718,6 +781,18 @@ class BuildManager:
718
781
  if "project" in data and "name" in data["project"]:
719
782
  publish_package_name = data["project"]["name"]
720
783
 
784
+ # Ensure we have package name and version for filtering
785
+ if is_subfolder_build and not publish_package_name:
786
+ raise ValueError(
787
+ "Could not determine package name for subfolder build. "
788
+ "Please specify --package-name explicitly."
789
+ )
790
+ if is_subfolder_build and not publish_version:
791
+ raise ValueError(
792
+ "Version is required for subfolder builds. "
793
+ "Please specify --version explicitly."
794
+ )
795
+
721
796
  publisher = Publisher(
722
797
  repository=repository,
723
798
  dist_dir=self.project_root / "dist",
@@ -219,7 +219,21 @@ class Publisher:
219
219
 
220
220
  if matches:
221
221
  dist_files.append(f)
222
+
223
+ # Debug output to help diagnose filtering issues
224
+ if dist_files:
225
+ print(f"Filtering: package='{self.package_name}', version='{self.version}'")
226
+ print(f"Matched {len(dist_files)} files:")
227
+ for f in dist_files:
228
+ print(f" - {f.name}")
222
229
  else:
230
+ # If no package name or version provided, warn and upload all files
231
+ if not self.package_name or not self.version:
232
+ print(
233
+ f"Warning: No package name or version specified for filtering. "
234
+ f"Uploading all {len(all_dist_files)} files in dist/ directory.",
235
+ file=sys.stderr,
236
+ )
223
237
  dist_files = all_dist_files
224
238
 
225
239
  if not dist_files:
@@ -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,117 @@ 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 _add_dependencies_to_pyproject(self, content: str, dependencies: list[str]) -> str:
594
+ """
595
+ Add dependencies to pyproject.toml content.
596
+
597
+ Adds the specified dependencies to the [project] section's dependencies list.
598
+ If dependencies already exist, merges them. If no dependencies section exists,
599
+ creates one.
600
+
601
+ Args:
602
+ content: Current pyproject.toml content
603
+ dependencies: List of dependency names to add
604
+
605
+ Returns:
606
+ Updated pyproject.toml content with dependencies added
607
+ """
608
+ if not dependencies:
609
+ return content
610
+
611
+ lines = content.split("\n")
612
+ result = []
613
+ in_project = False
614
+ in_dependencies = False
615
+ dependencies_added = False
616
+ existing_deps: set[str] = set()
617
+
618
+ # First pass: find existing dependencies
619
+ for line in lines:
620
+ if line.strip().startswith("[project]"):
621
+ in_project = True
622
+ elif line.strip().startswith("[") and in_project:
623
+ in_project = False
624
+ elif in_project and re.match(r"^\s*dependencies\s*=\s*\[", line):
625
+ in_dependencies = True
626
+ elif in_dependencies:
627
+ # Extract existing dependency names
628
+ dep_match = re.search(r'["\']([^"\']+)["\']', line)
629
+ if dep_match:
630
+ existing_deps.add(dep_match.group(1))
631
+ if line.strip().endswith("]"):
632
+ in_dependencies = False
633
+
634
+ # Merge with new dependencies
635
+ all_deps = sorted(existing_deps | set(dependencies))
636
+
637
+ # Second pass: build result with dependencies
638
+ in_project = False
639
+ in_dependencies = False
640
+ for line in lines:
641
+ if line.strip().startswith("[project]"):
642
+ in_project = True
643
+ result.append(line)
644
+ elif line.strip().startswith("[") and in_project:
645
+ # End of [project] section, add dependencies if not already present
646
+ if not dependencies_added:
647
+ result.append("dependencies = [")
648
+ for dep in all_deps:
649
+ result.append(f' "{dep}",')
650
+ result.append("]")
651
+ result.append("")
652
+ in_project = False
653
+ result.append(line)
654
+ elif in_project and re.match(r"^\s*dependencies\s*=\s*\[", line):
655
+ # Replace existing dependencies section
656
+ result.append("dependencies = [")
657
+ for dep in all_deps:
658
+ result.append(f' "{dep}",')
659
+ result.append("]")
660
+ dependencies_added = True
661
+ in_dependencies = True
662
+ elif in_dependencies:
663
+ # Skip lines in existing dependencies section (already replaced)
664
+ if line.strip().endswith("]"):
665
+ in_dependencies = False
666
+ else:
667
+ result.append(line)
668
+
669
+ # If [project] section exists but no dependencies were added, add them
670
+ if in_project and not dependencies_added:
671
+ result.append("dependencies = [")
672
+ for dep in all_deps:
673
+ result.append(f' "{dep}",')
674
+ result.append("]")
675
+ result.append("")
676
+
677
+ return "\n".join(result)
678
+
562
679
  def _handle_readme(self) -> None:
563
680
  """
564
681
  Handle README file for subfolder builds.
@@ -0,0 +1,96 @@
1
+ """Pytest configuration and fixtures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+
11
+ @pytest.fixture(autouse=True, scope="session")
12
+ def protect_repository_files():
13
+ """
14
+ Automatically protect repository files from test modifications.
15
+
16
+ This fixture backs up pyproject.toml and README.md at the start of the test session
17
+ and restores them at the end, ensuring tests never permanently modify repository files.
18
+ """
19
+ # Get the repository root (parent of tests directory)
20
+ repo_root = Path(__file__).parent.parent
21
+
22
+ pyproject_path = repo_root / "pyproject.toml"
23
+ readme_path = repo_root / "README.md"
24
+
25
+ # Backup original files if they exist
26
+ pyproject_backup = None
27
+ readme_backup = None
28
+ pyproject_existed = pyproject_path.exists()
29
+ readme_existed = readme_path.exists()
30
+
31
+ if pyproject_existed:
32
+ pyproject_backup = pyproject_path.read_bytes()
33
+ if readme_existed:
34
+ readme_backup = readme_path.read_bytes()
35
+
36
+ # Yield control to tests
37
+ yield
38
+
39
+ # Restore original files after all tests
40
+ if pyproject_backup and pyproject_path.exists():
41
+ try:
42
+ current_content = pyproject_path.read_bytes()
43
+ if current_content != pyproject_backup:
44
+ # File was modified, restore it
45
+ pyproject_path.write_bytes(pyproject_backup)
46
+ print(
47
+ "Warning: Restored modified pyproject.toml after test session",
48
+ file=sys.stderr,
49
+ )
50
+ except OSError as e:
51
+ print(
52
+ f"Warning: Could not restore pyproject.toml: {e}",
53
+ file=sys.stderr,
54
+ )
55
+ elif pyproject_backup and not pyproject_path.exists():
56
+ # File was deleted, restore it
57
+ try:
58
+ pyproject_path.write_bytes(pyproject_backup)
59
+ print(
60
+ "Warning: Restored deleted pyproject.toml after test session",
61
+ file=sys.stderr,
62
+ )
63
+ except OSError as e:
64
+ print(
65
+ f"Warning: Could not restore pyproject.toml: {e}",
66
+ file=sys.stderr,
67
+ )
68
+
69
+ if readme_backup and readme_path.exists():
70
+ try:
71
+ current_content = readme_path.read_bytes()
72
+ if current_content != readme_backup:
73
+ # File was modified, restore it
74
+ readme_path.write_bytes(readme_backup)
75
+ print(
76
+ "Warning: Restored modified README.md after test session",
77
+ file=sys.stderr,
78
+ )
79
+ except OSError as e:
80
+ print(
81
+ f"Warning: Could not restore README.md: {e}",
82
+ file=sys.stderr,
83
+ )
84
+ elif readme_backup and not readme_path.exists():
85
+ # File was deleted, restore it
86
+ try:
87
+ readme_path.write_bytes(readme_backup)
88
+ print(
89
+ "Warning: Restored deleted README.md after test session",
90
+ file=sys.stderr,
91
+ )
92
+ except OSError as e:
93
+ print(
94
+ f"Warning: Could not restore README.md: {e}",
95
+ file=sys.stderr,
96
+ )
@@ -0,0 +1 @@
1
+ # Temporary __init__.py for build
@@ -0,0 +1 @@
1
+ SOME_GLOBAL_VARIABLE = "some_global_variable"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
5
6
  from pathlib import Path
6
7
 
7
8
  import pytest
@@ -369,6 +370,7 @@ class TestRealFolderStructure:
369
370
  if not src_dir.exists():
370
371
  pytest.skip("Real test structure not found")
371
372
 
373
+ # This test only reads files, doesn't modify them, so no backup needed
372
374
  finder = ExternalDependencyFinder(project_root, src_dir)
373
375
  analyzer = ImportAnalyzer(project_root)
374
376
 
@@ -392,12 +394,28 @@ class TestRealFolderStructure:
392
394
  if not src_dir.exists():
393
395
  pytest.skip("Real test structure not found")
394
396
 
395
- manager = BuildManager(project_root, src_dir)
397
+ # Save original repository files that might be modified
398
+ original_pyproject = project_root / "pyproject.toml"
399
+ original_readme = project_root / "README.md"
400
+ pyproject_backup = None
401
+ readme_backup = None
402
+ pyproject_existed = original_pyproject.exists()
403
+ readme_existed = original_readme.exists()
396
404
 
397
- # Prepare build
398
- external_deps = manager.prepare_build()
405
+ if pyproject_existed:
406
+ pyproject_backup = original_pyproject.read_bytes()
407
+ if readme_existed:
408
+ readme_backup = original_readme.read_bytes()
409
+
410
+ # Check what files exist before the build
411
+ some_globals_existed_before = (src_dir / "some_globals.py").exists()
412
+
413
+ manager = BuildManager(project_root, src_dir)
399
414
 
400
415
  try:
416
+ # Prepare build
417
+ external_deps = manager.prepare_build()
418
+
401
419
  # Verify dependencies were found
402
420
  assert len(external_deps) >= 1
403
421
 
@@ -418,8 +436,36 @@ class TestRealFolderStructure:
418
436
  # Always cleanup
419
437
  manager.cleanup()
420
438
 
421
- # Verify cleanup
422
- assert not (src_dir / "some_globals.py").exists()
439
+ # Restore original repository files if they were modified
440
+ if pyproject_backup and original_pyproject.exists():
441
+ # Verify the file was restored correctly, or restore it manually
442
+ try:
443
+ current_content = original_pyproject.read_bytes()
444
+ if current_content != pyproject_backup:
445
+ # File was modified, restore it
446
+ original_pyproject.write_bytes(pyproject_backup)
447
+ except Exception as e:
448
+ print(f"Warning: Could not verify/restore pyproject.toml: {e}", file=sys.stderr)
449
+ elif pyproject_backup and not original_pyproject.exists():
450
+ # File was deleted, restore it
451
+ original_pyproject.write_bytes(pyproject_backup)
452
+
453
+ if readme_backup and original_readme.exists():
454
+ # Verify the file was restored correctly, or restore it manually
455
+ try:
456
+ current_content = original_readme.read_bytes()
457
+ if current_content != readme_backup:
458
+ # File was modified, restore it
459
+ original_readme.write_bytes(readme_backup)
460
+ except Exception as e:
461
+ print(f"Warning: Could not verify/restore README.md: {e}", file=sys.stderr)
462
+ elif readme_backup and not original_readme.exists():
463
+ # File was deleted, restore it
464
+ original_readme.write_bytes(readme_backup)
465
+
466
+ # Verify cleanup - only check if the file didn't exist before
467
+ if not some_globals_existed_before:
468
+ assert not (src_dir / "some_globals.py").exists()
423
469
  if (src_dir / "utility_folder").exists():
424
470
  # May have been there originally, so just check it's not in copied_dirs
425
471
  assert (src_dir / "utility_folder") not in manager.copied_dirs
@@ -454,6 +500,19 @@ class TestExclusionPatterns:
454
500
  if not src_dir.exists():
455
501
  pytest.skip("Real test structure not found")
456
502
 
503
+ # Save original repository files that might be modified
504
+ original_pyproject = project_root / "pyproject.toml"
505
+ original_readme = project_root / "README.md"
506
+ pyproject_backup = None
507
+ readme_backup = None
508
+ pyproject_existed = original_pyproject.exists()
509
+ readme_existed = original_readme.exists()
510
+
511
+ if pyproject_existed:
512
+ pyproject_backup = original_pyproject.read_bytes()
513
+ if readme_existed:
514
+ readme_backup = original_readme.read_bytes()
515
+
457
516
  manager = BuildManager(project_root, src_dir)
458
517
 
459
518
  try:
@@ -474,6 +533,27 @@ class TestExclusionPatterns:
474
533
  finally:
475
534
  manager.cleanup()
476
535
 
536
+ # Restore original repository files if they were modified
537
+ if pyproject_backup and original_pyproject.exists():
538
+ try:
539
+ current_content = original_pyproject.read_bytes()
540
+ if current_content != pyproject_backup:
541
+ original_pyproject.write_bytes(pyproject_backup)
542
+ except Exception as e:
543
+ print(f"Warning: Could not verify/restore pyproject.toml: {e}", file=sys.stderr)
544
+ elif pyproject_backup and not original_pyproject.exists():
545
+ original_pyproject.write_bytes(pyproject_backup)
546
+
547
+ if readme_backup and original_readme.exists():
548
+ try:
549
+ current_content = original_readme.read_bytes()
550
+ if current_content != readme_backup:
551
+ original_readme.write_bytes(readme_backup)
552
+ except Exception as e:
553
+ print(f"Warning: Could not verify/restore README.md: {e}", file=sys.stderr)
554
+ elif readme_backup and not original_readme.exists():
555
+ original_readme.write_bytes(readme_backup)
556
+
477
557
  def test_exclude_custom_patterns(self, test_project_root: Path) -> None:
478
558
  """Test that custom exclusion patterns work."""
479
559
  # Create a directory with a custom exclusion pattern
@@ -524,6 +524,39 @@ version = "3.0.0"
524
524
  parent_pyproject.write_text(original_content)
525
525
  config.restore()
526
526
 
527
+ def test_third_party_dependencies_added(self, test_project_with_pyproject: Path) -> None:
528
+ """Test that third-party dependencies are added to temporary pyproject.toml."""
529
+ project_root = test_project_with_pyproject
530
+ subfolder = project_root / "subfolder"
531
+
532
+ # Create a Python file that imports a third-party package
533
+ (subfolder / "module.py").write_text("import pypdf\nimport requests\n")
534
+
535
+ config = SubfolderBuildConfig(
536
+ project_root=project_root,
537
+ src_dir=subfolder,
538
+ version="1.0.0",
539
+ package_name="test-package",
540
+ )
541
+
542
+ config.create_temp_pyproject()
543
+
544
+ # Add third-party dependencies
545
+ config.add_third_party_dependencies(["pypdf", "requests"])
546
+
547
+ # Verify dependencies were added
548
+ pyproject_path = project_root / "pyproject.toml"
549
+ assert pyproject_path.exists()
550
+ content = pyproject_path.read_text()
551
+
552
+ # Check that dependencies section exists and contains the packages
553
+ assert "dependencies = [" in content
554
+ assert '"pypdf"' in content or "'pypdf'" in content
555
+ assert '"requests"' in content or "'requests'" in content
556
+
557
+ # Cleanup
558
+ config.restore()
559
+
527
560
 
528
561
  class TestSubfolderBuildWithoutParentPyproject:
529
562
  """Tests for subfolder builds when parent pyproject.toml doesn't exist."""
@@ -642,6 +675,39 @@ class TestSubfolderBuildTemporaryPyprojectCreation:
642
675
  # Cleanup
643
676
  config.restore()
644
677
 
678
+ def test_third_party_dependencies_added(self, test_project_with_pyproject: Path) -> None:
679
+ """Test that third-party dependencies are added to temporary pyproject.toml."""
680
+ project_root = test_project_with_pyproject
681
+ subfolder = project_root / "subfolder"
682
+
683
+ # Create a Python file that imports a third-party package
684
+ (subfolder / "module.py").write_text("import pypdf\nimport requests\n")
685
+
686
+ config = SubfolderBuildConfig(
687
+ project_root=project_root,
688
+ src_dir=subfolder,
689
+ version="1.0.0",
690
+ package_name="test-package",
691
+ )
692
+
693
+ config.create_temp_pyproject()
694
+
695
+ # Add third-party dependencies
696
+ config.add_third_party_dependencies(["pypdf", "requests"])
697
+
698
+ # Verify dependencies were added
699
+ pyproject_path = project_root / "pyproject.toml"
700
+ assert pyproject_path.exists()
701
+ content = pyproject_path.read_text()
702
+
703
+ # Check that dependencies section exists and contains the packages
704
+ assert "dependencies = [" in content
705
+ assert '"pypdf"' in content or "'pypdf'" in content
706
+ assert '"requests"' in content or "'requests'" in content
707
+
708
+ # Cleanup
709
+ config.restore()
710
+
645
711
  def test_temporary_pyproject_preserves_other_sections(
646
712
  self, test_project_with_pyproject: Path
647
713
  ) -> None:
@@ -1,44 +0,0 @@
1
- name: Publish to PyPI
2
-
3
- on:
4
- release:
5
- types: [published]
6
- workflow_dispatch: # Enable manual trigger.
7
-
8
- jobs:
9
- build-and-publish:
10
- runs-on: ubuntu-latest
11
- permissions:
12
- id-token: write # Mandatory for OIDC.
13
- contents: read
14
- steps:
15
- - name: Checkout (official GitHub action)
16
- uses: actions/checkout@v4
17
- with:
18
- # Important for versioning plugins:
19
- fetch-depth: 0
20
-
21
- - name: Install uv (official Astral action)
22
- uses: astral-sh/setup-uv@v5
23
- with:
24
- version: "0.9.5"
25
- enable-cache: true
26
- python-version: "3.12"
27
-
28
- - name: Set up Python (using uv)
29
- run: uv python install
30
-
31
- - name: Install all dependencies
32
- run: uv sync --all-extras
33
-
34
- - name: Run tests
35
- run: uv run pytest
36
-
37
- - name: Build package
38
- run: uv build
39
-
40
- - name: Publish to PyPI
41
- run: uv publish --trusted-publishing always
42
- # Although uv is newer and faster, the "official" publishing option is the one from PyPA,
43
- # which uses twine. If desired, replace `uv publish` with:
44
- # uses: pypa/gh-action-pypi-publish@release/v1