1use pyo3::exceptions::PyNotImplementedError;
6use pyo3::prelude::*;
7use pyo3::types::{PyDict, PyList};
8use pyo3::IntoPyObjectExt;
9use std::collections::HashMap;
10
11mod datetime;
12mod error;
13mod match_rs;
14mod parser;
15mod pattern_cache;
16mod pattern_normalize;
17mod result;
18mod results;
19mod types;
20mod unicode_offsets;
21
22pub(crate) use pattern_cache::extract_extra_types_identity;
23use unicode_offsets::search_byte_range;
24use pattern_cache::get_or_create_parser;
25
26pub use datetime::FixedTzOffset;
27pub use parser::{FindallIter, Format, FormatParser};
28pub use result::*;
29pub use results::Results;
30pub use types::conversion::*;
31pub use formatparse_core::strftime_to_regex;
33pub use formatparse_core::{FieldSpec, FieldType};
34pub use match_rs::Match;
35
36pub use error::PatternParseMismatch;
37
38#[pyfunction]
40#[pyo3(signature = (pattern, string, extra_types=None, case_sensitive=false, evaluate_result=true))]
41fn parse(
42 pattern: &str,
43 string: &str,
44 extra_types: Option<HashMap<String, Py<PyAny>>>,
45 case_sensitive: bool,
46 evaluate_result: bool,
47) -> PyResult<Option<Py<PyAny>>> {
48 formatparse_core::validate_input_length(string)
50 .map_err(pyo3::exceptions::PyValueError::new_err)?;
51
52 if string.contains('\0') {
54 return Err(pyo3::exceptions::PyValueError::new_err(
55 "Input string contains null byte",
56 ));
57 }
58
59 let extra_types_cloned = Python::attach(|py| -> Option<HashMap<String, Py<PyAny>>> {
61 extra_types.as_ref().map(|et| {
62 et.iter()
63 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
64 .collect()
65 })
66 });
67 match get_or_create_parser(pattern, extra_types_cloned) {
68 Ok(parser) => parser.parse_internal(
69 string,
70 case_sensitive,
71 extra_types.as_ref(),
72 evaluate_result,
73 ),
74 Err(e) => Python::attach(|py| {
75 if e.is_instance_of::<PyNotImplementedError>(py) {
76 return Err(e);
77 }
78 if e.is_instance_of::<crate::error::PatternParseMismatch>(py) {
79 return Ok(None);
80 }
81 Err(e)
82 }),
83 }
84}
85
86#[pyfunction]
92#[pyo3(signature = (pattern, strings, extra_types=None, case_sensitive=false, evaluate_result=true))]
93fn parse_batch(
94 pattern: &str,
95 strings: Vec<String>,
96 extra_types: Option<HashMap<String, Py<PyAny>>>,
97 case_sensitive: bool,
98 evaluate_result: bool,
99) -> PyResult<Py<PyAny>> {
100 for s in &strings {
101 formatparse_core::validate_input_length(s)
102 .map_err(pyo3::exceptions::PyValueError::new_err)?;
103 if s.contains('\0') {
104 return Err(pyo3::exceptions::PyValueError::new_err(
105 "Input string contains null byte",
106 ));
107 }
108 }
109
110 let extra_types_cloned = Python::attach(|py| -> Option<HashMap<String, Py<PyAny>>> {
111 extra_types.as_ref().map(|et| {
112 et.iter()
113 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
114 .collect()
115 })
116 });
117
118 let parser = match get_or_create_parser(pattern, extra_types_cloned) {
119 Ok(p) => p,
120 Err(e) => {
121 return Python::attach(|py| -> PyResult<Py<PyAny>> {
122 if e.is_instance_of::<PyNotImplementedError>(py) {
123 return Err(e);
124 }
125 if e.is_instance_of::<crate::error::PatternParseMismatch>(py) {
126 let none_obj = py.None().into_py_any(py)?;
127 let mut out: Vec<Py<PyAny>> = Vec::with_capacity(strings.len());
128 for _ in 0..strings.len() {
129 out.push(none_obj.clone_ref(py));
130 }
131 let items: Vec<_> = out.iter().map(|o| o.bind(py)).collect();
132 return PyList::new(py, items)?.into_py_any(py);
133 }
134 Err(e)
135 });
136 }
137 };
138
139 Python::attach(|py| -> PyResult<Py<PyAny>> {
140 let mut out: Vec<Py<PyAny>> = Vec::with_capacity(strings.len());
141 for s in &strings {
142 match parser.parse_internal(s, case_sensitive, extra_types.as_ref(), evaluate_result)? {
143 Some(obj) => out.push(obj),
144 None => out.push(py.None().into_py_any(py)?),
145 }
146 }
147 let items: Vec<_> = out.iter().map(|o| o.bind(py)).collect();
148 PyList::new(py, items)?.into_py_any(py)
149 })
150}
151
152#[pyfunction]
154#[pyo3(signature = (pattern, string, pos=0, endpos=None, extra_types=None, case_sensitive=true, evaluate_result=true))]
155fn search(
156 pattern: &str,
157 string: &str,
158 pos: usize,
159 endpos: Option<usize>,
160 extra_types: Option<HashMap<String, Py<PyAny>>>,
161 case_sensitive: bool,
162 evaluate_result: bool,
163) -> PyResult<Option<Py<PyAny>>> {
164 let Some((byte_start, byte_end)) = search_byte_range(string, pos, endpos) else {
166 return Ok(None);
167 };
168
169 formatparse_core::validate_input_length(string)
171 .map_err(pyo3::exceptions::PyValueError::new_err)?;
172
173 if string.contains('\0') {
175 return Err(pyo3::exceptions::PyValueError::new_err(
176 "Input string contains null byte",
177 ));
178 }
179
180 let extra_types_cloned = Python::attach(|py| -> Option<HashMap<String, Py<PyAny>>> {
181 extra_types.as_ref().map(|et| {
182 et.iter()
183 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
184 .collect()
185 })
186 });
187 let parser = get_or_create_parser(pattern, extra_types_cloned)?;
188 let search_string = &string[byte_start..byte_end];
189
190 if let Some(result) =
191 parser.search_pattern(search_string, case_sensitive, extra_types, evaluate_result)?
192 {
193 Python::attach(|py| {
195 if let Ok(parse_result) = result.bind(py).cast::<ParseResult>() {
196 let result_value = parse_result.borrow();
197 let adjusted = result_value
198 .clone()
199 .with_offset(byte_start)
200 .spans_as_char_indices(string);
201 Ok(Some(Py::new(py, adjusted)?.into_py_any(py)?))
202 } else if let Ok(match_obj) = result.bind(py).cast::<crate::match_rs::Match>() {
203 let adjusted = match_obj
204 .borrow()
205 .clone()
206 .with_offset(byte_start)
207 .spans_as_char_indices(string);
208 Ok(Some(Py::new(py, adjusted)?.into_py_any(py)?))
209 } else {
210 Ok(Some(result))
211 }
212 })
213 } else {
214 Ok(None)
215 }
216}
217
218#[pyfunction]
220#[pyo3(signature = (pattern, string, extra_types=None, case_sensitive=false, evaluate_result=true, max_matches=None))]
221fn findall(
222 pattern: &str,
223 string: &str,
224 extra_types: Option<HashMap<String, Py<PyAny>>>,
225 case_sensitive: bool,
226 evaluate_result: bool,
227 max_matches: Option<usize>,
228) -> PyResult<Py<PyAny>> {
229 formatparse_core::validate_input_length(string)
231 .map_err(pyo3::exceptions::PyValueError::new_err)?;
232
233 if string.contains('\0') {
235 return Err(pyo3::exceptions::PyValueError::new_err(
236 "Input string contains null byte",
237 ));
238 }
239
240 let extra_types_cloned = Python::attach(|py| -> Option<HashMap<String, Py<PyAny>>> {
241 extra_types.as_ref().map(|et| {
242 et.iter()
243 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
244 .collect()
245 })
246 });
247 let parser = get_or_create_parser(pattern, extra_types_cloned)?;
248 crate::parser::findall_engine::findall_matches(
249 parser,
250 string,
251 extra_types.as_ref(),
252 case_sensitive,
253 evaluate_result,
254 max_matches,
255 )
256}
257
258#[pyfunction]
262#[pyo3(signature = (pattern, string, extra_types=None, case_sensitive=false, evaluate_result=true, max_matches=None))]
263fn findall_iter(
264 py: Python<'_>,
265 pattern: &str,
266 string: &str,
267 extra_types: Option<HashMap<String, Py<PyAny>>>,
268 case_sensitive: bool,
269 evaluate_result: bool,
270 max_matches: Option<usize>,
271) -> PyResult<Py<FindallIter>> {
272 formatparse_core::validate_input_length(string)
273 .map_err(pyo3::exceptions::PyValueError::new_err)?;
274
275 if string.contains('\0') {
276 return Err(pyo3::exceptions::PyValueError::new_err(
277 "Input string contains null byte",
278 ));
279 }
280
281 let extra_types_cloned = Python::attach(|py| -> Option<HashMap<String, Py<PyAny>>> {
282 extra_types.as_ref().map(|et| {
283 et.iter()
284 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
285 .collect()
286 })
287 });
288 let parser = get_or_create_parser(pattern, extra_types_cloned)?;
289
290 let et_map = Python::attach(|py| -> HashMap<String, Py<PyAny>> {
291 extra_types
292 .as_ref()
293 .map(|et| {
294 et.iter()
295 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
296 .collect()
297 })
298 .unwrap_or_default()
299 });
300
301 Py::new(
302 py,
303 FindallIter::new(
304 parser,
305 string.to_string(),
306 case_sensitive,
307 evaluate_result,
308 et_map,
309 max_matches,
310 ),
311 )
312}
313
314#[pyfunction]
320#[pyo3(signature = (pattern, extra_types=None))]
321fn compile(
322 pattern: &str,
323 extra_types: Option<HashMap<String, Py<PyAny>>>,
324) -> PyResult<FormatParser> {
325 let extra_types_cloned = Python::attach(|py| -> Option<HashMap<String, Py<PyAny>>> {
326 extra_types.as_ref().map(|et| {
327 et.iter()
328 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
329 .collect()
330 })
331 });
332 let arc = get_or_create_parser(pattern, extra_types_cloned)?;
333 Ok((*arc).clone())
334}
335
336#[pyfunction]
338#[pyo3(signature = (format_string, _match_dict=None))]
339fn extract_format(
340 format_string: &str,
341 _match_dict: Option<&Bound<'_, PyDict>>,
342) -> PyResult<Py<PyAny>> {
343 use crate::types::FieldSpec;
344
345 let mut spec = FieldSpec::new();
347 formatparse_core::parser::pattern::parse_format_spec(format_string, &mut spec)
348 .map_err(crate::parser::pattern::pattern_compile_error_to_py)?;
349 formatparse_core::parser::pattern::validate_multiline_mvp(&spec)
350 .map_err(crate::parser::pattern::pattern_compile_error_to_py)?;
351
352 let type_str: String = if format_string == "%" {
355 "%".to_string()
356 } else {
357 let chars: Vec<char> = format_string.chars().collect();
360 let mut i = 0;
361 let len = chars.len();
362
363 if i < len && (chars[i] == '<' || chars[i] == '>' || chars[i] == '^' || chars[i] == '=') {
365 i += 1;
366 } else if i + 1 < len {
367 let ch = chars[i];
368 let next_ch = chars[i + 1];
369 if (next_ch == '<' || next_ch == '>' || next_ch == '^' || next_ch == '=')
370 && ch != next_ch
371 {
372 i += 2; }
374 }
375
376 if i < len && (chars[i] == '+' || chars[i] == '-' || chars[i] == ' ') {
378 i += 1;
379 }
380
381 if i < len && chars[i] == '#' {
383 i += 1;
384 }
385
386 if i < len && chars[i] == '0' {
388 i += 1;
389 }
390
391 while i < len && chars[i].is_ascii_digit() {
393 i += 1;
394 }
395
396 if i < len && chars[i] == ',' {
398 i += 1;
399 }
400
401 if i < len && chars[i] == '.' {
403 i += 1;
404 while i < len && chars[i].is_ascii_digit() {
405 i += 1;
406 }
407 }
408
409 if i < len {
411 format_string[i..].to_string()
412 } else {
413 "s".to_string() }
415 };
416
417 Python::attach(|py| {
419 let result = PyDict::new(py);
420 result.set_item("type", type_str)?;
421
422 if let Some(width) = spec.width {
424 result.set_item("width", width.to_string())?;
425 }
426
427 if let Some(precision) = spec.precision {
429 result.set_item("precision", precision.to_string())?;
430 }
431
432 if let Some(align) = spec.alignment {
434 result.set_item("align", align.to_string())?;
435 }
436
437 if let Some(fill) = spec.fill {
439 result.set_item("fill", fill.to_string())?;
440 }
441
442 if spec.zero_pad {
444 result.set_item("zero", true)?;
445 }
446
447 result.into_py_any(py)
448 })
449}
450
451#[pymodule]
453fn _formatparse(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
454 m.add(
455 "PatternParseMismatch",
456 py.get_type::<crate::error::PatternParseMismatch>(),
457 )?;
458 m.add_function(wrap_pyfunction!(parse, m)?)?;
459 m.add_function(wrap_pyfunction!(parse_batch, m)?)?;
460 m.add_function(wrap_pyfunction!(search, m)?)?;
461 m.add_function(wrap_pyfunction!(findall, m)?)?;
462 m.add_function(wrap_pyfunction!(findall_iter, m)?)?;
463 m.add_function(wrap_pyfunction!(compile, m)?)?;
464 m.add_function(wrap_pyfunction!(extract_format, m)?)?;
465 m.add_class::<ParseResult>()?;
466 m.add_class::<FormatParser>()?;
467 m.add_class::<Format>()?;
468 m.add_class::<FixedTzOffset>()?;
469 m.add_class::<Match>()?;
470 m.add_class::<Results>()?;
471 m.add_class::<FindallIter>()?;
472 Ok(())
473}