Skip to content

Commit

Permalink
Implement external import path
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter554 committed Jan 11, 2025
1 parent 33d7973 commit 7bedd90
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 7 deletions.
5 changes: 4 additions & 1 deletion src/imports_info/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
mod queries;

use crate::errors::Error;
pub use crate::imports_info::queries::external_imports::ExternalImportsQueries;
pub use crate::imports_info::queries::external_imports::{
ExternalImportsPathQuery, ExternalImportsPathQueryBuilder,
ExternalImportsPathQueryBuilderError, ExternalImportsQueries,
};
pub use crate::imports_info::queries::internal_imports::{
InternalImportsPathQuery, InternalImportsPathQueryBuilder,
InternalImportsPathQueryBuilderError, InternalImportsQueries,
Expand Down
231 changes: 229 additions & 2 deletions src/imports_info/queries/external_imports.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
use std::collections::{HashMap, HashSet};

use crate::errors::Error;
use crate::imports_info::{ImportMetadata, ImportsInfo};
use crate::package_info::PackageItemToken;
use crate::pypath::Pypath;
use crate::testpackage;
use anyhow::Result;
use derive_builder::Builder;
use derive_more::{IsVariant, Unwrap};
use derive_new::new;
use getset::Getters;
use maplit::hashset;
use pathfinding::prelude::bfs;
use std::collections::{HashMap, HashSet};

/// An object that allows querying external imports.
pub struct ExternalImportsQueries<'a> {
pub(crate) imports_info: &'a ImportsInfo,
}

/// An object representing an external imports path query.
#[derive(Debug, Clone, new, Getters, Builder)]
#[builder(setter(into))]
pub struct ExternalImportsPathQuery {
/// Package items from which paths may start.
#[new(into)]
#[getset(get = "pub")]
from: HashSet<PackageItemToken>,

/// External items where paths may end.
#[new(into)]
#[getset(get = "pub")]
to: HashSet<Pypath>,

/// Paths that would go via these package items should be excluded.
#[new(into)]
#[getset(get = "pub")]
#[builder(default)]
excluding_paths_via: HashSet<PackageItemToken>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, IsVariant, Unwrap)]
enum PathfindingNode<'a> {
Initial,
PackageItem(&'a PackageItemToken),
ExternalItem(&'a Pypath),
}

impl<'a> ExternalImportsQueries<'a> {
/// Returns a map of all the direct imports.
///
Expand Down Expand Up @@ -262,6 +296,124 @@ impl<'a> ExternalImportsQueries<'a> {
}
}

/// Returns the shortest import path or `None` if no path can be found.
///
/// ```
/// # use std::collections::HashSet;
/// # use anyhow::Result;
/// # use maplit::{hashmap, hashset};
/// # use pyimports::{testpackage, testutils::TestPackage};
/// use pyimports::package_info::PackageInfo;
/// use pyimports::imports_info::{ImportsInfo,ImportMetadata,ExternalImportsPathQueryBuilder};
///
/// # fn main() -> Result<()> {
/// let testpackage = testpackage! {
/// "__init__.py" => "",
/// "a.py" => "from testpackage import b",
/// "b.py" => "from testpackage import c",
/// "c.py" => "from django.db import models"
/// };
///
/// let package_info = PackageInfo::build(testpackage.path())?;
/// let imports_info = ImportsInfo::build(package_info)?;
///
/// let a = imports_info.package_info()
/// .get_item_by_pypath(&"testpackage.a".parse()?).unwrap()
/// .token();
/// let b = imports_info.package_info()
/// .get_item_by_pypath(&"testpackage.b".parse()?).unwrap()
/// .token();
/// let c = imports_info.package_info()
/// .get_item_by_pypath(&"testpackage.c".parse()?).unwrap()
/// .token();
///
/// assert_eq!(
/// imports_info.external_imports().find_path(
/// &ExternalImportsPathQueryBuilder::default()
/// .from(a)
/// .to(&"django.db.models".parse()?)
/// .build()?
/// )?,
/// Some((vec![a, b, c], "django.db.models".parse()?))
/// );
/// # Ok(())
/// # }
/// ```
pub fn find_path(
&'a self,
query: &ExternalImportsPathQuery,
) -> Result<Option<(Vec<PackageItemToken>, Pypath)>> {
for item in query.from.iter() {
self.imports_info.package_info.get_item(*item)?;
}
for item in query.excluding_paths_via.iter() {
self.imports_info.package_info.get_item(*item)?;
}

let empty_package_items = hashset! {};
let empty_external_items = hashset! {};

let path = bfs(
&PathfindingNode::Initial,
// Successors
|item| {
let internal_items = match item {
PathfindingNode::Initial => &query.from,
PathfindingNode::PackageItem(item) => {
self.imports_info.internal_imports.get(item).unwrap()
}
PathfindingNode::ExternalItem(_) => &empty_package_items,
};

let external_items = match item {
PathfindingNode::Initial => &empty_external_items,
PathfindingNode::PackageItem(item) => {
self.imports_info.external_imports.get(item).unwrap()
}
PathfindingNode::ExternalItem(_) => &empty_external_items,
};

let internal_items = internal_items
.difference(&query.excluding_paths_via)
.map(PathfindingNode::PackageItem);

let external_items = external_items.iter().map(PathfindingNode::ExternalItem);

internal_items.chain(external_items)
},
// Success
|item| match item {
PathfindingNode::Initial => false,
PathfindingNode::PackageItem(_) => false,
PathfindingNode::ExternalItem(pypath) => query.to.contains(pypath),
},
);

if path.is_none() {
return Ok(None);
}

let mut path = path.unwrap();
let external_item = path.pop().unwrap().unwrap_external_item().clone();

let path = path
.into_iter()
.skip(1)
.map(|item| match item {
PathfindingNode::PackageItem(item) => item,
_ => panic!(),
})
.cloned()
.collect::<Vec<_>>();

Ok(Some((path, external_item)))
}

/// Returns true if an import path exists.
pub fn path_exists(&'a self, query: &ExternalImportsPathQuery) -> Result<bool> {
Ok(self.find_path(query)?.is_some())
}

#[allow(dead_code)]
fn get_equal_to_or_descendant_imports(&self, pypath: &Pypath) -> HashSet<Pypath> {
self.imports_info
Expand Down Expand Up @@ -437,4 +589,79 @@ mod tests {

Ok(())
}

#[test]
fn test_find_path() -> Result<()> {
let testpackage = testpackage! {
"__init__.py" => "",
"a.py" => "from testpackage import b",
"b.py" => "from testpackage import c",
"c.py" => "from django.db import models"
};

let package_info = PackageInfo::build(testpackage.path())?;
let imports_info = ImportsInfo::build(package_info)?;

let a = imports_info._item("testpackage.a");
let b = imports_info._item("testpackage.b");
let c = imports_info._item("testpackage.c");

assert_eq!(
imports_info.external_imports().find_path(
&ExternalImportsPathQueryBuilder::default()
.from(a)
.to(&"django.db.models".parse()?)
.build()?
)?,
Some((vec![a, b, c], "django.db.models".parse()?))
);

Ok(())
}

#[test]
fn test_find_path_excluding_via() -> Result<()> {
let testpackage = testpackage! {
"__init__.py" => "",
"a.py" => "from testpackage import b, e",
"b.py" => "from testpackage import c",
"c.py" => "from django.db import models",
"d.py" => "from testpackage import c",
"e.py" => "from testpackage import d"
};

let package_info = PackageInfo::build(testpackage.path())?;
let imports_info = ImportsInfo::build(package_info)?;

let a = imports_info._item("testpackage.a");
let b = imports_info._item("testpackage.b");
let c = imports_info._item("testpackage.c");
let d = imports_info._item("testpackage.d");
let e = imports_info._item("testpackage.e");

// Sanity check
assert_eq!(
imports_info.external_imports().find_path(
&ExternalImportsPathQueryBuilder::default()
.from(a)
.to(&"django.db.models".parse()?)
.build()?
)?,
Some((vec![a, b, c], "django.db.models".parse()?))
);

// Excluding b we need to go via the longer path
assert_eq!(
imports_info.external_imports().find_path(
&ExternalImportsPathQueryBuilder::default()
.from(a)
.to(&"django.db.models".parse()?)
.excluding_paths_via(b)
.build()?
)?,
Some((vec![a, e, d, c], "django.db.models".parse()?))
);

Ok(())
}
}
10 changes: 6 additions & 4 deletions src/imports_info/queries/internal_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::imports_info::{ImportMetadata, ImportsInfo};
use crate::package_info::PackageItemToken;
use anyhow::Result;
use derive_builder::Builder;
use derive_more::{IsVariant, Unwrap};
use derive_new::new;
use getset::Getters;
use pathfinding::prelude::{bfs, bfs_reach};
Expand Down Expand Up @@ -97,7 +98,7 @@ pub struct InternalImportsPathQuery {
excluding_paths_via: HashSet<PackageItemToken>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, IsVariant, Unwrap)]
enum PathfindingNode<'a> {
Initial,
PackageItem(&'a PackageItemToken),
Expand Down Expand Up @@ -484,8 +485,7 @@ impl<'a> InternalImportsQueries<'a> {
}
}

