use std::borrow::Borrow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::fs;
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use tempfile::tempdir;
use crate::table::ColumnFormat;
use crate::table::Rowable;
use crate::table::RowableContext;
use crate::table::Tableable;
use crate::ureq_client::UreqClient;
use crate::dep_spec::DepOperator;
use crate::dep_spec::DepSpec;
use crate::env_marker::EnvMarkerState;
use crate::lock_file::LockFile;
use crate::package::Package;
use crate::pyproject::PyProjectInfo;
use crate::ureq_client::UreqClientLive;
use crate::util::logger;
use crate::util::path_normalize;
use crate::util::Anchor;
use crate::util::FlagLog;
use crate::util::ResultDynError;
static LOCK_PRIORITY: &[&str] = &[
"uv.lock",
"pylock.toml",
"poetry.lock",
"Pipfile.lock",
"requirements.lock",
"pixi.lock",
"requirements.txt",
"pyproject.toml",
];
pub struct DepManifestRecord {
pub dep_spec: DepSpec,
}
impl Rowable for DepManifestRecord {
fn to_rows(&self, _context: &RowableContext) -> Vec<Vec<String>> {
vec![vec![self.dep_spec.to_string()]]
}
}
pub struct DepManifestReport {
pub records: Vec<DepManifestRecord>,
}
impl Tableable<DepManifestRecord> for DepManifestReport {
fn get_header(&self) -> Vec<ColumnFormat> {
vec![ColumnFormat::new(
"# via fetter".to_string(),
false,
"#666666".to_string(),
)]
}
fn get_records(&self) -> &Vec<DepManifestRecord> {
&self.records
}
}
#[derive(Debug, Clone)]
enum DepSpecOOM {
One(DepSpec),
Many(Vec<DepSpec>),
}
impl DepSpecOOM {
fn into_many(self, dep: DepSpec) -> Self {
match self {
Self::One(existing) => Self::Many(vec![existing, dep]),
Self::Many(mut vec) => {
vec.push(dep);
Self::Many(vec)
}
}
}
}
#[derive(Debug, Clone)]
pub struct DepManifest {
dep_specs: HashMap<String, DepSpecOOM>,
pub(crate) env_marker_active: bool,
}
impl DepManifest {
pub fn try_from_iter<I, S>(ds_iter: I) -> ResultDynError<Self>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut env_marker_active = false;
let mut dep_specs: HashMap<String, DepSpecOOM> = HashMap::new();
for line in ds_iter {
let spec = line.as_ref().trim();
if spec.is_empty() {
continue;
}
let dep_spec = DepSpec::from_string(spec)?;
env_marker_active |= !dep_spec.env_marker.is_empty();
if let Some(dsoom) = dep_specs.remove(&dep_spec.key) {
dep_specs.insert(dep_spec.key.clone(), dsoom.into_many(dep_spec));
} else {
dep_specs.insert(dep_spec.key.clone(), DepSpecOOM::One(dep_spec));
}
}
Ok(DepManifest {
dep_specs,
env_marker_active,
})
}
pub(crate) fn from_dep_specs(dep_specs: &Vec<DepSpec>) -> ResultDynError<Self> {
let mut env_marker_active = false;
let mut ds: HashMap<String, DepSpecOOM> = HashMap::new();
for dep_spec in dep_specs {
env_marker_active |= !dep_spec.env_marker.is_empty();
if let Some(dsoom) = ds.remove(&dep_spec.key) {
ds.insert(dep_spec.key.clone(), dsoom.into_many(dep_spec.clone()));
} else {
ds.insert(dep_spec.key.clone(), DepSpecOOM::One(dep_spec.clone()));
}
}
Ok(DepManifest {
dep_specs: ds,
env_marker_active,
})
}
pub fn from_packages<I>(packages: I, anchor: Anchor) -> ResultDynError<Self>
where
I: IntoIterator,
I::Item: Borrow<Package>,
{
let mut package_name_to_package: HashMap<String, Vec<Package>> = HashMap::new();
for package in packages {
let pkg: &Package = package.borrow();
package_name_to_package
.entry(pkg.name.clone())
.or_default()
.push(pkg.clone());
}
let names: Vec<String> = package_name_to_package.keys().cloned().collect();
let mut dep_specs: Vec<DepSpec> = Vec::new();
for name in names {
let packages = match package_name_to_package.get_mut(&name) {
Some(packages) => packages,
None => continue,
};
packages.sort();
let pkg_min = match packages.first() {
Some(pkg) => pkg,
None => continue,
};
let pkg_max = match packages.last() {
Some(pkg) => pkg,
None => continue,
};
let ds = match anchor {
Anchor::Lower => {
DepSpec::from_package(pkg_min, DepOperator::GreaterThanOrEq)
}
Anchor::Upper => {
DepSpec::from_package(pkg_max, DepOperator::LessThanOrEq)
}
Anchor::Both => return Err("Not implemented".into()),
};
if let Ok(dep_spec) = ds {
dep_specs.push(dep_spec);
}
}
DepManifest::from_dep_specs(&dep_specs)
}
pub(crate) fn from_requirements_file(file_path: &Path) -> ResultDynError<Self> {
let mut files: VecDeque<PathBuf> = VecDeque::new();
files.push_back(file_path.to_path_buf());
let mut dep_specs: Vec<String> = Vec::new();
while !files.is_empty() {
let fp = files.pop_front().unwrap();
let file = File::open(&fp)
.map_err(|e| format!("Failed to open file: {fp:?} {e}"))?;
let lines = io::BufReader::new(file).lines();
for line in lines.map_while(Result::ok) {
let t = line.trim();
if t.is_empty() || t.starts_with('#') {
continue;
}
if let Some(post) = t.strip_prefix("-r ") {
if let Some(parent) = fp.parent() {
files.push_back(parent.join(post.trim()));
}
} else if let Some(post) = t.strip_prefix("--requirement ") {
if let Some(parent) = fp.parent() {
files.push_back(parent.join(post.trim()));
}
} else {
dep_specs.push(line);
}
}
}
Self::try_from_iter(dep_specs.iter())
}
pub(crate) fn from_pyproject(
content: &str,
options: Option<&Vec<String>>,
) -> ResultDynError<Self> {
let ppi = PyProjectInfo::new(content)?;
Self::try_from_iter(ppi.get_dependencies(options)?.iter())
}
pub(crate) fn from_pyproject_file(
file_path: &PathBuf,
bound_options: Option<&Vec<String>>,
) -> ResultDynError<Self> {
let content = fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read file: {e}"))?;
Self::from_pyproject(&content, bound_options)
}
pub fn from_url<U: UreqClient>(
client: &U,
url: &Path,
bound_options: Option<&Vec<String>>,
) -> ResultDynError<Self> {
let url_str = url.to_str().ok_or("Invalid URL")?;
let content = client.get(url_str)?;
if url_str.ends_with("pyproject.toml") {
Self::from_pyproject(&content, bound_options)
} else {
let lf = LockFile::new(content);
Self::try_from_iter(lf.get_dependencies(bound_options)?)
}
}
pub(crate) fn from_path(
file_path: &Path,
bound_options: Option<&Vec<String>>,
) -> ResultDynError<Self> {
let fp = path_normalize(file_path, true)?;
match fp.to_str() {
Some(s) if s.ends_with("pyproject.toml") => {
Self::from_pyproject_file(&fp, bound_options)
}
Some(s) if s.ends_with("requirements.txt") => {
Self::from_requirements_file(&fp)
}
Some(_) => {
let content = fs::read_to_string(fp)
.map_err(|e| format!("Failed to read file: {e}"))?;
let lf = LockFile::new(content);
Self::try_from_iter(lf.get_dependencies(bound_options)?)
}
None => Err("Path contains invalid UTF-8".into()),
}
}
pub(crate) fn from_dir(
dir: &Path,
bound_options: Option<&Vec<String>>,
log: FlagLog,
) -> ResultDynError<Self> {
match LOCK_PRIORITY
.iter()
.map(|file| dir.join(file))
.find(|path| path.exists())
{
Some(file_path) => {
logger!(
log,
module_path!(),
"Using file: {}",
file_path.display()
);
Self::from_path(&file_path, bound_options)
}
None => Err(format!(
"Cannot find lock file, requirements file, or pyproject.toml in directory: {}",
dir.display()
)
.into()),
}
}
pub fn from_git_repo(
url: &Path,
bound_options: Option<&Vec<String>>,
log: FlagLog,
) -> ResultDynError<Self> {
let tmp_dir = tempdir()
.map_err(|e| format!("Failed to create temporary directory: {e}"))?;
let repo_path = tmp_dir.path().join("repo");
let status = Command::new("git")
.args([
"clone",
"--depth",
"1",
url.to_str().unwrap(),
repo_path.to_str().unwrap(),
])
.status()
.map_err(|e| format!("Failed to execute git: {e}"))?;
if !status.success() {
return Err(format!("Git clone failed: {}", url.display()).into());
}
Self::from_dir(&repo_path, bound_options, log)
}
pub(crate) fn from_path_or_url(
file_path: &Path,
bound_options: Option<&Vec<String>>,
log: FlagLog,
) -> ResultDynError<Self> {
match file_path.to_str() {
Some(s) if s.ends_with(".git") => {
Self::from_git_repo(file_path, bound_options, log)
}
Some(s) if s.starts_with("http") => {
Self::from_url(&UreqClientLive, file_path, bound_options)
}
Some(_) => Self::from_path(file_path, bound_options),
None => Err("Path contains invalid UTF-8".into()),
}
}
pub(crate) fn iter_dep_specs(&self) -> impl Iterator<Item = &DepSpec> {
self.dep_specs.values().flat_map(|dsoom| match dsoom {
DepSpecOOM::One(ds) => vec![ds].into_iter(),
DepSpecOOM::Many(ds_vec) => ds_vec.iter().collect::<Vec<_>>().into_iter(),
})
}
fn keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self.dep_specs.keys().cloned().collect();
keys.sort_by_key(|name| name.to_lowercase());
keys
}
pub(crate) fn has_package(&self, package: &Package) -> bool {
self.dep_specs.contains_key(&package.key)
}
pub(crate) fn get_dep_specs(
&self,
key: &str,
) -> Option<std::slice::Iter<'_, DepSpec>> {
self.dep_specs.get(key).map(|dsoom| match dsoom {
DepSpecOOM::One(ds) => std::slice::from_ref(ds).iter(),
DepSpecOOM::Many(ds_vec) => ds_vec.iter(),
})
}
pub(crate) fn get_dep_spec_difference(
&self,
observed: &HashSet<&String>,
) -> Vec<&String> {
let mut dep_specs: Vec<&String> = self
.dep_specs
.keys()
.filter(|key| !observed.contains(key))
.collect();
dep_specs.sort();
dep_specs
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.dep_specs.len()
}
pub(crate) fn validate(
&self,
package: &Package,
permit_superset: bool,
env_marker_state: Option<&EnvMarkerState>,
) -> (bool, Option<&DepSpec>) {
if let Some(dsoom) = self.dep_specs.get(&package.key) {
match dsoom {
DepSpecOOM::One(ds) => {
if ds.env_marker.is_empty() {
(ds.validate_package(package), Some(ds))
} else {
let ems = env_marker_state.expect("EMS should be loaded");
if ds.validate_env_marker(ems) {
(ds.validate_package(package), Some(ds))
} else {
(permit_superset, None)
}
}
}
DepSpecOOM::Many(dsv) => {
if dsv.iter().any(|ds| !ds.env_marker.is_empty()) {
let ems = env_marker_state.expect("EMS should be loaded");
for ds in dsv {
if ds.validate_env_marker(ems) {
return (ds.validate_package(package), Some(ds));
}
}
} else {
for ds in dsv {
if ds.validate_package(package) {
return (true, Some(ds));
}
}
}
(permit_superset, None)
}
}
} else {
(permit_superset, None)
}
}
pub fn to_dep_manifest_report(&self) -> DepManifestReport {
let mut records = Vec::new();
for key in self.keys() {
if let Some(dsoom) = self.dep_specs.get(&key) {
match dsoom {
DepSpecOOM::One(ds) => {
records.push(DepManifestRecord {
dep_spec: ds.clone(),
});
}
DepSpecOOM::Many(dsv) => {
for ds in dsv {
records.push(DepManifestRecord {
dep_spec: ds.clone(),
});
}
}
};
}
}
DepManifestReport { records }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::package_durl::DirectURL;
use crate::ureq_client::UreqClientMock;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_dep_spec_a() {
let dm =
DepManifest::try_from_iter(["pk1>=0.2,<0.3", "pk2>=1,<3"].iter()).unwrap();
let p1 = Package::from_dist_info("pk2-2.0.dist-info", None, None).unwrap();
assert!(dm.validate(&p1, false, None).0);
let p2 = Package::from_dist_info("foo-2.0.dist-info", None, None).unwrap();
assert!(!dm.validate(&p2, false, None).0);
let p3 = Package::from_dist_info("pk1-0.2.5.dist-info", None, None).unwrap();
assert!(dm.validate(&p3, false, None).0);
let p3 = Package::from_dist_info("pk1-0.3.0.dist-info", None, None).unwrap();
assert!(!dm.validate(&p3, false, None).0);
}
#[test]
fn test_from_dep_specs_a() {
let ds = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("requests>=1.4").unwrap(),
];
let dm = DepManifest::from_dep_specs(&ds).unwrap();
assert_eq!(dm.len(), 2);
}
#[test]
fn test_from_requirements_a() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("requirements.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "# comment").unwrap();
writeln!(file, "pk1>=0.2, <0.3 ").unwrap();
writeln!(file).unwrap();
writeln!(file, "pk2>=1,<3").unwrap();
writeln!(file, "# ").unwrap();
let dep_manifest = DepManifest::from_requirements_file(&file_path).unwrap();
assert_eq!(dep_manifest.len(), 2);
let p1 = Package::from_name_version_durl("pk2", "2.1", None).unwrap();
assert!(dep_manifest.validate(&p1, false, None).0);
let p2 = Package::from_name_version_durl("pk2", "0.1", None).unwrap();
assert!(!dep_manifest.validate(&p2, false, None).0);
let p3 = Package::from_name_version_durl("pk1", "0.2.2.999", None).unwrap();
assert!(dep_manifest.validate(&p3, false, None).0);
let p4 = Package::from_name_version_durl("pk99", "0.2.2.999", None).unwrap();
assert!(!dep_manifest.validate(&p4, false, None).0);
}
#[test]
fn test_from_requirements_b() {
let content = r#"
termcolor==2.2.0
# via
# invsys (pyproject.toml)
# apache-airflow
terminado==0.18.1
# via notebook
testpath==0.6.0
# via nbconvert
text-unidecode==1.3
# via python-slugify
threadpoolctl==3.4.0
# via scikit-learn
toml==0.10.2
# via
# coverage
# pre-commit
tomlkit==0.12.4
# via pylint
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("requirements.txt");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm1 = DepManifest::from_requirements_file(&file_path).unwrap();
assert_eq!(dm1.len(), 7);
let p1 = Package::from_name_version_durl("termcolor", "2.2.0", None).unwrap();
assert!(dm1.validate(&p1, false, None).0);
let p2 = Package::from_name_version_durl("termcolor", "2.2.1", None).unwrap();
assert!(!dm1.validate(&p2, false, None).0);
let p3 = Package::from_name_version_durl("text-unicide", "1.3", None).unwrap();
assert!(!dm1.validate(&p3, false, None).0);
let p3 = Package::from_name_version_durl("text-unidecode", "1.3", None).unwrap();
assert!(dm1.validate(&p3, false, None).0);
}
#[test]
fn test_from_requirements_c() {
let content = r#"
opentelemetry-api==1.24.0
# via
# apache-airflow
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-sdk
opentelemetry-exporter-otlp==1.24.0
# via apache-airflow
opentelemetry-exporter-otlp-proto-common==1.24.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-exporter-otlp-proto-grpc==1.24.0
# via opentelemetry-exporter-otlp
opentelemetry-exporter-otlp-proto-http==1.24.0
# via opentelemetry-exporter-otlp
opentelemetry-proto==1.24.0
# via
# opentelemetry-exporter-otlp-proto-common
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-sdk==1.24.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-exporter-otlp-proto-http
opentelemetry-semantic-conventions==0.45b0
# via opentelemetry-sdk
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("requirements.txt");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm1 = DepManifest::from_requirements_file(&file_path).unwrap();
assert_eq!(dm1.len(), 8);
let p1 = Package::from_name_version_durl(
"opentelemetry-exporter-otlp-proto-grpc",
"1.24.0",
None,
)
.unwrap();
assert!(dm1.validate(&p1, false, None).0);
let p2 = Package::from_name_version_durl(
"opentelemetry-exporter-otlp-proto-grpc",
"1.24.1",
None,
)
.unwrap();
assert!(!dm1.validate(&p2, false, None).0);
let p3 = Package::from_name_version_durl(
"opentelemetry-exporter-otlp-proto-gpc",
"1.24.0",
None,
)
.unwrap();
assert!(!dm1.validate(&p3, false, None).0);
}
#[test]
fn test_from_requirements_d1() {
let content = r#"
python-slugify==8.0.4
# via
# apache-airflow
# python-nvd3
pytz==2023.3
pytzdata==2020.1
# via pendulum
pyyaml==6.0
pyzmq==26.0.0
readme-renderer==43.0
# via twine
redshift-connector==2.1.1
# via apache-airflow-providers-amazon
referencing==0.34.0
# via
# jsonschema
# jsonschema-specifications
regex==2024.4.16
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("requirements.txt");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm1 = DepManifest::from_requirements_file(&file_path).unwrap();
assert_eq!(dm1.len(), 9);
let p1 = Package::from_name_version_durl("regex", "2024.4.16", None).unwrap();
assert!(dm1.validate(&p1, false, None).0);
let p2 = Package::from_name_version_durl("regex", "2024.04.16", None).unwrap();
assert!(dm1.validate(&p2, false, None).0);
let p2 = Package::from_name_version_durl("regex", "2024.04.17", None).unwrap();
assert!(!dm1.validate(&p2, false, None).0);
}
#[test]
fn test_from_requirements_d2() {
let content = r#"
python-slugify==8.0.4
pytzdata==2020.1 # foo
pyzmq==26.0.0
readme-renderer==43.0 # bar
https://files.pythonhosted.org/packages/01/bb/7f594a891b8e2d9f26e07fa79bd63bb9e426c8dddf0dedf5429f008cdcda/arraykit-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl # BAZ
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("requirements.txt");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm1 = DepManifest::from_requirements_file(&file_path).unwrap();
let names: Vec<String> = dm1.keys().to_vec();
assert_eq!(
names,
vec![
"arraykit",
"python_slugify",
"pytzdata",
"pyzmq",
"readme_renderer"
]
)
}
#[test]
fn test_from_requirements_e() {
let content1 = r#"
python-slugify==8.0.4
pytz==2023.3
pytzdata==2020.1
pyyaml==6.0
pyzmq==26.0.0
"#;
let dir = tempdir().unwrap();
let fp1 = dir.path().join("requirements-a.txt");
let mut f1 = File::create(&fp1).unwrap();
write!(f1, "{}", content1).unwrap();
let content2 = r#"
readme-renderer==43.0
redshift-connector==2.1.1
referencing==0.34.0
regex==2024.4.16
-r requirements-a.txt
"#;
let fp2 = dir.path().join("requirements-b.txt");
let mut f2 = File::create(&fp2).unwrap();
write!(f2, "{}", content2).unwrap();
let dm1 = DepManifest::from_requirements_file(&fp2).unwrap();
assert_eq!(dm1.len(), 9);
}
#[test]
fn test_from_requirements_f() {
let content1 = r#"
python-slugify==8.0.4
pytz==2023.3
pytzdata==2020.1
pyyaml==6.0
pyzmq==26.0.0
"#;
let dir = tempdir().unwrap();
let fp1 = dir.path().join("requirements-a.txt");
let mut f1 = File::create(&fp1).unwrap();
write!(f1, "{}", content1).unwrap();
let content2 = r#"
readme-renderer==43.0
redshift-connector==2.1.1
--requirement requirements-a.txt
"#;
let fp2 = dir.path().join("requirements-b.txt");
let mut f2 = File::create(&fp2).unwrap();
write!(f2, "{}", content2).unwrap();
let content3 = r#"
referencing==0.34.0
regex==2024.4.16
--requirement requirements-b.txt
"#;
let fp3 = dir.path().join("requirements-c.txt");
let mut f3 = File::create(&fp3).unwrap();
write!(f3, "{}", content3).unwrap();
let dm1 = DepManifest::from_requirements_file(&fp3).unwrap();
assert_eq!(dm1.len(), 9);
}
#[test]
fn test_from_pyproject_a() {
let content = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
authors = [
{ name="Example Author", email="author@example.com" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm = DepManifest::from_pyproject_file(&file_path, None).unwrap();
assert_eq!(dm.keys(), vec!["django", "gidgethub", "httpx"])
}
#[test]
fn test_from_pyproject_b1() {
let content = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
authors = [
{ name="Example Author", email="author@example.com" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
[project.optional-dependencies]
gui = ["PyQt5"]
cli = [
"rich",
"click",
]
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let bound_options = vec!["cli".to_string()];
let dm1 =
DepManifest::from_pyproject_file(&file_path, Some(&bound_options)).unwrap();
assert_eq!(
dm1.keys(),
vec!["click", "django", "gidgethub", "httpx", "rich"]
);
let bound_options = vec!["cli".to_string(), "gui".to_string()];
let dm2 =
DepManifest::from_pyproject_file(&file_path, Some(&bound_options)).unwrap();
assert_eq!(
dm2.keys(),
vec!["click", "django", "gidgethub", "httpx", "pyqt5", "rich"]
);
let bound_options = vec!["gui".to_string()];
let dm3 =
DepManifest::from_pyproject_file(&file_path, Some(&bound_options)).unwrap();
assert_eq!(dm3.keys(), vec!["django", "gidgethub", "httpx", "pyqt5"]);
}
#[test]
fn test_from_pyproject_b2() {
let content = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
authors = [
{ name="Example Author", email="author@example.com" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
[project.optional-dependencies]
gui = ["PyQt5"]
cli = [
"rich",
"click",
]
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let bo = vec!["cli".to_string()];
let dm1 = DepManifest::from_pyproject_file(&file_path, Some(&bo)).unwrap();
assert_eq!(
dm1.keys(),
vec!["click", "django", "gidgethub", "httpx", "rich"]
);
let bo1 = vec!["cli".to_string(), "gu".to_string()];
assert!(DepManifest::from_pyproject_file(&file_path, Some(&bo1)).is_err());
let bo2 = vec!["cli".to_string(), "gui".to_string()];
assert!(DepManifest::from_pyproject_file(&file_path, Some(&bo2)).is_ok());
let bo3 = vec!["cli".to_string(), "gui".to_string(), "foo".to_string()];
assert!(DepManifest::from_pyproject_file(&file_path, Some(&bo3)).is_err());
}
#[test]
fn test_from_pyproject_c1() {
let content = r#"
[tool.poetry]
name = "poetry"
readme = "README.md"
[tool.poetry.urls]
Changelog = "https://python-poetry.org/history/"
[tool.poetry.dependencies]
python = "==3.9"
poetry-core = { git = "https://github.com/python-poetry/poetry-core.git", branch = "main" }
build = "==1.2.1"
cachecontrol = { version = "==0.14.0", extras = ["filecache"] }
cleo = "==2.1.0"
dulwich = "==0.22.1"
fastjsonschema = "==2.18.0"
importlib-metadata = { version = ">=4.4", python = "<3.10" }
installer = "==0.7.0"
keyring = "==25.1.0"
# packaging uses calver, so version is unclamped
packaging = ">=24.0"
pkginfo = "==1.10"
platformdirs = ">=3.0.0,<5"
pyproject-hooks = "==1.0.0"
requests = "==2.26"
requests-toolbelt = "==1.0.0"
shellingham = "==1.5"
tomli = { version = "==2.0.1", python = "<3.11" }
tomlkit = ">=0.11.4,<1.0.0"
# trove-classifiers uses calver, so version is unclamped
trove-classifiers = ">=2022.5.19"
virtualenv = "==20.26.6"
xattr = { version = "==1.0.0", markers = "sys_platform == 'darwin'" }
[tool.poetry.group.dev.dependencies]
pre-commit = ">=2.10"
setuptools = { version = ">=60", python = "<3.10" }
[tool.poetry.group.test.dependencies]
coverage = ">=7.2.0"
deepdiff = ">=6.3"
httpretty = ">=1.1"
jaraco-classes = ">=3.3.1"
pytest = ">=8.0"
pytest-cov = ">=4.0"
pytest-mock = ">=3.9"
pytest-randomly = ">=3.12"
pytest-xdist = { version = ">=3.1", extras = ["psutil"] }
[tool.poetry.group.typing.dependencies]
mypy = ">=1.8.0"
types-requests = ">=2.28.8"
# only used in github actions
[tool.poetry.group.github-actions]
optional = true
[tool.poetry.group.github-actions.dependencies]
pytest-github-actions-annotate-failures = "==0.1.7"
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm = DepManifest::from_pyproject_file(&file_path, None).unwrap();
assert_eq!(
dm.keys(),
vec![
"build",
"cachecontrol",
"cleo",
"dulwich",
"fastjsonschema",
"importlib_metadata",
"installer",
"keyring",
"packaging",
"pkginfo",
"platformdirs",
"poetry_core",
"pyproject_hooks",
"requests",
"requests_toolbelt",
"shellingham",
"tomli",
"tomlkit",
"trove_classifiers",
"virtualenv",
"xattr"
]
);
assert_eq!(
dm.get_dep_specs("cachecontrol")
.unwrap()
.next()
.unwrap()
.to_string(),
"cachecontrol==0.14.0; python_version == '3.9'"
);
assert_eq!(
dm.get_dep_specs("dulwich")
.unwrap()
.next()
.unwrap()
.to_string(),
"dulwich==0.22.1; python_version == '3.9'"
);
assert_eq!(
dm.get_dep_specs("tomli")
.unwrap()
.next()
.unwrap()
.to_string(),
"tomli==2.0.1; python_version == '3.9'"
);
assert_eq!(
dm.get_dep_specs("platformdirs")
.unwrap()
.next()
.unwrap()
.to_string(),
"platformdirs>=3.0.0,<5; python_version == '3.9'"
);
assert_eq!(
dm.get_dep_specs("importlib_metadata")
.unwrap()
.next()
.unwrap()
.to_string(),
"importlib-metadata>=4.4; python_version == '3.9'"
);
}
#[test]
fn test_from_pyproject_c2() {
let content = r#"
[tool.poetry]
name = "poetry"
include = [{ path = "tests", format = "sdist" }]
homepage = "https://python-poetry.org/"
[tool.poetry.urls]
Changelog = "https://python-poetry.org/history/"
[tool.poetry.dependencies]
python = "==3.9"
poetry-core = { git = "https://github.com/python-poetry/poetry-core.git", branch = "main" }
build = "==1.2.1"
cachecontrol = { version = "==0.14.0", extras = ["filecache"] }
cleo = "==2.1.0"
importlib-metadata = { version = ">=4.4", python = "<3.10" }
installer = "==0.7.0"
keyring = "==25.1.0"
trove-classifiers = ">=2022.5.19"
xattr = { version = "==1.0.0", markers = "sys_platform == 'darwin'" }
[tool.poetry.group.dev.dependencies]
pre-commit = ">=2.10"
setuptools = { version = ">=60", python = "<3.10" }
[tool.poetry.group.test.dependencies]
coverage = ">=7.2.0"
deepdiff = ">=6.3"
httpretty = ">=1.1"
jaraco-classes = ">=3.3.1"
pytest = ">=8.0"
pytest-cov = ">=4.0"
pytest-xdist = { version = ">=3.1", extras = ["psutil"] }
[tool.poetry.group.typing.dependencies]
mypy = ">=1.8.0"
types-requests = ">=2.28.8"
# only used in github actions
[tool.poetry.group.github-actions]
optional = true
[tool.poetry.group.github-actions.dependencies]
pytest-github-actions-annotate-failures = "==0.1.7"
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm1 = DepManifest::from_pyproject_file(&file_path, None).unwrap();
assert_eq!(
dm1.keys(),
vec![
"build",
"cachecontrol",
"cleo",
"importlib_metadata",
"installer",
"keyring",
"poetry_core",
"trove_classifiers",
"xattr"
]
);
let opts2 = vec!["test".to_string()];
let dm2 = DepManifest::from_pyproject_file(&file_path, Some(&opts2)).unwrap();
assert_eq!(
dm2.keys(),
vec![
"build",
"cachecontrol",
"cleo",
"coverage",
"deepdiff",
"httpretty",
"importlib_metadata",
"installer",
"jaraco_classes",
"keyring",
"poetry_core",
"pytest",
"pytest_cov",
"pytest_xdist",
"trove_classifiers",
"xattr"
]
);
assert_eq!(
dm2.get_dep_specs("coverage")
.unwrap()
.next()
.unwrap()
.to_string(),
"coverage>=7.2.0"
);
let opts3 = vec!["typing".to_string()];
let dm3 = DepManifest::from_pyproject_file(&file_path, Some(&opts3)).unwrap();
assert_eq!(
dm3.keys(),
vec![
"build",
"cachecontrol",
"cleo",
"importlib_metadata",
"installer",
"keyring",
"mypy",
"poetry_core",
"trove_classifiers",
"types_requests",
"xattr"
]
);
assert_eq!(
dm3.get_dep_specs("mypy")
.unwrap()
.next()
.unwrap()
.to_string(),
"mypy>=1.8.0"
);
let opts4 = vec!["typing".to_string(), "test".to_string()];
let dm4 = DepManifest::from_pyproject_file(&file_path, Some(&opts4)).unwrap();
assert_eq!(
dm4.keys(),
vec![
"build",
"cachecontrol",
"cleo",
"coverage",
"deepdiff",
"httpretty",
"importlib_metadata",
"installer",
"jaraco_classes",
"keyring",
"mypy",
"poetry_core",
"pytest",
"pytest_cov",
"pytest_xdist",
"trove_classifiers",
"types_requests",
"xattr"
]
);
let opts5 = vec!["typing".to_string(), "test".to_string(), "foo".to_string()];
assert!(DepManifest::from_pyproject_file(&file_path, Some(&opts5)).is_err());
}
#[test]
fn test_from_pyproject_d1() {
let content = r#"
[tool.poetry]
name = "poetry"
include = [{ path = "tests", format = "sdist" }]
[tool.poetry.dependencies]
python = "^3.9"
poetry-core = { git = "https://github.com/python-poetry/poetry-core.git", branch = "main" }
build = "^1.2.1"
cachecontrol = { version = "^0.14.0", extras = ["filecache"] }
cleo = "^2.1.0"
dulwich = "^0.22.1"
fastjsonschema = "^2.18.0"
importlib-metadata = { version = ">=4.4", python = "<3.10" }
installer = "^0.7.0"
keyring = "^25.1.0"
# packaging uses calver, so version is unclamped
packaging = ">=24.0"
pkginfo = "^1.10"
platformdirs = ">=3.0.0,<5"
pyproject-hooks = "^1.0.0"
requests = "^2.26"
requests-toolbelt = "^1.0.0"
shellingham = "^1.5"
tomli = { version = "^2.0.1", python = "<3.11" }
tomlkit = ">=0.11.4,<1.0.0"
# trove-classifiers uses calver, so version is unclamped
trove-classifiers = ">=2022.5.19"
virtualenv = "^20.26.6"
xattr = { version = "^1.0.0", markers = "sys_platform == 'darwin'" }
[tool.poetry.group.dev.dependencies]
pre-commit = ">=2.10"
setuptools = { version = ">=60", python = "<3.10" }
[tool.poetry.group.test.dependencies]
coverage = ">=7.2.0"
deepdiff = ">=6.3"
httpretty = ">=1.1"
jaraco-classes = ">=3.3.1"
pytest = ">=8.0"
pytest-cov = ">=4.0"
pytest-mock = ">=3.9"
pytest-randomly = ">=3.12"
pytest-xdist = { version = ">=3.1", extras = ["psutil"] }
[tool.poetry.group.typing.dependencies]
mypy = ">=1.8.0"
types-requests = ">=2.28.8"
# only used in github actions
[tool.poetry.group.github-actions]
optional = true
[tool.poetry.group.github-actions.dependencies]
pytest-github-actions-annotate-failures = "^0.1.7"
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm = DepManifest::from_pyproject_file(&file_path, None).unwrap();
assert_eq!(
dm.keys(),
vec![
"build",
"cachecontrol",
"cleo",
"dulwich",
"fastjsonschema",
"importlib_metadata",
"installer",
"keyring",
"packaging",
"pkginfo",
"platformdirs",
"poetry_core",
"pyproject_hooks",
"requests",
"requests_toolbelt",
"shellingham",
"tomli",
"tomlkit",
"trove_classifiers",
"virtualenv",
"xattr"
]
);
assert_eq!(
dm.get_dep_specs("cachecontrol")
.unwrap()
.next()
.unwrap()
.to_string(),
"cachecontrol^0.14.0; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("platformdirs")
.unwrap()
.next()
.unwrap()
.to_string(),
"platformdirs>=3.0.0,<5; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("build")
.unwrap()
.next()
.unwrap()
.to_string(),
"build^1.2.1; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("dulwich")
.unwrap()
.next()
.unwrap()
.to_string(),
"dulwich^0.22.1; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("platformdirs")
.unwrap()
.next()
.unwrap()
.to_string(),
"platformdirs>=3.0.0,<5; python_version ^ '3.9'"
);
}
#[test]
fn test_from_pyproject_d2() {
let content = r#"
[tool.poetry]
name = "poetry"
include = [{ path = "tests", format = "sdist" }]
[tool.poetry.dependencies]
python = "^3.9"
poetry-core = { git = "https://github.com/python-poetry/poetry-core.git", branch = "main" }
build = "^1.2.1"
cachecontrol = { version = "^0.14.0", extras = ["filecache"] }
cleo = "^2.1.0"
dulwich = "^0.22.1"
fastjsonschema = "^2.18.0"
importlib-metadata = { version = ">=4.4", python = "<3.10" }
installer = "^0.7.0"
keyring = "^25.1.0"
# packaging uses calver, so version is unclamped
packaging = ">=24.0"
pkginfo = "^1.10"
platformdirs = ">=3.0.0,<5"
pyproject-hooks = "^1.0.0"
requests = "^2.26"
requests-toolbelt = "^1.0.0"
shellingham = "^1.5"
tomli = { version = "^2.0.1", python = "<3.11" }
tomlkit = ">=0.11.4,<1.0.0"
# trove-classifiers uses calver, so version is unclamped
trove-classifiers = ">=2022.5.19"
virtualenv = "^20.26.6"
xattr = { version = "^1.0.0", markers = "sys_platform == 'darwin'" }
[tool.poetry.group.dev.dependencies]
pre-commit = ">=2.10"
setuptools = { version = ">=60", python = "<3.10" }
[tool.poetry.group.typing.dependencies]
mypy = ">=1.8.0"
types-requests = ">=2.28.8"
# only used in github actions
[tool.poetry.group.github-actions]
optional = true
[tool.poetry.group.github-actions.dependencies]
pytest-github-actions-annotate-failures = "^0.1.7"
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let bo = vec!["github-actions".to_string()];
let dm = DepManifest::from_pyproject_file(&file_path, Some(&bo)).unwrap();
assert_eq!(
dm.keys(),
vec![
"build",
"cachecontrol",
"cleo",
"dulwich",
"fastjsonschema",
"importlib_metadata",
"installer",
"keyring",
"packaging",
"pkginfo",
"platformdirs",
"poetry_core",
"pyproject_hooks",
"pytest_github_actions_annotate_failures",
"requests",
"requests_toolbelt",
"shellingham",
"tomli",
"tomlkit",
"trove_classifiers",
"virtualenv",
"xattr"
]
);
assert_eq!(
dm.get_dep_specs("cachecontrol")
.unwrap()
.next()
.unwrap()
.to_string(),
"cachecontrol^0.14.0; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("platformdirs")
.unwrap()
.next()
.unwrap()
.to_string(),
"platformdirs>=3.0.0,<5; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("build")
.unwrap()
.next()
.unwrap()
.to_string(),
"build^1.2.1; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("dulwich")
.unwrap()
.next()
.unwrap()
.to_string(),
"dulwich^0.22.1; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("platformdirs")
.unwrap()
.next()
.unwrap()
.to_string(),
"platformdirs>=3.0.0,<5; python_version ^ '3.9'"
);
assert_eq!(
dm.get_dep_specs("pytest_github_actions_annotate_failures")
.unwrap()
.next()
.unwrap()
.to_string(),
"pytest-github-actions-annotate-failures^0.1.7"
);
}
#[test]
fn test_from_pyproject_e1() {
let content = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
description = "A small example package"
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
[tool.poetry.group.dev.dependencies]
pre-commit = ">=2.10"
setuptools = { version = ">=60", python = "<3.10" }
"#;
let bo = vec!["dev".to_string()];
let dm1 = DepManifest::from_pyproject(content, Some(&bo)).unwrap();
assert_eq!(
dm1.keys(),
vec!["django", "gidgethub", "httpx", "pre_commit", "setuptools"]
);
let dm2 = DepManifest::from_pyproject(content, None).unwrap();
assert_eq!(dm2.keys(), vec!["django", "gidgethub", "httpx"]);
}
#[test]
fn test_from_url_a() {
let mock_get = r#"
dill>=0.3.9
six>=1.15.0
numpy>= 2.0
"#;
let mut mock_get_map = HashMap::new();
mock_get_map.insert("http://example.com".to_string(), mock_get.to_string());
let client = UreqClientMock {
mock_post: None,
mock_get: Some(mock_get_map),
};
let url = PathBuf::from("http://example.com/requirements.txt");
let dm = DepManifest::from_url(&client, &url, None).unwrap();
assert_eq!(dm.keys(), vec!["dill", "numpy", "six"])
}
#[test]
fn test_to_requirements_a() {
let ds = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("requests>=1.4").unwrap(),
DepSpec::from_string("static-frame>2.0,!=1.3").unwrap(),
];
let dm1 = DepManifest::from_dep_specs(&ds).unwrap();
let dir = tempdir().unwrap();
let file_path = dir.path().join("requirements.txt");
let dmr1 = dm1.to_dep_manifest_report();
dmr1.to_file(&file_path, ' ').unwrap();
let dm2 = DepManifest::from_requirements_file(&file_path).unwrap();
assert_eq!(dm2.len(), 3)
}
#[test]
fn test_get_dep_spec_a() {
let ds = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("requests>=1.4").unwrap(),
DepSpec::from_string("static-frame>2.0,!=1.3").unwrap(),
];
let dm1 = DepManifest::from_dep_specs(&ds).unwrap();
let ds1 = dm1.get_dep_specs("requests").unwrap().next().unwrap();
assert_eq!(format!("{}", ds1), "requests>=1.4");
}
#[test]
fn test_get_dep_spec_b() {
let ds = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("requests>=1.4").unwrap(),
DepSpec::from_string("static-frame>2.0,!=1.3").unwrap(),
];
let dm1 = DepManifest::from_dep_specs(&ds).unwrap();
assert!(dm1.get_dep_specs("foo").is_none());
}
#[test]
fn test_get_dep_spec_c() {
let ds = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("Cython==3.0.11").unwrap(),
];
let dm1 = DepManifest::from_dep_specs(&ds).unwrap();
let ds1 = dm1.get_dep_specs("cython").unwrap().next().unwrap();
assert_eq!(format!("{}", ds1), "Cython==3.0.11");
}
#[test]
fn test_get_dep_spec_difference_a() {
let ds = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("requests>=1.4").unwrap(),
DepSpec::from_string("static-frame>2.0,!=1.3").unwrap(),
];
let dm1 = DepManifest::from_dep_specs(&ds).unwrap();
let mut observed = HashSet::new();
let n1 = "static_frame".to_string();
observed.insert(&n1);
let post = dm1.get_dep_spec_difference(&observed);
assert_eq!(post, vec!["numpy", "requests"]);
}
#[test]
fn test_validate_a() {
let json_str = r#"
{"url": "https://github.com/pypa/packaging.git", "vcs_info": {"commit_id": "cf2cbe2aec28f87c6228a6fb136c27931c9af407", "revision": "cf2cbe2aec28f87c6228a6fb136c27931c9af407", "vcs": "git"}}
"#;
let durl: DirectURL = serde_json::from_str(json_str).unwrap();
let p1 =
Package::from_dist_info("packaging-24.2.dev0.dist-info", None, Some(durl))
.unwrap();
let ds1 = DepSpec::from_string("packaging @ git+https://github.com/pypa/packaging.git@cf2cbe2aec28f87c6228a6fb136c27931c9af407").unwrap();
let specs = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("requests>=1.4").unwrap(),
ds1,
];
let dm1 = DepManifest::from_dep_specs(&specs).unwrap();
assert!(dm1.validate(&p1, false, None).0);
}
#[test]
fn test_validate_b() {
let json_str = r#"
{"url": "https://github.com/pypa/packaging.git", "vcs_info": {"commit_id": "cf2cbe2aec28f87c6228a6fb136c27931c9af407", "revision": "cf2cbe2aec28f87c6228a6fb136c27931c9af407", "vcs": "git"}}
"#;
let durl: DirectURL = serde_json::from_str(json_str).unwrap();
let p1 =
Package::from_dist_info("packaging-24.2.dev0.dist-info", None, Some(durl))
.unwrap();
let ds1 = DepSpec::from_string("packaging @ git+https://foo@github.com/pypa/packaging.git@cf2cbe2aec28f87c6228a6fb136c27931c9af407").unwrap();
let specs = vec![
DepSpec::from_string("numpy==1.19.1").unwrap(),
DepSpec::from_string("requests>=1.4").unwrap(),
ds1,
];
let dm1 = DepManifest::from_dep_specs(&specs).unwrap();
assert!(!dm1.env_marker_active);
assert!(dm1.validate(&p1, false, None).0);
}
#[test]
fn test_from_dir_a() {
let content = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
description = "A small example package"
requires-python = ">=3.8"
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
"#;
let dir = tempdir().unwrap();
let file_path = dir.path().join("pyproject.toml");
let mut file = File::create(&file_path).unwrap();
write!(file, "{}", content).unwrap();
let dm = DepManifest::from_dir(dir.path(), None, FlagLog(false)).unwrap();
assert!(dm.env_marker_active);
assert_eq!(dm.keys(), vec!["django", "gidgethub", "httpx"]);
}
#[test]
fn test_from_dir_b() {
let dir = tempdir().unwrap();
let content1 = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
description = "A small example package"
requires-python = ">=3.8"
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
"#;
let fp1 = dir.path().join("pyproject.toml");
let mut file1 = File::create(&fp1).unwrap();
write!(file1, "{}", content1).unwrap();
let content2 = r#"
python-slugify==8.0.4
# via
# apache-airflow
# python-nvd3
pytz==2023.3
pytzdata==2020.1
# via pendulum
pyyaml==6.0
pyzmq==26.0.0
"#;
let fp2 = dir.path().join("requirements.txt");
let mut file2 = File::create(&fp2).unwrap();
write!(file2, "{}", content2).unwrap();
let dm = DepManifest::from_dir(dir.path(), None, FlagLog(false));
assert_eq!(
dm.unwrap().keys(),
vec!["python_slugify", "pytz", "pytzdata", "pyyaml", "pyzmq"]
);
}
#[test]
fn test_from_dir_c() {
let dir = tempdir().unwrap();
let content1 = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
description = "A small example package"
requires-python = ">=3.8"
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
"#;
let fp1 = dir.path().join("pyproject.toml");
let mut file1 = File::create(&fp1).unwrap();
write!(file1, "{}", content1).unwrap();
let content2 = r#"
python-slugify==8.0.4
# via
# apache-airflow
# python-nvd3
pytz==2023.3
pytzdata==2020.1
# via pendulum
pyyaml==6.0
pyzmq==26.0.0
"#;
let fp2 = dir.path().join("requirements.lock");
let mut file2 = File::create(&fp2).unwrap();
write!(file2, "{}", content2).unwrap();
let dm = DepManifest::from_dir(dir.path(), None, FlagLog(false));
assert_eq!(
dm.unwrap().keys(),
vec!["python_slugify", "pytz", "pytzdata", "pyyaml", "pyzmq"]
);
}
#[test]
fn test_from_dir_d() {
let dir = tempdir().unwrap();
let content1 = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
description = "A small example package"
requires-python = ">=3.8"
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
"#;
let fp1 = dir.path().join("pyproject.toml");
let mut file1 = File::create(&fp1).unwrap();
write!(file1, "{}", content1).unwrap();
let content2 = r#"
python-slugify==8.0.4
# via
# apache-airflow
# python-nvd3
pytz==2023.3
pytzdata==2020.1
# via pendulum
pyyaml==6.0
pyzmq==26.0.0
"#;
let fp2 = dir.path().join("requirements.txt");
let mut file2 = File::create(&fp2).unwrap();
write!(file2, "{}", content2).unwrap();
let content3 = r#"
version = 1
requires-python = ">=3.12"
[[distribution]]
name = "arraykit"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/35/d1d6cc29d930eff913e49fe0081149f5cb630a630cf35b329d811dc390e2/arraykit-0.10.0.tar.gz", hash = "sha256:ee890b71c6e60505a9a77ad653ecb9c879e0f1a887980359d7fbaf29d33d5446", size = 83187 }
[[distribution]]
name = "arraymap"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/89/1d8b77225282b1a37029755ff53f63b1566bab8da1ac0e88f2fb8187c490/arraymap-0.4.0.tar.gz", hash = "sha256:af1aa15f9f0c799888326561275052b4ea709b0a3a2ff58d01c55a447f8b1213", size = 24770 }
"#;
let fp3 = dir.path().join("uv.lock");
let mut file3 = File::create(&fp3).unwrap();
write!(file3, "{}", content3).unwrap();
let dm = DepManifest::from_dir(dir.path(), None, FlagLog(false));
assert_eq!(dm.unwrap().keys(), vec!["arraykit", "arraymap"]);
}
#[test]
fn test_from_dir_e() {
let dir = tempdir().unwrap();
let content1 = r#"
[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
description = "A small example package"
requires-python = ">=3.8"
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
]
"#;
let fp1 = dir.path().join("pyproject.toml");
let mut file1 = File::create(&fp1).unwrap();
write!(file1, "{}", content1).unwrap();
let content2 = r#"
python-slugify==8.0.4
# via
# apache-airflow
# python-nvd3
pytz==2023.3
pytzdata==2020.1
# via pendulum
pyyaml==6.0
pyzmq==26.0.0
"#;
let fp2 = dir.path().join("requirements.txt");
let mut file2 = File::create(&fp2).unwrap();
write!(file2, "{}", content2).unwrap();
let content3 = r#"
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
[[package]]
name = "certifi"
version = "2025.1.31"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
groups = ["main"]
"#;
let fp3 = dir.path().join("poetry.lock");
let mut file3 = File::create(&fp3).unwrap();
write!(file3, "{}", content3).unwrap();
let dm = DepManifest::from_dir(dir.path(), None, FlagLog(false));
assert_eq!(dm.unwrap().keys(), vec!["certifi", "charset_normalizer"]);
}
}