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