Repository navigation
fix(python): a top-level call of a harmless same-module function keeps the package namespace - #2713
Merged
Merged
Conversation
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 <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…s the namespace A top-level call withdrew the whole namespace (UNKNOWN, reason EFFECT): sqlalchemy's sql/__init__.py ends with __go(locals()) and lost all 108 re-exports. The call is now allowed when the callee is the latest binding of its name, a proven same-module function with no star import or unknown effect since; every argument is a safe expression or a bare locals()/globals() the module does not rebind; and the body (nested scopes included) has no global/nonlocal and never names a parameter, a function or class the module defined earlier, the module's own name, or a name that reaches a namespace (globals, vars, locals, exec, eval, setattr, delattr, __import__, sys, importlib, __name__, __dict__, __globals__, __builtins__, builtins, __self__, inspect, f_globals, f_locals, f_back, gc). Each submodule of the package the body imports becomes a package attribute and is shadowed at the call; in a plain module the imports install nothing. Every other call stays a whole-namespace effect. The function table and verdicts live in the extraction scratch; each callee is checked once. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
WHY: the check read an import's module path with strcspn, which runs to a NUL, but a tree-sitter source slice need not be NUL-terminated: the first segment is now found with memchr inside the slice. The leading dots move into py_ns_leading_dots; written inline, cppcheck 2.20 (the CI version) took the loop to always end at the slice length and reported three knownConditionTrueFalse findings on the branches that follow it. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…ace-effect-allowlist Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData
changed the base branch from
fix/py-namespace-string-future-facts
to
main
October 19, 2026 22:26
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2712; this PR's base is its branch, and it is retargeted to main once #2712 merges.
What
#2704 publishes a Python package's re-exports from the module's namespace facts. An unknown statement in those facts withdraws every re-export recorded before it. A top-level call is such a statement, because it can rebind any name of the module. sqlalchemy ends three of its packages with one:
sql.cast,sql.selectandsql.and_therefore never reached the export table. They resolved by name guesses to the methodsColumnElement.cast,FunctionElement.selectandBooleanClauseList.and_.This change (
extract_imports.c) keeps the namespace through a top-level call only when the callee provably cannot rebind the caller's names:locals()/globals()the module does not rebind;global/nonlocal, and never names:global);globals vars locals exec eval setattr delattr __import__ sys importlib __name__ __dict__ __globals__ __builtins__ builtins __self__ inspect f_globals f_locals f_back gc.__init__those names are shadowed at the call.Every other call stays a whole-namespace effect. Other languages are unchanged.
Evidence
Clean builds; base = #2712's head (docstrings and
__future__facts), head = this branch; par, repeat 2, noise 0.sql.*calls the barrier held:sql.and_×23 →_elements_constructors.and_;sql.select×22 →_selectable_constructors.select;sql.cast×7 →_elements_constructors.cast;sql.any_×2,sql.or_×1.import_map_suffixtolsp_module_attr:bindparam,text,null,case,literal_column,column,tuple_,orm.base.class_mapper, and others.__gopackages now publish. In sqlalchemy, re-exported names that other files import from the module fall from 572 to 258 withdrawn, and importer/name pairs from 4,888 to 464.Controls on the real files. Across the three repos exactly 7 top-level calls change their facts, and each was read on its source:
__go(locals())calls. Each is shadowed exactly by the submodules its body imports (for sql:base,coercions,elements,lambdas,selectable,schema,traversals,type_api,annotation).constants._load_platform_data(): mutates a module dict in place and calls an imported function; it rebinds no name.identity._register_providers(): ninefrom .X.provider import ...lines, which give nine correct subpackage shadows; it calls an importedregister.main()calls, which have no consumers.None of these calls rebinds a name.
Full payoff with F13. Most
from sqlalchemy import selectimports are still bound by the import resolver's project-wide name guess (WriteOnlyCollection.select) before the export table is consulted. The follow-up that binds a Python from-import inside the module it names turns the published names into retargets: of sqlalchemy's 1,091 IMPORTS edges to a method, 1,046 are predicted to retarget with this PR in place, against 429 without it.Tests
extraction: python_namespace_facts_init_call_allowlist:__goshape: the namespace survives, and the imported submodules are shadowed at the call;global(also nested);globals();sys.modules[__name__]; the own package name; a same-module helper; an imported callee; a rebound callee; a call before the definition; an argument with an effect; a reboundlocals.pipeline: pipeline_python_package_reexports_resolve_by_lspadds the__gopackage (a re-export resolves; a submodule the body imports is excluded) and a__gothat writes its parameter (excluded), across both drivers × full and incremental.lint-memory-corereports no growth; cppcheck 2.20 and clang-format are clean.