python-package-folder 2.0.5__tar.gz → 2.0.7__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 (47) hide show
  1. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/PKG-INFO +1 -1
  2. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/analyzer.py +73 -0
  3. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/manager.py +14 -4
  4. python_package_folder-2.0.7/tests/test_shared_subdirectory_imports.py +107 -0
  5. python_package_folder-2.0.7/tests/test_spreadsheet_creation_imports.py +162 -0
  6. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/.copier-answers.yml +0 -0
  7. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/.cursor/rules/general.mdc +0 -0
  8. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/.cursor/rules/python.mdc +0 -0
  9. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/.github/workflows/ci.yml +0 -0
  10. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/.github/workflows/publish.yml +0 -0
  11. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/.gitignore +0 -0
  12. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/.vscode/settings.json +0 -0
  13. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/LICENSE +0 -0
  14. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/Makefile +0 -0
  15. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/README.md +0 -0
  16. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/coverage.svg +0 -0
  17. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/development.md +0 -0
  18. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/installation.md +0 -0
  19. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/publishing.md +0 -0
  20. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/pyproject.toml +0 -0
  21. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/__init__.py +0 -0
  22. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/__main__.py +0 -0
  23. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/finder.py +0 -0
  24. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/publisher.py +0 -0
  25. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/py.typed +0 -0
  26. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/python_package_folder.py +0 -0
  27. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/subfolder_build.py +0 -0
  28. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/types.py +0 -0
  29. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/utils.py +0 -0
  30. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/src/python_package_folder/version.py +0 -0
  31. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/conftest.py +0 -0
  32. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/folder_structure/some_globals.py +0 -0
  33. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  34. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  35. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  36. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  37. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  38. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  39. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/test_build_with_external_deps.py +0 -0
  40. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/test_linting.py +0 -0
  41. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/test_publisher.py +0 -0
  42. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/test_subfolder_build.py +0 -0
  43. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/test_third_party_dependencies.py +0 -0
  44. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/test_utils.py +0 -0
  45. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/test_version_manager.py +0 -0
  46. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/tests/tests.py +0 -0
  47. {python_package_folder-2.0.5 → python_package_folder-2.0.7}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 2.0.5
3
+ Version: 2.0.7
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>
@@ -328,6 +328,79 @@ class ImportAnalyzer:
328
328
  if potential_file.exists():
329
329
  return potential_file
330
330
 
331
+ # Check all subdirectories in parent (not just common ones)
332
+ # This handles cases like src/data/spreadsheet_creation/spreadsheet_formatting_dataclasses.py
333
+ if parent.is_dir():
334
+ try:
335
+ for subdir in parent.iterdir():
336
+ if not subdir.is_dir():
337
+ continue
338
+ # Skip common excluded patterns
339
+ if subdir.name.startswith("_SS") or subdir.name.startswith("__SS"):
340
+ continue
341
+ # Check if module file exists directly in subdirectory
342
+ potential_subdir_file = subdir / f"{module_name.split('.')[-1]}.py"
343
+ if potential_subdir_file.exists():
344
+ return potential_subdir_file
345
+ # Check if module directory exists in subdirectory
346
+ potential_subdir_module = subdir / module_name.replace(".", "/")
347
+ if (
348
+ potential_subdir_module.is_dir()
349
+ and (potential_subdir_module / "__init__.py").exists()
350
+ ):
351
+ return potential_subdir_module / "__init__.py"
352
+ if potential_subdir_module.with_suffix(".py").is_file():
353
+ return potential_subdir_module.with_suffix(".py")
354
+ # Check nested subdirectories (e.g., data/spreadsheet_creation)
355
+ # Recursively check subdirectories up to 2 levels deep
356
+ try:
357
+ for nested_subdir in subdir.iterdir():
358
+ if not nested_subdir.is_dir():
359
+ continue
360
+ # Check if module file exists in nested subdirectory
361
+ potential_nested_file = (
362
+ nested_subdir / f"{module_name.split('.')[-1]}.py"
363
+ )
364
+ if potential_nested_file.exists():
365
+ return potential_nested_file
366
+ # Check if module directory exists in nested subdirectory
367
+ potential_nested_module = nested_subdir / module_name.replace(
368
+ ".", "/"
369
+ )
370
+ if (
371
+ potential_nested_module.is_dir()
372
+ and (potential_nested_module / "__init__.py").exists()
373
+ ):
374
+ return potential_nested_module / "__init__.py"
375
+ if potential_nested_module.with_suffix(".py").is_file():
376
+ return potential_nested_module.with_suffix(".py")
377
+ except (OSError, PermissionError):
378
+ # Skip nested directories we can't read
379
+ continue
380
+ except (OSError, PermissionError):
381
+ # Skip directories we can't read
382
+ continue
383
+
384
+ # Check common subdirectories in parent (e.g., _shared, shared, common)
385
+ # This handles cases like src/_shared/better_enum.py
386
+ common_subdirs = ["_shared", "shared", "common", "_common"]
387
+ for subdir_name in common_subdirs:
388
+ subdir = parent / subdir_name
389
+ if subdir.exists() and subdir.is_dir():
390
+ # Check if module file exists in subdirectory
391
+ potential_subdir_file = subdir / f"{module_name.split('.')[-1]}.py"
392
+ if potential_subdir_file.exists():
393
+ return potential_subdir_file
394
+ # Check if module directory exists in subdirectory
395
+ potential_subdir_module = subdir / module_name.replace(".", "/")
396
+ if (
397
+ potential_subdir_module.is_dir()
398
+ and (potential_subdir_module / "__init__.py").exists()
399
+ ):
400
+ return potential_subdir_module / "__init__.py"
401
+ if potential_subdir_module.with_suffix(".py").is_file():
402
+ return potential_subdir_module.with_suffix(".py")
403
+
331
404
  return None
