Remove `add` and `remove` commands (#1259)

## Summary

These add and remove dependencies from a `pyproject.toml` -- but they're
currently hidden, and don't match the rest of the workflow. We can
re-add them when the time is right.
This commit is contained in:
Charlie Marsh 2024-02-06 11:18:27 -08:00 committed by GitHub
parent d4bbaf1755
commit 62416286e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 0 additions and 924 deletions

15
Cargo.lock generated
View File

@ -2475,7 +2475,6 @@ dependencies = [
"puffin-resolver",
"puffin-traits",
"puffin-warnings",
"puffin-workspace",
"pypi-types",
"pyproject-toml",
"regex",
@ -2905,20 +2904,6 @@ dependencies = [
"rustc-hash",
]
[[package]]
name = "puffin-workspace"
version = "0.0.1"
dependencies = [
"fs-err",
"pep440_rs",
"pep508_rs",
"puffin-normalize",
"pyproject-toml",
"serde",
"thiserror",
"toml_edit",
]
[[package]]
name = "pyo3"
version = "0.20.2"

View File

@ -95,7 +95,6 @@ tokio = { version = "1.35.1", features = ["rt-multi-thread"] }
tokio-tar = { version = "0.3.1" }
tokio-util = { version = "0.7.10", features = ["compat"] }
toml = { version = "0.8.8" }
toml_edit = { version = "0.21.0" }
tracing = { version = "0.1.40" }
tracing-durations-export = { version = "0.2.0", features = ["plot"] }
tracing-indicatif = { version = "0.3.6" }

View File

@ -1,24 +0,0 @@
[package]
name = "puffin-workspace"
version = "0.0.1"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[lints]
workspace = true
[dependencies]
pep440_rs = { path = "../pep440-rs" }
pep508_rs = { path = "../pep508-rs" }
puffin-normalize = { path = "../puffin-normalize" }
fs-err = { workspace = true }
pyproject-toml = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml_edit = { workspace = true, features = ["serde"] }

View File

@ -1,26 +0,0 @@
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WorkspaceError {
#[error(transparent)]
IO(#[from] io::Error),
#[error(transparent)]
InvalidToml(#[from] toml_edit::TomlError),
#[error(transparent)]
InvalidPyproject(#[from] toml_edit::de::Error),
#[error(transparent)]
InvalidRequirement(#[from] pep508_rs::Pep508Error),
#[error("no `[project]` table found in `pyproject.toml`")]
MissingProjectTable,
#[error("no `[project.dependencies]` array found in `pyproject.toml`")]
MissingProjectDependenciesArray,
#[error("unable to find package: `{0}`")]
MissingPackage(String),
}

View File

@ -1,21 +0,0 @@
use std::path::{Path, PathBuf};
pub use error::WorkspaceError;
pub use verbatim::VerbatimRequirement;
pub use workspace::Workspace;
mod error;
mod toml;
mod verbatim;
mod workspace;
/// Find the closest `pyproject.toml` file to the given path.
pub fn find_pyproject_toml(path: impl AsRef<Path>) -> Option<PathBuf> {
for directory in path.as_ref().ancestors() {
let pyproject_toml = directory.join("pyproject.toml");
if pyproject_toml.is_file() {
return Some(pyproject_toml);
}
}
None
}

View File

@ -1,46 +0,0 @@
/// Reformat a TOML array to use multiline format.
pub(crate) fn format_multiline_array(dependencies: &mut toml_edit::Array) {
if dependencies.is_empty() {
dependencies.set_trailing("");
return;
}
for item in dependencies.iter_mut() {
let decor = item.decor_mut();
let mut prefix = String::new();
for comment in find_comments(decor.prefix()).chain(find_comments(decor.suffix())) {
prefix.push_str("\n ");
prefix.push_str(comment);
}
prefix.push_str("\n ");
decor.set_prefix(prefix);
decor.set_suffix("");
}
dependencies.set_trailing(&{
let mut comments = find_comments(Some(dependencies.trailing())).peekable();
let mut value = String::new();
if comments.peek().is_some() {
for comment in comments {
value.push_str("\n ");
value.push_str(comment);
}
}
value.push('\n');
value
});
dependencies.set_trailing_comma(true);
}
/// Return an iterator over the comments in a raw string.
fn find_comments(raw_string: Option<&toml_edit::RawString>) -> impl Iterator<Item = &str> {
raw_string
.and_then(toml_edit::RawString::as_str)
.unwrap_or("")
.lines()
.filter_map(|line| {
let line = line.trim();
line.starts_with('#').then_some(line)
})
}

View File

@ -1,23 +0,0 @@
use std::str::FromStr;
use pep508_rs::Requirement;
#[derive(Debug)]
pub struct VerbatimRequirement<'a> {
/// The name of the requirement as provided by the user.
pub given_name: &'a str,
/// The normalized requirement.
pub requirement: Requirement,
}
impl<'a> TryFrom<&'a str> for VerbatimRequirement<'a> {
type Error = pep508_rs::Pep508Error;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
let requirement = Requirement::from_str(s)?;
Ok(Self {
given_name: s,
requirement,
})
}
}

View File

@ -1,158 +0,0 @@
use std::io;
use std::path::Path;
use std::str::FromStr;
use fs_err as fs;
use pyproject_toml::PyProjectToml;
use toml_edit::Document;
use pep508_rs::Requirement;
use puffin_normalize::PackageName;
use crate::toml::format_multiline_array;
use crate::verbatim::VerbatimRequirement;
use crate::WorkspaceError;
#[derive(Debug)]
pub struct Workspace {
/// The parsed `pyproject.toml`.
#[allow(unused)]
pyproject_toml: PyProjectToml,
/// The raw document.
document: Document,
}
impl Workspace {
/// Add a dependency to the workspace.
pub fn add_dependency(&mut self, requirement: &VerbatimRequirement<'_>) {
let Some(project) = self
.document
.get_mut("project")
.map(|project| project.as_table_mut().unwrap())
else {
// No `project` table.
let mut dependencies = toml_edit::Array::new();
dependencies.push(requirement.given_name);
format_multiline_array(&mut dependencies);
let mut project = toml_edit::Table::new();
project.insert(
"dependencies",
toml_edit::Item::Value(toml_edit::Value::Array(dependencies)),
);
self.document
.insert("project", toml_edit::Item::Table(project));
return;
};
let Some(dependencies) = project
.get_mut("dependencies")
.map(|dependencies| dependencies.as_array_mut().unwrap())
else {
// No `dependencies` array.
let mut dependencies = toml_edit::Array::new();
dependencies.push(requirement.given_name);
format_multiline_array(&mut dependencies);
project.insert(
"dependencies",
toml_edit::Item::Value(toml_edit::Value::Array(dependencies)),
);
return;
};
let index = dependencies.iter().position(|item| {
let Some(item) = item.as_str() else {
return false;
};
let Ok(existing) = Requirement::from_str(item) else {
return false;
};
requirement.requirement.name == existing.name
});
if let Some(index) = index {
dependencies.replace(index, requirement.given_name);
} else {
dependencies.push(requirement.given_name);
}
format_multiline_array(dependencies);
}
/// Remove a dependency from the workspace.
pub fn remove_dependency(&mut self, name: &PackageName) -> Result<(), WorkspaceError> {
let Some(project) = self
.document
.get_mut("project")
.map(|project| project.as_table_mut().unwrap())
else {
return Err(WorkspaceError::MissingProjectTable);
};
let Some(dependencies) = project
.get_mut("dependencies")
.map(|dependencies| dependencies.as_array_mut().unwrap())
else {
return Err(WorkspaceError::MissingProjectDependenciesArray);
};
let index = dependencies.iter().position(|item| {
let Some(item) = item.as_str() else {
return false;
};
let Ok(existing) = Requirement::from_str(item) else {
return false;
};
name == &existing.name
});
let Some(index) = index else {
return Err(WorkspaceError::MissingPackage(name.to_string()));
};
dependencies.remove(index);
format_multiline_array(dependencies);
Ok(())
}
/// Save the workspace to disk.
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), WorkspaceError> {
let file = fs::File::create(path.as_ref())?;
self.write(file)
}
/// Write the workspace to a writer.
fn write(&self, mut writer: impl io::Write) -> Result<(), WorkspaceError> {
writer.write_all(self.document.to_string().as_bytes())?;
Ok(())
}
}
impl TryFrom<&Path> for Workspace {
type Error = WorkspaceError;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
// Read the `pyproject.toml` from disk.
let contents = fs::read_to_string(path)?;
// Parse the `pyproject.toml` file.
let pyproject_toml = toml_edit::de::from_str::<PyProjectToml>(&contents)?;
// Parse the raw document.
let document = contents.parse::<Document>()?;
Ok(Self {
pyproject_toml,
document,
})
}
}

View File

@ -34,7 +34,6 @@ puffin-normalize = { path = "../puffin-normalize" }
puffin-resolver = { path = "../puffin-resolver", features = ["clap"] }
puffin-traits = { path = "../puffin-traits" }
puffin-warnings = { path = "../puffin-warnings" }
puffin-workspace = { path = "../puffin-workspace" }
pypi-types = { path = "../pypi-types" }
requirements-txt = { path = "../requirements-txt" }

View File

@ -1,74 +0,0 @@
use std::path::PathBuf;
use anyhow::Result;
use miette::{Diagnostic, IntoDiagnostic};
use thiserror::Error;
use tracing::info;
use puffin_workspace::WorkspaceError;
use crate::commands::ExitStatus;
use crate::printer::Printer;
/// Add a dependency to the workspace.
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn add(name: &str, _printer: Printer) -> Result<ExitStatus> {
match add_impl(name) {
Ok(status) => Ok(status),
Err(err) => {
#[allow(clippy::print_stderr)]
{
eprint!("{err:?}");
}
Ok(ExitStatus::Failure)
}
}
}
#[derive(Error, Debug, Diagnostic)]
enum AddError {
#[error(
"Could not find a `pyproject.toml` file in the current directory or any of its parents"
)]
#[diagnostic(code(puffin::add::workspace_not_found))]
WorkspaceNotFound,
#[error("Failed to parse requirement: `{0}`")]
#[diagnostic(code(puffin::add::invalid_requirement))]
InvalidRequirement(String, #[source] pep508_rs::Pep508Error),
#[error("Failed to parse `pyproject.toml` at: `{0}`")]
#[diagnostic(code(puffin::add::parse))]
ParseError(PathBuf, #[source] WorkspaceError),
#[error("Failed to write `pyproject.toml` to: `{0}`")]
#[diagnostic(code(puffin::add::write))]
WriteError(PathBuf, #[source] WorkspaceError),
}
fn add_impl(name: &str) -> miette::Result<ExitStatus> {
let requirement = puffin_workspace::VerbatimRequirement::try_from(name)
.map_err(|err| AddError::InvalidRequirement(name.to_string(), err))?;
// Locate the workspace.
let cwd = std::env::current_dir().into_diagnostic()?;
let Some(workspace_root) = puffin_workspace::find_pyproject_toml(cwd) else {
return Err(AddError::WorkspaceNotFound.into());
};
info!("Found workspace at: {}", workspace_root.display());
// Parse the manifest.
let mut manifest = puffin_workspace::Workspace::try_from(workspace_root.as_path())
.map_err(|err| AddError::ParseError(workspace_root.clone(), err))?;
// Add the dependency.
manifest.add_dependency(&requirement);
// Write the manifest back to disk.
manifest
.save(&workspace_root)
.map_err(|err| AddError::WriteError(workspace_root.clone(), err))?;
Ok(ExitStatus::Success)
}

View File

@ -1,7 +1,6 @@
use std::process::ExitCode;
use std::time::Duration;
pub(crate) use add::add;
pub(crate) use clean::clean;
use distribution_types::InstalledMetadata;
pub(crate) use freeze::freeze;
@ -9,17 +8,14 @@ pub(crate) use pip_compile::{extra_name_with_clap_error, pip_compile, Upgrade};
pub(crate) use pip_install::pip_install;
pub(crate) use pip_sync::pip_sync;
pub(crate) use pip_uninstall::pip_uninstall;
pub(crate) use remove::remove;
pub(crate) use venv::venv;
mod add;
mod clean;
mod freeze;
mod pip_compile;
mod pip_install;
mod pip_sync;
mod pip_uninstall;
mod remove;
mod reporters;
mod venv;

View File

@ -1,74 +0,0 @@
use std::path::PathBuf;
use anyhow::Result;
use miette::{Diagnostic, IntoDiagnostic};
use thiserror::Error;
use tracing::info;
use puffin_normalize::PackageName;
use puffin_workspace::WorkspaceError;
use crate::commands::ExitStatus;
use crate::printer::Printer;
/// Remove a dependency from the workspace.
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn remove(name: &PackageName, _printer: Printer) -> Result<ExitStatus> {
match remove_impl(name) {
Ok(status) => Ok(status),
Err(err) => {
#[allow(clippy::print_stderr)]
{
eprint!("{err:?}");
}
Ok(ExitStatus::Failure)
}
}
}
#[derive(Error, Debug, Diagnostic)]
enum RemoveError {
#[error(
"Could not find a `pyproject.toml` file in the current directory or any of its parents"
)]
#[diagnostic(code(puffin::remove::workspace_not_found))]
WorkspaceNotFound,
#[error("Failed to parse `pyproject.toml` at: `{0}`")]
#[diagnostic(code(puffin::remove::parse))]
ParseError(PathBuf, #[source] WorkspaceError),
#[error("Failed to write `pyproject.toml` to: `{0}`")]
#[diagnostic(code(puffin::remove::write))]
WriteError(PathBuf, #[source] WorkspaceError),
#[error("Failed to remove `{0}` from `pyproject.toml`")]
#[diagnostic(code(puffin::remove::parse))]
RemovalError(String, #[source] WorkspaceError),
}
fn remove_impl(name: &PackageName) -> miette::Result<ExitStatus> {
// Locate the workspace.
let cwd = std::env::current_dir().into_diagnostic()?;
let Some(workspace_root) = puffin_workspace::find_pyproject_toml(cwd) else {
return Err(RemoveError::WorkspaceNotFound.into());
};
info!("Found workspace at: {}", workspace_root.display());
// Parse the manifest.
let mut manifest = puffin_workspace::Workspace::try_from(workspace_root.as_path())
.map_err(|err| RemoveError::ParseError(workspace_root.clone(), err))?;
// Remove the dependency.
manifest
.remove_dependency(name)
.map_err(|err| RemoveError::RemovalError(name.to_string(), err))?;
// Write the manifest back to disk.
manifest
.save(&workspace_root)
.map_err(|err| RemoveError::WriteError(workspace_root.clone(), err))?;
Ok(ExitStatus::Success)
}

View File

@ -110,12 +110,6 @@ enum Commands {
Venv(VenvArgs),
/// Clear the cache.
Clean(CleanArgs),
/// Add a dependency to the workspace.
#[clap(hide = true)]
Add(AddArgs),
/// Remove a dependency from the workspace.
#[clap(hide = true)]
Remove(RemoveArgs),
}
#[derive(Args)]
@ -878,8 +872,6 @@ async fn run() -> Result<ExitStatus> {
)
.await
}
Commands::Add(args) => commands::add(&args.name, printer),
Commands::Remove(args) => commands::remove(&args.name, printer),
}
}

