fix(resolution): explicit dotted aliases, one-line __main__ guards, scoped vars()

Review follow-up.
- `import pkg.settings as settings` binds `settings` even though it equals
  the last segment; the import mapping cannot tell it from a plain
  `import pkg.settings`, so the source line decides.
- `if __name__ == "__main__": stmt` on one line is script code, and a
  `globals()` write inside a `__main__` block no longer counts.
- `vars()` is the module dict only at module scope; inside a function it
  is the locals, so `return vars()` there no longer makes the type unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
This commit is contained in:
Josh66
2026-09-28 13:18:24 +00:00
parent 7aab29269a
commit 0a5fb69797
2 changed files with 24 additions and 4 deletions
+10
View File
@@ -1439,6 +1439,16 @@ def init():
namespace_import_binds_root: [{ 'pkg/__init__.py': '', 'pkg/settings.py': header,
'c.py': 'import pkg.settings\nfrom decoy import Decoy\n\nsettings = Bag()\nsettings.conn = Decoy()\n',
'd.py': 'from pkg import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, ['cb -> Store::fetch']],
explicit_alias_equal_to_leaf: [{ 'pkg/__init__.py': '', 'pkg/settings.py': header,
'reset.py': 'import pkg.settings as settings\nfrom decoy import Decoy\n\ndef reset():\n settings.conn = Decoy()\n',
'd.py': 'from pkg import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, []],
one_line_main_write: [{ 'settings.py': header,
'run.py': 'import settings\nfrom decoy import Decoy\n\nif __name__ == "__main__": settings.conn = Decoy()\n',
'c.py': 'import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, ['cb -> Store::fetch']],
globals_in_main_block: [{ 'm.py': header + 'if __name__ == "__main__":\n globals()["conn"] = Decoy()\n\ndef cb(pool):\n pool.submit(conn.fetch)\n' },
['cb -> Store::fetch']],
vars_in_function_is_locals: [{ 'm.py': header + 'def dump():\n return vars()\n\ndef cb(pool):\n pool.submit(conn.fetch)\n' },
['cb -> Store::fetch']],
example_write_is_production: [{ 'settings.py': header,
'examples/wire.py': 'import settings\nfrom decoy import Decoy\n\ndef wire():\n settings.conn = Decoy()\n',
'c.py': 'import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, []],
+14 -4
View File
@@ -684,7 +684,10 @@ function pythonModuleAliases(filePath: string, moduleFile: string, context: Reso
: /^\.+$/.test(m.source) ? `${m.source}${m.exportedName}` : `${m.source}.${m.exportedName}`;
const files = pythonModuleFiles(dotted, filePath, context);
if (!files.includes(moduleFile)) continue;
const plainDotted = m.isNamespace && m.source.includes('.') && m.localName === m.source.split('.').pop();
// The mapping cannot tell `import a.b` from `import a.b as b`; the source line can.
const explicit = new RegExp(`\\bimport\\s[^\\n]*\\b${m.source.replace(/\./g, '\\.')}\\s+as\\s+${m.localName}\\b`);
const plainDotted = m.isNamespace && m.source.includes('.') && m.localName === m.source.split('.').pop() &&
!pythonMemberLines(filePath, context).some(line => explicit.test(line));
(files.length === 1 ? aliases : ambiguous).add(plainDotted ? m.source : m.localName);
}
return { aliases: [...aliases], ambiguous: [...ambiguous] };
@@ -697,7 +700,9 @@ function pythonMainBlockLines(filePath: string, context: ResolutionContext): Set
const lines = pythonMemberLines(filePath, context);
const inside = new Set<number>();
for (let i = 0; i < lines.length; i++) {
if (!PYTHON_MAIN_GUARD.test(lines[i]!)) continue;
const guard = lines[i]!.match(PYTHON_MAIN_GUARD);
if (!guard) continue;
if (lines[i]!.slice(guard[0].length).trim()) inside.add(i); // `if __name__ == "__main__": stmt`
for (let j = i + 1; j < lines.length && !/^\S/.test(lines[j]!); j++) inside.add(j);
}
return inside;
@@ -773,12 +778,17 @@ function pythonExternalWrites(global: Node, context: ResolutionContext): { write
function pythonDynamicGlobalWrite(name: string, filePath: string, context: ResolutionContext): boolean {
const lines = pythonMemberLines(filePath, context);
const statements = pythonStatementStarts(filePath, context);
const namespace = /\bglobals\(\s*\)|\bvars\(\s*\)|\bsys\.modules\s*\[\s*__name__\s*\]/g;
const script = pythonMainBlockLines(filePath, context);
// `vars()` is the module dict only at module scope; inside a function it is the locals.
const moduleLines = new Set(pythonOwnLines(null, filePath, context));
for (let i = 0; i < lines.length; i++) {
if (!statements[i]) continue;
if (!statements[i] || script.has(i)) continue;
let statement = lines[i]!;
for (let j = i + 1; j < lines.length && !statements[j]; j++) statement += '\n' + lines[j];
const code = blankPythonStrings(statement);
const namespace = moduleLines.has(i)
? /\bglobals\(\s*\)|\bvars\(\s*\)|\bsys\.modules\s*\[\s*__name__\s*\]/g
: /\bglobals\(\s*\)|\bsys\.modules\s*\[\s*__name__\s*\]/g;
const uses = code.match(namespace)?.length ?? 0;
if (!uses) continue;
const targets = pythonAssignment(statement)?.targets ?? [];