#python-packages #import #parser #info #analyze #testpackage #package-info

pyimports

Parse and analyze the imports within a python package

23 releases (5 breaking)

0.6.7 Jan 16, 2025
0.6.6 Jan 15, 2025
0.5.2 Jan 6, 2025
0.4.0 Jan 5, 2025
0.1.0 Dec 10, 2023

#181 in Filesystem

Download history 1/week @ 2024-12-04 9/week @ 2024-12-11 271/week @ 2024-12-25 1198/week @ 2025-01-01 776/week @ 2025-01-08 337/week @ 2025-01-15

2,582 downloads per month

MIT and LGPL-3.0-only

225KB
4K SLoC

pyimports

CI Crates.io Docs

A rust crate for parsing and analyzing the imports within a python package.

Example

A short example (for more information refer to the docs):

use anyhow::Result;
use maplit::{hashmap,hashset};

use pyimports::prelude::*;
use pyimports::package_info::{PackageInfo,PackageItemToken};
use pyimports::imports_info::{ImportsInfo,InternalImportsPathQueryBuilder};

// You shouldn't use `testpackage!`, it just creates a fake python package
// in a temporary directory. It's (unfortunately) included in the public API
// so that it can be used in the doctests.
use pyimports::{testpackage,testutils::TestPackage};

fn main() -> Result<()> {
    let testpackage = testpackage! {
        "__init__.py" => "from testpackage import a, b",
        "a.py" => "from testpackage import b",
        "b.py" => "from testpackage import c, d",
        "c.py" => "from testpackage import d",
        "d.py" => ""
    };
    let package_info = PackageInfo::build(testpackage.path())?;
    let imports_info = ImportsInfo::build(package_info)?;

    let item = |pypath: &str| -> Result<PackageItemToken> {
        Ok(imports_info.package_info().get_item_by_pypath(&pypath.parse()?).unwrap().token())
    };

    let root_pkg = item("testpackage")?;
    let root_init = item("testpackage.__init__")?;
    let a = item("testpackage.a")?;
    let b = item("testpackage.b")?;
    let c = item("testpackage.c")?;
    let d = item("testpackage.d")?;

    assert_eq!(
        imports_info.internal_imports().get_direct_imports(),
        hashmap! {
            root_pkg => hashset!{root_init},
            root_init => hashset!{a, b},
            a => hashset!{b},
            b => hashset!{c, d},
            c => hashset!{d},
            d => hashset!{},
        }
    );

    assert_eq!(
        imports_info.internal_imports().get_items_directly_imported_by(root_init)?,
        hashset! {a, b}
    );

    assert_eq!(
        imports_info.internal_imports().get_items_that_directly_import(d)?,
        hashset! {b, c}
    );
    
    assert_eq!(
        imports_info.internal_imports().get_downstream_items(root_init)?,
        hashset! {a, b, c, d}
    );

    assert_eq!(
        imports_info.internal_imports().find_path(
            &InternalImportsPathQueryBuilder::default()
                .from(root_init)
                .to(d)
                .build()?
        )?,
        Some(vec![root_init, b, d])
    );

    Ok(())
}

Scope

This crate might be useful for something eventually, but right now it's mainly just a hobby project for me to learn about rust.

If you are looking for something more mature, try grimp/import-linter.

Limitations

The python parser used within this crate does not currently support python 3.12+ - see the related GitHub issue here.

Next steps

Some possible next steps that I may explore if/when I get time:

  • Fix issue with python parser, to support python 3.12+.
  • Performance benchmarking/improvements.
  • Python bindings (via maturin).
  • Higher level features e.g. import contracts, similar to import-linter.
  • Faster path calculations (via e.g. fast_paths).

Dependencies

~19MB
~356K SLoC