fix(python): give the alternative-branch shadow a valid reason

A namespace fact stream is checked by cbm_py_namespace_facts_valid, which
accepts a shadow only for the value, decorated, unproven-class and
unsupported reasons. The withdrawn TYPE_CHECKING re-export carried the
compound reason, so the whole stream was rejected and the package published
no names at all (a lazy-module package lost its TYPE_CHECKING exports). It
now carries the unsupported reason: the builder cannot tell which source the
name has at runtime. python_namespace_facts_type_checking_alternatives pins
both shapes as complete, valid streams.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-19 01:38:03 +02:00
1 parent eb9594d35c
commit 7c3f50605b
2 files changed
+41 -5

No files matched your search

+5 -5
View File
@@ -757,10 +757,10 @@ static bool py_ns_is_type_checking_if(PyNamespaceBuilder *b, TSNode node) {
* at runtime from wherever that branch says (the compat shape: `else: try:
* from runtime.place import name`). A static reader cannot tell which source
* the module ends up exporting, so every name such an import binds becomes a
* SHADOW (the re-export is withdrawn, as for a later assignment); a star
* import, or an import form the builder does not read, may bind any name, so
* the whole namespace becomes unknown. An iterative walk of the branch: deep
* nesting must not exhaust the stack. */
* SHADOW (reason UNSUPPORTED: the re-export is withdrawn, as for a later
* assignment); a star import, or an import form the builder does not read,
* may bind any name, so the whole namespace becomes unknown. An iterative
* walk of the branch: deep nesting must not exhaust the stack. */
static bool py_ns_alternative_imports(PyNamespaceBuilder *b, TSNode root) {
TSTreeCursor cursor = ts_tree_cursor_new(root);
bool done = false;
@@ -776,7 +776,7 @@ static bool py_ns_alternative_imports(PyNamespaceBuilder *b, TSNode root) {
star = b->facts.items[i].kind == CBM_PY_NS_IMPORT_STAR;
b->facts.items[i] = (CBMPyNamespaceFact){.kind = CBM_PY_NS_SHADOW,
.local_name = b->facts.items[i].local_name,
.reason = CBM_PY_NS_REASON_COMPOUND};
.reason = CBM_PY_NS_REASON_UNSUPPORTED};
}
if (star && !b->failure) {
b->facts.count = start;
+36
View File
@@ -12108,6 +12108,41 @@ TEST(python_namespace_facts_preserve_order_and_import_provenance) {
PASS();
}
/* The type checker view of `if TYPE_CHECKING:` publishes its consequence (a
* lazy-module package: the else branch only installs the module). A name an
* elif/else import binds again is withdrawn by a SHADOW: its runtime source is
* the alternative. Both streams stay COMPLETE and valid. */
TEST(python_namespace_facts_type_checking_alternatives) {
CBMFileResult *lazy = extract("from typing import TYPE_CHECKING\n"
"if TYPE_CHECKING:\n from .lazy_mod import Lazy\n"
"else:\n import sys\n",
CBM_LANG_PYTHON, "t", "lz/__init__.py");
CBMFileResult *compat = extract("from typing import TYPE_CHECKING\n"
"if TYPE_CHECKING:\n from .types_only import Shim\n"
"else:\n try:\n from .runtime_impl import Shim\n"
" except ImportError:\n Shim = None\n",
CBM_LANG_PYTHON, "t", "cmp/__init__.py");
bool ok = lazy && compat && lazy->py_namespace.status == CBM_PY_NS_COMPLETE &&
compat->py_namespace.status == CBM_PY_NS_COMPLETE &&
cbm_py_namespace_facts_valid(&lazy->py_namespace) &&
cbm_py_namespace_facts_valid(&compat->py_namespace) &&
lazy->py_namespace.count == 3 && compat->py_namespace.count == 3;
if (ok) {
const CBMPyNamespaceFact *l = lazy->py_namespace.items;
const CBMPyNamespaceFact *c = compat->py_namespace.items;
ok = l[1].kind == CBM_PY_NS_IMPORT_NAME && strcmp(l[1].local_name, "Lazy") == 0 &&
l[2].kind == CBM_PY_NS_SHADOW && strcmp(l[2].local_name, "sys") == 0 &&
c[1].kind == CBM_PY_NS_IMPORT_NAME && strcmp(c[1].local_name, "Shim") == 0 &&
c[2].kind == CBM_PY_NS_SHADOW && strcmp(c[2].local_name, "Shim") == 0;
}
if (lazy)
cbm_free_result(lazy);
if (compat)
cbm_free_result(compat);
ASSERT_TRUE(ok);
PASS();
}
TEST(python_namespace_facts_distinguish_all_operations_and_unknowns) {
static const char source[] = "__all__ = ['old']; __all__ = []\n"
"__all__ = dynamic\n"
@@ -12494,6 +12529,7 @@ SUITE(extraction) {
RUN_TEST(python_namespace_facts_distinguish_all_operations_and_unknowns);
RUN_TEST(python_namespace_facts_unknown_effects_and_body_stability);
RUN_TEST(python_namespace_facts_parse_errors_are_complete_unknown);
RUN_TEST(python_namespace_facts_type_checking_alternatives);
RUN_TEST(python_namespace_facts_capture_and_copy_fail_atomically);
RUN_TEST(python_namespace_facts_validation_rejects_partial_and_unknown_schema);
RUN_TEST(python_namespace_facts_compact_spill_and_copy_keep_owned_payload);