python-package-folder 5.1.4__tar.gz → 5.1.6__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.1.4 → python_package_folder-5.1.6}/PKG-INFO +1 -1
  2. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/coverage.svg +2 -2
  3. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/pyproject.toml +1 -1
  4. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/publisher.py +60 -14
  5. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/subfolder_build.py +38 -27
  6. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_exclude_patterns.py +56 -0
  7. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.copier-answers.yml +0 -0
  8. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  9. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  10. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.cursor/rules/general.mdc +0 -0
  11. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.cursor/rules/python.mdc +0 -0
  12. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.github/workflows/ci.yml +0 -0
  13. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.github/workflows/publish.yml +0 -0
  14. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.gitignore +0 -0
  15. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/.vscode/settings.json +0 -0
  16. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/LICENSE +0 -0
  17. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/MANIFEST.in +0 -0
  18. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/Makefile +0 -0
  19. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/README.md +0 -0
  20. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/development.md +0 -0
  21. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/docs/DEVELOPMENT.md +0 -0
  22. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/docs/INSTALLATION.md +0 -0
  23. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/docs/PUBLISHING.md +0 -0
  24. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/docs/REFERENCE.md +0 -0
  25. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/docs/USAGE.md +0 -0
  26. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/docs/VERSION_RESOLUTION.md +0 -0
  27. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/installation.md +0 -0
  28. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/publishing.md +0 -0
  29. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/__init__.py +0 -0
  30. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/__main__.py +0 -0
  31. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/analyzer.py +0 -0
  32. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/finder.py +0 -0
  33. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/manager.py +0 -0
  34. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/py.typed +0 -0
  35. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/python_package_folder.py +0 -0
  36. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/types.py +0 -0
  37. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/utils.py +0 -0
  38. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/version.py +0 -0
  39. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/src/python_package_folder/version_calculator.py +0 -0
  40. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/conftest.py +0 -0
  41. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_linting.py +0 -0
  50. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_preserve_directory_structure.py +0 -0
  51. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_publisher.py +0 -0
  52. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_shared_subdirectory_imports.py +0 -0
  53. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_spreadsheet_creation_imports.py +0 -0
  54. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_subfolder_build.py +0 -0
  55. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_third_party_dependencies.py +0 -0
  56. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_utils.py +0 -0
  57. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_version_calculator.py +0 -0
  58. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/tests/tests.py +0 -0
  60. {python_package_folder-5.1.4 → python_package_folder-5.1.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 5.1.4
3
+ Version: 5.1.6
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">66%</text>
18
- <text x="81" y="14">66%</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>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "5.1.4"
46
+ version = "5.1.6"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -8,6 +8,7 @@ repositories including PyPI, PyPI Test, and Azure Artifacts.
8
8
  from __future__ import annotations
9
9
 
10
10
  import getpass
11
+ import os
11
12
  import subprocess
12
13
  import sys
13
14
  from enum import Enum
@@ -19,6 +20,18 @@ except ImportError:
19
20
  keyring = None
20
21
 
21
22
 
23
+ def _is_non_interactive() -> bool:
24
+ """Check if running in a non-interactive environment (CI/CD)."""
25
+ # Check for common CI environment variables
26
+ ci_vars = ["GITHUB_ACTIONS", "CI", "CONTINUOUS_INTEGRATION", "TF_BUILD"]
27
+ if any(os.getenv(var) for var in ci_vars):
28
+ return True
29
+ # Check if stdin is not a TTY (non-interactive)
30
+ if not sys.stdin.isatty():
31
+ return True
32
+ return False
33
+
34
+
22
35
  class Repository(Enum):
23
36
  """
24
37
  Supported package repositories.
@@ -112,9 +125,9 @@ class Publisher:
112
125
  """
113
126
  Get credentials for publishing.
114
127
 
115
- Always prompts for username and password/token if not already provided.
116
- Does not use keyring to store/retrieve credentials - credentials must be
117
- provided via command-line arguments or will be prompted each time.
128
+ Prompts for username and password/token if not already provided.
129
+ In non-interactive environments (CI/CD), checks environment variables
130
+ or raises an error if credentials are missing.
118
131
 
119
132
  Returns:
120
133
  Tuple of (username, password/token)
@@ -122,21 +135,54 @@ class Publisher:
122
135
  username = self.username
123
136
  password = self.password
124
137
 
125
- # Always prompt if not provided via command-line arguments
126
- # We don't use keyring to avoid storing credentials
138
+ is_non_interactive_env = _is_non_interactive()
139
+
140
+ # Get username
127
141
  if not username:
128
- username = input(f"Enter username for {self.repository.value}: ").strip()
129
- if not username:
130
- raise ValueError("Username is required")
142
+ if is_non_interactive_env:
143
+ # Check environment variables
144
+ username = os.getenv("TWINE_USERNAME") or os.getenv("PYPI_USERNAME")
145
+ if not username:
146
+ raise ValueError(
147
+ f"Username is required for publishing to {self.repository.value} in CI/CD. "
148
+ "Please provide --username argument or set TWINE_USERNAME/PYPI_USERNAME environment variable."
149
+ )
150
+ else:
151
+ username = input(f"Enter username for {self.repository.value}: ").strip()
152
+ if not username:
153
+ raise ValueError("Username is required")
131
154
 
155
+ # Get password
132
156
  if not password:
133
- if self.repository == Repository.AZURE:
134
- prompt = f"Enter Azure Artifacts token for {username}: "
157
+ if is_non_interactive_env:
158
+ # Check environment variables (common names used by twine and CI/CD)
159
+ password = (
160
+ os.getenv("TWINE_PASSWORD")
161
+ or os.getenv("PYPI_PASSWORD")
162
+ or os.getenv("AZURE_ARTIFACTS_TOKEN") # For Azure
163
+ )
164
+ if not password:
165
+ raise ValueError(
166
+ f"Password/token is required for publishing to {self.repository.value} in CI/CD. "
167
+ "Please provide --password argument or set one of: "
168
+ "TWINE_PASSWORD, PYPI_PASSWORD, or AZURE_ARTIFACTS_TOKEN environment variable."
169
+ )
135
170
  else:
136
- prompt = f"Enter PyPI token for {username} (or __token__ for API token): "
137
- password = getpass.getpass(prompt)
138
- if not password:
139
- raise ValueError("Password/token is required")
171
+ if self.repository == Repository.AZURE:
172
+ prompt = f"Enter Azure Artifacts token for {username}: "
173
+ else:
174
+ prompt = f"Enter PyPI token for {username} (or __token__ for API token): "
175
+ try:
176
+ password = getpass.getpass(prompt)
177
+ except (EOFError, OSError):
178
+ # Handle non-interactive environments gracefully
179
+ raise ValueError(
180
+ f"Password/token is required for publishing to {self.repository.value}. "
181
+ "Cannot prompt for password in non-interactive environment. "
182
+ "Please provide --password argument or set TWINE_PASSWORD/PYPI_PASSWORD environment variable."
183
+ )
184
+ if not password:
185
+ raise ValueError("Password/token is required")
140
186
 
141
187
  # Auto-detect if password is an API token and adjust username
142
188
  if password.startswith("pypi-") or password.startswith("pypi_Ag"):
@@ -694,44 +694,55 @@ class SubfolderBuildConfig:
694
694
  # Usually after [dependency-groups] or at the end
695
695
  insert_index = len(result)
696
696
  tool_section_exists = False
697
+ tool_section_start = -1
698
+ tool_section_end = -1
699
+
700
+ # First, search specifically for [tool.python-package-folder]
697
701
  for i, line in enumerate(result):
698
702
  if line.strip() == "[tool.python-package-folder]":
699
703
  tool_section_exists = True
700
- insert_index = i
701
- break
702
- elif line.strip().startswith("[tool.") and i > 0:
703
- # Insert before other tool sections
704
- insert_index = i
704
+ tool_section_start = i
705
+ # Find end of section (next [section] or end of file)
706
+ for j in range(i + 1, len(result)):
707
+ if result[j].strip().startswith("["):
708
+ tool_section_end = j
709
+ break
710
+ if tool_section_end == -1:
711
+ tool_section_end = len(result)
705
712
  break
713
+
714
+ # If not found, find a good insertion point before other [tool.*] sections
715
+ if not tool_section_exists:
716
+ for i, line in enumerate(result):
717
+ if line.strip().startswith("[tool.") and i > 0:
718
+ # Insert before other tool sections
719
+ insert_index = i
720
+ break
706
721
 
707
722
  # Format exclude patterns
708
723
  patterns_str = ", ".join(f'"{p}"' for p in exclude_patterns)
709
- exclude_lines = [
710
- "",
711
- "[tool.python-package-folder]",
712
- f'exclude-patterns = [{patterns_str}]',
713
- ]
714
724
 
715
725
  if tool_section_exists:
716
- # Replace or update existing section
717
- end_index = insert_index + 1
718
- while end_index < len(result) and not result[end_index].strip().startswith("["):
719
- end_index += 1
720
- # Check if exclude-patterns already exists
721
- has_exclude_patterns = any(
722
- "exclude-patterns" in result[i] for i in range(insert_index, end_index)
723
- )
724
- if has_exclude_patterns:
725
- # Update existing exclude-patterns line
726
- for i in range(insert_index, end_index):
727
- if "exclude-patterns" in result[i]:
728
- result[i] = f'exclude-patterns = [{patterns_str}]'
729
- break
730
- else:
731
- # Add exclude-patterns to existing section
732
- result.insert(end_index - 1, f'exclude-patterns = [{patterns_str}]')
726
+ # Update existing section
727
+ # Check if exclude-patterns already exists in the section
728
+ has_exclude_patterns = False
729
+ for i in range(tool_section_start + 1, tool_section_end):
730
+ if "exclude-patterns" in result[i]:
731
+ has_exclude_patterns = True
732
+ # Update the existing line
733
+ result[i] = f'exclude-patterns = [{patterns_str}]'
734
+ break
735
+
736
+ if not has_exclude_patterns:
737
+ # Add exclude-patterns to existing section (before the next section)
738
+ result.insert(tool_section_end, f'exclude-patterns = [{patterns_str}]')
733
739
  else:
734
740
  # Insert new section
741
+ exclude_lines = [
742
+ "",
743
+ "[tool.python-package-folder]",
744
+ f'exclude-patterns = [{patterns_str}]',
745
+ ]
735
746
  result[insert_index:insert_index] = exclude_lines
736
747
 
737
748
  return "\n".join(result)
@@ -211,4 +211,60 @@ class TestExcludePatternsInBuild:
211
211
  assert ".*_test.*" in content
212
212
  assert "sandbox" in content
213
213
 
214
+ # Verify there's only ONE [tool.python-package-folder] section (no duplicates)
215
+ sections = [line for line in content.split("\n") if line.strip() == "[tool.python-package-folder]"]
216
+ assert len(sections) == 1, f"Found {len(sections)} duplicate [tool.python-package-folder] sections"
217
+
218
+ config.restore()
219
+
220
+ def test_exclude_patterns_no_duplicate_section(self, tmp_path: Path) -> None:
221
+ """Test that exclude patterns don't create duplicate sections when original already has it."""
222
+ project_root = tmp_path / "test_project"
223
+ project_root.mkdir()
224
+
225
+ # Create pyproject.toml with existing [tool.python-package-folder] section
226
+ pyproject_content = """[project]
227
+ name = "test-package"
228
+ version = "0.1.0"
229
+
230
+ [build-system]
231
+ requires = ["hatchling"]
232
+ build-backend = "hatchling.build"
233
+
234
+ [tool.hatch.build.targets.wheel]
235
+ packages = ["src/test_package"]
236
+
237
+ [tool.python-package-folder]
238
+ exclude-patterns = ["_SS", ".*_test.*"]
239
+
240
+ [tool.pylint.'TYPECHECK']
241
+ generated-members = ["networkx.*"]
242
+ """
243
+ (project_root / "pyproject.toml").write_text(pyproject_content)
244
+
245
+ # Create source directory
246
+ src_dir = project_root / "src" / "test_package"
247
+ src_dir.mkdir(parents=True)
248
+ (src_dir / "__init__.py").write_text("")
249
+
250
+ config = SubfolderBuildConfig(
251
+ project_root=project_root,
252
+ src_dir=src_dir,
253
+ package_name="test-package",
254
+ version="1.0.0",
255
+ )
256
+
257
+ # Create temporary pyproject.toml
258
+ temp_pyproject = config.create_temp_pyproject()
259
+ assert temp_pyproject is not None
260
+
261
+ # Check that there's only ONE [tool.python-package-folder] section
262
+ content = temp_pyproject.read_text()
263
+ sections = [line for line in content.split("\n") if line.strip() == "[tool.python-package-folder]"]
264
+ assert len(sections) == 1, f"Found {len(sections)} duplicate [tool.python-package-folder] sections: {sections}"
265
+
266
+ # Verify exclude-patterns is present
267
+ assert "exclude-patterns" in content
268
+ assert "_SS" in content
269
+
214
270
  config.restore()