diff --git a/Cargo.lock b/Cargo.lock index 313e9882..484cd664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -102,13 +111,35 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", "syn", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "diff" version = "0.1.13" @@ -298,7 +329,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17703a19c80bbdd0b7919f0f104f3b0597f7de4fc4e90a477c15366a5ba03faa" dependencies = [ - "derive_more", + "derive_more 0.99.18", "malachite", "num-integer", "num-traits", @@ -475,6 +506,7 @@ name = "pyimports" version = "0.3.7" dependencies = [ "anyhow", + "derive_more 1.0.0", "itertools 0.14.0", "lazy_static", "maplit", @@ -849,12 +881,24 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_names2" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 98f1db44..18f2308e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ regex = "1.11.1" lazy_static = "1.5.0" itertools = "0.14.0" tap = "1.0.1" +derive_more = { version = "1.0.0", features = ["full"] } [dev-dependencies] parameterized = "2.0.0" diff --git a/Taskfile.yml b/Taskfile.yml index d3355f95..04c5ec6e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,27 +5,27 @@ tasks: cmds: - cargo test - clippy.check: + check.clippy: cmds: - cargo clippy --all-targets -- -D warnings - clippy.fix: + fix.clippy: cmds: - cargo clippy --fix --allow-staged - fmt.check: + check.fmt: cmds: - cargo fmt --check - fmt.fix: + fix.fmt: cmds: - cargo fmt check: cmds: - task: test - - task: clippy.check - - task: fmt.check + - task: check.clippy + - task: check.fmt publish: requires: diff --git a/src/contracts/layers.rs b/src/contracts/layers.rs index afa993af..ba48ed1e 100644 --- a/src/contracts/layers.rs +++ b/src/contracts/layers.rs @@ -1,5 +1,105 @@ +//! The layers module provides a [LayeredArchitectureContract], which enforces a layered architecture. +//! +//! A layered architecture involves a set of layers, with rules for imports between layers: +//! +//! - Lower layers may not import higher layers. +//! - Siblings within a layer may be marked as independent, in which case they may +//! not import each other. +//! - Higher layers may import lower layers. By default higher layers may only import from the +//! immediately below layer. This restriction may be lifted via [LayeredArchitectureContract::allow_deep_imports]. +//! +//! # Example: Contract kept +//! +//! ``` +//! # use anyhow::Result; +//! # use pyimports::{testpackage,TestPackage}; +//! use pyimports::{PackageInfo,ImportsInfo}; +//! use pyimports::contracts::Contract; +//! use pyimports::contracts::layers::{LayeredArchitectureContract,Layer}; +//! +//! # fn main() -> Result<()> { +//! let testpackage = testpackage! { +//! "data.py" => "", +//! "domain.py" => "import testpackage.data", +//! "application.py" => "import testpackage.domain", +//! "interfaces.py" => "import testpackage.application" +//! }; +//! +//! let package_info = PackageInfo::build(testpackage.path())?; +//! let imports_info = ImportsInfo::build(package_info)?; +//! +//! let data = imports_info.package_info().get_item_by_pypath("testpackage.data")?.unwrap().token(); +//! let domain = imports_info.package_info().get_item_by_pypath("testpackage.domain")?.unwrap().token(); +//! let application = imports_info.package_info().get_item_by_pypath("testpackage.application")?.unwrap().token(); +//! let interfaces = imports_info.package_info().get_item_by_pypath("testpackage.interfaces")?.unwrap().token(); +//! +//! let contract = LayeredArchitectureContract::new(&[ +//! Layer::new([data], true), +//! Layer::new([domain], true), +//! Layer::new([application], true), +//! Layer::new([interfaces], true), +//! ]); +//! +//! let violations = contract.verify(&imports_info)?; +//! +//! assert!(violations.is_empty()); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Example: Contract violated +//! +//! ``` +//! # use anyhow::Result; +//! # use maplit::hashset; +//! # use pyimports::{testpackage,TestPackage}; +//! use pyimports::{PackageInfo,ImportsInfo}; +//! use pyimports::contracts::{Contract,ContractViolation,ForbiddenImport}; +//! use pyimports::contracts::layers::{LayeredArchitectureContract,Layer}; +//! +//! # fn main() -> Result<()> { +//! let testpackage = testpackage! { +//! "data.py" => "", +//! "domain.py" => "import testpackage.data", +//! "application.py" => " +//! import testpackage.domain +//! import testpackage.interfaces", +//! "interfaces.py" => "import testpackage.application" +//! }; +//! +//! let package_info = PackageInfo::build(testpackage.path())?; +//! let imports_info = ImportsInfo::build(package_info)?; +//! +//! let data = imports_info.package_info().get_item_by_pypath("testpackage.data")?.unwrap().token(); +//! let domain = imports_info.package_info().get_item_by_pypath("testpackage.domain")?.unwrap().token(); +//! let application = imports_info.package_info().get_item_by_pypath("testpackage.application")?.unwrap().token(); +//! let interfaces = imports_info.package_info().get_item_by_pypath("testpackage.interfaces")?.unwrap().token(); +//! +//! let contract = LayeredArchitectureContract::new(&[ +//! Layer::new([data], true), +//! Layer::new([domain], true), +//! Layer::new([application], true), +//! Layer::new([interfaces], true), +//! ]); +//! +//! let violations = contract.verify(&imports_info)?; +//! +//! assert!(!violations.is_empty()); +//! assert_eq!( +//! violations, +//! vec![ +//! ContractViolation::ForbiddenImport { +//! forbidden_import: ForbiddenImport::new(application, interfaces, hashset! {}), +//! path: vec![application, interfaces], +//! }, +//! ] +//! ); +//! # Ok(()) +//! # } +//! ``` + use crate::contracts::{Contract, ContractViolation, ForbiddenImport}; -use crate::{ImportsInfo, InternalImportsPathQuery, PackageItemToken, PackageItemTokens}; +use crate::{ExtendWithDescendants, ImportsInfo, InternalImportsPathQuery, PackageItemToken}; use anyhow::Result; use itertools::Itertools; use maplit::hashset; @@ -7,6 +107,8 @@ use rayon::prelude::*; use std::collections::HashSet; use tap::prelude::*; +/// A contract used to enforce a layered architecture. +/// See the [module-level documentation](./index.html) for more details. #[derive(Debug, Clone)] pub struct LayeredArchitectureContract { layers: Vec, @@ -16,6 +118,8 @@ pub struct LayeredArchitectureContract { } impl LayeredArchitectureContract { + /// Create a new [LayeredArchitectureContract]. + /// Layers should be listed from lowest to highest. pub fn new(layers: &[Layer]) -> Self { LayeredArchitectureContract { layers: layers.to_vec(), @@ -25,28 +129,30 @@ impl LayeredArchitectureContract { } } + /// Ignore the passed imports when verifying the contract. pub fn ignore_imports(mut self, imports: &[(PackageItemToken, PackageItemToken)]) -> Self { self.ignored_imports.extend(imports.to_vec()); self } + /// Ignore typechecking imports when verifying the contract. pub fn ignore_typechecking_imports(mut self) -> Self { self.ignore_typechecking_imports = true; self } + /// Allow deep imports. + /// + /// By default higher layers may only import the immediately below layer. + /// `allow_deep_imports` lifts this restriction. pub fn allow_deep_imports(mut self) -> Self { self.allow_deep_imports = true; self } - - pub fn layers(&self) -> &[Layer] { - &self.layers - } } impl Contract for LayeredArchitectureContract { - fn find_violations(&self, imports_info: &ImportsInfo) -> Result> { + fn verify(&self, imports_info: &ImportsInfo) -> Result> { // Assumption: It's best/reasonable to clone here and remove the ignored imports from the graph. // An alternative could be to ignore the imports dynamically via a new field on `InternalImportsPathQuery`. let imports_info = { @@ -70,11 +176,11 @@ impl Contract for LayeredArchitectureContract { let from = forbidden_import .from .conv::>() - .pipe(|ii| ii.extend_with_descendants(imports_info.package_info())); + .extend_with_descendants(imports_info.package_info()); let to = forbidden_import .to .conv::>() - .pipe(|ii| ii.extend_with_descendants(imports_info.package_info())); + .extend_with_descendants(imports_info.package_info()); let except_via = forbidden_import .except_via .clone() @@ -86,10 +192,12 @@ impl Contract for LayeredArchitectureContract { .to(to) .excluding_paths_via(except_via), )?; - if let Some(path) = path { violations.push(ContractViolation::ForbiddenImport { - forbidden_import, - path, - }) }; + if let Some(path) = path { + violations.push(ContractViolation::ForbiddenImport { + forbidden_import, + path, + }) + }; Ok(violations) }) .try_reduce(Vec::new, |mut all_violations, violations| -> Result<_> { @@ -101,6 +209,8 @@ impl Contract for LayeredArchitectureContract { } } +/// A layer within a layered architecture. +/// See the [module-level documentation](./index.html) for more details. #[derive(Debug, Clone)] pub struct Layer { siblings: HashSet, @@ -108,6 +218,7 @@ pub struct Layer { } impl Layer { + /// Creates a new layer. pub fn new>( siblings: T, siblings_independent: bool, @@ -117,14 +228,6 @@ impl Layer { siblings_independent, } } - - pub fn siblings(&self) -> &HashSet { - &self.siblings - } - - pub fn siblings_independent(&self) -> bool { - self.siblings_independent - } } fn get_forbidden_imports(layers: &[Layer], allow_deep_imports: bool) -> Vec { @@ -247,18 +350,18 @@ mod tests { #[test] fn test_layered_architecture_contract_ok() -> Result<()> { let testpackage = testpackage! { - "__init__.py" => "", - "data.py" => "", - "domain.py" => " + "__init__.py" => "", + "data.py" => "", + "domain.py" => " import testpackage.data ", - "application.py" => " + "application.py" => " import testpackage.domain ", - "interfaces.py" => " + "interfaces.py" => " import testpackage.application " - }; + }; let package_info = PackageInfo::build(testpackage.path())?; let imports_info = ImportsInfo::build(package_info)?; @@ -275,7 +378,7 @@ import testpackage.application Layer::new([interfaces], true), ]); - let violations = contract.find_violations(&imports_info)?; + let violations = contract.verify(&imports_info)?; assert!(violations.is_empty()); Ok(()) @@ -314,7 +417,7 @@ import testpackage.data Layer::new([interfaces], true), ]); - let violations = contract.find_violations(&imports_info)?; + let violations = contract.verify(&imports_info)?; let expected_violations = vec![ ContractViolation::ForbiddenImport { @@ -372,7 +475,7 @@ import testpackage.data ]) .ignore_imports(&[(interfaces, data)]); - let violations = contract.find_violations(&imports_info)?; + let violations = contract.verify(&imports_info)?; let expected_violations = [ContractViolation::ForbiddenImport { forbidden_import: ForbiddenImport::new(application, interfaces, hashset! {}), @@ -418,7 +521,7 @@ import testpackage.application Layer::new([application], true), Layer::new([interfaces], true), ]); - let violations = contract.find_violations(&imports_info)?; + let violations = contract.verify(&imports_info)?; let expected_violations = [ContractViolation::ForbiddenImport { forbidden_import: ForbiddenImport::new(application, data, hashset! {domain}), path: vec![application, data], @@ -436,7 +539,7 @@ import testpackage.application Layer::new([interfaces], true), ]) .allow_deep_imports(); - let violations = contract.find_violations(&imports_info)?; + let violations = contract.verify(&imports_info)?; assert!(violations.is_empty()); Ok(()) diff --git a/src/contracts/mod.rs b/src/contracts/mod.rs index 9badc412..0d3317d6 100644 --- a/src/contracts/mod.rs +++ b/src/contracts/mod.rs @@ -1,21 +1,33 @@ +//! The contracts module provides functionality to define and verify [Contract]s. + use crate::{ImportsInfo, PackageItemToken}; use anyhow::Result; use std::collections::HashSet; pub mod layers; +/// A contract defines a set of verifiable conditions +/// related to package imports that we wish to enforce. pub trait Contract { - fn find_violations(&self, imports_info: &ImportsInfo) -> Result>; + /// Verify the contract, returning a vector of violations. + /// The violations are not guaranteed to be exhaustive - this is up to the + /// specific contract implementation. + fn verify(&self, imports_info: &ImportsInfo) -> Result>; } +/// A violation of a contract. #[derive(Debug, Clone, PartialEq)] pub enum ContractViolation { + /// An import path which is forbidden by the contract. ForbiddenImport { + /// The import which is forbidden by the contract. forbidden_import: ForbiddenImport, + /// The specific path for this forbidden import. path: Vec, }, } +/// An import path which is forbidden. #[derive(Debug, Clone, PartialEq)] pub struct ForbiddenImport { from: PackageItemToken, @@ -24,7 +36,8 @@ pub struct ForbiddenImport { } impl ForbiddenImport { - fn new( + /// Creates a new [ForbiddenImport]. + pub fn new( from: PackageItemToken, to: PackageItemToken, except_via: HashSet, @@ -35,14 +48,4 @@ impl ForbiddenImport { except_via, } } - - pub fn from(&self) -> PackageItemToken { - self.from - } - pub fn to(&self) -> PackageItemToken { - self.to - } - pub fn except_via(&self) -> &HashSet { - &self.except_via - } } diff --git a/src/imports_info/mod.rs b/src/imports_info/mod.rs index 9828ff88..6ec6ca24 100644 --- a/src/imports_info/mod.rs +++ b/src/imports_info/mod.rs @@ -10,7 +10,6 @@ use crate::{ Error, PackageItemIterator, Pypath, }; use anyhow::Result; -use maplit::hashset; use rayon::iter::ParallelBridge; use rayon::prelude::*; use std::collections::{HashMap, HashSet}; @@ -34,68 +33,6 @@ pub enum ImportMetadata { ImplicitImport, } -impl From for HashSet { - fn from(value: PackageItemToken) -> Self { - hashset! { value } - } -} - -/// An extension trait to allow attaching methods to a set of [PackageItemToken]s. -pub trait PackageItemTokens { - /// Extend the set of package item tokens with any descendants. - /// - /// ``` - /// # use anyhow::Result; - /// # use maplit::hashset; - /// # use pyimports::{testpackage,TestPackage}; - /// use pyimports::prelude::*; - /// use pyimports::{PackageInfo,PackageItemToken}; - /// - /// # fn main() -> Result<()> { - /// let testpackage = testpackage! { - /// "a.py" => "", - /// "b/c.py" => "" - /// }; - /// - /// let package_info = PackageInfo::build(testpackage.path())?; - /// - /// let root = package_info.get_item_by_pypath("testpackage")?.unwrap().token(); - /// let a = package_info.get_item_by_pypath("testpackage.a")?.unwrap().token(); - /// let b = package_info.get_item_by_pypath("testpackage.b")?.unwrap().token(); - /// let c = package_info.get_item_by_pypath("testpackage.b.c")?.unwrap().token(); - /// - /// let package_item_tokens = hashset! {root}; - /// assert_eq!( - /// package_item_tokens.extend_with_descendants(&package_info), - /// hashset! {root, a, b, c} - /// ); - /// # Ok(()) - /// # } - /// ``` - fn extend_with_descendants(self, package_info: &PackageInfo) -> Self; -} - -impl PackageItemTokens for HashSet { - fn extend_with_descendants(self, package_info: &PackageInfo) -> Self { - let mut items = self.clone(); - - for item in self.iter() { - let descendants = match item { - PackageItemToken::Package(item) => match package_info.get_descendant_items(*item) { - Ok(descendants) => descendants.map(|item| item.token()).collect::>(), - Err(e) => match e.downcast_ref::() { - Some(Error::NotAPackage) => hashset! {}, - _ => panic!(), - }, - }, - PackageItemToken::Module(_) => hashset! {}, - }; - items.extend(descendants); - } - items - } -} - /// A rich representation of the imports within a python package. /// /// ``` diff --git a/src/imports_info/parse/mod.rs b/src/imports_info/parse/mod.rs index d3af7ee2..10271e10 100644 --- a/src/imports_info/parse/mod.rs +++ b/src/imports_info/parse/mod.rs @@ -1,41 +1,37 @@ mod ast_visit; +use crate::{errors::Error, Pypath}; use anyhow::Result; use rustpython_parser::{self, ast::Stmt, source_code::LinearLocator}; use std::{fs, path::Path}; - -use crate::{errors::Error, Pypath}; +use tap::Conv; #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) struct AbsoluteOrRelativePypath { - s: String, -} +pub(crate) struct AbsoluteOrRelativePypath(String); impl AbsoluteOrRelativePypath { pub fn new(s: &str) -> Self { - AbsoluteOrRelativePypath { s: s.to_string() } + AbsoluteOrRelativePypath(s.to_string()) } pub fn is_relative(&self) -> bool { - self.s.starts_with(".") + self.0.starts_with(".") } pub fn resolve_relative(&self, path: &Path, root_path: &Path) -> Pypath { if !self.is_relative() { - return Pypath { s: self.s.clone() }; + return Pypath::new(&self.0); } - let trimmed_pypath = self.s.trim_start_matches("."); + let trimmed_pypath = self.0.trim_start_matches("."); let base_pypath = { - let n = self.s.len() - trimmed_pypath.len(); + let n = self.0.len() - trimmed_pypath.len(); let mut base_path = path; for _ in 0..n { base_path = base_path.parent().unwrap(); } Pypath::from_path(base_path, root_path).unwrap() }; - Pypath { - s: base_pypath.s + "." + trimmed_pypath, - } + Pypath::new(&(base_pypath.conv::() + "." + trimmed_pypath)) } } diff --git a/src/lib.rs b/src/lib.rs index e29367de..e3a802ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,11 +20,11 @@ pub use testutils::TestPackage; pub use errors::Error; pub use imports_info::{ ExplicitImportMetadata, ExternalImportsQueries, ImportMetadata, ImportsInfo, - ImportsInfoBuildOptions, InternalImportsPathQuery, InternalImportsQueries, PackageItemTokens, + ImportsInfoBuildOptions, InternalImportsPathQuery, InternalImportsQueries, }; pub use package_info::{ - Module, ModuleToken, Package, PackageInfo, PackageItem, PackageItemIterator, PackageItemToken, - PackageToken, + ExtendWithDescendants, Module, ModuleToken, Package, PackageInfo, PackageItem, + PackageItemIterator, PackageItemToken, PackageToken, }; pub use pypath::{IntoPypath, Pypath}; @@ -34,7 +34,7 @@ pub use pypath::{IntoPypath, Pypath}; /// use pyimports::prelude::*; /// ``` pub mod prelude { + pub use crate::ExtendWithDescendants; pub use crate::IntoPypath; pub use crate::PackageItemIterator; - pub use crate::PackageItemTokens; } diff --git a/src/package_info/mod.rs b/src/package_info/mod.rs index 3b0e71d6..baaaac87 100644 --- a/src/package_info/mod.rs +++ b/src/package_info/mod.rs @@ -1,8 +1,12 @@ mod filesystem; mod queries; +use crate::Error; +use crate::{IntoPypath, Pypath}; use anyhow::Result; use core::fmt; +use maplit::hashset; +pub use queries::PackageItemIterator; use slotmap::{new_key_type, SlotMap}; use std::{ borrow::Borrow, @@ -10,10 +14,6 @@ use std::{ path::{Path, PathBuf}, }; -use crate::Error; -use crate::{IntoPypath, Pypath}; -pub use queries::PackageItemIterator; - new_key_type! { /// A token used to identify a python package within [PackageInfo]. /// See also [PackageItemToken]. @@ -427,7 +427,7 @@ impl PackageInfo { pub fn pypath_is_internal(&self, pypath: T) -> Result { let pypath = pypath.into_pypath()?; let root_pypath = &self.get_root().pypath; - Ok(root_pypath.contains(pypath.borrow())) + Ok(root_pypath.is_equal_to_or_ancestor_of(pypath.borrow())) } /// Checks whether the passed pypath is external. @@ -452,6 +452,70 @@ impl PackageInfo { } } +impl From for HashSet { + fn from(value: PackageItemToken) -> Self { + hashset! { value } + } +} + +/// Extends a collection of package item tokens with all descendant items. +/// +/// ``` +/// # use anyhow::Result; +/// # use maplit::hashset; +/// # use pyimports::{testpackage,TestPackage}; +/// use pyimports::prelude::*; +/// use pyimports::{PackageInfo,PackageItemToken}; +/// +/// # fn main() -> Result<()> { +/// let testpackage = testpackage! { +/// "a.py" => "", +/// "b/c.py" => "" +/// }; +/// +/// let package_info = PackageInfo::build(testpackage.path())?; +/// +/// let root = package_info.get_item_by_pypath("testpackage")?.unwrap().token(); +/// let a = package_info.get_item_by_pypath("testpackage.a")?.unwrap().token(); +/// let b = package_info.get_item_by_pypath("testpackage.b")?.unwrap().token(); +/// let c = package_info.get_item_by_pypath("testpackage.b.c")?.unwrap().token(); +/// +/// let package_item_tokens = hashset! {root}; +/// assert_eq!( +/// package_item_tokens.extend_with_descendants(&package_info), +/// hashset! {root, a, b, c} +/// ); +/// # Ok(()) +/// # } +/// ``` +pub trait ExtendWithDescendants: + Sized + Clone + IntoIterator + Extend +{ + /// Extend this collection of package item tokens with all descendant items. + fn extend_with_descendants(mut self, package_info: &PackageInfo) -> Self { + for item in self.clone().into_iter() { + let descendants = match item { + PackageItemToken::Package(item) => match package_info.get_descendant_items(item) { + Ok(descendants) => descendants.map(|item| item.token()).collect::>(), + Err(e) => match e.downcast_ref::() { + Some(Error::NotAPackage) => hashset! {}, + _ => panic!(), + }, + }, + PackageItemToken::Module(_) => hashset! {}, + }; + self.extend(descendants); + } + + self + } +} + +impl + Extend> + ExtendWithDescendants for T +{ +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/pypath.rs b/src/pypath.rs index a8404ff4..3838138e 100644 --- a/src/pypath.rs +++ b/src/pypath.rs @@ -1,9 +1,10 @@ use std::borrow::Borrow; -use std::fmt; use std::path::Path; use std::str::FromStr; use anyhow::Result; +use derive_more::derive::{Display, Into}; +use derive_more::Deref; use lazy_static::lazy_static; use regex::Regex; @@ -27,20 +28,12 @@ lazy_static! { /// let result = ".foo.bar".parse::(); /// assert!(result.is_err()); /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Pypath { - pub(crate) s: String, -} +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deref, Display, Into)] +pub struct Pypath(String); impl Pypath { pub(crate) fn new(s: &str) -> Pypath { - Pypath { s: s.to_string() } - } -} - -impl fmt::Display for Pypath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.s) + Pypath(s.to_string()) } } @@ -56,24 +49,6 @@ impl FromStr for Pypath { } } -impl AsRef for Pypath { - fn as_ref(&self) -> &str { - &self.s - } -} - -impl From for String { - fn from(value: Pypath) -> Self { - value.s - } -} - -impl<'a> From<&'a Pypath> for &'a str { - fn from(value: &'a Pypath) -> Self { - &value.s - } -} - impl Pypath { pub(crate) fn from_path(path: &Path, root_path: &Path) -> Result { let path = path.strip_prefix(root_path.parent().unwrap())?; @@ -85,7 +60,7 @@ impl Pypath { Ok(Pypath::new(&s)) } - /// Returns true if the passed pypath is contained by this pypath. + /// Returns true if this pypath is equal to or an ancestor of the passed pypath. /// /// # Example /// @@ -95,14 +70,14 @@ impl Pypath { /// let foo_bar: Pypath = "foo.bar".parse().unwrap(); /// let foo_bar_baz: Pypath = "foo.bar.baz".parse().unwrap(); /// - /// assert!(foo_bar.contains(&foo_bar_baz)); - /// assert!(!foo_bar_baz.contains(&foo_bar)); + /// assert!(foo_bar.is_equal_to_or_ancestor_of(&foo_bar_baz)); + /// assert!(!foo_bar_baz.is_equal_to_or_ancestor_of(&foo_bar)); /// ``` - pub fn contains(&self, other: &Pypath) -> bool { - self == other || other.s.starts_with(&(self.s.clone() + ".")) + pub fn is_equal_to_or_ancestor_of(&self, other: &Pypath) -> bool { + self == other || other.0.starts_with(&(self.0.clone() + ".")) } - /// Returns true if this pypath is contained by the passed pypath. + /// Returns true if this pypath is equal to or a descendant of the passed pypath. /// /// # Example /// @@ -112,11 +87,11 @@ impl Pypath { /// let foo_bar: Pypath = "foo.bar".parse().unwrap(); /// let foo_bar_baz: Pypath = "foo.bar.baz".parse().unwrap(); /// - /// assert!(!foo_bar.is_contained_by(&foo_bar_baz)); - /// assert!(foo_bar_baz.is_contained_by(&foo_bar)); + /// assert!(!foo_bar.is_equal_to_or_descendant_of(&foo_bar_baz)); + /// assert!(foo_bar_baz.is_equal_to_or_descendant_of(&foo_bar)); /// ``` - pub fn is_contained_by(&self, other: &Pypath) -> bool { - other.contains(self) + pub fn is_equal_to_or_descendant_of(&self, other: &Pypath) -> bool { + other.is_equal_to_or_ancestor_of(self) } /// Returns the parent of this pypath. @@ -132,9 +107,9 @@ impl Pypath { ///assert!(foo_bar_baz.parent() == foo_bar); /// ``` pub fn parent(&self) -> Self { - let mut v = self.s.split(".").collect::>(); + let mut v = self.0.split(".").collect::>(); v.pop(); - Pypath { s: v.join(".") } + Pypath(v.join(".")) } } @@ -208,19 +183,19 @@ mod tests { } #[test] - fn test_contains() -> Result<()> { - assert!(Pypath::new("foo.bar").contains(&Pypath::new("foo.bar"))); - assert!(Pypath::new("foo.bar").contains(&Pypath::new("foo.bar.baz"))); - assert!(!Pypath::new("foo.bar").contains(&Pypath::new("foo"))); + fn test_is_equal_or_ancestor() -> Result<()> { + assert!(Pypath::new("foo.bar").is_equal_to_or_ancestor_of(&Pypath::new("foo.bar"))); + assert!(Pypath::new("foo.bar").is_equal_to_or_ancestor_of(&Pypath::new("foo.bar.baz"))); + assert!(!Pypath::new("foo.bar").is_equal_to_or_ancestor_of(&Pypath::new("foo"))); Ok(()) } #[test] - fn test_contained_by() -> Result<()> { - assert!(Pypath::new("foo.bar").is_contained_by(&Pypath::new("foo.bar"))); - assert!(!Pypath::new("foo.bar").is_contained_by(&Pypath::new("foo.bar.baz"))); - assert!(Pypath::new("foo.bar").is_contained_by(&Pypath::new("foo"))); + fn test_is_equal_or_descendant() -> Result<()> { + assert!(Pypath::new("foo.bar").is_equal_to_or_descendant_of(&Pypath::new("foo.bar"))); + assert!(!Pypath::new("foo.bar").is_equal_to_or_descendant_of(&Pypath::new("foo.bar.baz"))); + assert!(Pypath::new("foo.bar").is_equal_to_or_descendant_of(&Pypath::new("foo"))); Ok(()) }