use std::cell::OnceCell;
use std::path::Path;
use super::files::ProjectFiles;
use crate::{StackLabel, labels};
const PYTHON_ENTRY_FILES: &[&str] = &["app.py", "main.py", "server.py", "wsgi.py", "asgi.py"];
const PYTHON_MANIFEST_FILES: &[&str] = &[
"pyproject.toml",
"requirements.txt",
"requirements-dev.txt",
"Pipfile",
"setup.py",
];
const PYTHON_LOCK_FILES: &[&str] = &["poetry.lock", "uv.lock"];
const DJANGO_SOURCE_PATTERNS: &[&str] = &[
"django.core.wsgi",
"django.core.asgi",
"django_settings_module",
"get_wsgi_application",
"get_asgi_application",
];
type PythonSourcePattern = (StackLabel, &'static [&'static str], &'static str);
pub const PYTHON_SOURCE_PATTERNS: &[PythonSourcePattern] = &[
(
labels::FASTAPI,
&["from fastapi import", "import fastapi"],
"fastapi(",
),
(
labels::STARLETTE,
&["from starlette.applications import", "import starlette"],
"starlette(",
),
(
labels::LITESTAR,
&["from litestar import", "import litestar"],
"litestar(",
),
(
labels::FLASK,
&["from flask import", "import flask"],
"flask(",
),
];
pub const PYTHON_DEPENDENCY_PATTERNS: &[(&str, StackLabel)] = &[
("django", labels::DJANGO),
("flask", labels::FLASK),
("fastapi", labels::FASTAPI),
("starlette", labels::STARLETTE),
("litestar", labels::LITESTAR),
];
pub(super) fn detect_python_project(
project_root: &Path,
files: &ProjectFiles,
python_process: bool,
) -> Option<StackLabel> {
if !is_python_project(files, python_process) {
return None;
}
if files.contains_exact("manage.py") {
return Some(labels::DJANGO);
}
detect_in_files(
project_root,
files,
PYTHON_ENTRY_FILES,
detect_python_framework_from_source,
)
.or_else(|| detect_python_framework_from_dependencies(project_root, files))
.or_else(|| (!python_process).then_some(labels::PYTHON))
}
fn is_python_project(files: &ProjectFiles, python_process: bool) -> bool {
if files.contains_exact("manage.py")
|| files.any_exact(PYTHON_MANIFEST_FILES)
|| files.any_exact(PYTHON_LOCK_FILES)
{
return true;
}
files.any_exact(PYTHON_ENTRY_FILES) && (python_process || !files.contains_exact("package.json"))
}
fn detect_in_files(
project_root: &Path,
files: &ProjectFiles,
file_names: &[&str],
detect: fn(&str) -> Option<StackLabel>,
) -> Option<StackLabel> {
file_names
.iter()
.filter_map(|file_name| files.read_text(project_root, file_name))
.find_map(|mut text| {
text.make_ascii_lowercase();
detect(&text)
})
}
fn detect_python_framework_from_dependencies(
project_root: &Path,
files: &ProjectFiles,
) -> Option<StackLabel> {
let locks = OnceCell::new();
let confirmed_by_locks = |package: &str| {
locks
.get_or_init(|| read_lock_files(project_root, files))
.iter()
.all(|lock| lock_lists_package(lock, package))
};
PYTHON_MANIFEST_FILES
.iter()
.filter_map(|file_name| files.read_text(project_root, file_name))
.find_map(|mut manifest| {
manifest.make_ascii_lowercase();
PYTHON_DEPENDENCY_PATTERNS
.iter()
.find(|(package, _)| {
declares_dependency(&manifest, package) && confirmed_by_locks(package)
})
.map(|(_, label)| label.clone())
})
}
fn read_lock_files(project_root: &Path, files: &ProjectFiles) -> Vec<String> {
PYTHON_LOCK_FILES
.iter()
.filter_map(|file_name| files.read_complete_text(project_root, file_name))
.map(|mut lock| {
lock.make_ascii_lowercase();
lock
})
.collect()
}
fn lock_lists_package(lock: &str, package: &str) -> bool {
lock.lines().any(|line| {
line.trim()
.strip_prefix("name = \"")
.and_then(|rest| rest.strip_suffix('"'))
== Some(package)
})
}
fn declares_dependency(manifest: &str, package: &str) -> bool {
manifest
.lines()
.filter(|line| !line.trim_start().starts_with('#'))
.any(|line| contains_dependency_token(line, package))
}
fn detect_python_framework_from_source(normalized: &str) -> Option<StackLabel> {
if contains_any(normalized, DJANGO_SOURCE_PATTERNS) {
return Some(labels::DJANGO);
}
PYTHON_SOURCE_PATTERNS
.iter()
.find(|(_, imports, constructor)| {
source_mentions_framework(normalized, imports, constructor)
})
.map(|(label, _, _)| label.clone())
}
fn contains_any(haystack: &str, needles: &[&str]) -> bool {
needles.iter().any(|needle| haystack.contains(needle))
}
fn source_mentions_framework(haystack: &str, imports: &[&str], constructor: &str) -> bool {
haystack.contains(constructor) && contains_any(haystack, imports)
}
fn contains_dependency_token(haystack: &str, token: &str) -> bool {
let bytes = haystack.as_bytes();
haystack.match_indices(token).any(|(start, _)| {
let before = start.checked_sub(1).and_then(|index| bytes.get(index));
let after = bytes.get(start + token.len());
is_dependency_boundary(before.copied()) && is_dependency_boundary(after.copied())
})
}
const fn is_dependency_boundary(byte: Option<u8>) -> bool {
match byte {
None => true,
Some(value) => !matches!(value, b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-'),
}
}