argyph-graph 1.0.2

Local-first MCP server giving AI coding agents fast, structured, and semantic context over any codebase.
Documentation
use camino::Utf8Path;

use super::{ImportResolver, ModuleTarget};
use crate::edge::Confidence;

pub struct PythonResolver;

impl ImportResolver for PythonResolver {
    fn resolve_import(
        &self,
        source_file: &Utf8Path,
        _module_path: &[String],
        raw: &str,
    ) -> Option<ModuleTarget> {
        let trimmed = raw.trim();
        if !trimmed.starts_with("from .") {
            return None;
        }

        let after_from = trimmed.strip_prefix("from ")?;
        let module_str = after_from.split(" import ").next()?.trim();

        let source_dir = source_file.parent()?;
        let mut file = source_dir.to_path_buf();

        let depth = module_str.chars().take_while(|c| *c == '.').count();
        for _ in 0..depth.saturating_sub(1) {
            if !file.pop() {}
        }

        let remaining = &module_str[depth..];
        if remaining.is_empty() {
            file.push("__init__");
        } else {
            for seg in remaining.split('.') {
                if !seg.is_empty() {
                    file.push(seg);
                }
            }
        }

        let candidate = format!("{file}.py");
        Some(ModuleTarget {
            file_path: super::normalize_path(&candidate),
            confidence: Confidence::Heuristic,
        })
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use camino::Utf8Path;

    #[test]
    fn resolve_relative_dot_import() {
        let resolver = PythonResolver;
        let path: Vec<String> = ["math"].iter().map(|s| s.to_string()).collect();
        let tgt = resolver
            .resolve_import(Utf8Path::new("src/main.py"), &path, "from .math import add")
            .expect("should resolve from .math import add");

        assert!(tgt.file_path.contains("src/math"), "got {}", tgt.file_path);
        assert!(tgt.file_path.ends_with(".py"));
    }

    #[test]
    fn resolve_parent_import() {
        let resolver = PythonResolver;
        let tgt = resolver
            .resolve_import(
                Utf8Path::new("src/subpkg/module.py"),
                &[],
                "from ..types import User",
            )
            .expect("should resolve from ..types import User");

        assert!(tgt.file_path.contains("src/types"), "got {}", tgt.file_path);
    }

    #[test]
    fn bare_import_returns_none() {
        let resolver = PythonResolver;
        let path: Vec<String> = ["os"].iter().map(|s| s.to_string()).collect();
        assert!(resolver
            .resolve_import(Utf8Path::new("src/main.py"), &path, "import os",)
            .is_none());
    }
}