diff --git a/__tests__/function-ref.test.ts b/__tests__/function-ref.test.ts index 3d1e42e8f..9138efd08 100644 --- a/__tests__/function-ref.test.ts +++ b/__tests__/function-ref.test.ts @@ -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' }, []], diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 48105a0a3..fae96780a 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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(); 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 ?? [];