Skip to content

Commit 8cd5cd6

Browse files
committed
Implement module directory iteration.
1 parent c6807f0 commit 8cd5cd6

8 files changed

Lines changed: 417 additions & 56 deletions

File tree

Cargo.lock

Lines changed: 274 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ asimov-config = { version = "25.1", default-features = false, features = [] }
2121
asimov-core = { version = "25.1", default-features = false, features = [] }
2222
asimov-directory = { version = "25.1", default-features = false, features = ["fs"] }
2323
derive_more = { version = "2", default-features = false, features = ["display"] }
24-
pyo3 = { version = "0.27", default-features = false, features = ["extension-module", "macros"] }
24+
pyo3 = { version = "0.28", default-features = false, features = ["extension-module", "macros"] }
25+
pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }
26+
tokio = { version = "1", default-features = false, features = ["full"] }
2527

2628
[patch.crates-io]
2729
asimov-config = { git = "https://github.com/asimov-platform/asimov.rs.git" }

src/asimov/sdk.pyi

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,20 @@ class ModuleDirectory:
3131
pass
3232
def is_enabled(self, module_name: str) -> bool:
3333
pass
34+
def __iter__(self) -> ModuleNameIterator:
35+
pass
36+
def __aiter__(self) -> ModuleNameIterator:
37+
pass
3438

3539
class ModuleNameIterator:
36-
pass
40+
def __iter__(self) -> ModuleNameIterator:
41+
pass
42+
def __aiter__(self) -> ModuleNameIterator:
43+
pass
44+
def __next__(self) -> str:
45+
pass
46+
async def __anext__(self) -> str:
47+
pass
3748

3849
class ProgramDirectory:
3950
@staticmethod

src/directory/module_directory.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
// This is free and unencumbered software released into the public domain.
22

3+
use super::ModuleNameIterator;
4+
use crate::util::wait_for_future;
35
use asimov_directory::{ModuleDirectory as _, fs::ModuleDirectory as FsModuleDirectory};
46
use pyo3::{exceptions::PyRuntimeError, prelude::*, types::PyString};
7+
use std::sync::Arc;
58

69
/// A module directory stored on a file system (e.g., `$HOME/.asimov/modules/`).
710
#[pyclass(frozen, module = "asimov", str = "{0}")]
8-
pub struct ModuleDirectory(pub(crate) FsModuleDirectory);
11+
pub struct ModuleDirectory(pub(crate) Arc<FsModuleDirectory>);
12+
13+
impl From<FsModuleDirectory> for ModuleDirectory {
14+
fn from(input: FsModuleDirectory) -> Self {
15+
Self(Arc::new(input))
16+
}
17+
}
918

1019
#[pymethods]
1120
impl ModuleDirectory {
@@ -19,7 +28,7 @@ impl ModuleDirectory {
1928
"Failed to open module directory", // TODO
2029
))?
2130
};
22-
Ok(Self(result))
31+
Ok(Self::from(result))
2332
}
2433

2534
#[new]
@@ -29,7 +38,7 @@ impl ModuleDirectory {
2938
"Failed to open module directory", // TODO
3039
))?
3140
};
32-
Ok(Self(result))
41+
Ok(Self::from(result))
3342
}
3443

3544
pub fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
@@ -46,4 +55,18 @@ impl ModuleDirectory {
4655
pub fn is_enabled(&self, module_name: String) -> bool {
4756
self.0.is_enabled(module_name)
4857
}
58+
59+
/// Returns an iterator over the installed modules.
60+
fn __iter__(&self, py: Python) -> PyResult<ModuleNameIterator> {
61+
self.__aiter__(py)
62+
}
63+
64+
/// Returns an iterator over the installed modules.
65+
fn __aiter__(&self, py: Python) -> PyResult<ModuleNameIterator> {
66+
let inner = Arc::clone(&self.0);
67+
wait_for_future(py, async move {
68+
let it = inner.iter_installed().await.unwrap();
69+
ModuleNameIterator::from(it)
70+
})
71+
}
4972
}

src/directory/module_iterators.rs

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,59 @@
11
// This is free and unencumbered software released into the public domain.
22

