Add support for --prefix (#4085)

## Summary

Closes #3076.
This commit is contained in:
Charlie Marsh
2024-06-06 16:15:28 -04:00
committed by GitHub
parent 677a7f157b
commit 52bdee2e85
17 changed files with 280 additions and 43 deletions
+34 -18
View File
@@ -1,18 +1,17 @@
use itertools::Either;
use std::borrow::Cow;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use uv_configuration::PreviewMode;
use same_file::is_same_file;
use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_fs::{LockedFile, Simplified};
use crate::discovery::{InterpreterRequest, SourceSelector, SystemPython};
use crate::virtualenv::{virtualenv_python_executable, PyVenvConfiguration};
use crate::{
find_default_interpreter, find_interpreter, Error, Interpreter, InterpreterSource, Target,
find_default_interpreter, find_interpreter, Error, Interpreter, InterpreterSource, Prefix,
Target,
};
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
@@ -159,6 +158,16 @@ impl PythonEnvironment {
}))
}
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--prefix` directory.
#[must_use]
pub fn with_prefix(self, prefix: Prefix) -> Self {
let inner = Arc::unwrap_or_clone(self.0);
Self(Arc::new(PythonEnvironmentShared {
interpreter: inner.interpreter.with_prefix(prefix),
..inner
}))
}
/// Returns the root (i.e., `prefix`) of the Python interpreter.
pub fn root(&self) -> &Path {
&self.0.root
@@ -189,20 +198,27 @@ impl PythonEnvironment {
///
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
/// still deduplicate the entries, returning a single path.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
if let Some(target) = self.0.interpreter.target() {
Either::Left(std::iter::once(target.root()))
pub fn site_packages(&self) -> impl Iterator<Item = Cow<Path>> {
let target = self.0.interpreter.target().map(Target::site_packages);
let prefix = self
.0
.interpreter
.prefix()
.map(|prefix| prefix.site_packages(self.0.interpreter.virtualenv()));
let interpreter = if target.is_none() && prefix.is_none() {
Some(self.0.interpreter.site_packages())
} else {
let purelib = self.0.interpreter.purelib();
let platlib = self.0.interpreter.platlib();
Either::Right(std::iter::once(purelib).chain(
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
None
} else {
Some(platlib)
},
))
}
None
};
target
.into_iter()
.flatten()
.map(Cow::Borrowed)
.chain(prefix.into_iter().flatten().map(Cow::Owned))
.chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
}
/// Returns the path to the `bin` directory inside this environment.
+42 -5
View File
@@ -5,6 +5,7 @@ use std::process::{Command, ExitStatus};
use configparser::ini::Ini;
use fs_err as fs;
use once_cell::sync::OnceCell;
use same_file::is_same_file;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{trace, warn};
@@ -20,7 +21,7 @@ use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
use uv_fs::{write_atomic_sync, PythonExt, Simplified};
use crate::pointer_size::PointerSize;
use crate::{PythonVersion, Target, VirtualEnvironment};
use crate::{Prefix, PythonVersion, Target, VirtualEnvironment};
/// A Python executable and its associated platform markers.
#[derive(Debug, Clone)]
@@ -38,6 +39,7 @@ pub struct Interpreter {
stdlib: PathBuf,
tags: OnceCell<Tags>,
target: Option<Target>,
prefix: Option<Prefix>,
pointer_size: PointerSize,
gil_disabled: bool,
}
@@ -69,6 +71,7 @@ impl Interpreter {
stdlib: info.stdlib,
tags: OnceCell::new(),
target: None,
prefix: None,
})
}
@@ -100,6 +103,7 @@ impl Interpreter {
stdlib: PathBuf::from("/dev/null"),
tags: OnceCell::new(),
target: None,
prefix: None,
pointer_size: PointerSize::_64,
gil_disabled: false,
}
@@ -113,13 +117,12 @@ impl Interpreter {
sys_executable: virtualenv.executable,
sys_prefix: virtualenv.root,
target: None,
prefix: None,
..self
}
}
/// Return a new [`Interpreter`] to install into the given `--target` directory.
///
/// Initializes the `--target` directory with the expected layout.
#[must_use]
pub fn with_target(self, target: Target) -> Self {
Self {
@@ -128,6 +131,15 @@ impl Interpreter {
}
}
/// Return a new [`Interpreter`] to install into the given `--prefix` directory.
#[must_use]
pub fn with_prefix(self, prefix: Prefix) -> Self {
Self {
prefix: Some(prefix),
..self
}
}
/// Returns the path to the Python virtual environment.
#[inline]
pub fn platform(&self) -> &Platform {
@@ -166,6 +178,11 @@ impl Interpreter {
self.target.is_some()
}
/// Returns `true` if the environment is a `--prefix` environment.
pub fn is_prefix(&self) -> bool {
self.prefix.is_some()
}
/// Returns `Some` if the environment is externally managed, optionally including an error
/// message from the `EXTERNALLY-MANAGED` file.
///
@@ -176,8 +193,8 @@ impl Interpreter {
return None;
}
// If we're installing into a target directory, it's never externally managed.
if self.is_target() {
// If we're installing into a target or prefix directory, it's never externally managed.
if self.is_target() || self.is_prefix() {
return None;
}
@@ -357,6 +374,11 @@ impl Interpreter {
self.target.as_ref()
}
/// Return the `--prefix` directory for this interpreter, if any.
pub fn prefix(&self) -> Option<&Prefix> {
self.prefix.as_ref()
}
/// Return the [`Layout`] environment used to install wheels into this interpreter.
pub fn layout(&self) -> Layout {
Layout {
@@ -365,6 +387,8 @@ impl Interpreter {
os_name: self.markers.os_name().to_string(),
scheme: if let Some(target) = self.target.as_ref() {
target.scheme()
} else if let Some(prefix) = self.prefix.as_ref() {
prefix.scheme(&self.virtualenv)
} else {
Scheme {
purelib: self.purelib().to_path_buf(),
@@ -387,6 +411,19 @@ impl Interpreter {
}
}
/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
let purelib = self.purelib();
let platlib = self.platlib();
std::iter::once(purelib).chain(
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
None
} else {
Some(platlib)
},
)
}
/// Check if the interpreter matches the given Python version.
///
/// If a patch version is present, we will require an exact match.
+2
View File
@@ -9,6 +9,7 @@ pub use crate::discovery::{
pub use crate::environment::PythonEnvironment;
pub use crate::interpreter::Interpreter;
pub use crate::pointer_size::PointerSize;
pub use crate::prefix::Prefix;
pub use crate::python_version::PythonVersion;
pub use crate::target::Target;
pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
@@ -21,6 +22,7 @@ mod interpreter;
pub mod managed;
pub mod platform;
mod pointer_size;
mod prefix;
mod py_launcher;
mod python_version;
mod target;
+43
View File
@@ -0,0 +1,43 @@
use std::path::{Path, PathBuf};
use pypi_types::Scheme;
/// A `--prefix` directory into which packages can be installed, separate from a virtual environment
/// or system Python interpreter.
#[derive(Debug, Clone)]
pub struct Prefix(PathBuf);
impl Prefix {
/// Return the [`Scheme`] for the `--prefix` directory.
pub fn scheme(&self, virtualenv: &Scheme) -> Scheme {
Scheme {
purelib: self.0.join(&virtualenv.purelib),
platlib: self.0.join(&virtualenv.platlib),
scripts: self.0.join(&virtualenv.scripts),
data: self.0.join(&virtualenv.data),
include: self.0.join(&virtualenv.include),
}
}
/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self, virtualenv: &Scheme) -> impl Iterator<Item = PathBuf> {
std::iter::once(self.0.join(&virtualenv.purelib))
}
/// Initialize the `--prefix` directory.
pub fn init(&self) -> std::io::Result<()> {
fs_err::create_dir_all(&self.0)?;
Ok(())
}
/// Return the path to the `--prefix` directory.
pub fn root(&self) -> &Path {
&self.0
}
}
impl From<PathBuf> for Prefix {
fn from(path: PathBuf) -> Self {
Self(path)
}
}
+5
View File
@@ -19,6 +19,11 @@ impl Target {
}
}
/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
std::iter::once(self.0.as_path())
}
/// Initialize the `--target` directory.
pub fn init(&self) -> std::io::Result<()> {
fs_err::create_dir_all(&self.0)?;
+1
View File
@@ -72,6 +72,7 @@ impl Combine for PipOptions {
.break_system_packages
.combine(other.break_system_packages),
target: self.target.combine(other.target),
prefix: self.prefix.combine(other.prefix),
index_url: self.index_url.combine(other.index_url),
extra_index_url: self.extra_index_url.combine(other.extra_index_url),
no_index: self.no_index.combine(other.no_index),
+1
View File
@@ -58,6 +58,7 @@ pub struct PipOptions {
pub system: Option<bool>,
pub break_system_packages: Option<bool>,
pub target: Option<PathBuf>,
pub prefix: Option<PathBuf>,
pub index_url: Option<IndexUrl>,
pub extra_index_url: Option<Vec<IndexUrl>>,
pub no_index: Option<bool>,
+32 -7
View File
@@ -731,10 +731,21 @@ pub(crate) struct PipSyncArgs {
pub(crate) no_break_system_packages: bool,
/// Install packages into the specified directory, rather than into the virtual environment
/// or system Python interpreter.
#[arg(long)]
/// or system Python interpreter. The packages will be installed at the top-level of the
/// directory
#[arg(long, conflicts_with = "prefix")]
pub(crate) target: Option<PathBuf>,
/// Install packages into `lib`, `bin`, and other top-level folders under the specified
/// directory, as if a virtual environment were created at the specified location.
///
/// In general, prefer the use of `--python` to install into an alternate environment, as
/// scripts and other artifacts installed via `--prefix` will reference the installing
/// interpreter, rather than any interpreter added to the `--prefix` directory, rendering them
/// non-portable.
#[arg(long, conflicts_with = "target")]
pub(crate) prefix: Option<PathBuf>,
/// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`.
#[arg(long, overrides_with("no_legacy_setup_py"))]
@@ -1087,10 +1098,21 @@ pub(crate) struct PipInstallArgs {
pub(crate) no_break_system_packages: bool,
/// Install packages into the specified directory, rather than into the virtual environment
/// or system Python interpreter.
#[arg(long)]
/// or system Python interpreter. The packages will be installed at the top-level of the
/// directory
#[arg(long, conflicts_with = "prefix")]
pub(crate) target: Option<PathBuf>,
/// Install packages into `lib`, `bin`, and other top-level folders under the specified
/// directory, as if a virtual environment were created at the specified location.
///
/// In general, prefer the use of `--python` to install into an alternate environment, as
/// scripts and other artifacts installed via `--prefix` will reference the installing
/// interpreter, rather than any interpreter added to the `--prefix` directory, rendering them
/// non-portable.
#[arg(long, conflicts_with = "target")]
pub(crate) prefix: Option<PathBuf>,
/// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`.
#[arg(long, overrides_with("no_legacy_setup_py"))]
@@ -1300,10 +1322,13 @@ pub(crate) struct PipUninstallArgs {
#[arg(long, overrides_with("break_system_packages"))]
pub(crate) no_break_system_packages: bool,
/// Uninstall packages from the specified directory, rather than from the virtual environment
/// or system Python interpreter.
#[arg(long)]
/// Uninstall packages from the specified `--target` directory.
#[arg(long, conflicts_with = "prefix")]
pub(crate) target: Option<PathBuf>,
/// Uninstall packages from the specified `--prefix` directory.
#[arg(long, conflicts_with = "target")]
pub(crate) prefix: Option<PathBuf>,
}
#[derive(Args)]
+1 -1
View File
@@ -134,7 +134,7 @@ pub(super) async fn compile_bytecode(
let start = std::time::Instant::now();
let mut files = 0;
for site_packages in venv.site_packages() {
files += compile_tree(site_packages, venv.python_executable(), cache.root())
files += compile_tree(&site_packages, venv.python_executable(), cache.root())
.await
.with_context(|| {
format!(
+10 -2
View File
@@ -22,7 +22,7 @@ use uv_dispatch::BuildDispatch;
use uv_fs::Simplified;
use uv_git::GitResolver;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_interpreter::{PythonEnvironment, PythonVersion, SystemPython, Target};
use uv_interpreter::{Prefix, PythonEnvironment, PythonVersion, SystemPython, Target};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, InMemoryIndex, OptionsBuilder, PreReleaseMode,
@@ -68,6 +68,7 @@ pub(crate) async fn pip_install(
system: bool,
break_system_packages: bool,
target: Option<Target>,
prefix: Option<Prefix>,
concurrency: Concurrency,
native_tls: bool,
preview: PreviewMode,
@@ -129,7 +130,7 @@ pub(crate) async fn pip_install(
venv.python_executable().user_display().cyan()
);
// Apply any `--target` directory.
// Apply any `--target` or `--prefix` directories.
let venv = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
@@ -137,6 +138,13 @@ pub(crate) async fn pip_install(
);
target.init()?;
venv.with_target(target)
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
prefix.init()?;
venv.with_prefix(prefix)
} else {
venv
};
+10 -2
View File
@@ -21,7 +21,7 @@ use uv_dispatch::BuildDispatch;
use uv_fs::Simplified;
use uv_git::GitResolver;
use uv_installer::SitePackages;
use uv_interpreter::{PythonEnvironment, PythonVersion, SystemPython, Target};
use uv_interpreter::{Prefix, PythonEnvironment, PythonVersion, SystemPython, Target};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, InMemoryIndex, OptionsBuilder, PreReleaseMode,
@@ -60,6 +60,7 @@ pub(crate) async fn pip_sync(
system: bool,
break_system_packages: bool,
target: Option<Target>,
prefix: Option<Prefix>,
concurrency: Concurrency,
native_tls: bool,
preview: PreviewMode,
@@ -124,7 +125,7 @@ pub(crate) async fn pip_sync(
venv.python_executable().user_display().cyan()
);
// Apply any `--target` directory.
// Apply any `--target` or `--prefix` directories.
let venv = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
@@ -132,6 +133,13 @@ pub(crate) async fn pip_sync(
);
target.init()?;
venv.with_target(target)
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
prefix.init()?;
venv.with_prefix(prefix)
} else {
venv
};
+10 -2
View File
@@ -13,7 +13,7 @@ use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{KeyringProviderType, PreviewMode};
use uv_fs::Simplified;
use uv_interpreter::{PythonEnvironment, SystemPython, Target};
use uv_interpreter::{Prefix, PythonEnvironment, SystemPython, Target};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use crate::commands::{elapsed, ExitStatus};
@@ -27,6 +27,7 @@ pub(crate) async fn pip_uninstall(
system: bool,
break_system_packages: bool,
target: Option<Target>,
prefix: Option<Prefix>,
cache: Cache,
connectivity: Connectivity,
native_tls: bool,
@@ -57,7 +58,7 @@ pub(crate) async fn pip_uninstall(
venv.python_executable().user_display().cyan(),
);
// Apply any `--target` directory.
// Apply any `--target` or `--prefix` directories.
let venv = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
@@ -65,6 +66,13 @@ pub(crate) async fn pip_uninstall(
);
target.init()?;
venv.with_target(target)
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
prefix.init()?;
venv.with_prefix(prefix)
} else {
venv
};
+3
View File
@@ -303,6 +303,7 @@ async fn run() -> Result<ExitStatus> {
args.shared.system,
args.shared.break_system_packages,
args.shared.target,
args.shared.prefix,
args.shared.concurrency,
globals.native_tls,
globals.preview,
@@ -379,6 +380,7 @@ async fn run() -> Result<ExitStatus> {
args.shared.system,
args.shared.break_system_packages,
args.shared.target,
args.shared.prefix,
args.shared.concurrency,
globals.native_tls,
globals.preview,
@@ -413,6 +415,7 @@ async fn run() -> Result<ExitStatus> {
args.shared.system,
args.shared.break_system_packages,
args.shared.target,
args.shared.prefix,
cache,
globals.connectivity,
globals.native_tls,
+10 -2
View File
@@ -15,7 +15,7 @@ use uv_configuration::{
Concurrency, ConfigSettings, ExtrasSpecification, IndexStrategy, KeyringProviderType, NoBinary,
NoBuild, PreviewMode, Reinstall, SetupPyStrategy, TargetTriple, Upgrade,
};
use uv_interpreter::{PythonVersion, Target};
use uv_interpreter::{Prefix, PythonVersion, Target};
use uv_normalize::PackageName;
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
use uv_workspace::{Combine, PipOptions, Workspace};
@@ -539,6 +539,7 @@ impl PipSyncSettings {
break_system_packages,
no_break_system_packages,
target,
prefix,
legacy_setup_py,
no_legacy_setup_py,
no_build_isolation,
@@ -577,6 +578,7 @@ impl PipSyncSettings {
system: flag(system, no_system),
break_system_packages: flag(break_system_packages, no_break_system_packages),
target,
prefix,
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_urls| {
extra_index_urls
@@ -672,6 +674,7 @@ impl PipInstallSettings {
break_system_packages,
no_break_system_packages,
target,
prefix,
legacy_setup_py,
no_legacy_setup_py,
no_build_isolation,
@@ -730,6 +733,7 @@ impl PipInstallSettings {
system: flag(system, no_system),
break_system_packages: flag(break_system_packages, no_break_system_packages),
target,
prefix,
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_urls| {
extra_index_urls
@@ -800,6 +804,7 @@ impl PipUninstallSettings {
break_system_packages,
no_break_system_packages,
target,
prefix,
} = args;
Self {
@@ -814,7 +819,7 @@ impl PipUninstallSettings {
system: flag(system, no_system),
break_system_packages: flag(break_system_packages, no_break_system_packages),
target,
prefix,
keyring_provider,
..PipOptions::default()
},
@@ -1073,6 +1078,7 @@ pub(crate) struct PipSharedSettings {
pub(crate) extras: ExtrasSpecification,
pub(crate) break_system_packages: bool,
pub(crate) target: Option<Target>,
pub(crate) prefix: Option<Prefix>,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) no_binary: NoBinary,
@@ -1113,6 +1119,7 @@ impl PipSharedSettings {
system,
break_system_packages,
target,
prefix,
index_url,
extra_index_url,
no_index,
@@ -1256,6 +1263,7 @@ impl PipSharedSettings {
.combine(break_system_packages)
.unwrap_or_default(),
target: args.target.combine(target).map(Target::from),
prefix: args.prefix.combine(prefix).map(Prefix::from),
no_binary: NoBinary::from_args(args.no_binary.combine(no_binary).unwrap_or_default()),
compile_bytecode: args
.compile_bytecode
+3 -3
View File
@@ -74,7 +74,7 @@ impl TestContext {
.expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
.to_path_buf();
let site_packages = site_packages_path(&venv, format!("python{python_version}"));
let site_packages = site_packages_path(&venv, &format!("python{python_version}"));
let python_version =
PythonVersion::from_str(python_version).expect("Tests must use valid Python versions");
@@ -364,12 +364,12 @@ impl TestContext {
pub fn site_packages(&self) -> PathBuf {
site_packages_path(
&self.venv,
format!("{}{}", self.python_kind(), self.python_version),
&format!("{}{}", self.python_kind(), self.python_version),
)
}
}
fn site_packages_path(venv: &Path, python: String) -> PathBuf {
pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
if cfg!(unix) {
venv.join("lib").join(python).join("site-packages")
} else if cfg!(windows) {
+67 -1
View File
@@ -16,7 +16,9 @@ use url::Url;
use common::{create_venv, uv_snapshot, venv_to_interpreter};
use uv_fs::Simplified;
use crate::common::{copy_dir_all, get_bin, run_and_format, TestContext, EXCLUDE_NEWER};
use crate::common::{
copy_dir_all, get_bin, run_and_format, site_packages_path, TestContext, EXCLUDE_NEWER,
};
mod common;
@@ -5174,6 +5176,70 @@ fn target_no_build_isolation() -> Result<()> {
Ok(())
}
/// Sync to a `--prefix` directory.
#[test]
fn prefix() -> Result<()> {
let context = TestContext::new("3.12");
// Install `iniconfig` to the target directory.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("iniconfig==2.0.0")?;
uv_snapshot!(sync(&context)
.arg("requirements.in")
.arg("--prefix")
.arg("prefix"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Ensure that we can't import the package.
context.assert_command("import iniconfig").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv))
.arg("-B")
.arg("-c")
.arg("import iniconfig")
.env(
"PYTHONPATH",
site_packages_path(&context.temp_dir.join("prefix"), "python3.12"),
)
.current_dir(&context.temp_dir)
.assert()
.success();
// Upgrade it.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("iniconfig==1.1.1")?;
uv_snapshot!(sync(&context)
.arg("requirements.in")
.arg("--prefix")
.arg("prefix"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
- iniconfig==2.0.0
+ iniconfig==1.1.1
"###);
Ok(())
}
/// Ensure that we install packages with markers on them.
#[test]
fn preserve_markers() -> Result<()> {
+6
View File
@@ -501,6 +501,12 @@
"null"
]
},
"prefix": {
"type": [
"string",
"null"
]
},
"prerelease": {
"anyOf": [
{