From 34ede4de888c55aab170c3d6349c60d8dc56e7f2 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Thu, 1 Oct 2026 04:34:57 +0000 Subject: [PATCH] fix(python): a module member follows the package's re-exports too (#2248) resolvePythonModuleMember (`mod.member` after `import mod` / `from pkg import mod`) looks the member up with pythonModuleSymbol, so a package's __init__.py re-exports lead to the definition. Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/python-package-reexports.test.ts | 22 ++++++++++++++++++++++ src/resolution/import-resolver.ts | 14 ++++---------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa68868b..2eaa9f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- A Python module member used through the imported module, like `generic.ObjectEditView` after `from netbox.views import generic`, `serializers.ValidationError` or `utils.assert_deserialize(…)`, now also follows the package's re-exports to its definition. Before, netbox's view classes lost their `generic.*` base classes, and mealie's test helpers called through `utils.` went unresolved. - A Python name imported from a package now follows the package's re-exports to where it's defined. That covers both `from .users import *` and `from .tokens import Token` in its `__init__.py`. Before, httpx's own tests' `httpx.Client(…)` and `httpx.URL(…)`, pytest's `pytest.raises(…)` and `pytest.param(…)`, and netbox's `from users.models import User` went unresolved or to a same-named class elsewhere, such as a test's `User` or an Alembic migration's copy of a model. - In NestJS projects, the provider naming convention (`…Service`, `…Controller`, …) now picks a class only from the reference's own file. Another file's provider is reached through its import, or by ordinary name matching that prefers the nearest declaration. Before, the convention's preference for `*.service.ts` files sent Nest's own specs' `TransientService` and `TestService` to same-named classes in unrelated sample apps. - In Astro projects, the component-name convention now applies only inside `.astro` files, where components are used. Before, a TypeScript type with a component's name was linked to that component: astrowind's `image?: Image` and `callToAction?: CallToAction` in its type declarations, and starlight's Playwright `page: Page` in its tests. diff --git a/__tests__/python-package-reexports.test.ts b/__tests__/python-package-reexports.test.ts index 45a97dd4..439e73e7 100644 --- a/__tests__/python-package-reexports.test.ts +++ b/__tests__/python-package-reexports.test.ts @@ -32,6 +32,22 @@ from .users import * `, 'netbox/alembic/versions/0001_tokens.py': `class Token: pass +`, + 'netbox/netbox/__init__.py': '', + 'netbox/netbox/views/__init__.py': '', + 'netbox/netbox/views/generic/__init__.py': `from .object_views import * +`, + 'netbox/netbox/views/generic/object_views.py': `class ObjectEditView: + pass +`, + 'netbox/dcim/views.py': `from netbox.views import generic + + +class DeviceEditView(generic.ObjectEditView): + pass +`, + 'netbox/extras/views.py': `class ObjectEditView: + pass `, 'netbox/core/views.py': `from users.models import Token, User @@ -53,6 +69,12 @@ afterAll(() => { }); describe('Python package re-exports', () => { + it('lead a module member to its definition', () => { + const ids = cg.getNodesInFile('netbox/dcim/views.py').map((n) => n.id); + const bases = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'extends').map((e) => cg.getNode(e.target)!.filePath); + expect(bases).toEqual(['netbox/netbox/views/generic/object_views.py']); + }); + it('lead an imported name to its definition, through star and named re-exports', () => { const ids = cg.getNodesInFile('netbox/core/views.py').map((n) => n.id); const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'instantiates').map((e) => cg.getNode(e.target)!.filePath).sort(); diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index 09544b83..d1ca2c1f 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -1897,16 +1897,10 @@ function resolvePythonModuleMember( } if (!resolvedPath || resolvedPath === ref.filePath) continue; - // Find the member as a top-level definition in the module file. Exclude - // `method` so `mod.foo` never lands on a same-named class method. - const target = context.getNodesInFile(resolvedPath).find( - (n) => - n.name === member && - (n.kind === 'function' || - n.kind === 'class' || - n.kind === 'variable' || - n.kind === 'constant') - ); + // Find the member as a top-level definition in the module file, or one it + // re-exports (a package's `__init__.py`). Exclude `method` so `mod.foo` + // never lands on a same-named class method. + const target = pythonModuleSymbol(resolvedPath, member, context, 0); if (target) { return { original: ref, targetNodeId: target.id, confidence: 0.85, resolvedBy: 'import' }; }