View File

@ -1,168 +0,0 @@
use std::process::Command;
use anyhow::Result;
use assert_fs::prelude::*;
use crate::common::{get_bin, puffin_snapshot};
mod common;
#[test]
fn missing_pyproject_toml() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
puffin_snapshot!(Command::new(get_bin())
.arg("add")
.arg("flask")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
puffin::add::workspace_not_found
× Could not find a `pyproject.toml` file in the current directory or any of
its parents
"###);
pyproject_toml.assert(predicates::path::missing());
Ok(())
}
#[test]
fn missing_project_table() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
puffin_snapshot!(Command::new(get_bin())
.arg("add")
.arg("flask")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
dependencies = [
"flask",
]
"#,
);
Ok(())
}
#[test]
fn missing_dependencies_array() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("add")
.arg("flask")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = [
"flask",
]
"#,
);
Ok(())
}
#[test]
fn replace_dependency() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = [
"flask==1.0.0",
]
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("add")
.arg("flask==2.0.0")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = [
"flask==2.0.0",
]
"#,
);
Ok(())
}
#[test]
fn reformat_array() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = ["flask==1.0.0"]
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("add")
.arg("requests")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = [
"flask==1.0.0",
"requests",
]
"#,
);
Ok(())
}

View File

@ -1,281 +0,0 @@
use std::process::Command;
use anyhow::Result;
use assert_fs::prelude::*;
use crate::common::{get_bin, puffin_snapshot};
mod common;
#[test]
fn missing_pyproject_toml() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("flask")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
puffin::remove::workspace_not_found
× Could not find a `pyproject.toml` file in the current directory or any of
its parents
"###);
pyproject_toml.assert(predicates::path::missing());
Ok(())
}
#[test]
fn missing_project_table() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("flask")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
puffin::remove::parse
× Failed to remove `flask` from `pyproject.toml`
no `[project]` table found in `pyproject.toml`
"###);
pyproject_toml.assert(predicates::str::is_empty());
Ok(())
}
#[test]
fn missing_dependencies_array() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("flask")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
puffin::remove::parse
× Failed to remove `flask` from `pyproject.toml`
no `[project.dependencies]` array found in `pyproject.toml`
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
"#,
);
Ok(())
}
#[test]
fn missing_dependency() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = [
"flask==1.0.0",
]
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("requests")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
puffin::remove::parse
× Failed to remove `requests` from `pyproject.toml`
unable to find package: `requests`
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = [
"flask==1.0.0",
]
"#,
);
Ok(())
}
#[test]
fn remove_dependency() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = [
"flask==1.0.0",
"requests",
]
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("flask")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = [
"requests",
]
"#,
);
Ok(())
}
#[test]
fn empty_array() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = [
"requests",
]
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("requests")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = []
"#,
);
Ok(())
}
#[test]
fn normalize_name() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = [
"flask==1.0.0",
"requests",
]
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("Flask")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = [
"requests",
]
"#,
);
Ok(())
}
#[test]
fn reformat_array() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = ["flask==1.0.0", "requests"]
"#,
)?;
puffin_snapshot!(Command::new(get_bin())
.arg("remove")
.arg("requests")
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
pyproject_toml.assert(
r#"[project]
name = "project"
dependencies = [
"flask==1.0.0",
]
"#,
);
Ok(())
}