mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
fix(python): avoid local matches for external dotted imports (#899)
This commit is contained in:
parent
ca527456f9
commit
a9f667d18e
24 changed files with 339 additions and 0 deletions
|
|
@ -21,6 +21,23 @@ export const pythonImportStrategy: ImportResolverStrategy = (rawImportPath, file
|
|||
}
|
||||
// PEP 328: unresolved relative imports should not fall through to suffix matching
|
||||
if (rawImportPath.startsWith('.')) return { kind: 'files', files: [] };
|
||||
|
||||
// External dotted imports like `django.apps` should not fall through to generic
|
||||
// suffix matching when the repo has unrelated local files such as `accounts/apps.py`.
|
||||
// Keep suffix fallback only when the leading segment appears somewhere in-repo,
|
||||
// which preserves existing internal absolute-import behavior like `accounts.models`.
|
||||
const pathLike = rawImportPath.replace(/\./g, '/');
|
||||
if (pathLike.includes('/')) {
|
||||
const [leadingSegment] = pathLike.split('/').filter(Boolean);
|
||||
const hasRepoCandidate =
|
||||
!!leadingSegment &&
|
||||
(ctx.index.get(`${leadingSegment}.py`) !== undefined ||
|
||||
ctx.index.get(`${leadingSegment}/__init__.py`) !== undefined ||
|
||||
ctx.index.getFilesInDir(leadingSegment, '.py').length > 0);
|
||||
|
||||
if (!hasRepoCandidate) return { kind: 'files', files: [] };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
|
|||
0
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/__init__.py
vendored
Normal file
0
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/__init__.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/admin.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/admin.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/apps.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/apps.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/models.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/models.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
email = models.EmailField(unique=True)
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/tests.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/tests.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/views.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/accounts/views.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/__init__.py
vendored
Normal file
0
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/__init__.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/admin.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/admin.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/apps.py
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/apps.py
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BillingConfig(AppConfig):
|
||||
name = 'billing'
|
||||
9
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/models.py
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/models.py
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from django.db import models
|
||||
from accounts.models import Customer
|
||||
|
||||
|
||||
class Invoice(models.Model):
|
||||
customer = models.ForeignKey(
|
||||
Customer, on_delete=models.CASCADE, related_name="invoices"
|
||||
)
|
||||
total_cents = models.PositiveIntegerField()
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/tests.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/tests.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/views.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/billing/views.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/__init__.py
vendored
Normal file
0
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/__init__.py
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/asgi.py
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/asgi.py
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for config project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
119
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/settings.py
vendored
Normal file
119
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/settings.py
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""
|
||||
Django settings for config project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 6.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-l(ss*u=y_x)vb!nuq*s1$eayoopfz4^kv2@s&o7wh_6==4&9#^"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"accounts.apps.AccountsConfig",
|
||||
"billing.apps.BillingConfig",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
22
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/urls.py
vendored
Normal file
22
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/urls.py
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"""
|
||||
URL configuration for config project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/6.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
16
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/wsgi.py
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/config/wsgi.py
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for config project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/manage.py
vendored
Executable file
22
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/manage.py
vendored
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/requirements.txt
vendored
Normal file
1
gitnexus/test/fixtures/lang-resolution/python-django-app-imports/requirements.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
Django==6.0.4
|
||||
|
|
@ -1846,6 +1846,61 @@ describe('Python module import CALLS resolution (Issue #337)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// External dotted imports: framework modules like django.apps must not resolve
|
||||
// to unrelated local basename matches such as accounts/apps.py or config/urls.py.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Python external dotted imports do not self-resolve to local files', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'python-django-app-imports'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('keeps the real local cross-app import: billing/models.py -> accounts/models.py', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const localImport = imports.find(
|
||||
(e) => e.sourceFilePath === 'billing/models.py' && e.targetFilePath === 'accounts/models.py',
|
||||
);
|
||||
expect(localImport).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not resolve django.apps in app configs to local apps.py files', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const appConfigImports = imports.filter((e) => e.sourceFilePath.endsWith('/apps.py'));
|
||||
expect(appConfigImports.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not resolve django.urls in config/urls.py to config/urls.py', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const urlsImport = imports.find(
|
||||
(e) => e.sourceFilePath === 'config/urls.py' && e.targetFilePath === 'config/urls.py',
|
||||
);
|
||||
expect(urlsImport).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not resolve django.core.asgi or django.core.wsgi to local config modules', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const asgiImport = imports.find(
|
||||
(e) => e.sourceFilePath === 'config/asgi.py' && e.targetFilePath === 'config/asgi.py',
|
||||
);
|
||||
const wsgiImport = imports.find(
|
||||
(e) => e.sourceFilePath === 'config/wsgi.py' && e.targetFilePath === 'config/wsgi.py',
|
||||
);
|
||||
|
||||
expect(asgiImport).toBeUndefined();
|
||||
expect(wsgiImport).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not resolve other django.* imports to local same-basename files', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const wrongTargets = new Set(['config/asgi.py', 'config/wsgi.py', 'config/urls.py']);
|
||||
const misresolvedFrameworkImports = imports.filter((e) => wrongTargets.has(e.targetFilePath));
|
||||
expect(misresolvedFrameworkImports.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 16: Method enrichment (isAbstract, parameterTypes, static methods)
|
||||
// models.py: Animal(ABC) with @abstractmethod speak, @staticmethod classify, breathe
|
||||
|
|
|
|||
|
|
@ -515,6 +515,35 @@ describe('pythonImportStrategy', () => {
|
|||
const result = pythonImportStrategy('os', 'src/app.py', ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('absorbs unresolved external dotted imports instead of suffix-matching local basename files', () => {
|
||||
const ctx = makeCtx(['accounts/apps.py', 'billing/apps.py']);
|
||||
const result = pythonImportStrategy('django.apps', 'accounts/apps.py', ctx);
|
||||
expect(result).toEqual({ kind: 'files', files: [] });
|
||||
});
|
||||
|
||||
it('absorbs unresolved nested external dotted imports like django.urls and django.core.*', () => {
|
||||
const ctx = makeCtx(['config/asgi.py', 'config/urls.py', 'config/wsgi.py']);
|
||||
|
||||
expect(pythonImportStrategy('django.urls', 'config/urls.py', ctx)).toEqual({
|
||||
kind: 'files',
|
||||
files: [],
|
||||
});
|
||||
expect(pythonImportStrategy('django.core.asgi', 'config/asgi.py', ctx)).toEqual({
|
||||
kind: 'files',
|
||||
files: [],
|
||||
});
|
||||
expect(pythonImportStrategy('django.core.wsgi', 'config/wsgi.py', ctx)).toEqual({
|
||||
kind: 'files',
|
||||
files: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps dotted internal imports unresolved here when the leading package exists in-repo', () => {
|
||||
const ctx = makeCtx(['accounts/models.py', 'billing/models.py']);
|
||||
const result = pythonImportStrategy('accounts.models', 'billing/models.py', ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue