depfix 0.1.0__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.
depfix/__init__.py ADDED
@@ -0,0 +1,196 @@
1
+ """Depfix: version-resolved imports with per-package dependency realms.
2
+
3
+ Importing this package performs no network, resolver, or subprocess work. Cold
4
+ preparation begins only when a load API is called.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import os
11
+ from types import ModuleType
12
+
13
+ from ._version import __version__
14
+ from .errors import (
15
+ ArtifactError,
16
+ BundleError,
17
+ DepfixError,
18
+ FrozenManifestError,
19
+ HashMismatchError,
20
+ ManifestError,
21
+ ManifestMismatchError,
22
+ ManifestNotFoundError,
23
+ ModuleDiscoveryError,
24
+ ModuleNotProvidedError,
25
+ MultipleImportModulesError,
26
+ NativeIsolationRequired,
27
+ NoImportModulesError,
28
+ OfflineArtifactMissingError,
29
+ ResolutionError,
30
+ SourceError,
31
+ SpecifierError,
32
+ UnsupportedManifestVersionError,
33
+ UnsupportedUvVersionError,
34
+ UvBackendError,
35
+ UvBootstrapError,
36
+ UvNotFoundError,
37
+ )
38
+ from .handles import PackageHandle
39
+ from .manager import activate_manifest, prepare_request
40
+ from .settings import Settings, configure, resolve_settings
41
+
42
+
43
+ def import_module(
44
+ specifier: str,
45
+ *,
46
+ module: str | None = None,
47
+ refresh: bool = False,
48
+ manifest: str | os.PathLike[str] | None = None,
49
+ frozen: bool | None = None,
50
+ offline: bool | None = None,
51
+ isolation: str | None = None,
52
+ ) -> ModuleType:
53
+ """Return exactly one canonical module or raise a typed discovery error."""
54
+ settings = resolve_settings(manifest=manifest, frozen=frozen, offline=offline)
55
+ runtime, request = prepare_request(
56
+ specifier,
57
+ module=module,
58
+ api="import_module",
59
+ refresh=refresh,
60
+ isolation=isolation,
61
+ settings=settings,
62
+ )
63
+ if not request.module:
64
+ raise NoImportModulesError("The resolved request has no selected import module", request=specifier)
65
+ return runtime.import_for_node(request.node, request.module)
66
+
67
+
68
+ def load_package(
69
+ specifier: str,
70
+ *,
71
+ refresh: bool = False,
72
+ manifest: str | os.PathLike[str] | None = None,
73
+ frozen: bool | None = None,
74
+ offline: bool | None = None,
75
+ isolation: str | None = None,
76
+ ) -> PackageHandle:
77
+ """Return one package handle without eagerly importing its root modules."""
78
+ settings = resolve_settings(manifest=manifest, frozen=frozen, offline=offline)
79
+ runtime, request = prepare_request(
80
+ specifier,
81
+ module=None,
82
+ api="load_package",
83
+ refresh=refresh,
84
+ isolation=isolation,
85
+ settings=settings,
86
+ )
87
+ return PackageHandle(runtime, request)
88
+
89
+
90
+ async def import_module_async(
91
+ specifier: str,
92
+ *,
93
+ module: str | None = None,
94
+ refresh: bool = False,
95
+ manifest: str | os.PathLike[str] | None = None,
96
+ frozen: bool | None = None,
97
+ offline: bool | None = None,
98
+ isolation: str | None = None,
99
+ ) -> ModuleType:
100
+ """Async-friendly wrapper sharing canonical identity with `import_module`."""
101
+ return await asyncio.to_thread(
102
+ import_module,
103
+ specifier,
104
+ module=module,
105
+ refresh=refresh,
106
+ manifest=manifest,
107
+ frozen=frozen,
108
+ offline=offline,
109
+ isolation=isolation,
110
+ )
111
+
112
+
113
+ async def load_package_async(
114
+ specifier: str,
115
+ *,
116
+ refresh: bool = False,
117
+ manifest: str | os.PathLike[str] | None = None,
118
+ frozen: bool | None = None,
119
+ offline: bool | None = None,
120
+ isolation: str | None = None,
121
+ ) -> PackageHandle:
122
+ return await asyncio.to_thread(
123
+ load_package,
124
+ specifier,
125
+ refresh=refresh,
126
+ manifest=manifest,
127
+ frozen=frozen,
128
+ offline=offline,
129
+ isolation=isolation,
130
+ )
131
+
132
+
133
+ def activate(
134
+ manifest: str | os.PathLike[str] = ".depfix/imports.lock",
135
+ *,
136
+ cache_dir: str | os.PathLike[str] | None = None,
137
+ ) -> object:
138
+ """Activate an already installed manifest for notebooks or embedding."""
139
+ settings = resolve_settings(manifest=manifest, cache_dir=cache_dir, frozen=True, discover=False)
140
+ assert settings.manifest is not None
141
+ return activate_manifest(settings.manifest, settings)
142
+
143
+
144
+ def _load_alias(name: str, identity: tuple[str, str, str, str]) -> ModuleType:
145
+ from .manager import load_generated_alias
146
+
147
+ return load_generated_alias(name, identity)
148
+
149
+
150
+ def _load_package_alias(name: str, identity: tuple[str, str, str, str]) -> PackageHandle:
151
+ from .manager import load_generated_package
152
+
153
+ return load_generated_package(name, identity)
154
+
155
+
156
+ def multiprocessing_initializer(
157
+ manifest: str | os.PathLike[str],
158
+ cache_dir: str | os.PathLike[str] | None = None,
159
+ ) -> None:
160
+ activate(manifest, cache_dir=cache_dir)
161
+
162
+
163
+ __all__ = [
164
+ "ArtifactError",
165
+ "BundleError",
166
+ "DepfixError",
167
+ "FrozenManifestError",
168
+ "HashMismatchError",
169
+ "ManifestError",
170
+ "ManifestMismatchError",
171
+ "ManifestNotFoundError",
172
+ "ModuleDiscoveryError",
173
+ "ModuleNotProvidedError",
174
+ "MultipleImportModulesError",
175
+ "NativeIsolationRequired",
176
+ "NoImportModulesError",
177
+ "OfflineArtifactMissingError",
178
+ "PackageHandle",
179
+ "ResolutionError",
180
+ "Settings",
181
+ "SourceError",
182
+ "SpecifierError",
183
+ "UnsupportedManifestVersionError",
184
+ "UnsupportedUvVersionError",
185
+ "UvBackendError",
186
+ "UvBootstrapError",
187
+ "UvNotFoundError",
188
+ "activate",
189
+ "configure",
190
+ "import_module",
191
+ "import_module_async",
192
+ "load_package",
193
+ "load_package_async",
194
+ "multiprocessing_initializer",
195
+ "__version__",
196
+ ]
depfix/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
depfix/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
depfix/aliases.py ADDED
@@ -0,0 +1,189 @@
1
+ """Generate the physical alias namespace used by editors and runtime code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ from .cache import Cache
11
+ from .models import LockedGraph, Node
12
+
13
+
14
+ def generate_aliases(graph: LockedGraph, cache: Cache, output_root: Path) -> Path:
15
+ package = output_root / "depfix_imports"
16
+ package.mkdir(parents=True, exist_ok=True)
17
+ alias_rows = {
18
+ alias.name: (graph.graph_id, alias.node, alias.module, alias.specifier)
19
+ for alias in sorted(graph.aliases, key=lambda value: value.name)
20
+ }
21
+ init = (
22
+ '"""Generated by Depfix; do not edit."""\n'
23
+ "from depfix import _load_alias as _depfix_load_alias\n"
24
+ "from depfix import _load_package_alias as _depfix_load_package_alias\n\n"
25
+ f"_ALIASES = {alias_rows!r}\n\n"
26
+ f"_PACKAGE_ALIASES = {sorted(alias.name for alias in graph.aliases if alias.api == 'load_package')!r}\n\n"
27
+ "def __getattr__(name):\n"
28
+ " if name not in _ALIASES:\n"
29
+ " raise AttributeError(name)\n"
30
+ " loader = _depfix_load_package_alias if name in _PACKAGE_ALIASES else _depfix_load_alias\n"
31
+ " value = loader(name, _ALIASES[name])\n"
32
+ " globals()[name] = value\n"
33
+ " return value\n\n"
34
+ f"__all__ = {sorted(alias_rows)!r}\n"
35
+ )
36
+ (package / "__init__.py").write_text(init, encoding="utf-8")
37
+ root_stub = "".join(f"from . import {name} as {name}\n" for name in sorted(alias_rows))
38
+ (package / "__init__.pyi").write_text(root_stub, encoding="utf-8")
39
+ (package / "py.typed").write_text("\n", encoding="utf-8")
40
+ manifest = {
41
+ "format_version": 1,
42
+ "graph_id": graph.graph_id,
43
+ "aliases": [
44
+ {"name": alias.name, "node": alias.node, "module": alias.module, "specifier": alias.specifier}
45
+ for alias in sorted(graph.aliases, key=lambda value: value.name)
46
+ ],
47
+ }
48
+ (package / "aliases.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
49
+ source_map = {
50
+ "format_version": 1,
51
+ "manifest_id": graph.graph_id,
52
+ "aliases": {
53
+ alias.name: {
54
+ "source_file": alias.source_file,
55
+ "line": alias.source_line,
56
+ "column": alias.source_column,
57
+ "specifier": alias.specifier,
58
+ "logical_module": alias.module,
59
+ "node": alias.node,
60
+ }
61
+ for alias in sorted(graph.aliases, key=lambda value: value.name)
62
+ },
63
+ }
64
+ (package / "source-map.json").write_text(
65
+ json.dumps(source_map, indent=2, sort_keys=True) + "\n",
66
+ encoding="utf-8",
67
+ )
68
+ nodes = graph.node_index
69
+ for alias in graph.aliases:
70
+ if alias.api == "load_package":
71
+ _generate_package_stub_tree(package, alias.name, nodes[alias.node], cache, alias_rows[alias.name])
72
+ else:
73
+ _generate_stub_tree(package, alias.name, alias.module, nodes[alias.node], cache)
74
+ _write_editor_snippets(output_root)
75
+ return package
76
+
77
+
78
+ def _generate_stub_tree(package: Path, alias_name: str, logical_module: str, node: Node, cache: Cache) -> None:
79
+ alias_root = package / alias_name
80
+ if alias_root.exists():
81
+ shutil.rmtree(alias_root)
82
+ alias_root.mkdir(parents=True)
83
+ artifact_root = cache.unpacked_path(node.artifact) / "purelib"
84
+ source_root = artifact_root.joinpath(*logical_module.split("."))
85
+ if source_root.is_dir():
86
+ typed = (source_root / "py.typed").exists()
87
+ for source in source_root.rglob("*"):
88
+ if not source.is_file():
89
+ continue
90
+ relative = source.relative_to(source_root)
91
+ if source.suffix == ".pyi":
92
+ target = alias_root / relative
93
+ elif typed and source.suffix == ".py":
94
+ target = (alias_root / relative).with_suffix(".pyi")
95
+ else:
96
+ continue
97
+ target.parent.mkdir(parents=True, exist_ok=True)
98
+ _copy_stub(source, target, logical_module, f"depfix_imports.{alias_name}")
99
+ else:
100
+ source_file = source_root.with_suffix(".py")
101
+ stub_file = source_root.with_suffix(".pyi")
102
+ selected = stub_file if stub_file.is_file() else source_file if source_file.is_file() else None
103
+ if selected is not None:
104
+ _copy_stub(selected, alias_root / "__init__.pyi", logical_module, f"depfix_imports.{alias_name}")
105
+ fallback = alias_root / "__init__.pyi"
106
+ if not fallback.exists():
107
+ fallback.write_text("# Untyped artifact: API is intentionally unknown to static analyzers.\n", encoding="utf-8")
108
+
109
+
110
+ def _generate_package_stub_tree(
111
+ package: Path,
112
+ alias_name: str,
113
+ node: Node,
114
+ cache: Cache,
115
+ identity: tuple[str, str, str, str],
116
+ ) -> None:
117
+ alias_root = package / alias_name
118
+ if alias_root.exists():
119
+ shutil.rmtree(alias_root)
120
+ alias_root.mkdir(parents=True)
121
+ runtime = (
122
+ '"""Generated Depfix package-module collection."""\n'
123
+ "from depfix import _load_package_alias\n"
124
+ f"_PACKAGE = _load_package_alias({alias_name!r}, {identity!r})\n"
125
+ "def __getattr__(name):\n"
126
+ " try:\n"
127
+ " return _PACKAGE.modules[name]\n"
128
+ " except Exception as exc:\n"
129
+ " raise AttributeError(name) from exc\n"
130
+ f"__all__ = {list(node.public_modules)!r}\n"
131
+ )
132
+ (alias_root / "__init__.py").write_text(runtime, encoding="utf-8")
133
+ imports: list[str] = []
134
+ artifact_root = cache.unpacked_path(node.artifact) / "purelib"
135
+ for logical in node.public_modules:
136
+ target_root = alias_root.joinpath(*logical.split("."))
137
+ source_root = artifact_root.joinpath(*logical.split("."))
138
+ if source_root.is_dir():
139
+ target_root.mkdir(parents=True, exist_ok=True)
140
+ typed = (source_root / "py.typed").exists()
141
+ for source in source_root.rglob("*"):
142
+ if not source.is_file() or (source.suffix != ".pyi" and not (typed and source.suffix == ".py")):
143
+ continue
144
+ relative = source.relative_to(source_root)
145
+ target = target_root / relative
146
+ if target.suffix == ".py":
147
+ target = target.with_suffix(".pyi")
148
+ _copy_stub(
149
+ source, target, logical.split(".", 1)[0], f"depfix_imports.{alias_name}.{logical.split('.', 1)[0]}"
150
+ )
151
+ (target_root / "__init__.pyi").touch(exist_ok=True)
152
+ else:
153
+ selected = source_root.with_suffix(".pyi")
154
+ if not selected.is_file():
155
+ selected = source_root.with_suffix(".py")
156
+ target_root.parent.mkdir(parents=True, exist_ok=True)
157
+ target = target_root.with_suffix(".pyi")
158
+ if selected.is_file():
159
+ _copy_stub(
160
+ selected,
161
+ target,
162
+ logical.split(".", 1)[0],
163
+ f"depfix_imports.{alias_name}.{logical.split('.', 1)[0]}",
164
+ )
165
+ else:
166
+ target.write_text("# Untyped module.\n", encoding="utf-8")
167
+ top = logical.split(".", 1)[0]
168
+ imports.append(f"from . import {top} as {top}\n")
169
+ (alias_root / "__init__.pyi").write_text("".join(dict.fromkeys(imports)), encoding="utf-8")
170
+
171
+
172
+ def _copy_stub(source: Path, target: Path, logical_root: str, alias_root: str) -> None:
173
+ target.parent.mkdir(parents=True, exist_ok=True)
174
+ text = source.read_text(encoding="utf-8")
175
+ escaped = re.escape(logical_root)
176
+ text = re.sub(rf"(?m)^(\s*from\s+){escaped}(?=\.|\s+import\b)", rf"\1{alias_root}", text)
177
+ text = re.sub(rf"(?m)^(\s*import\s+){escaped}(?=\.|\s|$)", rf"\1{alias_root}", text)
178
+ target.write_text(text, encoding="utf-8")
179
+
180
+
181
+ def _write_editor_snippets(output_root: Path) -> None:
182
+ generated = str(output_root.resolve())
183
+ (output_root / "pyrightconfig.depfix.json").write_text(
184
+ json.dumps({"extraPaths": [generated], "stubPath": generated}, indent=2) + "\n", encoding="utf-8"
185
+ )
186
+ (output_root / "mypy.depfix.ini").write_text(f"[mypy]\nmypy_path = {generated}\n", encoding="utf-8")
187
+ (output_root / "PYCHARM.md").write_text(
188
+ "Mark this generated directory as a Sources Root in Settings > Project Structure.\n", encoding="utf-8"
189
+ )