332
405
 
333
406
  def is_third_party(self, module_name: str) -> bool:
@@ -541,8 +541,16 @@ class BuildManager:
541
541
  if root_module in stdlib_modules:
542
542
  continue
543
543
 
544
+ # Skip if it's local or external (already copied, don't add as dependency)
545
+ if imp.classification in ("local", "external"):
546
+ continue
547
+
544
548
  # If classified as third_party, try to get actual package name
545
549
  if imp.classification == "third_party":
550
+ # Double-check: if it resolves to a file in src_dir, it's actually local
551
+ # (might have been copied and now resolves locally)
552
+ if imp.resolved_path and imp.resolved_path.is_relative_to(self.src_dir):
553
+ continue # Skip - it's a local file, not a third-party package
546
554
  # Check cache first
547
555
  if root_module not in package_name_cache:
548
556
  package_name_cache[root_module] = self._get_package_name_from_import(
@@ -555,21 +563,23 @@ class BuildManager:
555
563
  # Fallback to using the import name
556
564
  third_party_packages.add(root_module)
557
565
  # If it's ambiguous or unresolved, and not stdlib/local/external,
558
- # it's likely a third-party package that needs to be declared
566
+ # only add as dependency if we can verify it's actually an installed package
559
567
  elif imp.classification == "ambiguous" or imp.classification is None:
560
568
  # Check if it's not a local or external module
561
569
  if not imp.resolved_path:
570
+ # Try to verify it's actually an installed package before adding
562
571
  # Check cache first
563
572
  if root_module not in package_name_cache:
564
573
  package_name_cache[root_module] = self._get_package_name_from_import(
565
574
  imp.module_name
566
575
  )
567
576
  actual_package = package_name_cache[root_module]
577
+ # Only add if we can verify it's an actual installed package
578
+ # Don't add ambiguous imports that we can't verify
568
579
  if actual_package:
569
580
  third_party_packages.add(actual_package)
570
- else:
571
- # Fallback: use import name (will be normalized later)
572
- third_party_packages.add(root_module)
581
+ # If we can't verify it's a package, don't add it
582
+ # (it's likely a local file that wasn't resolved properly)
573
583
 
574
584
  if total_files > 50:
575
585
  print() # New line after progress indicator
@@ -0,0 +1,107 @@
1
+ """Tests for imports from _shared subdirectories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from python_package_folder import BuildManager, ImportAnalyzer, ImportInfo
10
+
11
+
12
+ @pytest.fixture
13
+ def test_project_with_shared(tmp_path: Path) -> Path:
14
+ """Create a test project with _shared subdirectory."""
15
+ project_root = tmp_path / "test_project"
16
+ project_root.mkdir()
17
+
18
+ # Create src directory
19
+ src_dir = project_root / "src"
20
+ src_dir.mkdir()
21
+
22
+ # Create _shared directory with better_enum.py
23
+ shared_dir = src_dir / "_shared"
24
+ shared_dir.mkdir()
25
+ (shared_dir / "better_enum.py").write_text(
26
+ """class Enum:
27
+ pass
28
+ """
29
+ )
30
+
31
+ # Create subfolder_to_build
32
+ subfolder = src_dir / "integration" / "empty_drawing_detection"
33
+ subfolder.mkdir(parents=True)
34
+ (subfolder / "module.py").write_text(
35
+ """from better_enum import Enum
36
+
37
+ def use_enum():
38
+ return Enum
39
+ """
40
+ )
41
+
42
+ return project_root
43
+
44
+
45
+ class TestSharedSubdirectoryImports:
46
+ """Tests for imports from _shared subdirectories."""
47
+
48
+ def test_resolve_better_enum_from_shared(self, test_project_with_shared: Path) -> None:
49
+ """Test that better_enum is resolved from src/_shared/better_enum.py."""
50
+ project_root = test_project_with_shared
51
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
52
+
53
+ analyzer = ImportAnalyzer(project_root)
54
+
55
+ # Create import info for better_enum
56
+ import_info = ImportInfo(
57
+ module_name="better_enum",
58
+ import_type="from",
59
+ line_number=1,
60
+ file_path=src_dir / "module.py",
61
+ )
62
+
63
+ # Classify the import
64
+ analyzer.classify_import(import_info, src_dir)
65
+
66
+ # Should be classified as external (not third_party)
67
+ assert import_info.classification == "external"
68
+ assert import_info.resolved_path is not None
69
+ assert import_info.resolved_path.name == "better_enum.py"
70
+ # Should resolve to src/_shared/better_enum.py
71
+ assert "_shared" in str(import_info.resolved_path)
72
+ assert import_info.resolved_path.exists()
73
+
74
+ def test_better_enum_copied_not_added_as_dependency(
75
+ self, test_project_with_shared: Path
76
+ ) -> None:
77
+ """Test that better_enum is copied as external dependency, not added as third-party."""
78
+ project_root = test_project_with_shared
79
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
80
+
81
+ manager = BuildManager(project_root, src_dir)
82
+
83
+ # Prepare build
84
+ external_deps = manager.prepare_build(version="1.0.0", package_name="test-package")
85
+
86
+ # Should find better_enum as an external dependency to copy
87
+ better_enum_deps = [dep for dep in external_deps if "better_enum" in dep.import_name]
88
+ assert len(better_enum_deps) > 0, "better_enum should be found as external dependency"
89
+
90
+ # Verify _shared directory was copied (which contains better_enum.py)
91
+ copied_shared_dir = src_dir / "_shared"
92
+ assert copied_shared_dir.exists(), "_shared directory should be copied to subfolder"
93
+ copied_file = copied_shared_dir / "better_enum.py"
94
+ assert copied_file.exists(), "better_enum.py should be in copied _shared directory"
95
+
96
+ # Check that subfolder_config exists (for subfolder builds)
97
+ if manager.subfolder_config:
98
+ # Read the temporary pyproject.toml
99
+ pyproject_path = project_root / "pyproject.toml"
100
+ if pyproject_path.exists():
101
+ content = pyproject_path.read_text()
102
+ # Should NOT have better-enum or better_enum in dependencies
103
+ # (it should be copied, not added as dependency)
104
+ assert '"better-enum"' not in content
105
+ assert '"better_enum"' not in content
106
+
107
+ manager.cleanup()
@@ -0,0 +1,162 @@
1
+ """Tests for imports from spreadsheet_creation subdirectory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from python_package_folder import BuildManager, ImportAnalyzer, ImportInfo
10
+
11
+
12
+ @pytest.fixture
13
+ def test_project_with_spreadsheet_creation(tmp_path: Path) -> Path:
14
+ """Create a test project with spreadsheet_creation subdirectory."""
15
+ project_root = tmp_path / "test_project"
16
+ project_root.mkdir()
17
+
18
+ # Create src directory
19
+ src_dir = project_root / "src"
20
+ src_dir.mkdir()
21
+
22
+ # Create data/spreadsheet_creation directory with files
23
+ data_dir = src_dir / "data"
24
+ data_dir.mkdir()
25
+ spreadsheet_creation_dir = data_dir / "spreadsheet_creation"
26
+ spreadsheet_creation_dir.mkdir()
27
+ (spreadsheet_creation_dir / "spreadsheet_formatting_dataclasses.py").write_text(
28
+ """class DataClass:
29
+ pass
30
+ """
31
+ )
32
+ (spreadsheet_creation_dir / "spreadsheet_utils.py").write_text(
33
+ """def util_func():
34
+ pass
35
+ """
36
+ )
37
+
38
+ # Create subfolder_to_build
39
+ subfolder = src_dir / "integration" / "empty_drawing_detection"
40
+ subfolder.mkdir(parents=True)
41
+ (subfolder / "module.py").write_text(
42
+ """from spreadsheet_formatting_dataclasses import DataClass
43
+ from spreadsheet_utils import util_func
44
+
45
+ def use_spreadsheet():
46
+ return DataClass, util_func
47
+ """
48
+ )
49
+
50
+ return project_root
51
+
52
+
53
+ class TestSpreadsheetCreationImports:
54
+ """Tests for imports from spreadsheet_creation subdirectory."""
55
+
56
+ def test_resolve_spreadsheet_formatting_dataclasses(
57
+ self, test_project_with_spreadsheet_creation: Path
58
+ ) -> None:
59
+ """Test that spreadsheet_formatting_dataclasses is resolved from data/spreadsheet_creation."""
60
+ project_root = test_project_with_spreadsheet_creation
61
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
62
+
63
+ analyzer = ImportAnalyzer(project_root)
64
+
65
+ # Create import info for spreadsheet_formatting_dataclasses
66
+ import_info = ImportInfo(
67
+ module_name="spreadsheet_formatting_dataclasses",
68
+ import_type="from",
69
+ line_number=1,
70
+ file_path=src_dir / "module.py",
71
+ )
72
+
73
+ # Classify the import
74
+ analyzer.classify_import(import_info, src_dir)
75
+
76
+ # Should be classified as external (not third_party)
77
+ assert import_info.classification == "external"
78
+ assert import_info.resolved_path is not None
79
+ assert import_info.resolved_path.name == "spreadsheet_formatting_dataclasses.py"
80
+ # Should resolve to src/data/spreadsheet_creation/spreadsheet_formatting_dataclasses.py
81
+ assert "spreadsheet_creation" in str(import_info.resolved_path)
82
+ assert import_info.resolved_path.exists()
83
+
84
+ def test_resolve_spreadsheet_utils(self, test_project_with_spreadsheet_creation: Path) -> None:
85
+ """Test that spreadsheet_utils is resolved from data/spreadsheet_creation."""
86
+ project_root = test_project_with_spreadsheet_creation
87
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
88
+
89
+ analyzer = ImportAnalyzer(project_root)
90
+
91
+ # Create import info for spreadsheet_utils
92
+ import_info = ImportInfo(
93
+ module_name="spreadsheet_utils",
94
+ import_type="from",
95
+ line_number=2,
96
+ file_path=src_dir / "module.py",
97
+ )
98
+
99
+ # Classify the import
100
+ analyzer.classify_import(import_info, src_dir)
101
+
102
+ # Should be classified as external (not third_party)
103
+ assert import_info.classification == "external"
104
+ assert import_info.resolved_path is not None
105
+ assert import_info.resolved_path.name == "spreadsheet_utils.py"
106
+ assert "spreadsheet_creation" in str(import_info.resolved_path)
107
+ assert import_info.resolved_path.exists()
108
+
109
+ def test_spreadsheet_modules_copied_not_added_as_dependencies(
110
+ self, test_project_with_spreadsheet_creation: Path
111
+ ) -> None:
112
+ """Test that spreadsheet modules are copied, not added as dependencies."""
113
+ project_root = test_project_with_spreadsheet_creation
114
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
115
+
116
+ # Create pyproject.toml for the test
117
+ (project_root / "pyproject.toml").write_text(
118
+ """[project]
119
+ name = "test-package"
120
+ version = "0.1.0"
121
+
122
+ [tool.hatch.build.targets.wheel]
123
+ packages = ["src/test_package"]
124
+ """
125
+ )
126
+
127
+ manager = BuildManager(project_root, src_dir)
128
+
129
+ # Prepare build
130
+ external_deps = manager.prepare_build(version="1.0.0", package_name="test-package")
131
+
132
+ # Should find spreadsheet modules as external dependencies to copy
133
+ spreadsheet_deps = [
134
+ dep for dep in external_deps if "spreadsheet" in dep.import_name.lower()
135
+ ]
136
+ assert len(spreadsheet_deps) > 0, (
137
+ "spreadsheet modules should be found as external dependencies"
138
+ )
139
+
140
+ # Verify spreadsheet_creation directory was copied
141
+ copied_dir = src_dir / "spreadsheet_creation"
142
+ assert copied_dir.exists(), "spreadsheet_creation directory should be copied"
143
+ assert (copied_dir / "spreadsheet_formatting_dataclasses.py").exists(), (
144
+ "spreadsheet_formatting_dataclasses.py should be copied"
145
+ )
146
+ assert (copied_dir / "spreadsheet_utils.py").exists(), (
147
+ "spreadsheet_utils.py should be copied"
148
+ )
149
+
150
+ # Check that subfolder_config exists (for subfolder builds)
151
+ if manager.subfolder_config:
152
+ # Read the temporary pyproject.toml
153
+ pyproject_path = project_root / "pyproject.toml"
154
+ if pyproject_path.exists():
155
+ content = pyproject_path.read_text()
156
+ # Should NOT have spreadsheet-formatting-dataclasses or spreadsheet-utils in dependencies
157
+ assert '"spreadsheet-formatting-dataclasses"' not in content
158
+ assert '"spreadsheet-utils"' not in content
159
+ assert '"spreadsheet_formatting_dataclasses"' not in content
160
+ assert '"spreadsheet_utils"' not in content
161
+
162
+ manager.cleanup()