rust-pgn-reader-python-binding 3.2.0__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/Cargo.lock +3 -3
  2. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/Cargo.toml +3 -1
  3. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/PKG-INFO +1 -1
  4. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/benches/parquet_bench.rs +1 -1
  5. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/rust_pgn_reader_python_binding.pyi +13 -8
  6. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/lib.rs +120 -58
  7. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/test.py +73 -16
  8. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/.github/workflows/CI.yml +0 -0
  9. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/.gitignore +0 -0
  10. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/README.md +0 -0
  11. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/pyproject.toml +0 -0
  12. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/bench.py +0 -0
  13. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/bench_parquet.py +0 -0
  14. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/bench_parquet_arrow.py +0 -0
  15. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/bench_parquet_parallel.py +0 -0
  16. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/comment_parsing.rs +0 -0
  17. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/example_rowgroup.py +0 -0
  18. {rust_pgn_reader_python_binding-3.2.0 → rust_pgn_reader_python_binding-3.3.0}/src/example_str.py +0 -0
@@ -840,9 +840,9 @@ dependencies = [
840
840
 
841
841
  [[package]]
842
842
  name = "libc"
843
- version = "0.2.169"
843
+ version = "0.2.174"
844
844
  source = "registry+https://github.com/rust-lang/crates.io-index"
845
- checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
845
+ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
846
846
 
847
847
  [[package]]
848
848
  name = "libm"
@@ -1386,7 +1386,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
1386
1386
 
1387
1387
  [[package]]
1388
1388
  name = "rust_pgn_reader_python_binding"
1389
- version = "3.2.0"
1389
+ version = "3.3.0"
1390
1390
  dependencies = [
1391
1391
  "arrow",
1392
1392
  "arrow-array",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rust_pgn_reader_python_binding"
3
- version = "3.2.0"
3
+ version = "3.3.0"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -29,3 +29,5 @@ arrow = "55"
29
29
  harness = false
30
30
  name = "parquet_bench"
31
31
 
32
+ [profile.bench]
33
+ debug = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rust_pgn_reader_python_binding
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -43,7 +43,7 @@ pub fn bench_parquet() {
43
43
  // Measure start time
44
44
  let start = Instant::now();
45
45
 
46
- let result = parse_multiple_games_native(&movetexts, None);
46
+ let result = parse_multiple_games_native(&movetexts, None, false);
47
47
 
48
48
  let duration = start.elapsed();
49
49
  println!("Time taken: {:?}", duration);
@@ -29,21 +29,26 @@ class PositionStatus:
29
29
  class MoveExtractor:
30
30
  moves: List[PyUciMove]
31
31
  valid_moves: bool
32
- comments: List[str]
33
- evals: List[float]
34
- clock_times: List[Tuple[int, int, float]]
32
+ comments: List[Optional[str]]
33
+ evals: List[Optional[float]]
34
+ clock_times: List[Optional[Tuple[int, int, float]]]
35
35
  outcome: Optional[str]
36
36
  headers: List[Tuple[str, str]]
37
- castling_rights: List[Tuple[bool, bool, bool, bool]]
37
+ castling_rights: List[Optional[Tuple[bool, bool, bool, bool]]]
38
38
  position_status: Optional[PositionStatus]
39
39
 
40
- def __init__(self) -> None: ...
40
+ def __init__(self, store_legal_moves: bool = False) -> None: ...
41
41
  def turn(self) -> bool: ...
42
+ def update_position_status(self) -> None: ...
43
+ @property
44
+ def legal_moves(self) -> List[List[PyUciMove]]: ...
42
45
 
43
- def parse_game(pgn: str) -> MoveExtractor: ...
46
+ def parse_game(pgn: str, store_legal_moves: bool = False) -> MoveExtractor: ...
44
47
  def parse_games(
45
- pgns: List[str], num_threads: Optional[int] = None
48
+ pgns: List[str], num_threads: Optional[int] = None, store_legal_moves: bool = False
46
49
  ) -> List[MoveExtractor]: ...
47
50
  def parse_game_moves_arrow_chunked_array(
48
- pgn_chunked_array: pyarrow.ChunkedArray, num_threads: Optional[int] = None
51
+ pgn_chunked_array: pyarrow.ChunkedArray,
52
+ num_threads: Optional[int] = None,
53
+ store_legal_moves: bool = False,
49
54
  ) -> List[MoveExtractor]: ...
@@ -1,12 +1,12 @@
1
- use crate::comment_parsing::{parse_comments, CommentContent, ParsedTag};
1
+ use crate::comment_parsing::{CommentContent, ParsedTag, parse_comments};
2
2
  use arrow_array::{Array, LargeStringArray, StringArray};
3
3
  use pgn_reader::{KnownOutcome, Outcome, RawComment, RawTag, Reader, SanPlus, Skip, Visitor};
4
4
  use pyo3::prelude::*;
5
5
  use pyo3_arrow::PyChunkedArray;
6
- use rayon::prelude::*;
7
6
  use rayon::ThreadPoolBuilder;
7
+ use rayon::prelude::*;
8
8
  use shakmaty::Color;
9
- use shakmaty::{uci::UciMove, Chess, Position, Role, Square};
9
+ use shakmaty::{Chess, Position, Role, Square, uci::UciMove};
10
10
  use std::io::Cursor;
11
11
  use std::ops::ControlFlow;
12
12
 
@@ -111,17 +111,21 @@ pub struct MoveExtractor {
111
111
  #[pyo3(get)]
112
112
  moves: Vec<PyUciMove>,
113
113
 
114
+ store_legal_moves: bool,
115
+ flat_legal_moves: Vec<PyUciMove>,
116
+ legal_moves_offsets: Vec<usize>,
117
+
114
118
  #[pyo3(get)]
115
119
  valid_moves: bool,
116
120
 
117
121
  #[pyo3(get)]
118
- comments: Vec<String>,
122
+ comments: Vec<Option<String>>,
119
123
 
120
124
  #[pyo3(get)]
121
- evals: Vec<f64>,
125
+ evals: Vec<Option<f64>>,
122
126
 
123
127
  #[pyo3(get)]
124
- clock_times: Vec<(u32, u8, f64)>,
128
+ clock_times: Vec<Option<(u32, u8, f64)>>,
125
129
 
126
130
  #[pyo3(get)]
127
131
  outcome: Option<String>,
@@ -130,7 +134,7 @@ pub struct MoveExtractor {
130
134
  headers: Vec<(String, String)>,
131
135
 
132
136
  #[pyo3(get)]
133
- castling_rights: Vec<(bool, bool, bool, bool)>,
137
+ castling_rights: Vec<Option<(bool, bool, bool, bool)>>,
134
138
 
135
139
  #[pyo3(get)]
136
140
  position_status: Option<PositionStatus>,
@@ -141,9 +145,13 @@ pub struct MoveExtractor {
141
145
  #[pymethods]
142
146
  impl MoveExtractor {
143
147
  #[new]
144
- fn new() -> MoveExtractor {
148
+ #[pyo3(signature = (store_legal_moves = false))]
149
+ fn new(store_legal_moves: bool) -> MoveExtractor {
145
150
  MoveExtractor {
146
151
  moves: Vec::with_capacity(100),
152
+ store_legal_moves,
153
+ flat_legal_moves: Vec::with_capacity(if store_legal_moves { 100 * 30 } else { 0 }), // Pre-allocate for moves
154
+ legal_moves_offsets: Vec::with_capacity(if store_legal_moves { 100 } else { 0 }), // Pre-allocate for offsets
147
155
  pos: Chess::default(),
148
156
  valid_moves: true,
149
157
  comments: Vec::with_capacity(100),
@@ -172,7 +180,31 @@ impl MoveExtractor {
172
180
  castling_bitboard.contains(shakmaty::Square::H8),
173
181
  );
174
182
 
175
- self.castling_rights.push(castling_rights);
183
+ self.castling_rights.push(Some(castling_rights));
184
+ }
185
+
186
+ fn push_legal_moves(&mut self) {
187
+ // Record the starting offset for the current position's legal moves.
188
+ self.legal_moves_offsets.push(self.flat_legal_moves.len());
189
+
190
+ let legal_moves_for_pos = self.pos.legal_moves();
191
+ self.flat_legal_moves.reserve(legal_moves_for_pos.len());
192
+
193
+ for m in legal_moves_for_pos {
194
+ let uci_move_obj = UciMove::from_standard(m);
195
+ if let UciMove::Normal {
196
+ from,
197
+ to,
198
+ promotion: promo_opt,
199
+ } = uci_move_obj
200
+ {
201
+ self.flat_legal_moves.push(PyUciMove {
202
+ from_square: from as u8,
203
+ to_square: to as u8,
204
+ promotion: promo_opt.map(|p_role| p_role as u8),
205
+ });
206
+ }
207
+ }
176
208
  }
177
209
 
178
210
  fn update_position_status(&mut self) {
@@ -192,6 +224,27 @@ impl MoveExtractor {
192
224
  },
193
225
  });
194
226
  }
227
+
228
+ #[getter]
229
+ fn legal_moves(&self) -> Vec<Vec<PyUciMove>> {
230
+ let mut result = Vec::with_capacity(self.legal_moves_offsets.len());
231
+ if self.legal_moves_offsets.is_empty() {
232
+ return result;
233
+ }
234
+
235
+ for i in 0..self.legal_moves_offsets.len() - 1 {
236
+ let start = self.legal_moves_offsets[i];
237
+ let end = self.legal_moves_offsets[i + 1];
238
+ result.push(self.flat_legal_moves[start..end].to_vec());
239
+ }
240
+
241
+ // Handle the last chunk
242
+ if let Some(&start) = self.legal_moves_offsets.last() {
243
+ result.push(self.flat_legal_moves[start..].to_vec());
244
+ }
245
+
246
+ result
247
+ }
195
248
  }
196
249
 
197
250
  impl Visitor for MoveExtractor {
@@ -219,25 +272,36 @@ impl Visitor for MoveExtractor {
219
272
  fn begin_movetext(&mut self, tags: Self::Tags) -> ControlFlow<Self::Output, Self::Movetext> {
220
273
  self.headers = tags;
221
274
  self.moves.clear();
275
+ self.flat_legal_moves.clear();
276
+ self.legal_moves_offsets.clear();
222
277
  self.pos = Chess::default();
223
278
  self.valid_moves = true;
224
279
  self.comments.clear();
225
280
  self.evals.clear();
226
281
  self.clock_times.clear();
282
+ self.castling_rights.clear();
227
283
 
228
284
  self.push_castling_bitboards();
285
+ if self.store_legal_moves {
286
+ self.push_legal_moves();
287
+ }
229
288
  ControlFlow::Continue(())
230
289
  }
231
290
 
291
+ // Roughly half the time during parsing is spent here in san()
232
292
  fn san(
233
293
  &mut self,
234
294
  _movetext: &mut Self::Movetext,
235
295
  san_plus: SanPlus,
236
296
  ) -> ControlFlow<Self::Output> {
237
297
  if self.valid_moves {
298
+ // Most of the function time is spent calculating to_move()
238
299
  match san_plus.san.to_move(&self.pos) {
239
300
  Ok(m) => {
240
301
  self.pos.play_unchecked(m);
302
+ if self.store_legal_moves {
303
+ self.push_legal_moves();
304
+ }
241
305
  let uci_move_obj = UciMove::from_standard(m);
242
306
 
243
307
  match uci_move_obj {
@@ -253,12 +317,20 @@ impl Visitor for MoveExtractor {
253
317
  };
254
318
  self.moves.push(py_uci_move);
255
319
  self.push_castling_bitboards();
320
+
321
+ // Push placeholders to keep vectors in sync
322
+ self.comments.push(None);
323
+ self.evals.push(None);
324
+ self.clock_times.push(None);
256
325
  }
257
326
  _ => {
258
327
  // This case handles UciMove::Put and UciMove::Null,
259
328
  // which are not expected from standard PGN moves
260
329
  // that PyUciMove is designed to represent.
261
- eprintln!("Unexpected UCI move type from standard PGN move: {:?}. Game moves might be invalid.", uci_move_obj);
330
+ eprintln!(
331
+ "Unexpected UCI move type from standard PGN move: {:?}. Game moves might be invalid.",
332
+ uci_move_obj
333
+ );
262
334
  self.valid_moves = false;
263
335
  }
264
336
  }
@@ -283,60 +355,47 @@ impl Visitor for MoveExtractor {
283
355
  eprintln!("Unparsed remaining input: {:?}", remaining_input);
284
356
  return ControlFlow::Continue(());
285
357
  }
286
- let mut eval_encountered = false;
287
- let mut clk_time_encountered = false;
358
+
288
359
  let mut move_comments = String::new();
289
360
 
290
361
  for content in parsed_comments {
291
362
  match content {
292
363
  CommentContent::Text(text) => {
293
364
  if !text.trim().is_empty() {
365
+ if !move_comments.is_empty() {
366
+ move_comments.push(' ');
367
+ }
294
368
  move_comments.push_str(&text);
295
369
  }
296
370
  }
297
371
  CommentContent::Tag(tag_content) => match tag_content {
298
372
  ParsedTag::Eval(eval_value) => {
299
- if eval_encountered {
300
- eprintln!(
301
- "Multiple Eval values found in comment: {:?}",
302
- _comment
303
- );
304
- // Potentially skip this eval or handle as an error
305
- continue;
373
+ if let Some(last_eval) = self.evals.last_mut() {
374
+ *last_eval = Some(eval_value);
306
375
  }
307
- eval_encountered = true;
308
- self.evals.push(eval_value);
309
376
  }
310
377
  ParsedTag::Mate(mate_value) => {
311
- // For now, add mate info to the general comments string
312
- // A dedicated field could be added to MoveExtractor if needed
313
378
  if !move_comments.is_empty() && !move_comments.ends_with(' ') {
314
379
  move_comments.push(' ');
315
380
  }
316
381
  move_comments.push_str(&format!("[Mate {}]", mate_value));
317
- // If evals should also store mate, convert mate_value to f64
318
- // e.g., self.evals.push(mate_value as f64 * 1000.0); // Or some indicator
319
382
  }
320
383
  ParsedTag::ClkTime {
321
384
  hours,
322
385
  minutes,
323
386
  seconds,
324
387
  } => {
325
- if clk_time_encountered {
326
- eprintln!(
327
- "Multiple ClkTime values found in comment: {:?}",
328
- _comment
329
- );
330
- // Potentially skip this clk or handle as an error
331
- continue;
388
+ if let Some(last_clk) = self.clock_times.last_mut() {
389
+ *last_clk = Some((hours, minutes, seconds));
332
390
  }
333
- clk_time_encountered = true;
334
- self.clock_times.push((hours, minutes, seconds));
335
391
  }
336
392
  },
337
393
  }
338
394
  }
339
- self.comments.push(move_comments);
395
+
396
+ if let Some(last_comment) = self.comments.last_mut() {
397
+ *last_comment = Some(move_comments);
398
+ }
340
399
  }
341
400
  Err(e) => {
342
401
  eprintln!("Error parsing comment: {:?}", e);
@@ -374,9 +433,12 @@ impl Visitor for MoveExtractor {
374
433
  }
375
434
 
376
435
  // --- Native Rust versions (no PyResult) ---
377
- pub fn parse_single_game_native(pgn: &str) -> Result<MoveExtractor, String> {
436
+ pub fn parse_single_game_native(
437
+ pgn: &str,
438
+ store_legal_moves: bool,
439
+ ) -> Result<MoveExtractor, String> {
378
440
  let mut reader = Reader::new(Cursor::new(pgn));
379
- let mut extractor = MoveExtractor::new();
441
+ let mut extractor = MoveExtractor::new(store_legal_moves);
380
442
  match reader.read_game(&mut extractor) {
381
443
  Ok(Some(_)) => Ok(extractor),
382
444
  Ok(None) => Err("No game found in PGN".to_string()),
@@ -387,6 +449,7 @@ pub fn parse_single_game_native(pgn: &str) -> Result<MoveExtractor, String> {
387
449
  pub fn parse_multiple_games_native(
388
450
  pgns: &Vec<String>,
389
451
  num_threads: Option<usize>,
452
+ store_legal_moves: bool,
390
453
  ) -> Result<Vec<MoveExtractor>, String> {
391
454
  let num_threads = num_threads.unwrap_or_else(num_cpus::get);
392
455
 
@@ -397,25 +460,16 @@ pub fn parse_multiple_games_native(
397
460
  .expect("Failed to build Rayon thread pool");
398
461
 
399
462
  thread_pool.install(|| {
400
- let results: Vec<Result<MoveExtractor, String>> = pgns
401
- .par_iter()
402
- .map(|pgn| parse_single_game_native(pgn))
403
- .collect();
404
-
405
- let mut extractors: Vec<MoveExtractor> = Vec::with_capacity(results.len());
406
- for res in results {
407
- match res {
408
- Ok(extractor) => extractors.push(extractor),
409
- Err(e) => return Err(e),
410
- }
411
- }
412
- Ok(extractors)
463
+ pgns.par_iter()
464
+ .map(|pgn| parse_single_game_native(pgn, store_legal_moves))
465
+ .collect()
413
466
  })
414
467
  }
415
468
 
416
469
  fn _parse_game_moves_from_arrow_chunks_native(
417
470
  pgn_chunked_array: &PyChunkedArray,
418
471
  num_threads: Option<usize>,
472
+ store_legal_moves: bool,
419
473
  ) -> Result<Vec<MoveExtractor>, String> {
420
474
  let num_threads = num_threads.unwrap_or_else(num_cpus::get);
421
475
  let thread_pool = ThreadPoolBuilder::new()
@@ -452,7 +506,7 @@ fn _parse_game_moves_from_arrow_chunks_native(
452
506
  thread_pool.install(|| {
453
507
  pgn_str_slices
454
508
  .par_iter()
455
- .map(|&pgn_s| parse_single_game_native(pgn_s))
509
+ .map(|&pgn_s| parse_single_game_native(pgn_s, store_legal_moves))
456
510
  .collect::<Result<Vec<MoveExtractor>, String>>()
457
511
  })
458
512
  }
@@ -461,25 +515,33 @@ fn _parse_game_moves_from_arrow_chunks_native(
461
515
  // TODO check if I can call py.allow_threads and release GIL
462
516
  // see https://docs.rs/pyo3-arrow/0.10.1/pyo3_arrow/
463
517
  #[pyfunction]
518
+ #[pyo3(signature = (pgn, store_legal_moves = false))]
464
519
  /// Parses a single PGN game string.
465
- fn parse_game(pgn: &str) -> PyResult<MoveExtractor> {
466
- parse_single_game_native(pgn).map_err(pyo3::exceptions::PyValueError::new_err)
520
+ fn parse_game(pgn: &str, store_legal_moves: bool) -> PyResult<MoveExtractor> {
521
+ parse_single_game_native(pgn, store_legal_moves)
522
+ .map_err(pyo3::exceptions::PyValueError::new_err)
467
523
  }
468
524
 
469
525
  /// In parallel, parse a set of games
470
526
  #[pyfunction]
471
- #[pyo3(signature = (pgns, num_threads=None))]
472
- fn parse_games(pgns: Vec<String>, num_threads: Option<usize>) -> PyResult<Vec<MoveExtractor>> {
473
- parse_multiple_games_native(&pgns, num_threads).map_err(pyo3::exceptions::PyValueError::new_err)
527
+ #[pyo3(signature = (pgns, num_threads=None, store_legal_moves=false))]
528
+ fn parse_games(
529
+ pgns: Vec<String>,
530
+ num_threads: Option<usize>,
531
+ store_legal_moves: bool,
532
+ ) -> PyResult<Vec<MoveExtractor>> {
533
+ parse_multiple_games_native(&pgns, num_threads, store_legal_moves)
534
+ .map_err(pyo3::exceptions::PyValueError::new_err)
474
535
  }
475
536
 
476
537
  #[pyfunction]
477
- #[pyo3(signature = (pgn_chunked_array, num_threads=None))]
538
+ #[pyo3(signature = (pgn_chunked_array, num_threads=None, store_legal_moves=false))]
478
539
  fn parse_game_moves_arrow_chunked_array(
479
540
  pgn_chunked_array: PyChunkedArray,
480
541
  num_threads: Option<usize>,
542
+ store_legal_moves: bool,
481
543
  ) -> PyResult<Vec<MoveExtractor>> {
482
- _parse_game_moves_from_arrow_chunks_native(&pgn_chunked_array, num_threads)
544
+ _parse_game_moves_from_arrow_chunks_native(&pgn_chunked_array, num_threads, store_legal_moves)
483
545
  .map_err(pyo3::exceptions::PyValueError::new_err)
484
546
  }
485
547
 
@@ -78,16 +78,28 @@ class TestPgnExtraction(unittest.TestCase):
78
78
  "f5f8",
79
79
  "h8h7",
80
80
  ]
81
- comments_reference = ["asdf", "hello"]
81
+
82
+ comments_reference = [
83
+ "asdf",
84
+ None,
85
+ None,
86
+ None,
87
+ None,
88
+ None,
89
+ "hello",
90
+ ] + [None for _ in range(57)]
91
+
82
92
  valid_reference = True
83
- evals_reference = []
84
- clock_times_reference = []
93
+ evals_reference = [None for _ in range(len(moves_reference))]
94
+ clock_times_reference = [None for _ in range(len(moves_reference))]
85
95
 
86
96
  self.assertTrue([str(move) for move in extractor.moves] == moves_reference)
87
97
  self.assertTrue(extractor.comments == comments_reference)
88
98
  self.assertTrue(extractor.valid_moves == valid_reference)
89
99
  self.assertTrue(extractor.evals == evals_reference)
90
100
  self.assertTrue(extractor.clock_times == clock_times_reference)
101
+
102
+ assert extractor.position_status is not None # appease the type checker
91
103
  self.assertFalse(extractor.position_status.is_checkmate)
92
104
  self.assertFalse(extractor.position_status.is_stalemate)
93
105
  self.assertFalse(extractor.position_status.is_game_over)
@@ -183,10 +195,10 @@ class TestPgnExtraction(unittest.TestCase):
183
195
  "a4a5",
184
196
  "b3d5",
185
197
  ]
186
- comments_reference = []
198
+ comments_reference = [None for _ in range(len(moves_reference))]
187
199
  valid_reference = True
188
- evals_reference = []
189
- clock_times_reference = []
200
+ evals_reference = [None for _ in range(len(moves_reference))]
201
+ clock_times_reference = [None for _ in range(len(moves_reference))]
190
202
  headers_reference = [
191
203
  ("Event", "Rated Classical game"),
192
204
  ("Site", "https://lichess.org/lhy6ehiv"),
@@ -211,6 +223,8 @@ class TestPgnExtraction(unittest.TestCase):
211
223
  self.assertTrue(extractor.evals == evals_reference)
212
224
  self.assertTrue(extractor.clock_times == clock_times_reference)
213
225
  self.assertTrue(extractor.headers == headers_reference)
226
+
227
+ assert extractor.position_status is not None # appease the type checker
214
228
  self.assertFalse(extractor.position_status.is_checkmate)
215
229
  self.assertFalse(extractor.position_status.is_stalemate)
216
230
  self.assertFalse(extractor.position_status.is_game_over)
@@ -326,6 +340,8 @@ class TestPgnExtraction(unittest.TestCase):
326
340
  self.assertTrue(extractor.valid_moves == valid_reference)
327
341
  self.assertTrue(extractor.evals == evals_reference)
328
342
  self.assertTrue(extractor.clock_times == clock_times_reference)
343
+
344
+ assert extractor.position_status is not None # appease the type checker
329
345
  self.assertFalse(extractor.position_status.is_checkmate)
330
346
  self.assertFalse(extractor.position_status.is_stalemate)
331
347
  self.assertFalse(extractor.position_status.is_game_over)
@@ -337,11 +353,6 @@ class TestPgnExtraction(unittest.TestCase):
337
353
  ]
338
354
  extractor = rust_pgn_reader_python_binding.parse_games(pgns)
339
355
 
340
- comments_reference = [[], ["asdf", "hello"]]
341
-
342
- self.assertTrue(extractor[0].comments == comments_reference[0])
343
- self.assertTrue(extractor[1].comments == comments_reference[1])
344
-
345
356
  moves_reference = [
346
357
  [
347
358
  "g1f3",
@@ -431,12 +442,37 @@ class TestPgnExtraction(unittest.TestCase):
431
442
  ],
432
443
  ]
433
444
 
445
+ comments_reference = [
446
+ [None for _ in range(len(moves_reference[0]))],
447
+ [
448
+ "asdf",
449
+ None,
450
+ None,
451
+ None,
452
+ None,
453
+ None,
454
+ "hello",
455
+ None,
456
+ None,
457
+ None,
458
+ None,
459
+ None,
460
+ None,
461
+ None,
462
+ ],
463
+ ]
464
+
465
+ self.assertTrue(extractor[0].comments == comments_reference[0])
466
+ self.assertTrue(extractor[1].comments == comments_reference[1])
467
+
434
468
  self.assertTrue(
435
469
  [str(move) for move in extractor[0].moves] == moves_reference[0]
436
470
  )
437
471
  self.assertTrue(
438
472
  [str(move) for move in extractor[1].moves] == moves_reference[1]
439
473
  )
474
+ assert extractor[0].position_status is not None # appease the type checker
475
+
440
476
  self.assertTrue(extractor[0].position_status.is_checkmate)
441
477
  self.assertFalse(extractor[0].position_status.is_stalemate)
442
478
  self.assertTrue(extractor[0].position_status.is_game_over)
@@ -446,6 +482,7 @@ class TestPgnExtraction(unittest.TestCase):
446
482
 
447
483
  self.assertTrue(extractor[1].position_status is None)
448
484
  extractor[1].update_position_status()
485
+ assert extractor[1].position_status is not None # appease the type checker
449
486
  self.assertFalse(extractor[1].position_status.is_checkmate)
450
487
  self.assertFalse(extractor[1].position_status.is_stalemate)
451
488
  self.assertFalse(extractor[1].position_status.is_game_over)
@@ -496,11 +533,6 @@ class TestPgnExtraction(unittest.TestCase):
496
533
  )
497
534
  )
498
535
 
499
- comments_reference = [[], ["asdf", "hello"]]
500
-
501
- self.assertTrue(extractors[0].comments == comments_reference[0])
502
- self.assertTrue(extractors[1].comments == comments_reference[1])
503
-
504
536
  moves_reference = [
505
537
  [
506
538
  "g1f3",
@@ -597,7 +629,31 @@ class TestPgnExtraction(unittest.TestCase):
597
629
  [str(move) for move in extractors[1].moves] == moves_reference[1]
598
630
  )
599
631
 
632
+ comments_reference = [
633
+ [None for _ in range(len(moves_reference[0]))],
634
+ [
635
+ "asdf",
636
+ None,
637
+ None,
638
+ None,
639
+ None,
640
+ None,
641
+ "hello",
642
+ None,
643
+ None,
644
+ None,
645
+ None,
646
+ None,
647
+ None,
648
+ None,
649
+ ],
650
+ ]
651
+
652
+ self.assertTrue(extractors[0].comments == comments_reference[0])
653
+ self.assertTrue(extractors[1].comments == comments_reference[1])
654
+
600
655
  extractors[0].update_position_status() # Ensure status is calculated
656
+ assert extractors[0].position_status is not None # appease the type checker
601
657
  self.assertTrue(extractors[0].position_status.is_checkmate)
602
658
  self.assertFalse(extractors[0].position_status.is_stalemate)
603
659
  self.assertTrue(extractors[0].position_status.is_game_over)
@@ -613,6 +669,7 @@ class TestPgnExtraction(unittest.TestCase):
613
669
  extractors[1].position_status is None
614
670
  ) # Not set by default for parse_game_moves_arrow_chunked_array
615
671
  extractors[1].update_position_status()
672
+ assert extractors[1].position_status is not None # appease the type checker
616
673
  self.assertFalse(extractors[1].position_status.is_checkmate)
617
674
  self.assertFalse(extractors[1].position_status.is_stalemate)
618
675
  self.assertFalse(extractors[1].position_status.is_game_over)