Add mpd() query functionality
This commit is contained in:
parent
34487c02eb
commit
3b756bf0ad
13 changed files with 378 additions and 1 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -212,6 +212,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bufstream"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.9.1"
|
||||
|
@ -1182,6 +1188,17 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpd"
|
||||
version = "0.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57a20784da57fa01bf7910a5da686d9f39ff37feaa774856b71f050e4331bf82"
|
||||
dependencies = [
|
||||
"bufstream",
|
||||
"rustc-serialize",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpris-player"
|
||||
version = "0.6.1"
|
||||
|
@ -1206,6 +1223,7 @@ dependencies = [
|
|||
"bliss-audio-symphonia",
|
||||
"criterion",
|
||||
"dirs",
|
||||
"mpd",
|
||||
"rand",
|
||||
"regex 1.5.5",
|
||||
"rusqlite",
|
||||
|
@ -1906,6 +1924,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
|
|
|
@ -16,6 +16,7 @@ regex = { version = "1" }
|
|||
rand = { version = "0.8" }
|
||||
shellexpand = { version = "2.1", optional = true }
|
||||
bliss-audio-symphonia = { version = "0.4", optional = true, path = "../bliss-rs" }
|
||||
mpd = { version = "0.0.12", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3"
|
||||
|
@ -26,6 +27,6 @@ harness = false
|
|||
|
||||
[features]
|
||||
default = [ "music_library", "ergonomics", "advanced" ]
|
||||
music_library = [ "symphonia" ] # song metadata parsing and database auto-population
|
||||
music_library = [ "symphonia", "mpd" ] # song metadata parsing and database auto-population
|
||||
ergonomics = ["shellexpand"] # niceties like ~ in pathes
|
||||
advanced = ["bliss-audio-symphonia"] # advanced language features like bliss playlist generation
|
||||
|
|
|
@ -91,6 +91,10 @@ Replace items matching the filter with operation1 and replace items not matching
|
|||
|
||||
Keep only items which are do not duplicate another item, or keep only items whoes specified field does not duplicate another item's same field. The first non-duplicated instance of an item is always the one that is kept.
|
||||
|
||||
#### ?? -- e.g. `iterable.(??);`
|
||||
|
||||
Keep only the items that contain at least one field (not including the filename field).
|
||||
|
||||
### Functions
|
||||
Similar to most other languages: `function_name(param1, param2, etc.);`.
|
||||
These always return an iterable which can be manipulated with other syntax (filters, sorters, etc.).
|
||||
|
@ -131,6 +135,10 @@ Repeat the iterable count times, or infinite times if count is omitted.
|
|||
|
||||
Retrieve all files from a folder, matching a regex pattern.
|
||||
|
||||
#### mpd(address, term = value, term2 = value2, ...);
|
||||
|
||||
Retrieve songs from a music player daemon at `address`. If compiled without the `music_library` feature, this is equivalent to `empty()`.
|
||||
|
||||
#### reset(iterable);
|
||||
|
||||
Explicitly reset an iterable. This useful for reusing an iterable variable.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#[cfg(feature = "advanced")]
|
||||
use super::processing::advanced::{MpsDefaultAnalyzer, MpsMusicAnalyzer};
|
||||
use super::processing::database::{MpsDatabaseQuerier, MpsSQLiteExecutor};
|
||||
#[cfg(feature = "mpd")]
|
||||
use super::processing::database::{MpsMpdQuerier, MpsMpdExecutor};
|
||||
use super::processing::general::{
|
||||
MpsFilesystemExecutor, MpsFilesystemQuerier, MpsOpStorage, MpsVariableStorer,
|
||||
};
|
||||
|
@ -13,6 +15,8 @@ pub struct MpsContext {
|
|||
pub filesystem: Box<dyn MpsFilesystemQuerier>,
|
||||
#[cfg(feature = "advanced")]
|
||||
pub analysis: Box<dyn MpsMusicAnalyzer>,
|
||||
#[cfg(feature = "mpd")]
|
||||
pub mpd_database: Box<dyn MpsMpdQuerier>,
|
||||
}
|
||||
|
||||
impl Default for MpsContext {
|
||||
|
@ -23,6 +27,8 @@ impl Default for MpsContext {
|
|||
filesystem: Box::new(MpsFilesystemExecutor::default()),
|
||||
#[cfg(feature = "advanced")]
|
||||
analysis: Box::new(MpsDefaultAnalyzer::default()),
|
||||
#[cfg(feature = "mpd")]
|
||||
mpd_database: Box::new(MpsMpdExecutor::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
|
|||
// functions don't enforce bracket coherence
|
||||
// -- function().() is valid despite the ).( in between brackets
|
||||
.add(crate::lang::vocabulary::sql_function_factory())
|
||||
.add(crate::lang::vocabulary::mpd_query_function_factory())
|
||||
.add(crate::lang::vocabulary::simple_sql_function_factory())
|
||||
.add(crate::lang::vocabulary::repeat_function_factory())
|
||||
.add(crate::lang::vocabulary::AssignStatementFactory)
|
||||
|
|
|
@ -29,6 +29,11 @@ impl MpsItem {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn set_field_chain2(mut self, name: &str, value: MpsTypePrimitive) -> Self {
|
||||
self.set_field(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, name: &str) -> Option<MpsTypePrimitive> {
|
||||
self.fields.remove(name)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ mod empties;
|
|||
pub(crate) mod empty;
|
||||
mod files;
|
||||
mod intersection;
|
||||
mod mpd_query;
|
||||
mod repeat;
|
||||
mod reset;
|
||||
mod sql_init;
|
||||
|
@ -14,6 +15,7 @@ pub use empties::{empties_function_factory, EmptiesStatementFactory};
|
|||
pub use empty::{empty_function_factory, EmptyStatementFactory};
|
||||
pub use files::{files_function_factory, FilesStatementFactory};
|
||||
pub use intersection::{intersection_function_factory, IntersectionStatementFactory};
|
||||
pub use mpd_query::{mpd_query_function_factory, MpdQueryStatementFactory};
|
||||
pub use repeat::{repeat_function_factory, RepeatStatementFactory};
|
||||
pub use reset::{reset_function_factory, ResetStatementFactory};
|
||||
pub use sql_init::{sql_init_function_factory, SqlInitStatementFactory};
|
||||
|
|
193
mps-interpreter/src/lang/vocabulary/mpd_query.rs
Normal file
193
mps-interpreter/src/lang/vocabulary/mpd_query.rs
Normal file
|
@ -0,0 +1,193 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::fmt::{Debug, Display, Error, Formatter};
|
||||
use std::iter::Iterator;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::tokens::MpsToken;
|
||||
use crate::MpsContext;
|
||||
|
||||
use crate::lang::{MpsLanguageDictionary, repeated_tokens, Lookup};
|
||||
use crate::lang::{MpsFunctionFactory, MpsFunctionStatementFactory, MpsIteratorItem, MpsOp, PseudoOp};
|
||||
use crate::lang::{RuntimeError, SyntaxError, RuntimeOp};
|
||||
use crate::lang::utility::{assert_token, assert_token_raw};
|
||||
use crate::lang::MpsTypePrimitive;
|
||||
use crate::processing::general::MpsType;
|
||||
use crate::MpsItem;
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
#[derive(Debug)]
|
||||
pub struct MpdQueryStatement {
|
||||
context: Option<MpsContext>,
|
||||
addr: Lookup,
|
||||
params: Vec<(String, Lookup)>,
|
||||
results: Option<VecDeque<MpsItem>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
impl Display for MpdQueryStatement {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||
write!(f, "empty()")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
impl std::clone::Clone for MpdQueryStatement {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
context: None,
|
||||
addr: self.addr.clone(),
|
||||
params: self.params.clone(),
|
||||
results: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
impl Iterator for MpdQueryStatement {
|
||||
type Item = MpsIteratorItem;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
//let ctx = self.context.as_mut().unwrap();
|
||||
if self.results.is_none() {
|
||||
self.results = Some(VecDeque::with_capacity(0)); // in case of failure
|
||||
// build address
|
||||
let addr_str = match self.addr.get(self.context.as_mut().unwrap()) {
|
||||
Ok(MpsType::Primitive(a)) => a.as_str(),
|
||||
Ok(x) => return Some(Err(
|
||||
RuntimeError {
|
||||
line: 0,
|
||||
msg: format!("Cannot use non-primitive `{}` as IP address", x),
|
||||
op: PseudoOp::from_printable(self),
|
||||
}
|
||||
)),
|
||||
Err(e) => return Some(Err(e.with(RuntimeOp(PseudoOp::from_printable(self))))),
|
||||
};
|
||||
let addr: SocketAddr = match addr_str.parse() {
|
||||
Ok(a) => a,
|
||||
Err(e) => return Some(Err(RuntimeError {
|
||||
line: 0,
|
||||
op: PseudoOp::from_printable(self),
|
||||
msg: format!("Cannot convert `{}` to IP Address: {}", addr_str, e),
|
||||
}))
|
||||
};
|
||||
// build params
|
||||
let mut new_params = Vec::<(&str, String)>::with_capacity(self.params.len());
|
||||
for (term, value) in self.params.iter() {
|
||||
let static_val = match value.get(self.context.as_mut().unwrap()) {
|
||||
Ok(MpsType::Primitive(a)) => a.as_str(),
|
||||
Ok(x) => return Some(Err(
|
||||
RuntimeError {
|
||||
line: 0,
|
||||
msg: format!("Cannot use non-primitive `{}` MPS query value", x),
|
||||
op: PseudoOp::from_printable(self),
|
||||
}
|
||||
)),
|
||||
Err(e) => return Some(Err(e.with(RuntimeOp(PseudoOp::from_printable(self))))),
|
||||
};
|
||||
new_params.push((term, static_val));
|
||||
}
|
||||
self.results = Some(match self.context.as_mut().unwrap().mpd_database.one_shot_search(addr, new_params) {
|
||||
Ok(items) => items,
|
||||
Err(e) => return Some(Err(e.with(RuntimeOp(PseudoOp::from_printable(self)))))
|
||||
});
|
||||
}
|
||||
let results = self.results.as_mut().unwrap();
|
||||
results.pop_front().map(|x| Ok(x))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, Some(0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
impl MpsOp for MpdQueryStatement {
|
||||
fn enter(&mut self, ctx: MpsContext) {
|
||||
self.context = Some(ctx)
|
||||
}
|
||||
|
||||
fn escape(&mut self) -> MpsContext {
|
||||
self.context.take().unwrap()
|
||||
}
|
||||
|
||||
fn is_resetable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> Result<(), RuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dup(&self) -> Box<dyn MpsOp> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
pub struct MpdQueryFunctionFactory;
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
impl MpsFunctionFactory<MpdQueryStatement> for MpdQueryFunctionFactory {
|
||||
fn is_function(&self, name: &str) -> bool {
|
||||
name == "mpd"
|
||||
}
|
||||
|
||||
fn build_function_params(
|
||||
&self,
|
||||
_name: String,
|
||||
tokens: &mut VecDeque<MpsToken>,
|
||||
_dict: &MpsLanguageDictionary,
|
||||
) -> Result<MpdQueryStatement, SyntaxError> {
|
||||
// mpd(address, term = value, ...)
|
||||
let addr_lookup = Lookup::parse(tokens)?;
|
||||
if tokens.is_empty() {
|
||||
Ok(MpdQueryStatement {
|
||||
context: None,
|
||||
addr: addr_lookup,
|
||||
params: vec![("any".to_string(), Lookup::Static(MpsType::Primitive(MpsTypePrimitive::String("".to_owned()))))],
|
||||
results: None,
|
||||
})
|
||||
} else {
|
||||
assert_token_raw(MpsToken::Comma, tokens)?;
|
||||
let keyword_params = repeated_tokens(
|
||||
|tokens| {
|
||||
let term = assert_token(
|
||||
|t| match t {
|
||||
MpsToken::Name(n) => Some(n),
|
||||
_ => None,
|
||||
},
|
||||
MpsToken::Name("term".to_string()),
|
||||
tokens)?;
|
||||
assert_token_raw(MpsToken::Equals, tokens)?;
|
||||
let val = Lookup::parse(tokens)?;
|
||||
Ok(Some((term, val)))
|
||||
},
|
||||
MpsToken::Comma
|
||||
).ingest_all(tokens)?;
|
||||
Ok(MpdQueryStatement {
|
||||
context: None,
|
||||
addr: addr_lookup,
|
||||
params: keyword_params,
|
||||
results: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
pub type MpdQueryStatementFactory = MpsFunctionStatementFactory<MpdQueryStatement, MpdQueryFunctionFactory>;
|
||||
|
||||
#[cfg(feature = "mpd")]
|
||||
#[inline(always)]
|
||||
pub fn mpd_query_function_factory() -> MpdQueryStatementFactory {
|
||||
MpdQueryStatementFactory::new(MpdQueryFunctionFactory)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mpd"))]
|
||||
pub type MpdQueryStatementFactory = super::EmptyStatementFactory;
|
||||
|
||||
#[cfg(not(feature = "mpd"))]
|
||||
#[inline(always)]
|
||||
pub fn mpd_query_function_factory() -> MpdQueryStatementFactory {
|
||||
super::empty_function_factory()
|
||||
}
|
|
@ -133,6 +133,10 @@
|
|||
//!
|
||||
//! Retrieve all files from a folder, matching a regex pattern.
|
||||
//!
|
||||
//! ### mpd(address, term = value, term2 = value2, ...);
|
||||
//!
|
||||
//! Retrieve songs from a music player daemon at `address`. If compiled without the `music_library` feature, this is equivalent to `empty()`.
|
||||
//!
|
||||
//! ### reset(iterable);
|
||||
//!
|
||||
//! Explicitly reset an iterable. This useful for reusing an iterable variable.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
mod filesystem;
|
||||
#[cfg(feature = "advanced")]
|
||||
mod music_analysis;
|
||||
#[cfg(feature = "mpd")]
|
||||
mod mpd;
|
||||
mod sql;
|
||||
mod variables;
|
||||
|
||||
|
@ -8,6 +10,8 @@ mod variables;
|
|||
|
||||
pub mod database {
|
||||
pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryResult};
|
||||
#[cfg(feature = "mpd")]
|
||||
pub use super::mpd::{MpsMpdQuerier, MpsMpdExecutor};
|
||||
}
|
||||
|
||||
pub mod general {
|
||||
|
|
106
mps-interpreter/src/processing/mpd.rs
Normal file
106
mps-interpreter/src/processing/mpd.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use core::fmt::Debug;
|
||||
use std::collections::VecDeque;
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::iter::Iterator;
|
||||
|
||||
use mpd::Client;
|
||||
use mpd::{Query, Term, Song};
|
||||
|
||||
use crate::lang::RuntimeMsg;
|
||||
use crate::MpsItem;
|
||||
use crate::lang::MpsTypePrimitive;
|
||||
|
||||
/// Music Player Daemon interface for interacting with it's database
|
||||
pub trait MpsMpdQuerier: Debug {
|
||||
fn connect(&mut self, addr: SocketAddr) -> Result<(), RuntimeMsg>;
|
||||
|
||||
fn search(&mut self, params: Vec<(&str, String)>) -> Result<VecDeque<MpsItem>, RuntimeMsg>;
|
||||
|
||||
fn one_shot_search(&self, addr: SocketAddr, params: Vec<(&str, String)>) -> Result<VecDeque<MpsItem>, RuntimeMsg>;
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MpsMpdExecutor {
|
||||
connection: Option<Client<TcpStream>>,
|
||||
}
|
||||
|
||||
impl MpsMpdQuerier for MpsMpdExecutor {
|
||||
fn connect(&mut self, addr: SocketAddr) -> Result<(), RuntimeMsg> {
|
||||
self.connection = Some(Client::connect(addr).map_err(|e| RuntimeMsg(format!("MPD connection error: {}", e)))?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn search(&mut self, params: Vec<(&str, String)>) -> Result<VecDeque<MpsItem>, RuntimeMsg> {
|
||||
if self.connection.is_none() {
|
||||
return Err(RuntimeMsg("MPD not connected".to_string()));
|
||||
}
|
||||
//let music_dir = self.connection.as_mut().unwrap().music_directory().map_err(|e| RuntimeMsg(format!("MPD command error: {}", e)))?;
|
||||
let mut query = Query::new();
|
||||
let mut query_mut = &mut query;
|
||||
for (term, value) in params {
|
||||
query_mut = query_mut.and(str_to_term(term), value);
|
||||
}
|
||||
let songs = self.connection.as_mut().unwrap().search(query_mut, None).map_err(|e| RuntimeMsg(format!("MPD search error: {}", e)))?;
|
||||
Ok(songs.into_iter().map(|x| song_to_item(x)).collect())
|
||||
}
|
||||
|
||||
fn one_shot_search(&self, addr: SocketAddr, params: Vec<(&str, String)>) -> Result<VecDeque<MpsItem>, RuntimeMsg> {
|
||||
let mut connection = Client::connect(addr).map_err(|e| RuntimeMsg(format!("MPD connection error: {}", e)))?;
|
||||
//let music_dir = connection.music_directory().map_err(|e| RuntimeMsg(format!("MPD command error: {}", e)))?;
|
||||
let mut query = Query::new();
|
||||
let mut query_mut = &mut query;
|
||||
for (term, value) in params {
|
||||
query_mut = query_mut.and(str_to_term(term), value);
|
||||
}
|
||||
let songs = connection.search(query_mut, None).map_err(|e| RuntimeMsg(format!("MPD search error: {}", e)))?;
|
||||
Ok(songs.into_iter().map(|x| song_to_item(x)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn song_to_item(song: Song) -> MpsItem {
|
||||
let mut item = MpsItem::new();
|
||||
//item.set_field("filename", format!("{}{}{}", root_dir, std::path::MAIN_SEPARATOR, song.file).into());
|
||||
item.set_field("filename", format!("mpd://{}", song.file).into());
|
||||
if let Some(name) = song.name {
|
||||
item.set_field("name", name.into());
|
||||
}
|
||||
if let Some(title) = song.title {
|
||||
item.set_field("title", title.into());
|
||||
}
|
||||
|
||||
/*
|
||||
if let Some(last_mod) = song.last_mod {
|
||||
item.set_field("last_modified", last_modified.into());
|
||||
}
|
||||
*/
|
||||
|
||||
if let Some(dur) = song.duration {
|
||||
item.set_field("duration", dur.num_seconds().into());
|
||||
}
|
||||
if let Some(place) = song.place {
|
||||
item.set_field("tracknumber", (place.pos as u64).into());
|
||||
}
|
||||
|
||||
/*
|
||||
if let Some(range) = song.range {
|
||||
item.set_field("range", range.into());
|
||||
}
|
||||
*/
|
||||
|
||||
for (tag, value) in song.tags {
|
||||
item.set_field(&tag, MpsTypePrimitive::parse(value));
|
||||
}
|
||||
item
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn str_to_term<'a>(s: &'a str) -> Term<'a> {
|
||||
match s {
|
||||
"any" => Term::Any,
|
||||
"file" => Term::File,
|
||||
"base" => Term::Base,
|
||||
"lastmod" => Term::LastMod,
|
||||
x => Term::Tag(x.into())
|
||||
}
|
||||
}
|
|
@ -829,3 +829,22 @@ fn execute_nonemptyfilter_line() -> Result<(), MpsError> {
|
|||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_mpdfunction_line() -> Result<(), MpsError> {
|
||||
execute_single_line(
|
||||
"mpd(`127.0.0.1:6600`, artist=`Bruno Mars`)",
|
||||
false,
|
||||
true,
|
||||
)?;
|
||||
execute_single_line(
|
||||
"mpd(`127.0.0.1:6600`, title=`something very long that should match absolutely nothing, probably, hopefully...`)",
|
||||
true,
|
||||
true,
|
||||
)?;
|
||||
execute_single_line(
|
||||
"mpd(`127.0.0.1:6600`)",
|
||||
false,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,6 +33,10 @@ These always return an iterable which can be manipulated.
|
|||
files(folder = `path/to/music`, recursive = true|false, regex = `pattern`)
|
||||
Retrieve all files from a folder, matching a regex pattern.
|
||||
|
||||
mpd(address, term = value, term2 = value2, ...);
|
||||
|
||||
Retrieve songs from a music player daemon at address. If compiled without the music_library feature, this is equivalent to the empty() function.
|
||||
|
||||
reset(iterable)
|
||||
Explicitly reset an iterable. This useful for reusing an iterable variable.
|
||||
|
||||
|
|
Loading…
Reference in a new issue