katalyst-engine 4.0.0__tar.gz → 5.0.1a1__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.
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/.gitignore +11 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/CHANGELOG.md +25 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/PKG-INFO +4 -2
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/pyproject.toml +5 -1
- katalyst_engine-5.0.1a1/src/katalyst_engine/__init__.py +15 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/bundle/__init__.py +8 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/bundle/discovery.py +55 -4
- katalyst_engine-5.0.1a1/src/katalyst_engine/bundle/loader.py +213 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/bundle/path_loader.py +125 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/bundle/policy.py +144 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/bundle/protocol.py +8 -1
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/__init__.py +25 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/core/errors.py +33 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/core/matching.py +234 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/core/relation.py +282 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/evolution/__init__.py +62 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/evolution/diff.py +1246 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/evolution/gate.py +514 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/evolution/migration.py +652 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/evolution/release.py +294 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/extensions/registry.py +18 -3
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/extensions/trigger.py +8 -10
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/model/__init__.py +2 -2
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/model/manager.py +3 -3
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/model/query.py +44 -15
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/replication/job.py +2 -2
- katalyst_engine-5.0.1a1/src/katalyst_engine/schema/__init__.py +73 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/schema/conflict.py +77 -0
- katalyst_engine-5.0.1a1/src/katalyst_engine/schema/definition.py +751 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/schema/manager.py +9 -2
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/schema/registry.py +234 -10
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/source/registry.py +12 -21
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/toolkit/file_ops.py +5 -6
- katalyst_engine-5.0.1a1/src/katalyst_engine/toolkit/linkml.py +454 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/bundle/test_discovery.py +35 -0
- katalyst_engine-5.0.1a1/tests/bundle/test_loader_policy.py +355 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/bundle/test_path_loader.py +57 -0
- katalyst_engine-5.0.1a1/tests/conftest.py +20 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_evolvable.py +2 -2
- katalyst_engine-5.0.1a1/tests/core/test_matching.py +259 -0
- katalyst_engine-5.0.1a1/tests/core/test_matching_consolidation.py +79 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_relation.py +14 -14
- katalyst_engine-5.0.1a1/tests/core/test_relation_selectors.py +345 -0
- katalyst_engine-5.0.1a1/tests/evolution/conftest.py +165 -0
- katalyst_engine-5.0.1a1/tests/evolution/test_diff.py +219 -0
- katalyst_engine-5.0.1a1/tests/evolution/test_gate.py +187 -0
- katalyst_engine-5.0.1a1/tests/evolution/test_migration.py +301 -0
- katalyst_engine-5.0.1a1/tests/evolution/test_release.py +126 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/extensions/test_registry.py +43 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/model/test_materializer.py +9 -9
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/model/test_query.py +17 -17
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/replication/test_engine.py +5 -5
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/schema/test_definition.py +12 -12
- katalyst_engine-5.0.1a1/tests/schema/test_engine_boundary_guard.py +49 -0
- katalyst_engine-5.0.1a1/tests/schema/test_field_role.py +83 -0
- katalyst_engine-5.0.1a1/tests/schema/test_fieldspec_allowed_values.py +60 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/schema/test_manager.py +2 -2
- katalyst_engine-5.0.1a1/tests/schema/test_plugin_settings.py +57 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/schema/test_registry.py +5 -5
- katalyst_engine-5.0.1a1/tests/schema/test_registry_provenance.py +326 -0
- katalyst_engine-5.0.1a1/tests/schema/test_storage_policy.py +159 -0
- katalyst_engine-5.0.1a1/tests/test_version.py +40 -0
- katalyst_engine-5.0.1a1/tests/toolkit/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/toolkit/test_file_ops.py +9 -3
- katalyst_engine-5.0.1a1/tests/toolkit/test_linkml_bundle.py +165 -0
- katalyst_engine-4.0.0/src/katalyst_engine/__init__.py +0 -6
- katalyst_engine-4.0.0/src/katalyst_engine/bundle/loader.py +0 -134
- katalyst_engine-4.0.0/src/katalyst_engine/core/relation.py +0 -151
- katalyst_engine-4.0.0/src/katalyst_engine/schema/__init__.py +0 -35
- katalyst_engine-4.0.0/src/katalyst_engine/schema/definition.py +0 -278
- katalyst_engine-4.0.0/tests/conftest.py +0 -1
- katalyst_engine-4.0.0/uv.lock +0 -371
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/README.md +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/compatibility.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/compositional.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/definitive.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/evolvable.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/identity.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/lifecycle.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/core/version.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/discovery/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/discovery/declaration.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/discovery/dispatcher.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/discovery/protocol.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/events/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/events/bus.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/events/event.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/extensions/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/extensions/capability.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/extensions/discovery.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/extensions/effector.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/extensions/provider.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/model/materializer.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/model/node.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/model/store.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/py.typed +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/replication/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/replication/engine.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/replication/transform.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/resolution/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/resolution/conflict.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/resolution/engine.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/resolution/strategies.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/schema/versioning.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/snapshot/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/snapshot/diff.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/snapshot/snapshot.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/source/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/source/manifest.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/source/source.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/toolkit/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/src/katalyst_engine/toolkit/rendering.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/bundle/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/bundle/test_loader.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/bundle/test_protocol.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_compatibility.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_compositional.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_definitive.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_identity.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_init.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_lifecycle.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/core/test_version.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/discovery/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/discovery/test_declaration.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/discovery/test_dispatcher.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/events/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/events/test_bus.py +0 -0
- {katalyst_engine-4.0.0/tests/extensions → katalyst_engine-5.0.1a1/tests/evolution}/__init__.py +0 -0
- {katalyst_engine-4.0.0/tests/model → katalyst_engine-5.0.1a1/tests/extensions}/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/extensions/test_effector.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/extensions/test_provider.py +0 -0
- {katalyst_engine-4.0.0/tests/replication → katalyst_engine-5.0.1a1/tests/model}/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/model/test_node.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/model/test_store.py +0 -0
- {katalyst_engine-4.0.0/tests/resolution → katalyst_engine-5.0.1a1/tests/replication}/__init__.py +0 -0
- {katalyst_engine-4.0.0/tests/schema → katalyst_engine-5.0.1a1/tests/resolution}/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/resolution/test_engine.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/resolution/test_strategies.py +0 -0
- {katalyst_engine-4.0.0/tests/snapshot → katalyst_engine-5.0.1a1/tests/schema}/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/schema/test_versioning.py +0 -0
- {katalyst_engine-4.0.0/tests/source → katalyst_engine-5.0.1a1/tests/snapshot}/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/snapshot/test_diff.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/snapshot/test_snapshot.py +0 -0
- {katalyst_engine-4.0.0/tests/toolkit → katalyst_engine-5.0.1a1/tests/source}/__init__.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/source/test_source.py +0 -0
- {katalyst_engine-4.0.0 → katalyst_engine-5.0.1a1}/tests/toolkit/test_rendering.py +0 -0
|
@@ -15,6 +15,9 @@ downloads/
|
|
|
15
15
|
eggs/
|
|
16
16
|
.eggs/
|
|
17
17
|
lib/
|
|
18
|
+
# Not a Python build directory: Bard's SPA keeps shared helpers (utils.ts, download.ts) in
|
|
19
|
+
# src/lib, and ignoring it left a fresh clone unable to build or test the SPA.
|
|
20
|
+
!plugins/bard/web/src/lib/
|
|
18
21
|
lib64/
|
|
19
22
|
parts/
|
|
20
23
|
sdist/
|
|
@@ -95,6 +98,11 @@ Thumbs.db
|
|
|
95
98
|
# Browser/Playwright state files
|
|
96
99
|
browser-state-*.json
|
|
97
100
|
|
|
101
|
+
# Agent harness scratch (run exhaust, never the design bundle)
|
|
102
|
+
.opencode/sessions/
|
|
103
|
+
.opencode/analysis/
|
|
104
|
+
.opencode/package-lock.json
|
|
105
|
+
|
|
98
106
|
# Build artifacts
|
|
99
107
|
site/
|
|
100
108
|
stack-tests/cucumber-report/
|
|
@@ -110,3 +118,6 @@ taxonomy/src/katalyst_taxonomy/engine/generated/
|
|
|
110
118
|
.HANDOFF-bdd-coverage.md
|
|
111
119
|
.HANDOFF-*.md
|
|
112
120
|
docs/superpowers/
|
|
121
|
+
|
|
122
|
+
# Node dependencies — installed, never committed
|
|
123
|
+
node_modules/
|
|
@@ -10,6 +10,31 @@ workspace packages. Tags use the `engine-vX.Y.Z` prefix.
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
### Changed (breaking)
|
|
16
|
+
|
|
17
|
+
- **`FieldSpec.field_type` must be one of `FIELD_TYPES`** (`string`, `integer`, `number`,
|
|
18
|
+
`boolean`, `list`, `map`, `ref`), exported from `katalyst_engine.schema`. Any other
|
|
19
|
+
value is refused when the field is declared; a consumer that met an unknown type could
|
|
20
|
+
only leave it unchecked. (katalyst-taxonomy#255)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`ScaffoldPolicy.variant_annotation`**: the `spec.annotations` key a kind's creation
|
|
25
|
+
template records a node's scaffold variant under (katalyst-forge's `layer`: `layerType`),
|
|
26
|
+
so a runtime reads it from the declaration instead of knowing the name.
|
|
27
|
+
- **`FieldSpec.map_fields`**: for a `map` field, the keys whose types it declares
|
|
28
|
+
(`scope.layer_types` is a `list`). Declaring them on a field that is not a map is
|
|
29
|
+
refused. Taxonomy lint checks them, and `kata tax create/update --set scope.layer_types=x`
|
|
30
|
+
parses by them. (katalyst-taxonomy#254)
|
|
31
|
+
|
|
32
|
+
## [4.1.0] -- 2026-06-26
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`FieldSpec.allowed_values`** -- a generic, taxonomy-agnostic field-constraint mechanism. When non-empty, it declares the permitted values for a field; empty (the default `()`) means unconstrained. Consumers (e.g. the taxonomy v2 linter) project it to a JSON Schema `enum`. The engine holds only the mechanism; domain-specific values (such as the `work_item` lifecycle) live in the forge consumer, keeping the engine taxonomy-agnostic. Additive and backward-compatible. (#143, #138)
|
|
37
|
+
|
|
13
38
|
## [4.0.0] -- 2026-06-16
|
|
14
39
|
|
|
15
40
|
### Removed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: katalyst-engine
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.1a1
|
|
4
4
|
Summary: A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs
|
|
5
5
|
Project-URL: Repository, https://github.com/esimplicityinc/katalyst-taxonomy
|
|
6
6
|
Author: eSimplicity
|
|
@@ -14,7 +14,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Typing :: Typed
|
|
16
16
|
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: katalyst-core<1,>=0.1
|
|
17
18
|
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: pyyaml>=6
|
|
18
20
|
Description-Content-Type: text/markdown
|
|
19
21
|
|
|
20
22
|
# katalyst-engine
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "katalyst-engine"
|
|
3
|
-
version = "
|
|
3
|
+
version = "5.0.1a1"
|
|
4
4
|
description = "A distributed declaration framework — primitives for schema-aware, multi-source, evolvable artifact graphs"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Proprietary" }
|
|
@@ -23,6 +23,10 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
25
|
"pydantic>=2.0",
|
|
26
|
+
# LinkML is the only schema source: bundles build their definitions from the
|
|
27
|
+
# LinkML modules katalyst-core opens (toolkit/linkml.py).
|
|
28
|
+
"katalyst-core>=0.1,<1",
|
|
29
|
+
"pyyaml>=6",
|
|
26
30
|
]
|
|
27
31
|
|
|
28
32
|
[project.urls]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""katalyst-engine — A distributed declaration framework.
|
|
2
|
+
|
|
3
|
+
Primitives for schema-aware, multi-source, evolvable artifact graphs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError
|
|
7
|
+
from importlib.metadata import version as _dist_version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
# Generated from pyproject.toml rather than restated here. The literal this
|
|
11
|
+
# replaced read "0.1.0" — the original placeholder, carried untouched across four
|
|
12
|
+
# major releases, because nothing asserted the two agreed.
|
|
13
|
+
__version__ = _dist_version("katalyst-engine")
|
|
14
|
+
except PackageNotFoundError: # pragma: no cover - source tree without an install
|
|
15
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -11,6 +11,11 @@ from katalyst_engine.bundle.discovery import (
|
|
|
11
11
|
DiscoveredBundle,
|
|
12
12
|
)
|
|
13
13
|
from katalyst_engine.bundle.loader import BundleLoader
|
|
14
|
+
from katalyst_engine.bundle.policy import (
|
|
15
|
+
BundleLoadPolicy,
|
|
16
|
+
BundleVersionConflictError,
|
|
17
|
+
constraint_is_satisfied,
|
|
18
|
+
)
|
|
14
19
|
from katalyst_engine.bundle.protocol import (
|
|
15
20
|
Bundle,
|
|
16
21
|
BundleManifest,
|
|
@@ -22,9 +27,12 @@ __all__ = [
|
|
|
22
27
|
"BUNDLE_ENTRY_POINT_GROUP",
|
|
23
28
|
"Bundle",
|
|
24
29
|
"BundleDiscovery",
|
|
30
|
+
"BundleLoadPolicy",
|
|
25
31
|
"BundleLoader",
|
|
26
32
|
"BundleManifest",
|
|
33
|
+
"BundleVersionConflictError",
|
|
27
34
|
"DiscoveredBundle",
|
|
35
|
+
"constraint_is_satisfied",
|
|
28
36
|
"get_creation_templates",
|
|
29
37
|
"get_scaffold_templates",
|
|
30
38
|
]
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import importlib.metadata
|
|
12
12
|
import logging
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
|
|
15
16
|
from katalyst_engine.bundle.protocol import Bundle
|
|
@@ -20,6 +21,22 @@ logger = logging.getLogger(__name__)
|
|
|
20
21
|
BUNDLE_ENTRY_POINT_GROUP = "katalyst_engine.bundles"
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _distribution_name(ep: importlib.metadata.EntryPoint) -> str:
|
|
25
|
+
"""Distribution that registered *ep*, or ``""`` when it cannot be determined.
|
|
26
|
+
|
|
27
|
+
``EntryPoint.dist`` is populated by ``entry_points()`` on 3.12 but is not part
|
|
28
|
+
of the type stub's guarantees and is ``None`` for a hand-built EntryPoint, so
|
|
29
|
+
this stays defensive: provenance is nice to have, never worth an exception.
|
|
30
|
+
"""
|
|
31
|
+
dist = ep.dist
|
|
32
|
+
if dist is None:
|
|
33
|
+
return ""
|
|
34
|
+
try:
|
|
35
|
+
return dist.name
|
|
36
|
+
except Exception: # noqa: BLE001 - metadata can be malformed on disk
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
|
|
23
40
|
@dataclass(frozen=True, slots=True)
|
|
24
41
|
class DiscoveredBundle:
|
|
25
42
|
"""Metadata about a discovered (but not yet loaded) bundle.
|
|
@@ -37,17 +54,40 @@ class DiscoveredBundle:
|
|
|
37
54
|
group: str = BUNDLE_ENTRY_POINT_GROUP
|
|
38
55
|
"""Entry point group this was discovered from."""
|
|
39
56
|
|
|
57
|
+
distribution: str = ""
|
|
58
|
+
"""Installed distribution that registered the entry point, e.g. ``katalyst-forge``.
|
|
59
|
+
|
|
60
|
+
Distinct from :attr:`name`, which is the entry-point name and is the handle
|
|
61
|
+
configuration uses to *select* a bundle. The distribution is the handle you
|
|
62
|
+
``pip install`` or uninstall, and a report that gave only one of the two left
|
|
63
|
+
half the questions about a kind's provenance unanswerable (issue #218).
|
|
64
|
+
Empty for bundles that did not arrive over an entry point.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
loader: Callable[[], Bundle] | None = None
|
|
68
|
+
"""Optional pre-resolved factory, used instead of importing ``entry_point``.
|
|
69
|
+
|
|
70
|
+
Bundles that did not come from an entry point — a remote bundle
|
|
71
|
+
path-imported from the lock file, or a test double — are still describable as
|
|
72
|
+
a ``DiscoveredBundle`` this way, which is what lets *one* discovery object
|
|
73
|
+
carry every bundle a process will load (issue #218). ``entry_point`` remains
|
|
74
|
+
meaningful as provenance even when it is not the import path.
|
|
75
|
+
"""
|
|
76
|
+
|
|
40
77
|
def load(self) -> Bundle:
|
|
41
|
-
"""Load and instantiate the bundle
|
|
78
|
+
"""Load and instantiate the bundle.
|
|
42
79
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
no arguments.
|
|
80
|
+
When :attr:`loader` is set it is called and its result returned. Otherwise
|
|
81
|
+
the entry point must be a callable that returns a Bundle instance; if it's
|
|
82
|
+
a class, it will be instantiated with no arguments.
|
|
46
83
|
|
|
47
84
|
Raises:
|
|
48
85
|
ImportError: If the module cannot be imported.
|
|
49
86
|
TypeError: If the loaded object is not a valid Bundle.
|
|
50
87
|
"""
|
|
88
|
+
if self.loader is not None:
|
|
89
|
+
return self.loader()
|
|
90
|
+
|
|
51
91
|
module_path, _, attr_name = self.entry_point.rpartition(":")
|
|
52
92
|
if not attr_name:
|
|
53
93
|
# Entry point format is "module.path" (no colon)
|
|
@@ -111,6 +151,7 @@ class BundleDiscovery:
|
|
|
111
151
|
name=ep.name,
|
|
112
152
|
entry_point=ep.value,
|
|
113
153
|
group=self.group,
|
|
154
|
+
distribution=_distribution_name(ep),
|
|
114
155
|
)
|
|
115
156
|
self._discovered[ep.name] = db
|
|
116
157
|
logger.debug("Discovered bundle: %s -> %s", ep.name, ep.value)
|
|
@@ -156,3 +197,13 @@ class BundleDiscovery:
|
|
|
156
197
|
"""Remove all discovered bundles and reset scan state."""
|
|
157
198
|
self._discovered.clear()
|
|
158
199
|
self._scanned = False
|
|
200
|
+
|
|
201
|
+
def mark_scanned(self) -> None:
|
|
202
|
+
"""Declare this discovery authoritative without scanning entry points.
|
|
203
|
+
|
|
204
|
+
A caller that pre-populates a *filtered* set needs "scanned" to mean
|
|
205
|
+
"complete", not "non-empty". Without this, an explicitly empty set would
|
|
206
|
+
trigger a lazy full scan on first access and silently reintroduce every
|
|
207
|
+
bundle the filter had just removed (issue #218).
|
|
208
|
+
"""
|
|
209
|
+
self._scanned = True
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Bundle loader — discover and register all installed bundles.
|
|
2
|
+
|
|
3
|
+
Provides a high-level ``load_bundles()`` function that discovers
|
|
4
|
+
bundles via entry points, loads them, and registers them with
|
|
5
|
+
a SchemaManager. This is the standard startup path for any
|
|
6
|
+
application using the engine.
|
|
7
|
+
|
|
8
|
+
Loading is governed by a :class:`~katalyst_engine.bundle.policy.BundleLoadPolicy`
|
|
9
|
+
(issue #218): version constraints are checked against
|
|
10
|
+
:attr:`BundleManifest.version` the moment the manifest is read, provenance is
|
|
11
|
+
recorded so the registry can attribute every kind to a bundle, and
|
|
12
|
+
``BundleManifest.entry_point`` is populated for bundles that arrived over an
|
|
13
|
+
entry point so provenance is reportable.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
|
|
21
|
+
from katalyst_engine.bundle.discovery import BundleDiscovery, DiscoveredBundle
|
|
22
|
+
from katalyst_engine.bundle.policy import BundleLoadPolicy
|
|
23
|
+
from katalyst_engine.bundle.protocol import Bundle, BundleManifest
|
|
24
|
+
from katalyst_engine.core.errors import EngineConfigurationError
|
|
25
|
+
from katalyst_engine.schema.manager import SchemaManager
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class BundleLoader:
|
|
32
|
+
"""Loads and registers bundles with a SchemaManager.
|
|
33
|
+
|
|
34
|
+
Manages the lifecycle of bundle discovery → loading → registration.
|
|
35
|
+
Tracks which bundles have been loaded and their manifests.
|
|
36
|
+
|
|
37
|
+
Usage::
|
|
38
|
+
|
|
39
|
+
loader = BundleLoader(schema_manager)
|
|
40
|
+
loader.discover_and_register()
|
|
41
|
+
# Or manually:
|
|
42
|
+
loader.register(my_bundle)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
manager: SchemaManager
|
|
46
|
+
"""The SchemaManager to register bundles with."""
|
|
47
|
+
|
|
48
|
+
policy: BundleLoadPolicy = field(default_factory=BundleLoadPolicy)
|
|
49
|
+
"""Version constraints and conflict handling. Defaults to unconstrained."""
|
|
50
|
+
|
|
51
|
+
_loaded: dict[str, Bundle] = field(default_factory=lambda: dict[str, Bundle]())
|
|
52
|
+
_manifests: dict[str, BundleManifest] = field(
|
|
53
|
+
default_factory=lambda: dict[str, BundleManifest]()
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def register(
|
|
57
|
+
self,
|
|
58
|
+
bundle: Bundle,
|
|
59
|
+
*,
|
|
60
|
+
entry_point: str = "",
|
|
61
|
+
discovered_name: str = "",
|
|
62
|
+
) -> BundleManifest:
|
|
63
|
+
"""Register a single bundle with the SchemaManager.
|
|
64
|
+
|
|
65
|
+
The version constraint is checked *before* anything is registered, so a
|
|
66
|
+
pin violation leaves the registry exactly as it was rather than
|
|
67
|
+
half-populated.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
bundle: A Bundle instance to register.
|
|
71
|
+
entry_point: The entry point string this bundle arrived over, if any.
|
|
72
|
+
Recorded on the returned manifest — ``BundleManifest.entry_point``
|
|
73
|
+
has been declared and unpopulated since the protocol was written.
|
|
74
|
+
discovered_name: The discovery key, if different from the manifest
|
|
75
|
+
name. Consulted when matching version constraints so config may
|
|
76
|
+
name a bundle by its entry-point name.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The bundle's manifest, with ``entry_point`` filled in when known.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If a bundle with the same identity is already loaded.
|
|
83
|
+
BundleVersionConflictError: If the configured version constraint is
|
|
84
|
+
not satisfied.
|
|
85
|
+
SchemaVersionMismatchError: If the bundle's LinkML modules do not carry
|
|
86
|
+
its version.
|
|
87
|
+
SchemaConflictError: If another bundle already provides one of this
|
|
88
|
+
bundle's kinds, wings, relationships or directory names.
|
|
89
|
+
"""
|
|
90
|
+
manifest = bundle.manifest
|
|
91
|
+
fqn = manifest.identity.fqn
|
|
92
|
+
name = manifest.identity.name
|
|
93
|
+
|
|
94
|
+
if fqn in self._loaded:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Bundle already registered: {fqn!r}. "
|
|
97
|
+
f"Unregister it first or use a different identity."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self.policy.check(fqn, name, discovered_name, version=manifest.version)
|
|
101
|
+
if manifest.modules:
|
|
102
|
+
from katalyst_engine.evolution.release import bundle_release
|
|
103
|
+
|
|
104
|
+
bundle_release(bundle).check_version()
|
|
105
|
+
|
|
106
|
+
if entry_point and not manifest.entry_point:
|
|
107
|
+
manifest = manifest.model_copy(update={"entry_point": entry_point})
|
|
108
|
+
|
|
109
|
+
provider = name or discovered_name or fqn
|
|
110
|
+
with self.manager.registry.providing(provider):
|
|
111
|
+
bundle.register(self.manager)
|
|
112
|
+
|
|
113
|
+
self._loaded[fqn] = bundle
|
|
114
|
+
self._manifests[fqn] = manifest
|
|
115
|
+
|
|
116
|
+
logger.info(
|
|
117
|
+
"Registered bundle %r: %d kinds, %d wings",
|
|
118
|
+
fqn,
|
|
119
|
+
len(manifest.provided_kinds),
|
|
120
|
+
len(manifest.wing_names),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return manifest
|
|
124
|
+
|
|
125
|
+
def discover_and_register(
|
|
126
|
+
self,
|
|
127
|
+
discovery: BundleDiscovery | None = None,
|
|
128
|
+
*,
|
|
129
|
+
strict: bool | None = None,
|
|
130
|
+
) -> list[BundleManifest]:
|
|
131
|
+
"""Discover bundles via entry points and register them all.
|
|
132
|
+
|
|
133
|
+
Configuration failures — an unsatisfied version pin, two bundles
|
|
134
|
+
claiming one kind — always propagate. They are statements about what the
|
|
135
|
+
operator asked for, and logging them would turn a misconfiguration into a
|
|
136
|
+
silently reduced type system.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
discovery: Optional pre-configured BundleDiscovery. If None, creates
|
|
140
|
+
a new one and scans entry points. Pass a pre-populated discovery
|
|
141
|
+
to load a *filtered* set — this is the seam the taxonomy layer's
|
|
142
|
+
config-aware discovery factory uses.
|
|
143
|
+
strict: Whether a bundle that fails for a non-configuration reason
|
|
144
|
+
(broken import, corrupt package) aborts the whole load. Defaults
|
|
145
|
+
to :attr:`BundleLoadPolicy.strict`.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of manifests for all successfully registered bundles.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
EngineConfigurationError: On any configuration failure.
|
|
152
|
+
Exception: Under ``strict``, whatever the failing bundle raised,
|
|
153
|
+
chained to a message naming it.
|
|
154
|
+
"""
|
|
155
|
+
if discovery is None:
|
|
156
|
+
discovery = BundleDiscovery()
|
|
157
|
+
discovery.scan()
|
|
158
|
+
|
|
159
|
+
fail_fast = self.policy.strict if strict is None else strict
|
|
160
|
+
manifests: list[BundleManifest] = []
|
|
161
|
+
|
|
162
|
+
for discovered in discovery.all():
|
|
163
|
+
try:
|
|
164
|
+
bundle = discovered.load()
|
|
165
|
+
manifest = self.register(
|
|
166
|
+
bundle,
|
|
167
|
+
entry_point=discovered.entry_point,
|
|
168
|
+
discovered_name=discovered.name,
|
|
169
|
+
)
|
|
170
|
+
manifests.append(manifest)
|
|
171
|
+
except EngineConfigurationError:
|
|
172
|
+
raise
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
if fail_fast:
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
f"Failed to load schema bundle {discovered.name!r}: {exc}"
|
|
177
|
+
) from exc
|
|
178
|
+
logger.exception("Failed to load bundle %r", discovered.name)
|
|
179
|
+
|
|
180
|
+
return manifests
|
|
181
|
+
|
|
182
|
+
def register_discovered(self, discovered: DiscoveredBundle) -> BundleManifest:
|
|
183
|
+
"""Load a single DiscoveredBundle and register it.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
discovered: A discovered bundle to load and register.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The bundle's manifest.
|
|
190
|
+
"""
|
|
191
|
+
bundle = discovered.load()
|
|
192
|
+
return self.register(
|
|
193
|
+
bundle,
|
|
194
|
+
entry_point=discovered.entry_point,
|
|
195
|
+
discovered_name=discovered.name,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def get_manifest(self, fqn: str) -> BundleManifest | None:
|
|
199
|
+
"""Look up a loaded bundle's manifest by FQN."""
|
|
200
|
+
return self._manifests.get(fqn)
|
|
201
|
+
|
|
202
|
+
def all_manifests(self) -> list[BundleManifest]:
|
|
203
|
+
"""Return all loaded bundle manifests, sorted by FQN."""
|
|
204
|
+
return sorted(self._manifests.values(), key=lambda m: m.identity.fqn)
|
|
205
|
+
|
|
206
|
+
def is_loaded(self, fqn: str) -> bool:
|
|
207
|
+
"""Check if a bundle with the given FQN is loaded."""
|
|
208
|
+
return fqn in self._loaded
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def loaded_count(self) -> int:
|
|
212
|
+
"""Number of loaded bundles."""
|
|
213
|
+
return len(self._loaded)
|
|
@@ -18,10 +18,12 @@ bundle's own code executes:
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import importlib
|
|
21
22
|
import importlib.util
|
|
22
23
|
import sys
|
|
23
24
|
from dataclasses import dataclass
|
|
24
25
|
from pathlib import Path
|
|
26
|
+
from types import ModuleType
|
|
25
27
|
|
|
26
28
|
from katalyst_engine.bundle.protocol import Bundle
|
|
27
29
|
|
|
@@ -139,6 +141,14 @@ def load_bundle_from_path(target: PathImportTarget) -> Bundle:
|
|
|
139
141
|
module_file = target.module_file
|
|
140
142
|
if not module_file.is_file():
|
|
141
143
|
raise BundleLoadError(f"Bundle module file not found: {module_file}")
|
|
144
|
+
if module_file.suffix in (".yaml", ".yml"):
|
|
145
|
+
# A LinkML schema bundle: nothing to import, nothing to run.
|
|
146
|
+
from katalyst_engine.toolkit.linkml import LinkMLBundleError, LinkMLFileBundle
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
return LinkMLFileBundle(module_file)
|
|
150
|
+
except LinkMLBundleError as exc:
|
|
151
|
+
raise BundleLoadError(str(exc)) from exc
|
|
142
152
|
|
|
143
153
|
# Derive a stable, unique module name from the file path.
|
|
144
154
|
module_name = f"_katalyst_remote_bundle_{abs(hash(str(module_file)))}"
|
|
@@ -178,3 +188,118 @@ def load_bundle_from_path(target: PathImportTarget) -> Bundle:
|
|
|
178
188
|
finally:
|
|
179
189
|
if added_to_path and str(package_root) in sys.path:
|
|
180
190
|
sys.path.remove(str(package_root))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# Importing a package under its real name (code plugins)
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PackageShadowError(ValueError):
|
|
199
|
+
"""A package path-imported by name would shadow one importable from elsewhere."""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
#: Top-level package name -> the identity (root + content digest) it was imported with.
|
|
203
|
+
_PACKAGES: dict[str, str] = {}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _drop_package(package: str) -> None:
|
|
207
|
+
for name in [n for n in sys.modules if n == package or n.startswith(f"{package}.")]:
|
|
208
|
+
del sys.modules[name]
|
|
209
|
+
_PACKAGES.pop(package, None)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _drop_bytecode(package_dir: Path) -> None:
|
|
213
|
+
import shutil
|
|
214
|
+
|
|
215
|
+
if package_dir.is_dir():
|
|
216
|
+
for cache in package_dir.rglob("__pycache__"):
|
|
217
|
+
shutil.rmtree(cache, ignore_errors=True)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _importable_elsewhere(package: str, root: Path) -> str | None:
|
|
221
|
+
existing = sys.modules.get(package)
|
|
222
|
+
if existing is not None:
|
|
223
|
+
return str(getattr(existing, "__file__", None) or repr(existing))
|
|
224
|
+
try:
|
|
225
|
+
spec = importlib.util.find_spec(package)
|
|
226
|
+
except (ImportError, ValueError):
|
|
227
|
+
return None
|
|
228
|
+
if spec is None:
|
|
229
|
+
return None
|
|
230
|
+
origin = spec.origin or next(iter(spec.submodule_search_locations or ()), None)
|
|
231
|
+
if origin is None:
|
|
232
|
+
return "a namespace package"
|
|
233
|
+
try:
|
|
234
|
+
if Path(origin).resolve().is_relative_to(root.resolve()):
|
|
235
|
+
return None
|
|
236
|
+
except OSError:
|
|
237
|
+
pass
|
|
238
|
+
return origin
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def import_module_from_root(root: Path, module_path: str, *, identity: str = "") -> ModuleType:
|
|
242
|
+
"""Import dotted *module_path* from *root* under its **real** name; ``sys.path`` untouched.
|
|
243
|
+
|
|
244
|
+
Unlike :func:`load_bundle_from_path`, which imports one file under a mangled name,
|
|
245
|
+
this registers the top-level package from *root* in ``sys.modules`` with
|
|
246
|
+
``submodule_search_locations`` set, so the package's own absolute and relative
|
|
247
|
+
imports of its submodules resolve from *root*. Used for code plugins, which are
|
|
248
|
+
ordinary packages.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
root: The directory that plays ``sys.path`` for the package.
|
|
252
|
+
module_path: ``pkg`` or ``pkg.sub.module``.
|
|
253
|
+
identity: What this import is of (root plus content digest). Importing the same
|
|
254
|
+
package with a different identity forgets the previous modules first, so a
|
|
255
|
+
changed tree is what runs.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
PackageShadowError: The package is importable from somewhere else (an installed
|
|
259
|
+
distribution, the standard library) — registering it would hijack it.
|
|
260
|
+
BaseException: Whatever the package's own code raises; its modules are
|
|
261
|
+
forgotten first.
|
|
262
|
+
"""
|
|
263
|
+
package = module_path.split(".", 1)[0]
|
|
264
|
+
identity = identity or str(root)
|
|
265
|
+
if _PACKAGES.get(package) not in (None, identity):
|
|
266
|
+
_drop_package(package)
|
|
267
|
+
# Bytecode is validated by source mtime (whole seconds) and size, so content
|
|
268
|
+
# changed within the same second at the same size would run stale code.
|
|
269
|
+
_drop_bytecode(root / package)
|
|
270
|
+
importlib.invalidate_caches()
|
|
271
|
+
if package not in _PACKAGES:
|
|
272
|
+
elsewhere = _importable_elsewhere(package, root)
|
|
273
|
+
if elsewhere is not None:
|
|
274
|
+
raise PackageShadowError(
|
|
275
|
+
f"Package {package!r} is already importable from {elsewhere}; a package "
|
|
276
|
+
"path-imported from a source may not shadow an existing module."
|
|
277
|
+
)
|
|
278
|
+
package_dir = root / package
|
|
279
|
+
init = package_dir / "__init__.py"
|
|
280
|
+
if init.is_file():
|
|
281
|
+
spec = importlib.util.spec_from_file_location(
|
|
282
|
+
package, init, submodule_search_locations=[str(package_dir)]
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
spec = importlib.util.spec_from_file_location(package, root / f"{package}.py")
|
|
286
|
+
if spec is None or spec.loader is None:
|
|
287
|
+
raise BundleLoadError(f"Could not create an import spec for {package!r} under {root}")
|
|
288
|
+
module = importlib.util.module_from_spec(spec)
|
|
289
|
+
sys.modules[package] = module
|
|
290
|
+
try:
|
|
291
|
+
spec.loader.exec_module(module)
|
|
292
|
+
except BaseException:
|
|
293
|
+
_drop_package(package)
|
|
294
|
+
raise
|
|
295
|
+
_PACKAGES[package] = identity
|
|
296
|
+
try:
|
|
297
|
+
return importlib.import_module(module_path)
|
|
298
|
+
except BaseException:
|
|
299
|
+
_drop_package(package)
|
|
300
|
+
raise
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def path_imported_package(package: str) -> bool:
|
|
304
|
+
"""Whether *package* was imported by :func:`import_module_from_root` in this process."""
|
|
305
|
+
return package in _PACKAGES
|