3-
use asimov_directory::fs::ModuleDirectory as FsModuleDirectory;
4-
use pyo3::prelude::*;
3+
use crate::util::wait_for_future;
4+
use asimov_directory::{ModuleNameIterator as _, fs::ModuleNameIterator as FsModuleNameIterator};
5+
use pyo3::{
6+
exceptions::{PyStopAsyncIteration, PyStopIteration},
7+
prelude::*,
8+
};
9+
use std::sync::Arc;
10+
use tokio::sync::Mutex;
511

6-
/// TODO
7-
#[pyclass(frozen, module = "asimov", str = "{0}")]
8-
pub struct ModuleNameIterator(pub(crate) FsModuleDirectory);
12+
/// An iterator over module names in the module directory.
13+
#[pyclass(frozen, module = "asimov", str = "ModuleNameIterator")]
14+
pub struct ModuleNameIterator(pub(crate) Arc<Mutex<FsModuleNameIterator>>);
15+
16+
impl From<FsModuleNameIterator> for ModuleNameIterator {
17+
fn from(input: FsModuleNameIterator) -> Self {
18+
Self(Arc::new(Mutex::new(input)))
19+
}
20+
}
921

1022
#[pymethods]
11-
impl ModuleNameIterator {}
23+
impl ModuleNameIterator {
24+
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
25+
slf
26+
}
27+
28+
fn __aiter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
29+
slf
30+
}
31+
32+
fn __next__(&self, py: Python) -> PyResult<String> {
33+
let inner = Arc::clone(&self.0);
34+
let next = async move {
35+
let mut lock = inner.lock().await;
36+
let item = lock.next().await;
37+
drop(lock);
38+
match item {
39+
Some(name) => Ok(name),
40+
None => Err(PyStopIteration::new_err("")),
41+
}
42+
};
43+
wait_for_future(py, next).flatten()
44+
}
45+
46+
fn __anext__<'py>(&'py self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
47+
let inner = Arc::clone(&self.0);
48+
let anext = async move {
49+
let mut lock = inner.lock().await;
50+
let item = lock.next().await;
51+
drop(lock);
52+
match item {
53+
Some(name) => Ok(name),
54+
None => Err(PyStopAsyncIteration::new_err("")),
55+
}
56+
};
57+
pyo3_async_runtimes::tokio::future_into_py(py, anext)
58+
}
59+
}

src/directory/state_directory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl StateDirectory {
7070
"Failed to open module directory", // TODO
7171
))?
7272
};
73-
Ok(ModuleDirectory(result))
73+
Ok(ModuleDirectory::from(result))
7474
}
7575

7676
/// Opens the program directory under this state directory.

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ mod sdk {
3434

3535
mod directory;
3636
use directory::*;
37+
38+
mod util;

src/util.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// This is free and unencumbered software released into the public domain.
2+
3+
use pyo3::{PyResult, Python};
4+
use std::{sync::OnceLock, time::Duration};
5+
use tokio::runtime::Runtime;
6+
7+
pub fn runtime() -> &'static Runtime {
8+
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
9+
static RUNTIME_PID: OnceLock<u32> = OnceLock::new();
10+
let current_pid = std::process::id();
11+
let runtime_pid = *RUNTIME_PID.get_or_init(|| current_pid);
12+
if current_pid != runtime_pid {
13+
panic!("Forked process detected, bailing out."); // TODO
14+
}
15+
RUNTIME.get_or_init(|| Runtime::new().expect("should create the Tokio runtime"))
16+
}
17+
18+
pub fn wait_for_future<F>(py: Python, fut: F) -> PyResult<F::Output>
19+
where
20+
F: Future + Send,
21+
F::Output: Send,
22+
{
23+
let runtime: &Runtime = &runtime();
24+
const INTERVAL_CHECK_SIGNALS: Duration = Duration::from_millis(500);
25+
26+
py.run(cr"pass", None, None)?;
27+
py.check_signals()?;
28+
29+
py.detach(|| {
30+
runtime.block_on(async {
31+
tokio::pin!(fut);
32+
loop {
33+
tokio::select! {
34+
res = &mut fut => break Ok(res),
35+
_ = tokio::time::sleep(INTERVAL_CHECK_SIGNALS) => {
36+
Python::attach(|py| {
37+
py.run(cr"pass", None, None)?;
38+
py.check_signals()
39+
})?;
40+
}
41+
}
42+
}
43+
})
44+
})
45+
}

0 commit comments

Comments
 (0)