python-package-folder 5.0.0__tar.gz → 5.0.1__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 (60) hide show
  1. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/PKG-INFO +1 -1
  2. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/pyproject.toml +1 -1
  3. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/python_package_folder.py +9 -0
  4. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/version_calculator.py +121 -14
  5. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_version_calculator.py +12 -4
  6. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.copier-answers.yml +0 -0
  7. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  8. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  9. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.cursor/rules/general.mdc +0 -0
  10. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.cursor/rules/python.mdc +0 -0
  11. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.github/workflows/ci.yml +0 -0
  12. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.github/workflows/publish.yml +0 -0
  13. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.gitignore +0 -0
  14. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/.vscode/settings.json +0 -0
  15. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/LICENSE +0 -0
  16. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/MANIFEST.in +0 -0
  17. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/Makefile +0 -0
  18. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/README.md +0 -0
  19. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/coverage.svg +0 -0
  20. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/development.md +0 -0
  21. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/docs/DEVELOPMENT.md +0 -0
  22. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/docs/INSTALLATION.md +0 -0
  23. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/docs/PUBLISHING.md +0 -0
  24. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/docs/REFERENCE.md +0 -0
  25. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/docs/USAGE.md +0 -0
  26. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/docs/VERSION_RESOLUTION.md +0 -0
  27. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/installation.md +0 -0
  28. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/publishing.md +0 -0
  29. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/__init__.py +0 -0
  30. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/__main__.py +0 -0
  31. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/_hatch_build.py +0 -0
  32. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/analyzer.py +0 -0
  33. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/finder.py +0 -0
  34. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/manager.py +0 -0
  35. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/publisher.py +0 -0
  36. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/py.typed +0 -0
  37. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/subfolder_build.py +0 -0
  38. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/types.py +0 -0
  39. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/utils.py +0 -0
  40. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/src/python_package_folder/version.py +0 -0
  41. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/conftest.py +0 -0
  42. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/folder_structure/some_globals.py +0 -0
  43. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  44. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  45. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  46. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  47. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  48. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  49. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_build_with_external_deps.py +0 -0
  50. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_linting.py +0 -0
  51. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_publisher.py +0 -0
  53. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_subfolder_build.py +0 -0
  56. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_utils.py +0 -0
  58. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/tests/tests.py +0 -0
  60. {python_package_folder-5.0.0 → python_package_folder-5.0.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.0.0
3
+ Version: 5.0.1
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>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "5.0.0"
46
+ version = "5.0.1"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -10,6 +10,8 @@ It can be invoked via:
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import logging
14
+ import os
13
15
  import subprocess
14
16
  import sys
15
17
  from pathlib import Path
@@ -18,6 +20,13 @@ from .manager import BuildManager
18
20
  from .utils import find_project_root, find_source_directory
19
21
  from .version_calculator import resolve_version
20
22
 
23
+ # Configure logging for version resolution
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format="%(levelname)s: %(message)s",
27
+ handlers=[logging.StreamHandler(sys.stderr)],
28
+ )
29
+
21
30
 
22
31
  def is_github_actions() -> bool:
23
32
  """Check if running in GitHub Actions."""
@@ -10,12 +10,15 @@ Reference: https://semantic-release.gitbook.io/semantic-release/
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import logging
13
14
  import re
14
15
  import subprocess
15
16
  from pathlib import Path
16
17
 
17
18
  import requests
18
19
 
20
+ logger = logging.getLogger(__name__)
21
+
19
22
 
