python-package-folder 1.1.3__py3-none-any.whl
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/__init__.py +24 -0
- python_package_folder/__main__.py +13 -0
- python_package_folder/analyzer.py +313 -0
- python_package_folder/finder.py +234 -0
- python_package_folder/manager.py +539 -0
- python_package_folder/publisher.py +310 -0
- python_package_folder/py.typed +0 -0
- python_package_folder/python_package_folder.py +239 -0
- python_package_folder/subfolder_build.py +477 -0
- python_package_folder/types.py +66 -0
- python_package_folder/utils.py +106 -0
- python_package_folder/version.py +253 -0
- python_package_folder-1.1.3.dist-info/METADATA +795 -0
- python_package_folder-1.1.3.dist-info/RECORD +17 -0
- python_package_folder-1.1.3.dist-info/WHEEL +4 -0
- python_package_folder-1.1.3.dist-info/entry_points.txt +2 -0
- python_package_folder-1.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Python package folder - Build Python packages with external dependency management."""
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
"BuildManager",
|
|
5
|
+
"ExternalDependency",
|
|
6
|
+
"ExternalDependencyFinder",
|
|
7
|
+
"ImportAnalyzer",
|
|
8
|
+
"ImportInfo",
|
|
9
|
+
"Publisher",
|
|
10
|
+
"Repository",
|
|
11
|
+
"SubfolderBuildConfig",
|
|
12
|
+
"VersionManager",
|
|
13
|
+
"find_project_root",
|
|
14
|
+
"find_source_directory",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .analyzer import ImportAnalyzer
|
|
18
|
+
from .finder import ExternalDependencyFinder
|
|
19
|
+
from .manager import BuildManager
|
|
20
|
+
from .publisher import Publisher, Repository
|
|
21
|
+
from .subfolder_build import SubfolderBuildConfig
|
|
22
|
+
from .types import ExternalDependency, ImportInfo
|
|
23
|
+
from .utils import find_project_root, find_source_directory
|
|
24
|
+
from .version import VersionManager
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Allow running the package as a module.
|
|
3
|
+
|
|
4
|
+
This module enables running the package with:
|
|
5
|
+
python -m python_package_folder
|
|
6
|
+
|
|
7
|
+
It simply delegates to the main() function from python_package_folder.py.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .python_package_folder import main
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
exit(main())
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Import analysis functionality.
|
|
3
|
+
|
|
4
|
+
This module provides the ImportAnalyzer class which is responsible for:
|
|
5
|
+
- Finding all Python files in a directory tree
|
|
6
|
+
- Extracting import statements using AST parsing
|
|
7
|
+
- Classifying imports as stdlib, third-party, local, external, or ambiguous
|
|
8
|
+
- Resolving import paths to actual file locations
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import importlib.util
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .types import ImportInfo
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ImportAnalyzer:
|
|
22
|
+
"""
|
|
23
|
+
Analyzes Python files to extract and classify import statements.
|
|
24
|
+
|
|
25
|
+
This class uses Python's AST module to parse Python files and extract
|
|
26
|
+
all import statements. It can classify imports into different categories
|
|
27
|
+
and resolve their file paths.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
project_root: Root directory of the project
|
|
31
|
+
_stdlib_modules: Cached set of standard library module names
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, project_root: Path) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Initialize the import analyzer.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
project_root: Root directory of the project to analyze
|
|
40
|
+
"""
|
|
41
|
+
self.project_root = project_root.resolve()
|
|
42
|
+
self._stdlib_modules: set[str] | None = None
|
|
43
|
+
|
|
44
|
+
def find_all_python_files(self, directory: Path) -> list[Path]:
|
|
45
|
+
"""
|
|
46
|
+
Recursively find all Python files in a directory.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
directory: Directory to search for Python files
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of paths to all .py files found in the directory tree
|
|
53
|
+
"""
|
|
54
|
+
return [path for path in directory.rglob("*.py") if path.is_file()]
|
|
55
|
+
|
|
56
|
+
def extract_imports(self, file_path: Path) -> list[ImportInfo]:
|
|
57
|
+
"""
|
|
58
|
+
Extract all import statements from a Python file.
|
|
59
|
+
|
|
60
|
+
Uses AST parsing to find both `import` and `from ... import` statements.
|
|
61
|
+
Handles syntax errors gracefully by returning an empty list.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
file_path: Path to the Python file to analyze
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of ImportInfo objects representing all imports found in the file
|
|
68
|
+
"""
|
|
69
|
+
imports: list[ImportInfo] = []
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
content = file_path.read_text(encoding="utf-8")
|
|
73
|
+
tree = ast.parse(content, filename=str(file_path))
|
|
74
|
+
except (SyntaxError, UnicodeDecodeError) as e:
|
|
75
|
+
print(f"Warning: Could not parse {file_path}: {e}", file=sys.stderr)
|
|
76
|
+
return imports
|
|
77
|
+
|
|
78
|
+
for node in ast.walk(tree):
|
|
79
|
+
if isinstance(node, ast.Import):
|
|
80
|
+
for alias in node.names:
|
|
81
|
+
imports.append(
|
|
82
|
+
ImportInfo(
|
|
83
|
+
module_name=alias.name,
|
|
84
|
+
import_type="import",
|
|
85
|
+
line_number=node.lineno,
|
|
86
|
+
file_path=file_path,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
elif isinstance(node, ast.ImportFrom):
|
|
90
|
+
if node.module:
|
|
91
|
+
imports.append(
|
|
92
|
+
ImportInfo(
|
|
93
|
+
module_name=node.module,
|
|
94
|
+
import_type="from",
|
|
95
|
+
from_module=node.module,
|
|
96
|
+
line_number=node.lineno,
|
|
97
|
+
file_path=file_path,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return imports
|
|
102
|
+
|
|
103
|
+
def get_stdlib_modules(self) -> set[str]:
|
|
104
|
+
"""
|
|
105
|
+
Get a set of standard library module names.
|
|
106
|
+
|
|
107
|
+
Caches the result for performance. Attempts to discover stdlib modules
|
|
108
|
+
by examining the Python installation directory, with a fallback list
|
|
109
|
+
of common standard library modules.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Set of standard library module names
|
|
113
|
+
"""
|
|
114
|
+
if self._stdlib_modules is not None:
|
|
115
|
+
return self._stdlib_modules
|
|
116
|
+
|
|
117
|
+
stdlib_modules: set[str] = set()
|
|
118
|
+
|
|
119
|
+
# Get standard library path
|
|
120
|
+
stdlib_path = (
|
|
121
|
+
Path(sys.executable).parent
|
|
122
|
+
/ "lib"
|
|
123
|
+
/ f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
124
|
+
)
|
|
125
|
+
if not stdlib_path.exists():
|
|
126
|
+
# Fallback: try to import and check sys.path
|
|
127
|
+
import sysconfig
|
|
128
|
+
|
|
129
|
+
stdlib_path = Path(sysconfig.get_path("stdlib"))
|
|
130
|
+
|
|
131
|
+
if stdlib_path.exists():
|
|
132
|
+
for item in stdlib_path.iterdir():
|
|
133
|
+
if item.is_file() and item.suffix == ".py":
|
|
134
|
+
stdlib_modules.add(item.stem)
|
|
135
|
+
elif (
|
|
136
|
+
item.is_dir()
|
|
137
|
+
and not item.name.startswith("_")
|
|
138
|
+
and (item / "__init__.py").exists()
|
|
139
|
+
):
|
|
140
|
+
stdlib_modules.add(item.name)
|
|
141
|
+
|
|
142
|
+
# Add common stdlib modules that might not be in the directory
|
|
143
|
+
common_stdlib = {
|
|
144
|
+
"sys",
|
|
145
|
+
"os",
|
|
146
|
+
"json",
|
|
147
|
+
"pathlib",
|
|
148
|
+
"typing",
|
|
149
|
+
"collections",
|
|
150
|
+
"itertools",
|
|
151
|
+
"functools",
|
|
152
|
+
"dataclasses",
|
|
153
|
+
"enum",
|
|
154
|
+
"abc",
|
|
155
|
+
"contextlib",
|
|
156
|
+
"io",
|
|
157
|
+
"textwrap",
|
|
158
|
+
"ast",
|
|
159
|
+
"importlib",
|
|
160
|
+
"shutil",
|
|
161
|
+
"subprocess",
|
|
162
|
+
}
|
|
163
|
+
stdlib_modules.update(common_stdlib)
|
|
164
|
+
|
|
165
|
+
self._stdlib_modules = stdlib_modules
|
|
166
|
+
return stdlib_modules
|
|
167
|
+
|
|
168
|
+
def classify_import(self, import_info: ImportInfo, src_dir: Path) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Classify an import as stdlib, third-party, local, external, or ambiguous.
|
|
171
|
+
|
|
172
|
+
Modifies the ImportInfo object in place, setting its classification
|
|
173
|
+
and resolved_path attributes.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
import_info: ImportInfo object to classify
|
|
177
|
+
src_dir: Source directory to use for determining local vs external
|
|
178
|
+
"""
|
|
179
|
+
module_name = import_info.module_name
|
|
180
|
+
stdlib_modules = self.get_stdlib_modules()
|
|
181
|
+
|
|
182
|
+
# Check if it's a standard library module
|
|
183
|
+
root_module = module_name.split(".")[0]
|
|
184
|
+
if root_module in stdlib_modules:
|
|
185
|
+
import_info.classification = "stdlib"
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# Try to resolve as a local import
|
|
189
|
+
resolved = self.resolve_local_import(import_info, src_dir)
|
|
190
|
+
if resolved is not None:
|
|
191
|
+
if resolved.is_relative_to(src_dir):
|
|
192
|
+
import_info.classification = "local"
|
|
193
|
+
else:
|
|
194
|
+
import_info.classification = "external"
|
|
195
|
+
import_info.resolved_path = resolved
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Check if it's a third-party package (in site-packages)
|
|
199
|
+
if self.is_third_party(module_name):
|
|
200
|
+
import_info.classification = "third_party"
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Mark as ambiguous if we can't determine
|
|
204
|
+
import_info.classification = "ambiguous"
|
|
205
|
+
|
|
206
|
+
def resolve_local_import(self, import_info: ImportInfo, src_dir: Path) -> Path | None:
|
|
207
|
+
"""
|
|
208
|
+
Try to resolve a local import to a file path.
|
|
209
|
+
|
|
210
|
+
Handles both relative imports (starting with .) and absolute imports.
|
|
211
|
+
Checks multiple potential locations including parent directories
|
|
212
|
+
of the source directory.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
import_info: ImportInfo object with the import to resolve
|
|
216
|
+
src_dir: Source directory to use as reference
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Path to the resolved file, or None if not found
|
|
220
|
+
"""
|
|
221
|
+
module_name = import_info.module_name
|
|
222
|
+
|
|
223
|
+
# Handle relative imports
|
|
224
|
+
if import_info.file_path:
|
|
225
|
+
file_dir = import_info.file_path.parent
|
|
226
|
+
if module_name.startswith("."):
|
|
227
|
+
# Relative import
|
|
228
|
+
parts = module_name.split(".")
|
|
229
|
+
level = sum(1 for p in parts if p == "")
|
|
230
|
+
module_parts = [p for p in parts if p]
|
|
231
|
+
|
|
232
|
+
if level > 0:
|
|
233
|
+
# Go up 'level' directories
|
|
234
|
+
current = file_dir
|
|
235
|
+
for _ in range(level - 1):
|
|
236
|
+
current = current.parent
|
|
237
|
+
base_path = current
|
|
238
|
+
else:
|
|
239
|
+
base_path = file_dir
|
|
240
|
+
|
|
241
|
+
if module_parts:
|
|
242
|
+
potential_path = base_path / "/".join(module_parts)
|
|
243
|
+
else:
|
|
244
|
+
potential_path = base_path
|
|
245
|
+
|
|
246
|
+
# Try as module
|
|
247
|
+
if (potential_path / "__init__.py").exists():
|
|
248
|
+
return potential_path / "__init__.py"
|
|
249
|
+
if potential_path.with_suffix(".py").exists():
|
|
250
|
+
return potential_path.with_suffix(".py")
|
|
251
|
+
if potential_path.is_dir() and (potential_path / "__init__.py").exists():
|
|
252
|
+
return potential_path / "__init__.py"
|
|
253
|
+
|
|
254
|
+
# Handle absolute imports - check if it's in the project
|
|
255
|
+
# First check if it's in src_dir
|
|
256
|
+
module_path_str = module_name.replace(".", "/")
|
|
257
|
+
potential_paths = [
|
|
258
|
+
src_dir / module_path_str / "__init__.py",
|
|
259
|
+
(src_dir / module_path_str).with_suffix(".py"),
|
|
260
|
+
self.project_root / module_path_str / "__init__.py",
|
|
261
|
+
(self.project_root / module_path_str).with_suffix(".py"),
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
for path in potential_paths:
|
|
265
|
+
if path.exists():
|
|
266
|
+
return path
|
|
267
|
+
|
|
268
|
+
# Check parent directories of src_dir and project_root for external dependencies
|
|
269
|
+
# Check up to project_root's parent
|
|
270
|
+
check_dirs = [src_dir.parent] + list(src_dir.parents)
|
|
271
|
+
for parent in check_dirs:
|
|
272
|
+
if parent == self.project_root.parent:
|
|
273
|
+
break
|
|
274
|
+
if not parent.exists():
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
# Try as module directory
|
|
278
|
+
potential = parent / module_name.replace(".", "/")
|
|
279
|
+
if potential.is_dir() and (potential / "__init__.py").exists():
|
|
280
|
+
return potential / "__init__.py"
|
|
281
|
+
if potential.with_suffix(".py").is_file():
|
|
282
|
+
return potential.with_suffix(".py")
|
|
283
|
+
|
|
284
|
+
# Also check if the parent itself contains the module
|
|
285
|
+
potential_file = parent / f"{module_name.split('.')[-1]}.py"
|
|
286
|
+
if potential_file.exists():
|
|
287
|
+
return potential_file
|
|
288
|
+
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def is_third_party(self, module_name: str) -> bool:
|
|
292
|
+
"""
|
|
293
|
+
Check if a module is a third-party package.
|
|
294
|
+
|
|
295
|
+
Uses importlib to find the module and checks if its location
|
|
296
|
+
is in site-packages or dist-packages.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
module_name: Name of the module to check
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
True if the module is a third-party package, False otherwise
|
|
303
|
+
"""
|
|
304
|
+
root_module = module_name.split(".")[0]
|
|
305
|
+
try:
|
|
306
|
+
spec = importlib.util.find_spec(root_module)
|
|
307
|
+
if spec and spec.origin:
|
|
308
|
+
origin_path = Path(spec.origin)
|
|
309
|
+
# Check if it's in site-packages
|
|
310
|
+
return "site-packages" in str(origin_path) or "dist-packages" in str(origin_path)
|
|
311
|
+
except (ImportError, ValueError, AttributeError):
|
|
312
|
+
pass
|
|
313
|
+
return False
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
External dependency finding functionality.
|
|
3
|
+
|
|
4
|
+
This module provides the ExternalDependencyFinder class which identifies
|
|
5
|
+
files and directories that are imported from outside the source directory
|
|
6
|
+
and need to be temporarily copied during the build process.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .analyzer import ImportAnalyzer
|
|
14
|
+
from .types import ExternalDependency
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExternalDependencyFinder:
|
|
18
|
+
"""
|
|
19
|
+
Finds external dependencies that need to be copied.
|
|
20
|
+
|
|
21
|
+
This class analyzes Python files to identify imports that reference
|
|
22
|
+
modules outside the source directory. It determines which files or
|
|
23
|
+
directories need to be copied and where they should be placed.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
project_root: Root directory of the project
|
|
27
|
+
src_dir: Source directory where the package code lives
|
|
28
|
+
analyzer: ImportAnalyzer instance for analyzing imports
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self, project_root: Path, src_dir: Path, exclude_patterns: list[str] | None = None
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Initialize the dependency finder.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
project_root: Root directory of the project
|
|
39
|
+
src_dir: Source directory to analyze
|
|
40
|
+
exclude_patterns: Additional patterns to exclude (default: common sandbox patterns)
|
|
41
|
+
"""
|
|
42
|
+
self.project_root = project_root.resolve()
|
|
43
|
+
self.src_dir = src_dir.resolve()
|
|
44
|
+
self.analyzer = ImportAnalyzer(project_root)
|
|
45
|
+
# Patterns for directories/files to exclude (sandbox, skip, etc.)
|
|
46
|
+
default_patterns = [
|
|
47
|
+
"_SS",
|
|
48
|
+
"__SS",
|
|
49
|
+
"_sandbox",
|
|
50
|
+
"__sandbox",
|
|
51
|
+
"_skip",
|
|
52
|
+
"__skip",
|
|
53
|
+
"_test",
|
|
54
|
+
"__test__",
|
|
55
|
+
]
|
|
56
|
+
self.exclude_patterns = default_patterns + (exclude_patterns or [])
|
|
57
|
+
|
|
58
|
+
def find_external_dependencies(self, python_files: list[Path]) -> list[ExternalDependency]:
|
|
59
|
+
"""
|
|
60
|
+
Find all external dependencies that need to be copied.
|
|
61
|
+
|
|
62
|
+
Analyzes all provided Python files, classifies their imports,
|
|
63
|
+
and identifies which external files/directories need to be copied
|
|
64
|
+
into the source directory.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
python_files: List of Python file paths to analyze
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of ExternalDependency objects representing files/directories
|
|
71
|
+
that need to be copied
|
|
72
|
+
"""
|
|
73
|
+
external_deps: list[ExternalDependency] = []
|
|
74
|
+
seen_paths: set[Path] = set()
|
|
75
|
+
|
|
76
|
+
for file_path in python_files:
|
|
77
|
+
imports = self.analyzer.extract_imports(file_path)
|
|
78
|
+
for imp in imports:
|
|
79
|
+
self.analyzer.classify_import(imp, self.src_dir)
|
|
80
|
+
|
|
81
|
+
if imp.classification == "external" and imp.resolved_path:
|
|
82
|
+
source_path = imp.resolved_path
|
|
83
|
+
|
|
84
|
+
# Skip excluded paths (sandbox directories, etc.)
|
|
85
|
+
if self._should_exclude_path(source_path):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# For files, only copy parent directory if it's a package
|
|
89
|
+
# Otherwise, copy just the individual file
|
|
90
|
+
if source_path.is_file():
|
|
91
|
+
parent_dir = source_path.parent
|
|
92
|
+
|
|
93
|
+
# Only copy parent directory if:
|
|
94
|
+
# 1. It's a package (has __init__.py), OR
|
|
95
|
+
# 2. Files from it are actually imported (which is the case here)
|
|
96
|
+
# But only copy the immediate parent, not entire directory trees
|
|
97
|
+
parent_is_package = (parent_dir / "__init__.py").exists()
|
|
98
|
+
files_are_imported = True # Always true when processing an import
|
|
99
|
+
|
|
100
|
+
# Only copy immediate parent directory, not grandparent directories
|
|
101
|
+
# This prevents copying entire trees like models/Information_extraction
|
|
102
|
+
# when we only need models/Information_extraction/_shared_ie
|
|
103
|
+
should_copy_dir = (
|
|
104
|
+
not self._should_exclude_path(parent_dir)
|
|
105
|
+
and (
|
|
106
|
+
parent_is_package or files_are_imported
|
|
107
|
+
) # Package OR files imported
|
|
108
|
+
and not parent_dir.is_relative_to(self.src_dir)
|
|
109
|
+
and not self.src_dir.is_relative_to(parent_dir)
|
|
110
|
+
and parent_dir != self.project_root
|
|
111
|
+
and parent_dir != self.project_root.parent
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if should_copy_dir:
|
|
115
|
+
# Copy the directory instead of just the file
|
|
116
|
+
track_path = parent_dir
|
|
117
|
+
source_path = parent_dir
|
|
118
|
+
else:
|
|
119
|
+
# Copy just the file
|
|
120
|
+
track_path = source_path
|
|
121
|
+
elif source_path.is_dir():
|
|
122
|
+
# Don't copy directories that contain src_dir
|
|
123
|
+
if self.src_dir.is_relative_to(source_path):
|
|
124
|
+
continue
|
|
125
|
+
track_path = source_path
|
|
126
|
+
else:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if track_path in seen_paths:
|
|
130
|
+
continue
|
|
131
|
+
seen_paths.add(track_path)
|
|
132
|
+
|
|
133
|
+
# Determine target path within src_dir
|
|
134
|
+
target_path = self._determine_target_path(source_path, imp.module_name)
|
|
135
|
+
|
|
136
|
+
if target_path:
|
|
137
|
+
# Only add if source is actually outside src_dir
|
|
138
|
+
if not source_path.is_relative_to(self.src_dir):
|
|
139
|
+
external_deps.append(
|
|
140
|
+
ExternalDependency(
|
|
141
|
+
source_path=source_path,
|
|
142
|
+
target_path=target_path,
|
|
143
|
+
import_name=imp.module_name,
|
|
144
|
+
file_path=file_path,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return external_deps
|
|
149
|
+
|
|
150
|
+
def _determine_target_path(self, source_path: Path, module_name: str) -> Path | None:
|
|
151
|
+
"""
|
|
152
|
+
Determine where an external file should be copied within src_dir.
|
|
153
|
+
|
|
154
|
+
For files, attempts to maintain the module structure. For directories,
|
|
155
|
+
places them directly in src_dir with their original name.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
source_path: Path to the source file or directory
|
|
159
|
+
module_name: Module name from the import statement
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Target path within src_dir, or None if cannot be determined
|
|
163
|
+
"""
|
|
164
|
+
if not source_path.exists():
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
# Always create target within src_dir
|
|
168
|
+
module_parts = module_name.split(".")
|
|
169
|
+
|
|
170
|
+
if source_path.is_file():
|
|
171
|
+
# For a file, create the directory structure based on module name
|
|
172
|
+
if len(module_parts) > 1:
|
|
173
|
+
# It's a submodule, create the directory structure
|
|
174
|
+
target = self.src_dir / "/".join(module_parts[:-1]) / source_path.name
|
|
175
|
+
else:
|
|
176
|
+
# Top-level module - try to find the main package directory
|
|
177
|
+
# or create a matching structure
|
|
178
|
+
main_pkg = self._find_main_package()
|
|
179
|
+
if main_pkg:
|
|
180
|
+
target = main_pkg / source_path.name
|
|
181
|
+
else:
|
|
182
|
+
target = self.src_dir / source_path.name
|
|
183
|
+
return target
|
|
184
|
+
|
|
185
|
+
# If it's a directory, copy the whole directory
|
|
186
|
+
if source_path.is_dir():
|
|
187
|
+
# Use the directory name directly in src_dir
|
|
188
|
+
target = self.src_dir / source_path.name
|
|
189
|
+
return target
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def _should_exclude_path(self, path: Path) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Check if a path should be excluded from copying.
|
|
196
|
+
|
|
197
|
+
Excludes paths that match common sandbox/skip patterns like _SS, __SS, etc.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
path: Path to check
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
True if the path should be excluded, False otherwise
|
|
204
|
+
"""
|
|
205
|
+
# Check each component of the path
|
|
206
|
+
for part in path.parts:
|
|
207
|
+
for pattern in self.exclude_patterns:
|
|
208
|
+
# Match if part equals pattern or starts with pattern
|
|
209
|
+
if part == pattern or part.startswith(pattern):
|
|
210
|
+
return True
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def _find_main_package(self) -> Path | None:
|
|
214
|
+
"""
|
|
215
|
+
Find the main package directory within src_dir.
|
|
216
|
+
|
|
217
|
+
Looks for directories containing __init__.py files, which indicate
|
|
218
|
+
Python packages.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Path to the main package directory, or None if not found
|
|
222
|
+
"""
|
|
223
|
+
if not self.src_dir.exists():
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
# Look for directories with __init__.py
|
|
227
|
+
package_dirs = [
|
|
228
|
+
d for d in self.src_dir.iterdir() if d.is_dir() and (d / "__init__.py").exists()
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
if package_dirs:
|
|
232
|
+
return package_dirs[0]
|
|
233
|
+
|
|
234
|
+
return None
|