From 74b115e929d458b24ee714c9121d7cf2e8642ea5 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 19 Oct 2026 04:23:06 +0200 Subject: [PATCH] test(python): a top-level __go(locals()) keeps the package namespace sqlalchemy's sql/__init__.py ends with __go(locals()). The callee is a same-module function that never touches its parameter or the module's globals; only the submodules its body imports become package attributes. The extraction test pins the exact shape (the namespace survives, the imported submodules are shadowed at the call) and its controls: the body uses the parameter, global (also nested), globals(), sys.modules[__name__], the own package name, a same-module helper; an imported, rebound or not yet defined callee; an argument with an effect; a module-level locals. The pipeline fixture adds the same shape end to end, with a re-export the body's submodule import replaces and a __go that writes its parameter. Signed-off-by: Martin Vogel --- tests/test_extraction.c | 111 ++++++++++++++++++++++++++++++++++++++++ tests/test_pipeline.c | 30 ++++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 3391eeb8a..ebfa48f8c 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -12408,6 +12408,116 @@ TEST(python_namespace_facts_string_statements_and_future_imports) { PASS(); } +/* A top-level call of a same-module function is a namespace effect, unless the + * callee provably cannot rebind the caller's names: sqlalchemy's + * `sql/__init__.py` ends with `__go(locals())`. Every submodule its body + * imports becomes a package attribute, so those names are shadowed at the + * call; everything else of the namespace survives. */ +static bool py_facts_has_effect(const CBMFileResult *r) { + for (int i = 0; r && i < r->py_namespace.count; i++) { + if (r->py_namespace.items[i].kind == CBM_PY_NS_UNKNOWN && + r->py_namespace.items[i].reason == CBM_PY_NS_REASON_EFFECT) + return true; + } + return false; +} + +static int py_facts_count_kind(const CBMFileResult *r, CBMPyNamespaceFactKind kind) { + int n = 0; + for (int i = 0; r && i < r->py_namespace.count; i++) + n += r->py_namespace.items[i].kind == kind; + return n; +} + +static bool py_facts_valid_complete(const CBMFileResult *r) { + return r && !r->has_error && r->py_namespace.status == CBM_PY_NS_COMPLETE && + cbm_py_namespace_facts_valid(&r->py_namespace); +} + +TEST(python_namespace_facts_init_call_allowlist) { + static const char go[] = "from typing import TYPE_CHECKING, Any\n" + "from .impl import base\n" + "from .elements import cast\n" + "def __go(lcls: Any) -> None:\n" + " from .. import util as _sa_util\n" + " from . import base\n" + " from . import coercions, elements as _el\n" + " if not TYPE_CHECKING:\n" + " base.coercions = _el.coercions = coercions\n" + " from .annotation import _prepare_annotations\n" + " _prepare_annotations(cast, cast)\n" + " _sa_util.preloaded.import_prefix(\"sqlalchemy.sql\")\n" + "\n" + "__go(locals())\n"; + CBMFileResult *r = extract(go, CBM_LANG_PYTHON, "t", "sql/__init__.py"); + bool ok = py_facts_valid_complete(r) && !py_facts_has_effect(r) && r->py_namespace.count == 9; + if (ok) { + const CBMPyNamespaceFact *f = r->py_namespace.items; + static const char *const shadowed[] = {"base", "coercions", "elements", "annotation"}; + ok = f[2].kind == CBM_PY_NS_IMPORT_NAME && strcmp(f[2].local_name, "base") == 0 && + f[4].kind == CBM_PY_NS_OWN_DEF && strcmp(f[4].local_name, "__go") == 0; + for (int i = 0; ok && i < 4; i++) { + ok = f[5 + i].kind == CBM_PY_NS_SHADOW && f[5 + i].local_name && + strcmp(f[5 + i].local_name, shadowed[i]) == 0; + } + } + if (!ok && r) { + for (int i = 0; i < r->py_namespace.count; i++) { + const CBMPyNamespaceFact *f = &r->py_namespace.items[i]; + printf(" __go fact %d: kind %d reason %d name %s\n", i, (int)f->kind, (int)f->reason, + f->local_name ? f->local_name : "-"); + } + } + if (r) + cbm_free_result(r); + ASSERT_TRUE(ok); /* RED while every top-level call withdraws the namespace */ + + /* A plain module is no package: the body's imports set attributes of its + * parent package, never of the module itself. */ + r = extract("def __go(lcls):\n from . import base\n\n__go(locals())\n", CBM_LANG_PYTHON, + "t", "sql/mod.py"); + ok = py_facts_valid_complete(r) && !py_facts_has_effect(r) && + py_facts_count_kind(r, CBM_PY_NS_SHADOW) == 0; + if (r) + cbm_free_result(r); + ASSERT_TRUE(ok); + + /* Each of these can rebind a name of the caller's namespace (or is not + * provably a same-module function): the call stays a whole-namespace + * effect. */ + static const char *const effects[][2] = { + {"param", "def __go(lcls):\n lcls['base'] = 1\n\n__go(locals())\n"}, + {"global", "def __go(lcls):\n global base\n base = 1\n\n__go(locals())\n"}, + {"nested global", "def __go(lcls):\n def inner():\n global base\n" + " base = 1\n inner()\n\n__go(locals())\n"}, + {"globals()", "def __go(lcls):\n globals()['base'] = 1\n\n__go(locals())\n"}, + {"sys.modules", "def __go(lcls):\n import sys\n sys.modules[__name__].base = 1\n" + "\n__go(locals())\n"}, + {"imported callee", "from .setup import __go\n__go(locals())\n"}, + {"own package name", "def __go(lcls):\n from .. import sql\n sql.base = 1\n" + "\n__go(locals())\n"}, + {"same-module helper", "def helper():\n pass\n\ndef __go(lcls):\n helper()\n" + "\n__go(locals())\n"}, + {"rebound callee", "def __go(lcls):\n pass\n\n__go = other\n__go(locals())\n"}, + {"call before def", "__go(locals())\n\ndef __go(lcls):\n pass\n"}, + {"argument effect", "def __go(lcls):\n pass\n\n__go(make())\n"}, + {"rebound locals", "def locals():\n return {}\n\ndef __go(lcls):\n pass\n" + "\n__go(locals())\n"}, + }; + for (size_t i = 0; i < sizeof(effects) / sizeof(effects[0]); i++) { + char src[512]; + snprintf(src, sizeof(src), "from .impl import base\n%s", effects[i][1]); + r = extract(src, CBM_LANG_PYTHON, "t", "sql/__init__.py"); + ok = py_facts_valid_complete(r) && py_facts_has_effect(r); + if (!ok) + printf(" control '%s' is no namespace effect\n", effects[i][0]); + if (r) + cbm_free_result(r); + ASSERT_TRUE(ok); + } + PASS(); +} + TEST(python_namespace_facts_distinguish_all_operations_and_unknowns) { static const char source[] = "__all__ = ['old']; __all__ = []\n" "__all__ = dynamic\n" @@ -12796,6 +12906,7 @@ SUITE(extraction) { RUN_TEST(python_namespace_facts_parse_errors_are_complete_unknown); RUN_TEST(python_namespace_facts_type_checking_alternatives); RUN_TEST(python_namespace_facts_string_statements_and_future_imports); + RUN_TEST(python_namespace_facts_init_call_allowlist); 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); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 03cdb1933..2ee50f479 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -14423,16 +14423,20 @@ static const char *const py_reexport_calls[][2] = { {"ctl_from_fn", "direct.impl.direct_fn"}, {"lazy_tc", "lz.lazy_mod.Lazy"}, {"doc_reexport", "docp.impl.Doc"}, + {"go_reexport", "gop.impl.Thing"}, }; enum { PY_REEXPORT_N_CALLS = sizeof(py_reexport_calls) / sizeof(py_reexport_calls[0]) }; /* Calls that must NOT reach their target by the LSP: an `__all__`-unlisted * name, a private name under a star, a re-export the module shadows later, - * and one made before a whole-namespace barrier (a top-level `try:`). */ + * one made before a whole-namespace barrier (a top-level `try:`), a + * re-export a submodule import of `__go` replaces, and one behind a `__go` + * that writes its parameter. */ static const char *const py_reexport_excluded[][2] = { {"excl_unlisted", "pkg.all_mod.Unlisted"}, {"excl_private", "pkg.star_mod._private_fn"}, {"excl_shadowed", "shad.impl.Gone"}, {"excl_barrier", "barr.impl.Before"}, {"excl_compat", "cmp.types_only.Shim"}, {"excl_fstring", "fsp.impl.Kept"}, + {"excl_go_submodule", "gop.impl.base"}, {"excl_go_param", "gox.impl.Kept"}, }; enum { PY_REEXPORT_N_EXCLUDED = sizeof(py_reexport_excluded) / sizeof(py_reexport_excluded[0]) }; @@ -14505,6 +14509,25 @@ static int py_reexport_run(bool parallel, bool incremental, bool *lsp, bool *exc "from .impl import Kept\n" "f\"{Kept}\"\n"); write_temp_file(tmp, "app/fsp/impl.py", "class Kept:\n pass\n"); + /* sqlalchemy's sql/__init__.py shape: a top-level `__go(locals())`. */ + write_temp_file(tmp, "app/gop/__init__.py", + "from typing import Any\n" + "from .impl import Thing\n" + "from .impl import base\n\n\n" + "def __go(lcls: Any) -> None:\n" + " from . import base\n" + " from .annotation import prepare\n" + " prepare(Thing)\n\n\n" + "__go(locals())\n"); + write_temp_file(tmp, "app/gop/impl.py", + "class Thing:\n pass\n\n\ndef base():\n return 1\n"); + write_temp_file(tmp, "app/gop/base.py", "def helper():\n return 1\n"); + write_temp_file(tmp, "app/gop/annotation.py", "def prepare(cls):\n return cls\n"); + write_temp_file(tmp, "app/gox/__init__.py", + "from .impl import Kept\n\n\n" + "def __go(lcls):\n lcls[\"Kept\"] = None\n\n\n" + "__go(locals())\n"); + write_temp_file(tmp, "app/gox/impl.py", "class Kept:\n pass\n"); write_temp_file(tmp, "app/main.py", "from app import pkg\n" "from app import cyc\n" @@ -14514,6 +14537,8 @@ static int py_reexport_run(bool parallel, bool incremental, bool *lsp, bool *exc "from app import cmp\n" "from app import docp\n" "from app import fsp\n" + "from app import gop\n" + "from app import gox\n" "from app.db import models\n" "from app.pkg import Explicit, Listed, Deep, star_fn, Renamed\n" "from app.direct.impl import Direct, direct_fn\n\n\n" @@ -14541,6 +14566,9 @@ static int py_reexport_run(bool parallel, bool incremental, bool *lsp, bool *exc "def lazy_tc():\n lz.Lazy()\n\n\n" "def doc_reexport():\n docp.Doc()\n\n\n" "def excl_fstring():\n fsp.Kept()\n\n\n" + "def go_reexport():\n gop.Thing()\n\n\n" + "def excl_go_submodule():\n gop.base()\n\n\n" + "def excl_go_param():\n gox.Kept()\n\n\n" "def ctl_from_cls():\n Direct()\n\n\n" "def ctl_from_fn():\n direct_fn()\n"); for (int i = 0; parallel && i < 52; i++) {