20
23
  def query_registry_version(
21
24
  package_name: str,
@@ -38,13 +41,26 @@ def query_registry_version(
38
41
 
39
42
  try:
40
43
  if repository in ("pypi", "testpypi"):
41
- return _query_pypi_version(package_name, repository)
44
+ logger.info(f"Querying {repository} for package '{package_name}'")
45
+ version = _query_pypi_version(package_name, repository)
46
+ if version:
47
+ logger.info(f"Found version {version} on {repository}")
48
+ else:
49
+ logger.info(f"Package '{package_name}' not found on {repository} (first release)")
50
+ return version
42
51
  elif repository == "azure":
43
52
  if not repository_url:
53
+ logger.warning("Azure Artifacts repository URL not provided")
44
54
  return None
45
- return _query_azure_artifacts_version(package_name, repository_url)
46
- except Exception:
47
- # Log error but don't fail - fall back to git tags
55
+ logger.info(f"Querying Azure Artifacts for package '{package_name}' at {repository_url}")
56
+ version = _query_azure_artifacts_version(package_name, repository_url)
57
+ if version:
58
+ logger.info(f"Found version {version} on Azure Artifacts")
59
+ else:
60
+ logger.info(f"Could not retrieve version from Azure Artifacts for '{package_name}' (will fall back to git tags)")
61
+ return version
62
+ except Exception as e:
63
+ logger.warning(f"Error querying {repository} for package '{package_name}': {e}", exc_info=True)
48
64
  return None
49
65
 
50
66
  return None
@@ -68,6 +84,7 @@ def _query_pypi_version(package_name: str, registry: str) -> str | None:
68
84
  response = requests.get(url, timeout=10)
69
85
  if response.status_code == 404:
70
86
  # Package doesn't exist yet (first release)
87
+ logger.debug(f"Package '{package_name}' not found on {registry} (404)")
71
88
  return None
72
89
  if response.status_code == 200:
73
90
  json_data = response.json()
@@ -80,9 +97,16 @@ def _query_pypi_version(package_name: str, registry: str) -> str | None:
80
97
  # Sort versions and get latest
81
98
  versions = sorted(releases.keys(), key=_parse_version_for_sort)
82
99
  version = versions[-1] if versions else None
100
+ if version:
101
+ logger.debug(f"Retrieved version {version} from {registry} for '{package_name}'")
83
102
  return version
103
+ logger.warning(f"Unexpected status code {response.status_code} from {registry} for '{package_name}'")
84
104
  return None
85
- except Exception:
105
+ except requests.RequestException as e:
106
+ logger.warning(f"Network error querying {registry} for '{package_name}': {e}")
107
+ return None
108
+ except Exception as e:
109
+ logger.warning(f"Error parsing response from {registry} for '{package_name}': {e}", exc_info=True)
86
110
  return None
87
111
 
88
112
 
@@ -111,17 +135,35 @@ def _query_azure_artifacts_version(
111
135
  simple_index_url = repository_url.replace("/upload", f"/simple/{package_name}/")
112
136
  else:
113
137
  simple_index_url = repository_url.rstrip("/") + f"/simple/{package_name}/"
114
- except Exception:
138
+ logger.debug(f"Constructed Azure Artifacts simple index URL: {simple_index_url}")
139
+ except Exception as e:
140
+ logger.warning(f"Error constructing Azure Artifacts URL for '{package_name}': {e}")
115
141
  return None
116
142
 
117
143
  try:
118
144
  response = requests.get(simple_index_url, timeout=5)
145
+ logger.debug(f"Azure Artifacts response status: {response.status_code}")
146
+
147
+ if response.status_code == 401:
148
+ logger.warning(f"Authentication required for Azure Artifacts (401). Package '{package_name}' may require authentication to query.")
149
+ elif response.status_code == 403:
150
+ logger.warning(f"Access forbidden for Azure Artifacts (403). Package '{package_name}' may not be accessible or requires different permissions.")
151
+ elif response.status_code == 404:
152
+ logger.debug(f"Package '{package_name}' not found on Azure Artifacts (404) - first release")
153
+ elif response.status_code != 200:
154
+ logger.warning(f"Unexpected status code {response.status_code} from Azure Artifacts for '{package_name}'")
155
+
119
156
  # Azure Artifacts simple index returns HTML, not JSON
120
157
  # Parsing HTML is complex and may require authentication
121
158
  # For now, we'll return None to fall back to git tags
122
159
  # This can be enhanced later with proper HTML parsing or API endpoint discovery
160
+ logger.info(f"Azure Artifacts version query not fully implemented (HTML parsing required). Falling back to git tags.")
123
161
  return None
124
- except Exception:
162
+ except requests.RequestException as e:
163
+ logger.warning(f"Network error querying Azure Artifacts for '{package_name}': {e}")
164
+ return None
165
+ except Exception as e:
166
+ logger.warning(f"Unexpected error querying Azure Artifacts for '{package_name}': {e}", exc_info=True)
125
167
  return None
126
168
 
127
169
 
@@ -465,26 +507,91 @@ def resolve_version(
465
507
  # Step 1: Try to get baseline version from registry
466
508
  baseline_version = None
467
509
  if repository and package_name:
510
+ logger.info(f"Attempting to query {repository} for baseline version of '{package_name}'")
468
511
  baseline_version = query_registry_version(package_name, repository, repository_url)
469
512
 
470
513
  # Step 2: Fallback to git tags if registry query failed
471
514
  if not baseline_version:
515
+ logger.info(f"Registry query did not return a version, falling back to git tags")
516
+ if is_subfolder:
517
+ logger.debug(f"Looking for subfolder git tags matching '{package_name}-v*'")
518
+ else:
519
+ logger.debug("Looking for main package git tags matching 'v*'")
472
520
  baseline_version = get_latest_git_tag(project_root, package_name, is_subfolder)
521
+ if baseline_version:
522
+ logger.info(f"Found baseline version {baseline_version} from git tags")
523
+ else:
524
+ logger.info("No git tags found")
473
525
 
474
- # Step 3: If still no baseline, we can't calculate next version
526
+ # Step 3: If still no baseline, this is likely the first release
527
+ # Default to 0.0.0 as the starting version (standard semantic-release behavior)
475
528
  if not baseline_version:
476
- # This could be the first release - we'd need to decide on a starting version
477
- # For now, return None to indicate we can't determine the version
478
- return None, "No baseline version found (no registry version or git tags)"
479
-
480
- # Step 4: Get commits since baseline
481
- commits = get_commits_since(project_root, baseline_version, subfolder_path, package_name)
529
+ logger.info("No baseline version found (no registry version or git tags). Treating as first release (baseline: 0.0.0)")
530
+ baseline_version = "0.0.0"
531
+ # For first release, get all commits (no baseline to compare against)
532
+ # Use HEAD as the reference point to get all commits
533
+ try:
534
+ cmd = ["git", "log", "--format=%B", "HEAD"]
535
+ if subfolder_path:
536
+ # Convert to relative path from project root
537
+ if subfolder_path.is_absolute():
538
+ rel_path = subfolder_path.relative_to(project_root)
539
+ else:
540
+ rel_path = subfolder_path
541
+ cmd.append("--")
542
+ cmd.append(str(rel_path))
543
+
544
+ result = subprocess.run(
545
+ cmd,
546
+ cwd=project_root,
547
+ capture_output=True,
548
+ text=True,
549
+ check=False,
550
+ )
551
+
552
+ commits = []
553
+ if result.returncode == 0:
554
+ # Split commits (they're separated by double newlines in --format=%B)
555
+ current_commit = []
556
+ for line in result.stdout.split("\n"):
557
+ if line.strip() == "" and current_commit:
558
+ commit_msg = "\n".join(current_commit).strip()
559
+ if commit_msg:
560
+ commits.append(commit_msg)
561
+ current_commit = []
562
+ else:
563
+ current_commit.append(line)
564
+
565
+ # Don't forget the last commit if there's no trailing newline
566
+ if current_commit:
567
+ commit_msg = "\n".join(current_commit).strip()
568
+ if commit_msg:
569
+ commits.append(commit_msg)
570
+ logger.debug(f"Retrieved {len(commits)} commits for first release")
571
+ except Exception as e:
572
+ logger.warning(f"Error retrieving commits for first release: {e}", exc_info=True)
573
+ commits = []
574
+ else:
575
+ # Step 4: Get commits since baseline
576
+ logger.info(f"Retrieving commits since version {baseline_version}")
577
+ if subfolder_path:
578
+ logger.debug(f"Filtering commits for subfolder path: {subfolder_path}")
579
+ commits = get_commits_since(project_root, baseline_version, subfolder_path, package_name)
580
+ logger.debug(f"Found {len(commits)} commits since {baseline_version}")
482
581
 
483
582
  # Step 5: Calculate next version
583
+ logger.info(f"Calculating next version from baseline {baseline_version} and {len(commits)} commits")
484
584
  next_version = calculate_next_version(baseline_version, commits)
485
585
 
486
586
  if next_version:
587
+ logger.info(f"Calculated next version: {next_version}")
487
588
  return next_version, None
488
589
  else:
489
590
  # No relevant commits for version bump
591
+ # For first release (0.0.0), default to 0.1.0 if there are any commits
592
+ if baseline_version == "0.0.0" and commits:
593
+ # Even if commits don't match conventional format, start at 0.1.0 for first release
594
+ logger.info("No conventional commits found, but commits exist. Defaulting to 0.1.0 for first release")
595
+ return "0.1.0", None
596
+ logger.info("No version bump needed (no relevant conventional commits found)")
490
597
  return None, None
@@ -414,14 +414,22 @@ class TestResolveVersion:
414
414
 
415
415
  @patch("python_package_folder.version_calculator.query_registry_version")
416
416
  @patch("python_package_folder.version_calculator.get_latest_git_tag")
417
+ @patch("python_package_folder.version_calculator.subprocess.run")
417
418
  def test_resolve_version_no_baseline(
418
419
  self,
420
+ mock_subprocess: MagicMock,
419
421
  mock_git_tag: MagicMock,
420
422
  mock_registry: MagicMock,
421
423
  ) -> None:
422
- """Test error when no baseline version is found."""
424
+ """Test first release behavior when no baseline version is found."""
423
425
  mock_registry.return_value = None
424
426
  mock_git_tag.return_value = None
427
+
428
+ # Mock git log for first release with conventional commits
429
+ mock_result = Mock()
430
+ mock_result.returncode = 0
431
+ mock_result.stdout = "feat: initial feature\n\n"
432
+ mock_subprocess.return_value = mock_result
425
433
 
426
434
  version, error = resolve_version(
427
435
  Path("/tmp/test"),
@@ -429,9 +437,9 @@ class TestResolveVersion:
429
437
  repository="pypi",
430
438
  )
431
439
 
432
- assert version is None
433
- assert error is not None
434
- assert "baseline version" in error.lower()
440
+ # For first release with feat commit, should calculate 0.1.0 (0.0.0 + minor)
441
+ assert version == "0.1.0"
442
+ assert error is None
435
443
 
436
444
  @patch("python_package_folder.version_calculator.query_registry_version")
437
445
  @patch("python_package_folder.version_calculator.get_commits_since")