/// Returns the shortest import path between the passed package items,
/// or `None` if no path can be found.
/// Returns the shortest import path or `None` if no path can be found.
///
/// ```
/// # use std::collections::HashSet;
Expand Down Expand Up @@ -547,6 +547,7 @@ impl<'a> InternalImportsQueries<'a> {

let path = bfs(
&PathfindingNode::Initial,
// Successors
|item| {
let items = match item {
PathfindingNode::Initial => &query.from,
Expand All @@ -559,6 +560,7 @@ impl<'a> InternalImportsQueries<'a> {
.difference(&query.excluding_paths_via)
.map(PathfindingNode::PackageItem)
},
// Success
|item| match item {
PathfindingNode::Initial => false,
PathfindingNode::PackageItem(item) => query.to.contains(item),
Expand All @@ -579,7 +581,7 @@ impl<'a> InternalImportsQueries<'a> {
Ok(path)
}

/// Returns true if an import path exists between the passed package items.
/// Returns true if an import path exists.
///
/// ```
/// # use std::collections::HashSet;
Expand Down
13 changes: 13 additions & 0 deletions src/pypath.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! The `pypath` module provides utilities for working with dotted python import paths.
use std::collections::HashSet;
use std::path::Path;
use std::str::FromStr;

Expand All @@ -9,6 +10,7 @@ use anyhow::Result;
use derive_more::derive::{Display, Into};
use derive_more::Deref;
use lazy_static::lazy_static;
use maplit::hashset;
use regex::Regex;

lazy_static! {
Expand Down Expand Up @@ -156,6 +158,17 @@ impl Pypath {
}
}

impl From<Pypath> for HashSet<Pypath> {
fn from(p: Pypath) -> Self {
hashset! {p}
}
}

impl From<&Pypath> for HashSet<Pypath> {
fn from(p: &Pypath) -> Self {
hashset! {p.clone()}
}
}
#[cfg(test)]
mod tests {
use super::*;
Expand Down

0 comments on commit 7bedd90

Please sign in to comment.