Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ pyo3-ffi = { path = "pyo3-ffi", version = "=0.27.1" }

# support crates for macros feature
pyo3-macros = { path = "pyo3-macros", version = "=0.27.1", optional = true }
indoc = { version = "2.0.1", optional = true }
unindent = { version = "0.2.1", optional = true }

# support crate for multiple-pymethods feature
inventory = { version = "0.3.5", optional = true }
Expand Down Expand Up @@ -98,7 +96,7 @@ experimental-async = ["macros", "pyo3-macros/experimental-async"]
experimental-inspect = ["pyo3-macros/experimental-inspect"]

# Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc.
macros = ["pyo3-macros", "indoc", "unindent"]
macros = ["pyo3-macros"]

# Enables multiple #[pymethods] per #[pyclass]
multiple-pymethods = ["inventory", "pyo3-macros/multiple-pymethods"]
Expand Down
1 change: 1 addition & 0 deletions newsfragments/5608.packaging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Drop `indoc` and `unindent` dependencies.
17 changes: 8 additions & 9 deletions src/conversions/num_bigint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,6 @@ mod tests {
use crate::exceptions::PyTypeError;
use crate::test_utils::generate_unique_module_name;
use crate::types::{PyAnyMethods as _, PyDict, PyModule};
use indoc::indoc;
use pyo3_ffi::c_str;

fn rust_fib<T>() -> impl Iterator<Item = T>
Expand Down Expand Up @@ -390,15 +389,15 @@ mod tests {
}

fn python_index_class(py: Python<'_>) -> Bound<'_, PyModule> {
let index_code = c_str!(indoc!(
let index_code = c_str!(
r#"
class C:
def __init__(self, x):
self.x = x
def __index__(self):
return self.x
"#
));
class C:
def __init__(self, x):
self.x = x
def __index__(self):
return self.x
"#
);
PyModule::from_code(
py,
index_code,
Expand Down
1 change: 1 addition & 0 deletions src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ pub mod pymethods;
pub mod pymodule;
#[doc(hidden)]
pub mod trampoline;
pub mod unindent;
pub mod wrap;
2 changes: 1 addition & 1 deletion src/impl_/concat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub const fn combine_to_array<const LEN: usize>(pieces: &[&[u8]]) -> [u8; LEN] {
}

/// Replacement for `slice::copy_from_slice`, which is const from 1.87
const fn slice_copy_from_slice(out: &mut [u8], src: &[u8]) {
pub(crate) const fn slice_copy_from_slice(out: &mut [u8], src: &[u8]) {
let mut i = 0;
while i < src.len() {
out[i] = src[i];
Expand Down
229 changes: 229 additions & 0 deletions src/impl_/unindent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
use crate::impl_::concat::slice_copy_from_slice;

/// This is a reimplementation of the `indoc` crate's unindent functionality:
///
/// 1. Count the leading spaces of each line, ignoring the first line and any lines that are empty or contain spaces only.
/// 2. Take the minimum.
/// 3. If the first line is empty i.e. the string begins with a newline, remove the first line.
/// 4. Remove the computed number of spaces from the beginning of each line.
const fn unindent_bytes(bytes: &mut [u8]) -> usize {
if bytes.is_empty() {
// nothing to do
return bytes.len();
}

// scan for leading spaces (ignoring first line and empty lines)
let mut i = 0;

// skip first line
i = advance_to_next_line(bytes, i);

let mut to_unindent = usize::MAX;

// for remaining lines, count leading spaces
'lines: while i < bytes.len() {
let line_leading_spaces = count_spaces(bytes, i);
i += line_leading_spaces;

// line only had spaces, ignore for the count
if let Some(eol) = consume_eol(bytes, i) {
i = eol;
continue 'lines;
}

// this line has content, consider its leading spaces
if line_leading_spaces < to_unindent {
to_unindent = line_leading_spaces;
}

i = advance_to_next_line(bytes, i);
}

if to_unindent == usize::MAX {
// all lines were empty, nothing to unindent
return bytes.len();
}

// now copy from the original buffer, bringing values forward as needed
let mut read_idx = 0;
let mut write_idx = 0;

match consume_eol(bytes, read_idx) {
// skip empty first line
Some(eol) => read_idx = eol,
// copy non-empty first line as-is
None => {
while read_idx < bytes.len() {
let value = bytes[read_idx];
bytes[write_idx] = value;
read_idx += 1;
write_idx += 1;
if value == b'\n' {
break;
}
}
}
};

while read_idx < bytes.len() {
let mut leading_spaces_skipped = 0;
while leading_spaces_skipped < to_unindent
&& read_idx < bytes.len()
&& bytes[read_idx] == b' '
{
leading_spaces_skipped += 1;
read_idx += 1;
}

assert!(
leading_spaces_skipped == to_unindent || consume_eol(bytes, read_idx).is_some(),
"removed fewer spaces than expected on non-empty line"
);

// copy remainder of line
while read_idx < bytes.len() {
let value = bytes[read_idx];
bytes[write_idx] = value;
read_idx += 1;
write_idx += 1;
if value == b'\n' {
break;
}
}
}

write_idx
}

const fn advance_to_next_line(bytes: &[u8], mut i: usize) -> usize {
while i < bytes.len() {
if let Some(eol) = consume_eol(bytes, i) {
return eol;
}
i += 1;
}
i
}

const fn count_spaces(bytes: &[u8], mut i: usize) -> usize {
let mut count = 0;
while i < bytes.len() && bytes[i] == b' ' {
count += 1;
i += 1;
}
count
}

const fn consume_eol(bytes: &[u8], i: usize) -> Option<usize> {
if bytes.len() == i {
// special case: treat end of buffer as EOL without consuming anything
Some(i)
} else if bytes.len() > i && bytes[i] == b'\n' {
Some(i + 1)
} else if bytes[i] == b'\r' && bytes.len() > i + 1 && bytes[i + 1] == b'\n' {
Some(i + 2)
} else {
None
}
}

pub const fn unindent_sized<const N: usize>(src: &[u8]) -> ([u8; N], usize) {
let mut out: [u8; N] = [0; N];
slice_copy_from_slice(&mut out, src);
let new_len = unindent_bytes(&mut out);
(out, new_len)
}

/// Helper for `py_run!` macro which unindents a string at compile time.
#[macro_export]
#[doc(hidden)]
macro_rules! unindent {
($value:expr) => {{
const RAW: &str = $value;
const LEN: usize = RAW.len();
const UNINDENTED: ([u8; LEN], usize) =
$crate::impl_::unindent::unindent_sized::<LEN>(RAW.as_bytes());
// SAFETY: this removes only spaces and preserves all other contents
unsafe { ::core::str::from_utf8_unchecked(UNINDENTED.0.split_at(UNINDENTED.1).0) }
}};
}

pub use crate::unindent;

/// Equivalent of the `unindent!` macro, but works at runtime.
pub fn unindent(s: &str) -> String {
let mut bytes = s.as_bytes().to_owned();
let unindented_size = unindent_bytes(&mut bytes);
bytes.resize(unindented_size, 0);
String::from_utf8(bytes).unwrap()
}

#[cfg(test)]
mod tests {
use super::*;

const SAMPLE_1_WITH_FIRST_LINE: &str = " first line
line one

line two
";

const UNINDENTED_1: &str = " first line\nline one\n\n line two\n";

const SAMPLE_2_EMPTY_FIRST_LINE: &str = "
line one

line two
";
const UNINDENTED_2: &str = "line one\n\n line two\n";

const SAMPLE_3_NO_INDENT: &str = "
no indent
here";

const UNINDENTED_3: &str = "no indent\n here";

const ALL_CASES: &[(&str, &str)] = &[
(SAMPLE_1_WITH_FIRST_LINE, UNINDENTED_1),
(SAMPLE_2_EMPTY_FIRST_LINE, UNINDENTED_2),
(SAMPLE_3_NO_INDENT, UNINDENTED_3),
];

// run const tests for each sample to ensure they work at compile time

#[test]
fn test_unindent_const() {
const UNINDENTED: &str = unindent!(SAMPLE_1_WITH_FIRST_LINE);
assert_eq!(UNINDENTED, UNINDENTED_1);
}

#[test]
fn test_unindent_const_removes_empty_first_line() {
const UNINDENTED: &str = unindent!(SAMPLE_2_EMPTY_FIRST_LINE);
assert_eq!(UNINDENTED, UNINDENTED_2);
}

#[test]
fn test_unindent_const_no_indent() {
const UNINDENTED: &str = unindent!(SAMPLE_3_NO_INDENT);
assert_eq!(UNINDENTED, UNINDENTED_3);
}

#[test]
fn test_unindent_macro_runtime() {
// this variation on the test ensures full coverage (const eval not included in coverage)
const INDENTED: &str = SAMPLE_1_WITH_FIRST_LINE;
const LEN: usize = INDENTED.len();
let (unindented, unindented_size) = unindent_sized::<LEN>(INDENTED.as_bytes());
let unindented = std::str::from_utf8(&unindented[..unindented_size]).unwrap();
assert_eq!(unindented, UNINDENTED_1);
}

#[test]
fn test_unindent_function() {
for (indented, expected) in ALL_CASES {
let unindented = unindent(indented);
assert_eq!(&unindented, expected);
}
}
}
7 changes: 0 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,6 @@ pub mod class {
}
}

#[cfg(feature = "macros")]
#[doc(hidden)]
pub use {
indoc, // Re-exported for py_run
unindent, // Re-exported for py_run
};

#[cfg(all(feature = "macros", feature = "multiple-pymethods"))]
#[doc(hidden)]
pub use inventory; // Re-exported for `#[pyclass]` and `#[pymethods]` with `multiple-pymethods`.
Expand Down
19 changes: 13 additions & 6 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,27 @@
/// ```
#[macro_export]
macro_rules! py_run {
// unindent the code at compile time
($py:expr, $($val:ident)+, $code:literal) => {{
$crate::py_run_impl!($py, $($val)+, $crate::indoc::indoc!($code))
}};
($py:expr, $($val:ident)+, $code:expr) => {{
$crate::py_run_impl!($py, $($val)+, $crate::unindent::unindent($code))
$crate::py_run_impl!($py, $($val)+, $crate::impl_::unindent::unindent!($code))
}};
($py:expr, *$dict:expr, $code:literal) => {{
$crate::py_run_impl!($py, *$dict, $crate::indoc::indoc!($code))
$crate::py_run_impl!($py, *$dict, $crate::impl_::unindent::unindent!($code))
}};
// unindent the code at runtime
($py:expr, $($val:ident)+, $code:expr) => {{
$crate::py_run_impl!($py, $($val)+, $crate::impl_::unindent::unindent($code))
}};
($py:expr, *$dict:expr, $code:expr) => {{
$crate::py_run_impl!($py, *$dict, $crate::unindent::unindent($code))
$crate::py_run_impl!($py, *$dict, $crate::impl_::unindent::unindent($code))
}};
}

/// Internal implementation of the `py_run!` macro.
///
/// FIXME: this currently unconditionally allocates a `CString`. We should consider making this not so:
/// - Maybe require users to pass `&CStr` / `CString`?
/// - Maybe adjust the `unindent` code to produce `&Cstr` / `Cstring`?
#[macro_export]
#[doc(hidden)]
macro_rules! py_run_impl {
Expand Down
4 changes: 2 additions & 2 deletions tests/test_class_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ impl SuperClass {
fn subclass_new() {
Python::attach(|py| {
let super_cls = py.get_type::<SuperClass>();
let source = pyo3_ffi::c_str!(pyo3::indoc::indoc!(
let source = pyo3_ffi::c_str!(
r#"
class Class(SuperClass):
def __new__(cls):
Expand All @@ -168,7 +168,7 @@ class Class(SuperClass):
c = Class()
assert c.from_rust is False
"#
));
);
let globals = PyModule::import(py, "__main__").unwrap().dict();
globals.set_item("SuperClass", super_cls).unwrap();
py.run(source, Some(&globals), None)
Expand Down
8 changes: 4 additions & 4 deletions tests/test_coroutine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn handle_windows(test: &str) -> String {
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
"#;
pyo3::unindent::unindent(set_event_loop_policy) + &pyo3::unindent::unindent(test)
pyo3::impl_::unindent::unindent(set_event_loop_policy) + &pyo3::impl_::unindent::unindent(test)
}

#[test]
Expand Down Expand Up @@ -149,7 +149,7 @@ fn cancelled_coroutine() {
globals.set_item("sleep", sleep).unwrap();
let err = py
.run(
&CString::new(pyo3::unindent::unindent(&handle_windows(test))).unwrap(),
&CString::new(pyo3::impl_::unindent::unindent(&handle_windows(test))).unwrap(),
Some(&globals),
None,
)
Expand Down Expand Up @@ -189,7 +189,7 @@ fn coroutine_cancel_handle() {
.set_item("cancellable_sleep", cancellable_sleep)
.unwrap();
py.run(
&CString::new(pyo3::unindent::unindent(&handle_windows(test))).unwrap(),
&CString::new(pyo3::impl_::unindent::unindent(&handle_windows(test))).unwrap(),
Some(&globals),
None,
)
Expand Down Expand Up @@ -219,7 +219,7 @@ fn coroutine_is_cancelled() {
let globals = PyDict::new(py);
globals.set_item("sleep_loop", sleep_loop).unwrap();
py.run(
&CString::new(pyo3::unindent::unindent(&handle_windows(test))).unwrap(),
&CString::new(pyo3::impl_::unindent::unindent(&handle_windows(test))).unwrap(),
Some(&globals),
None,
)
Expand Down
Loading
Loading