pybcsv 1.4.1__tar.gz → 1.4.3__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 (114) hide show
  1. {pybcsv-1.4.1 → pybcsv-1.4.3}/CMakeLists.txt +3 -4
  2. {pybcsv-1.4.1 → pybcsv-1.4.3}/PKG-INFO +1 -3
  3. {pybcsv-1.4.1 → pybcsv-1.4.3}/README.md +0 -2
  4. {pybcsv-1.4.1 → pybcsv-1.4.3}/benchmarks/run_pybcsv_benchmarks.py +2 -2
  5. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/bcsv_c_api.h +41 -0
  6. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/std_charconv_compat.h +18 -4
  7. {pybcsv-1.4.1 → pybcsv-1.4.3}/pybcsv/__init__.py +0 -3
  8. {pybcsv-1.4.1 → pybcsv-1.4.3}/pybcsv/_version.py +3 -3
  9. {pybcsv-1.4.1 → pybcsv-1.4.3}/pybcsv/bindings.cpp +135 -139
  10. {pybcsv-1.4.1 → pybcsv-1.4.3}/pybcsv/pandas_utils.py +0 -6
  11. pybcsv-1.4.1/tests/run_all_tests.py +0 -233
  12. pybcsv-1.4.1/tests/run_tests.py +0 -92
  13. {pybcsv-1.4.1 → pybcsv-1.4.3}/BUILD_NOTES.md +0 -0
  14. {pybcsv-1.4.1 → pybcsv-1.4.3}/LICENSE +0 -0
  15. {pybcsv-1.4.1 → pybcsv-1.4.3}/MANIFEST.in +0 -0
  16. {pybcsv-1.4.1 → pybcsv-1.4.3}/benchmarks/README.md +0 -0
  17. {pybcsv-1.4.1 → pybcsv-1.4.3}/examples/README.md +0 -0
  18. {pybcsv-1.4.1 → pybcsv-1.4.3}/examples/advanced_usage.py +0 -0
  19. {pybcsv-1.4.1 → pybcsv-1.4.3}/examples/basic_usage.py +0 -0
  20. {pybcsv-1.4.1 → pybcsv-1.4.3}/examples/pandas_integration.py +0 -0
  21. {pybcsv-1.4.1 → pybcsv-1.4.3}/examples/performance_benchmark.py +0 -0
  22. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/bcsv.h +0 -0
  23. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/bcsv.hpp +0 -0
  24. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/bitset.h +0 -0
  25. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/bitset.hpp +0 -0
  26. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/byte_buffer.h +0 -0
  27. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/checksum.hpp +0 -0
  28. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_file/file_codec_concept.h +0 -0
  29. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_file/file_codec_dispatch.h +0 -0
  30. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_file/file_codec_packet001.h +0 -0
  31. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_file/file_codec_packet_lz4_001.h +0 -0
  32. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_file/file_codec_packet_lz4_batch001.h +0 -0
  33. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_file/file_codec_stream001.h +0 -0
  34. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_file/file_codec_stream_lz4_001.h +0 -0
  35. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_row/row_codec_delta002.h +0 -0
  36. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_row/row_codec_delta002.hpp +0 -0
  37. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_row/row_codec_dispatch.h +0 -0
  38. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_row/row_codec_flat001.h +0 -0
  39. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_row/row_codec_flat001.hpp +0 -0
  40. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_row/row_codec_zoh001.h +0 -0
  41. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/codec_row/row_codec_zoh001.hpp +0 -0
  42. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/column_name_index.h +0 -0
  43. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/column_name_index.hpp +0 -0
  44. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/csv_reader.h +0 -0
  45. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/csv_reader.hpp +0 -0
  46. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/csv_writer.h +0 -0
  47. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/csv_writer.hpp +0 -0
  48. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/definitions.h +0 -0
  49. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/file_footer.h +0 -0
  50. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/file_header.h +0 -0
  51. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/file_header.hpp +0 -0
  52. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/layout.h +0 -0
  53. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/layout.hpp +0 -0
  54. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/layout_guard.h +0 -0
  55. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/lz4_block.hpp +0 -0
  56. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/lz4_stream.hpp +0 -0
  57. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/packet_header.h +0 -0
  58. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/reader.h +0 -0
  59. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/reader.hpp +0 -0
  60. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/reader_concept.h +0 -0
  61. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/row.h +0 -0
  62. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/row.hpp +0 -0
  63. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/row_visitors.h +0 -0
  64. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler.h +0 -0
  65. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler.hpp +0 -0
  66. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_ast.h +0 -0
  67. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_compiler.h +0 -0
  68. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_parser.h +0 -0
  69. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_tokenizer.h +0 -0
  70. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_types.h +0 -0
  71. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_vm.h +0 -0
  72. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_vm.hpp +0 -0
  73. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/sampler/sampler_window.h +0 -0
  74. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/vle.hpp +0 -0
  75. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/writer.h +0 -0
  76. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/writer.hpp +0 -0
  77. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/bcsv/writer_concept.h +0 -0
  78. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4.c +0 -0
  79. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4.h +0 -0
  80. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4file.c +0 -0
  81. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4file.h +0 -0
  82. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4frame.c +0 -0
  83. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4frame.h +0 -0
  84. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4frame_static.h +0 -0
  85. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4hc.c +0 -0
  86. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/lz4hc.h +0 -0
  87. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/xxhash.c +0 -0
  88. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/lz4-1.10.0/xxhash.h +0 -0
  89. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/xxHash-0.8.3/xxhash.c +0 -0
  90. {pybcsv-1.4.1 → pybcsv-1.4.3}/include/xxHash-0.8.3/xxhash.h +0 -0
  91. {pybcsv-1.4.1 → pybcsv-1.4.3}/pybcsv/__version__.py +0 -0
  92. {pybcsv-1.4.1 → pybcsv-1.4.3}/pybcsv/polars_utils.py +0 -0
  93. {pybcsv-1.4.1 → pybcsv-1.4.3}/pybcsv/py.typed +0 -0
  94. {pybcsv-1.4.1 → pybcsv-1.4.3}/pyproject.toml +0 -0
  95. {pybcsv-1.4.1 → pybcsv-1.4.3}/sync_headers.py +0 -0
  96. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/README.md +0 -0
  97. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/conftest.py +0 -0
  98. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_basic_functionality.py +0 -0
  99. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_count_rows.py +0 -0
  100. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_csv_writer_reader.py +0 -0
  101. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_data_types.py +0 -0
  102. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_interoperability.py +0 -0
  103. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_minimal_string.py +0 -0
  104. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_performance_edge_cases.py +0 -0
  105. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_arrow.py +0 -0
  106. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_codecs.py +0 -0
  107. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_columnar.py +0 -0
  108. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_core.py +0 -0
  109. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_direct_access.py +0 -0
  110. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_errors.py +0 -0
  111. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_pandas.py +0 -0
  112. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_performance.py +0 -0
  113. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_polars.py +0 -0
  114. {pybcsv-1.4.1 → pybcsv-1.4.3}/tests/test_pybcsv_sampler.py +0 -0
@@ -119,7 +119,6 @@ if(MSVC)
119
119
  /O2
120
120
  /std:c++20
121
121
  )
122
- target_compile_definitions(_bcsv PRIVATE BCSV_HAS_BATCH_CODEC=1)
123
122
  elseif(APPLE)
124
123
  # Apple Clang ships LLVM 18-era libc++ where jthread/stop_token are
125
124
  # still experimental (guarded by _LIBCPP_HAS_NO_EXPERIMENTAL_STOP_TOKEN).
@@ -129,15 +128,15 @@ elseif(APPLE)
129
128
  -fexperimental-library
130
129
  -mmacosx-version-min=13.3
131
130
  )
132
- target_compile_definitions(_bcsv PRIVATE BCSV_HAS_BATCH_CODEC=1)
133
131
  else()
134
- # Linux: full C++20 with batch codec + pthreads
132
+ # Linux: full C++20 with pthreads
135
133
  target_compile_options(_bcsv PRIVATE
136
134
  -O3 -Wall -Wextra
137
135
  )
138
- target_compile_definitions(_bcsv PRIVATE BCSV_HAS_BATCH_CODEC=1)
139
136
  target_link_libraries(_bcsv PRIVATE pthread)
140
137
  endif()
141
138
 
139
+ target_compile_definitions(_bcsv PRIVATE BCSV_HAS_BATCH_CODEC=1)
140
+
142
141
  # ── Install ─────────────────────────────────────────────────────────────
143
142
  install(TARGETS _bcsv LIBRARY DESTINATION pybcsv)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybcsv
3
- Version: 1.4.1
3
+ Version: 1.4.3
4
4
  Summary: High-performance Python bindings for the BCSV (Binary CSV) library with pandas integration
5
5
  Keywords: csv,binary,data-science,compression,performance,pandas,numpy,big-data,data-processing
6
6
  Author-Email: Tobias Weber <weber.tobias.md@gmail.com>
@@ -318,8 +318,6 @@ pybcsv.to_csv(bcsv_file, csv_file)
318
318
  pybcsv.read_columns(filename) -> dict[str, np.ndarray | list[str]]
319
319
  pybcsv.write_columns(filename, columns, col_order, col_types,
320
320
  row_codec="delta", compression_level=1)
321
- pybcsv.read_to_dataframe(filename, columns=None) -> pd.DataFrame
322
-
323
321
  # Type utilities
324
322
  pybcsv.type_to_string(column_type) -> str
325
323
  ```
@@ -272,8 +272,6 @@ pybcsv.to_csv(bcsv_file, csv_file)
272
272
  pybcsv.read_columns(filename) -> dict[str, np.ndarray | list[str]]
273
273
  pybcsv.write_columns(filename, columns, col_order, col_types,
274
274
  row_codec="delta", compression_level=1)
275
- pybcsv.read_to_dataframe(filename, columns=None) -> pd.DataFrame
276
-
277
275
  # Type utilities
278
276
  pybcsv.type_to_string(column_type) -> str
279
277
  ```
@@ -291,9 +291,9 @@ def write_dataframe_mode(file_path: Path, columns_spec, rows: list[list], row_co
291
291
 
292
292
 
293
293
  def read_dataframe_mode(file_path: Path) -> tuple[float, int]:
294
- """Read using C++ read_to_dataframe (single call)."""
294
+ """Read using pybcsv.read_dataframe (Arrow fast-path)."""
295
295
  start = time.perf_counter()
296
- df = pybcsv.read_to_dataframe(str(file_path))
296
+ df = pybcsv.read_dataframe(str(file_path))
297
297
  elapsed_ms = (time.perf_counter() - start) * 1000.0
298
298
  return elapsed_ms, len(df)
299
299
 
@@ -339,6 +339,47 @@ size_t bcsv_sampler_source_row_pos (const_bcsv_sampler_t sampler);
339
339
  // Last error/compile-error message (empty if no error)
340
340
  const char* bcsv_sampler_error_msg (const_bcsv_sampler_t sampler);
341
341
 
342
+ // ============================================================================
343
+ // Columnar Bulk I/O API
344
+ // ============================================================================
345
+ // Read up to max_rows rows from an opened reader into column-oriented buffers.
346
+ // bufs is an array of num_cols pointers. For numeric columns, each pointer must
347
+ // point to a pre-allocated buffer of element type matching the column type.
348
+ // For string columns, the pointer is ignored (strings are stored internally
349
+ // and retrieved via bcsv_reader_column_string).
350
+ // Returns the number of rows actually read (may be less than max_rows at EOF).
351
+ size_t bcsv_reader_read_columns (bcsv_reader_t reader, void** bufs,
352
+ size_t num_cols, size_t max_rows);
353
+
354
+ // Retrieve a string from the internal buffer after bcsv_reader_read_columns.
355
+ // col is the column index, row is the row index (0-based within the batch).
356
+ // Returns empty string if indices are out of range.
357
+ const char* bcsv_reader_column_string (bcsv_reader_t reader, size_t col, size_t row);
358
+
359
+ // Number of strings stored for a given column after bcsv_reader_read_columns.
360
+ size_t bcsv_reader_column_string_count(bcsv_reader_t reader, size_t col);
361
+
362
+ // Pack all strings for a column into a contiguous null-terminated buffer.
363
+ // Returns total bytes needed (including null terminators).
364
+ // If out_buf is NULL or buf_size is 0, returns required size without writing.
365
+ size_t bcsv_reader_column_strings_packed(bcsv_reader_t reader, size_t col,
366
+ char* out_buf, size_t buf_size);
367
+
368
+ // Write num_rows rows from column-oriented buffers through an opened writer.
369
+ // bufs is an array of num_cols pointers. For numeric columns, each pointer
370
+ // must point to a buffer of element type matching the column type.
371
+ // For string columns, bufs[c] must be a const char** (array of C string
372
+ // pointers, one per row). Returns true on success.
373
+ bool bcsv_writer_write_columns (bcsv_writer_t writer, const void** bufs,
374
+ size_t num_cols, size_t num_rows);
375
+
376
+ // Return the compact data size in bytes for a single row (sum of fixed-size
377
+ // column widths — excludes strings). Useful for buffer pre-allocation.
378
+ size_t bcsv_layout_row_data_size (const_bcsv_layout_t layout);
379
+
380
+ // Retrieve file flags as reported by the reader (only valid after open).
381
+ int bcsv_reader_file_flags (const_bcsv_reader_t reader);
382
+
342
383
  #ifdef __cplusplus
343
384
  }
344
385
  #endif
@@ -113,19 +113,33 @@ namespace bcsv::compat {
113
113
  return std::to_chars(first, last, value);
114
114
  }
115
115
 
116
- /// float fallback via snprintf.
116
+ /// float fallback via snprintf — shortest round-trip representation.
117
117
  inline std::to_chars_result to_chars(char* first, char* last, float value) {
118
118
  auto bufSize = static_cast<size_t>(last - first);
119
- int n = std::snprintf(first, bufSize, "%.9g", value);
119
+ // Try digits10 first (6 digits); if round-trip fails, use max_digits10 (9).
120
+ int n = std::snprintf(first, bufSize, "%.6g", value);
121
+ if (n >= 0 && static_cast<size_t>(n) < bufSize) {
122
+ float check = std::strtof(first, nullptr);
123
+ if (check != value) {
124
+ n = std::snprintf(first, bufSize, "%.9g", value);
125
+ }
126
+ }
120
127
  if (n < 0 || static_cast<size_t>(n) >= bufSize)
121
128
  return {last, std::errc::value_too_large};
122
129
  return {first + n, std::errc{}};
123
130
  }
124
131
 
125
- /// double fallback via snprintf.
132
+ /// double fallback via snprintf — shortest round-trip representation.
126
133
  inline std::to_chars_result to_chars(char* first, char* last, double value) {
127
134
  auto bufSize = static_cast<size_t>(last - first);
128
- int n = std::snprintf(first, bufSize, "%.17g", value);
135
+ // Try digits10 first (15 digits); if round-trip fails, use max_digits10 (17).
136
+ int n = std::snprintf(first, bufSize, "%.15g", value);
137
+ if (n >= 0 && static_cast<size_t>(n) < bufSize) {
138
+ double check = std::strtod(first, nullptr);
139
+ if (check != value) {
140
+ n = std::snprintf(first, bufSize, "%.17g", value);
141
+ }
142
+ }
129
143
  if (n < 0 || static_cast<size_t>(n) >= bufSize)
130
144
  return {last, std::errc::value_too_large};
131
145
  return {first + n, std::errc{}};
@@ -39,8 +39,6 @@ except ImportError:
39
39
  raise ImportError(_STUB_MSG)
40
40
  def write_columns(*args, **kwargs):
41
41
  raise ImportError(_STUB_MSG)
42
- def read_to_dataframe(*args, **kwargs):
43
- raise ImportError(_STUB_MSG)
44
42
  def type_to_string(*args, **kwargs):
45
43
  raise ImportError(_STUB_MSG)
46
44
  def read_to_arrow(*args, **kwargs):
@@ -101,7 +99,6 @@ __all__ = [
101
99
  # Columnar I/O
102
100
  "read_columns",
103
101
  "write_columns",
104
- "read_to_dataframe",
105
102
  # Arrow interop
106
103
  "read_to_arrow",
107
104
  "write_from_arrow",
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.4.1'
32
- __version_tuple__ = version_tuple = (1, 4, 1)
31
+ __version__ = version = '1.4.3'
32
+ __version_tuple__ = version_tuple = (1, 4, 3)
33
33
 
34
- __commit_id__ = commit_id = 'g85f1911cd'
34
+ __commit_id__ = commit_id = 'gb295a0343'
@@ -261,6 +261,40 @@ namespace {
261
261
  }
262
262
  }
263
263
 
264
+ // ── Shared columnar write loop ────────────────────────────────────
265
+ // Used by both write_columns and write_from_arrow.
266
+
267
+ inline void write_columnar_core(
268
+ const bcsv::Layout& layout,
269
+ const std::string& row_codec,
270
+ const std::string& filename,
271
+ size_t num_rows, size_t num_cols,
272
+ const std::vector<const void*>& bufs,
273
+ const std::vector<std::vector<std::string>>& string_cols,
274
+ const std::vector<bool>& is_string,
275
+ const std::vector<bcsv::ColumnType>& col_types,
276
+ size_t compression_level,
277
+ bcsv::FileFlags flags) {
278
+ PyWriter pw(layout, row_codec);
279
+ pw.visit([&](auto& w) {
280
+ nb::gil_scoped_release release;
281
+ if (!w.open(filename, true, compression_level, 64, flags))
282
+ throw std::runtime_error("Failed to open file for writing: " + filename);
283
+ for (size_t r = 0; r < num_rows; ++r) {
284
+ auto& row = w.row();
285
+ for (size_t c = 0; c < num_cols; ++c) {
286
+ if (is_string[c]) {
287
+ row.set(c, string_cols[c][r]);
288
+ } else {
289
+ set_from_numpy(row, c, col_types[c], bufs[c], r);
290
+ }
291
+ }
292
+ w.writeRow();
293
+ }
294
+ w.close();
295
+ });
296
+ }
297
+
264
298
  // ── Shared binding helpers for reader-like / writer-like classes ───
265
299
  // Attaches read_row, read_all, __enter__, __exit__, __iter__, __next__
266
300
  // to any class that has .readNext(), .row(), .layout(), .close().
@@ -275,8 +309,18 @@ namespace {
275
309
  .def("read_all", [](ReaderT& r) -> nb::list {
276
310
  nb::list result;
277
311
  const auto& layout = r.layout();
278
- while (r.readNext())
279
- result.append(row_to_list(r.row(), layout));
312
+ const size_t n = layout.columnCount();
313
+ // Cache column types to avoid per-row layout lookups
314
+ std::vector<bcsv::ColumnType> col_types(n);
315
+ for (size_t i = 0; i < n; ++i)
316
+ col_types[i] = layout.columnType(i);
317
+ while (r.readNext()) {
318
+ const auto& row = r.row();
319
+ nb::list row_list;
320
+ for (size_t i = 0; i < n; ++i)
321
+ row_list.append(extract_column_value(row, i, col_types[i]));
322
+ result.append(std::move(row_list));
323
+ }
280
324
  return result;
281
325
  })
282
326
  .def("__enter__", [](ReaderT& r) -> ReaderT& { return r; }, nb::rv_policy::reference)
@@ -602,7 +646,10 @@ NB_MODULE(_bcsv, m) {
602
646
  return success;
603
647
  }, nb::arg("filename"))
604
648
  .def("layout", &ReaderT::layout, nb::rv_policy::reference_internal)
605
- .def("read_next", &ReaderT::readNext)
649
+ .def("read_next", [](ReaderT& r) {
650
+ nb::gil_scoped_release release;
651
+ return r.readNext();
652
+ })
606
653
  .def("close", [](ReaderT& reader) { nb::gil_scoped_release release; reader.close(); })
607
654
  .def("is_open", &ReaderT::isOpen)
608
655
  .def("file_flags", [](ReaderT& r) -> int { return static_cast<int>(r.fileFlags()); })
@@ -638,7 +685,81 @@ NB_MODULE(_bcsv, m) {
638
685
  result[nb::cast(layout.columnName(i))] =
639
686
  extract_column_value(row, i, layout.columnType(i));
640
687
  return result;
641
- }, "Get the current row as a dict {column_name: value}");
688
+ }, "Get the current row as a dict {column_name: value}")
689
+ .def("read_batch", [](ReaderT& r, size_t batch_size) -> nb::object {
690
+ const auto& layout = r.layout();
691
+ const size_t num_cols = layout.columnCount();
692
+ if (num_cols == 0)
693
+ throw std::runtime_error("Reader has no columns");
694
+
695
+ // Cache column metadata
696
+ std::vector<bcsv::ColumnType> col_types(num_cols);
697
+ std::vector<std::string> col_names(num_cols);
698
+ std::vector<bool> is_str(num_cols, false);
699
+ for (size_t c = 0; c < num_cols; ++c) {
700
+ col_types[c] = layout.columnType(c);
701
+ col_names[c] = layout.columnName(c);
702
+ is_str[c] = (col_types[c] == bcsv::ColumnType::STRING);
703
+ }
704
+
705
+ // Allocate numpy arrays (numeric) and C++ string vectors (strings)
706
+ auto np = nb::module_::import_("numpy");
707
+ std::vector<nb::object> arrays(num_cols);
708
+ std::vector<void*> bufs(num_cols, nullptr);
709
+ std::vector<std::vector<std::string>> string_cols(num_cols);
710
+
711
+ for (size_t c = 0; c < num_cols; ++c) {
712
+ if (is_str[c]) {
713
+ string_cols[c].reserve(batch_size);
714
+ } else {
715
+ nb::object shape = nb::make_tuple(static_cast<int64_t>(batch_size));
716
+ arrays[c] = np.attr("empty")(shape,
717
+ "dtype"_a = bcsv_type_to_numpy_dtype(col_types[c]));
718
+ bufs[c] = reinterpret_cast<void*>(
719
+ nb::cast<intptr_t>(arrays[c].attr("ctypes").attr("data")));
720
+ }
721
+ }
722
+
723
+ // Read loop under GIL release
724
+ size_t rows_read = 0;
725
+ {
726
+ nb::gil_scoped_release release;
727
+ while (rows_read < batch_size && r.readNext()) {
728
+ const auto& row = r.row();
729
+ for (size_t c = 0; c < num_cols; ++c) {
730
+ if (is_str[c]) {
731
+ string_cols[c].emplace_back(row.template get<std::string>(c));
732
+ } else {
733
+ fill_numpy_cell(row, c, col_types[c], bufs[c], rows_read);
734
+ }
735
+ }
736
+ ++rows_read;
737
+ }
738
+ }
739
+
740
+ if (rows_read == 0) return nb::none();
741
+
742
+ // Build result dict
743
+ nb::dict result;
744
+ for (size_t c = 0; c < num_cols; ++c) {
745
+ if (is_str[c]) {
746
+ nb::list str_list;
747
+ for (auto& s : string_cols[c])
748
+ str_list.append(nb::cast(std::move(s)));
749
+ result[nb::cast(col_names[c])] = std::move(str_list);
750
+ } else {
751
+ if (rows_read < batch_size) {
752
+ // Slice to actual row count
753
+ auto stop = nb::int_(static_cast<int64_t>(rows_read));
754
+ auto sl = nb::steal(PySlice_New(Py_None, stop.ptr(), Py_None));
755
+ arrays[c] = arrays[c][sl];
756
+ }
757
+ result[nb::cast(col_names[c])] = std::move(arrays[c]);
758
+ }
759
+ }
760
+ return result;
761
+ }, nb::arg("batch_size") = 10000,
762
+ "Read up to batch_size rows into a dict of numpy arrays/lists. Returns None at EOF.");
642
763
  bind_reader_iteration<ReaderT>(reader_cls);
643
764
 
644
765
  // Utility functions
@@ -908,102 +1029,6 @@ NB_MODULE(_bcsv, m) {
908
1029
  }, nb::arg("filename"),
909
1030
  "Read a BCSV file into a dict of numpy arrays (numeric) and lists (strings)");
910
1031
 
911
- // ── Columnar I/O: read_to_dataframe ────────────────────────────────
912
-
913
- m.def("read_to_dataframe", [](const std::string& filename,
914
- const std::optional<nb::list>& columns) -> nb::object {
915
- // Phase 1: Open via ReaderDirectAccess
916
- bcsv::ReaderDirectAccess<bcsv::Layout> reader;
917
- {
918
- nb::gil_scoped_release release;
919
- if (!reader.open(filename, true)) {
920
- throw std::runtime_error("Failed to open file: " + filename);
921
- }
922
- }
923
- const size_t num_rows = reader.rowCount();
924
- const auto& layout = reader.layout();
925
- const size_t num_cols = layout.columnCount();
926
- if (num_cols == 0)
927
- throw std::runtime_error("File has no columns: " + filename);
928
-
929
- // Cache column metadata
930
- std::vector<std::string> col_names(num_cols);
931
- std::vector<bcsv::ColumnType> col_types(num_cols);
932
- std::vector<bool> is_string(num_cols, false);
933
- for (size_t i = 0; i < num_cols; ++i) {
934
- col_names[i] = layout.columnName(i);
935
- col_types[i] = layout.columnType(i);
936
- is_string[i] = (col_types[i] == bcsv::ColumnType::STRING);
937
- }
938
-
939
- // Phase 2: Allocate numpy arrays / string vectors
940
- auto np = nb::module_::import_("numpy");
941
- std::vector<nb::object> arrays(num_cols);
942
- std::vector<void*> bufs(num_cols, nullptr);
943
- std::vector<std::vector<std::string>> string_cols(num_cols);
944
-
945
- for (size_t c = 0; c < num_cols; ++c) {
946
- if (is_string[c]) {
947
- string_cols[c].reserve(num_rows);
948
- } else {
949
- nb::object shape = nb::make_tuple(static_cast<int64_t>(num_rows));
950
- arrays[c] = np.attr("empty")(shape, "dtype"_a = bcsv_type_to_numpy_dtype(col_types[c]));
951
- bufs[c] = reinterpret_cast<void*>(nb::cast<intptr_t>(arrays[c].attr("ctypes").attr("data")));
952
- }
953
- }
954
-
955
- // Phase 3: Entire read loop under GIL release
956
- {
957
- nb::gil_scoped_release release;
958
- size_t row_idx = 0;
959
- while (reader.readNext()) {
960
- const auto& row = reader.row();
961
- for (size_t c = 0; c < num_cols; ++c) {
962
- if (is_string[c]) {
963
- string_cols[c].emplace_back(row.template get<std::string>(c));
964
- } else {
965
- fill_numpy_cell(row, c, col_types[c], bufs[c], row_idx);
966
- }
967
- }
968
- ++row_idx;
969
- }
970
- reader.close();
971
- }
972
-
973
- // Phase 4: Build column dict
974
- nb::dict col_dict;
975
- for (size_t c = 0; c < num_cols; ++c) {
976
- if (is_string[c]) {
977
- nb::list str_list;
978
- for (size_t i = 0; i < string_cols[c].size(); ++i) {
979
- str_list.append(nb::cast(std::move(string_cols[c][i])));
980
- }
981
- col_dict[nb::cast(col_names[c])] = std::move(str_list);
982
- } else {
983
- col_dict[nb::cast(col_names[c])] = std::move(arrays[c]);
984
- }
985
- }
986
-
987
- // Phase 5: Filter columns if requested
988
- if (columns.has_value()) {
989
- nb::dict filtered;
990
- const nb::list& cols_list = columns.value();
991
- for (size_t i = 0; i < cols_list.size(); ++i) {
992
- nb::handle col_name = cols_list[i];
993
- auto key = nb::cast<std::string>(col_name);
994
- if (col_dict.contains(nb::cast(key))) {
995
- filtered[col_name] = col_dict[col_name];
996
- }
997
- }
998
- col_dict = std::move(filtered);
999
- }
1000
-
1001
- // Phase 6: Construct pd.DataFrame in C++
1002
- auto pd = nb::module_::import_("pandas");
1003
- return pd.attr("DataFrame")(col_dict);
1004
- }, nb::arg("filename"), nb::arg("columns") = nb::none(),
1005
- "Read a BCSV file directly into a pandas DataFrame");
1006
-
1007
1032
  // ── Columnar I/O: write_columns ────────────────────────────────────
1008
1033
 
1009
1034
  m.def("write_columns", [](const std::string& filename, const nb::dict& columns,
@@ -1063,26 +1088,10 @@ NB_MODULE(_bcsv, m) {
1063
1088
  }
1064
1089
  }
1065
1090
 
1066
- // Create writer and run entire write loop under GIL release
1067
- PyWriter pw(layout, row_codec);
1068
- pw.visit([&](auto& w) {
1069
- nb::gil_scoped_release release;
1070
- if (!w.open(filename, true, compression_level, 64, flags)) {
1071
- throw std::runtime_error("Failed to open file for writing: " + filename);
1072
- }
1073
- for (size_t r = 0; r < num_rows; ++r) {
1074
- auto& row = w.row();
1075
- for (size_t c = 0; c < num_cols; ++c) {
1076
- if (is_string[c]) {
1077
- row.set(c, string_cols[c][r]);
1078
- } else {
1079
- set_from_numpy(row, c, col_types[c], bufs[c], r);
1080
- }
1081
- }
1082
- w.writeRow();
1083
- }
1084
- w.close();
1085
- });
1091
+ // Write via shared helper
1092
+ write_columnar_core(layout, row_codec, filename, num_rows, num_cols,
1093
+ bufs, string_cols, is_string, col_types,
1094
+ compression_level, flags);
1086
1095
  }, nb::arg("filename"), nb::arg("columns"), nb::arg("col_order"),
1087
1096
  nb::arg("col_types"), nb::arg("row_codec") = "delta",
1088
1097
  nb::arg("compression_level") = 1,
@@ -1493,25 +1502,12 @@ NB_MODULE(_bcsv, m) {
1493
1502
  }
1494
1503
  }
1495
1504
 
1496
- // Write under GIL release
1497
- PyWriter pw(layout, row_codec);
1498
- pw.visit([&](auto& w) {
1499
- nb::gil_scoped_release release;
1500
- if (!w.open(filename, true, compression_level, 64, flags))
1501
- throw std::runtime_error("Failed to open file for writing: " + filename);
1502
- for (int64_t r = 0; r < num_rows; ++r) {
1503
- auto& row = w.row();
1504
- for (int64_t c = 0; c < num_cols; ++c) {
1505
- if (is_string[c]) {
1506
- row.set(c, string_cols[c][r]);
1507
- } else {
1508
- set_from_numpy(row, c, col_types[c], bufs[c], static_cast<size_t>(r));
1509
- }
1510
- }
1511
- w.writeRow();
1512
- }
1513
- w.close();
1514
- });
1505
+ // Write via shared helper
1506
+ write_columnar_core(layout, row_codec, filename,
1507
+ static_cast<size_t>(num_rows),
1508
+ static_cast<size_t>(num_cols),
1509
+ bufs, string_cols, is_string, col_types,
1510
+ compression_level, flags);
1515
1511
  }, nb::arg("filename"), nb::arg("table"),
1516
1512
  nb::arg("row_codec") = "delta",
1517
1513
  nb::arg("compression_level") = 1,
@@ -23,12 +23,10 @@ from . import Layout, ColumnType, Writer, Reader, FileFlags
23
23
  # Import columnar I/O if available (requires numpy headers in bindings)
24
24
  try:
25
25
  from ._bcsv import read_columns as _read_columns, write_columns as _write_columns
26
- from ._bcsv import read_to_dataframe as _read_to_dataframe
27
26
  _COLUMNAR_AVAILABLE = True
28
27
  except ImportError:
29
28
  try:
30
29
  from _bcsv import read_columns as _read_columns, write_columns as _write_columns
31
- from _bcsv import read_to_dataframe as _read_to_dataframe
32
30
  _COLUMNAR_AVAILABLE = True
33
31
  except ImportError:
34
32
  _COLUMNAR_AVAILABLE = False
@@ -185,10 +183,6 @@ def read_dataframe(filename: str,
185
183
  table = _read_to_arrow(filename, columns=columns)
186
184
  return table.to_pandas()
187
185
 
188
- # Fast path: C++ read_to_dataframe (single call, constructs DataFrame in C++)
189
- if _COLUMNAR_AVAILABLE:
190
- return _read_to_dataframe(filename, columns)
191
-
192
186
  reader = Reader()
193
187
  try:
194
188
  if not reader.open(filename):
@@ -1,233 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- # Copyright (c) 2025-2026 Tobias Weber <weber.tobias.md@gmail.com>
4
- #
5
- # This file is part of the BCSV library.
6
- #
7
- # Licensed under the MIT License. See LICENSE file in the project root
8
- # for full license information.
9
-
10
- """
11
- Comprehensive test runner for PyBCSV unit tests.
12
- Runs all test suites and provides a summary report.
13
- """
14
-
15
- import unittest
16
- import sys
17
- import os
18
- import time
19
- from io import StringIO
20
-
21
- # Add the parent directory to Python path to import tests
22
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23
-
24
- # Import all test modules
25
- from test_basic_functionality import TestBasicFunctionality
26
- from test_interoperability import TestInteroperability
27
- from test_error_handling_edge_cases import TestErrorHandling
28
- from test_pandas_integration import TestPandasIntegration
29
- from test_performance_edge_cases import TestPerformanceEdgeCases
30
-
31
-
32
- class ColoredTextTestResult(unittest.TextTestResult):
33
- """Test result with colored output for better readability."""
34
-
35
- def startTest(self, test):
36
- super().startTest(test)
37
- self.stream.write(f"Running {test._testMethodName}... ")
38
- self.stream.flush()
39
-
40
- def addSuccess(self, test):
41
- super().addSuccess(test)
42
- self.stream.write("✅ PASS\n")
43
- self.stream.flush()
44
-
45
- def addError(self, test, err):
46
- super().addError(test, err)
47
- self.stream.write("❌ ERROR\n")
48
- self.stream.flush()
49
-
50
- def addFailure(self, test, err):
51
- super().addFailure(test, err)
52
- self.stream.write("❌ FAIL\n")
53
- self.stream.flush()
54
-
55
- def addSkip(self, test, reason):
56
- super().addSkip(test, reason)
57
- self.stream.write(f"⏭️ SKIP ({reason})\n")
58
- self.stream.flush()
59
-
60
-
61
- def run_test_suite(test_class, suite_name):
62
- """Run a specific test suite and return results."""
63
- print(f"\n{'='*60}")
64
- print(f"🧪 {suite_name}")
65
- print(f"{'='*60}")
66
-
67
- # Create test suite
68
- loader = unittest.TestLoader()
69
- suite = loader.loadTestsFromTestCase(test_class)
70
-
71
- # Run tests with custom result class
72
- stream = sys.stdout
73
- runner = unittest.TextTestRunner(
74
- stream=stream,
75
- verbosity=0,
76
- resultclass=ColoredTextTestResult
77
- )
78
-
79
- start_time = time.time()
80
- result = runner.run(suite)
81
- end_time = time.time()
82
-
83
- # Print summary for this suite
84
- total_tests = result.testsRun
85
- failures = len(result.failures)
86
- errors = len(result.errors)
87
- skipped = len(result.skipped)
88
- passed = total_tests - failures - errors - skipped
89
-
90
- print(f"\n📊 Suite Summary:")
91
- print(f" ✅ Passed: {passed}/{total_tests}")
92
- if failures > 0:
93
- print(f" ❌ Failed: {failures}")
94
- if errors > 0:
95
- print(f" 💥 Errors: {errors}")
96
- if skipped > 0:
97
- print(f" ⏭️ Skipped: {skipped}")
98
- print(f" ⏱️ Time: {end_time - start_time:.2f}s")
99
-
100
- return result, end_time - start_time
101
-
102
-
103
- def print_detailed_failures(all_results):
104
- """Print detailed information about failures and errors."""
105
- has_failures = False
106
-
107
- for suite_name, result, _ in all_results:
108
- if result.failures or result.errors:
109
- if not has_failures:
110
- print(f"\n{'='*60}")
111
- print("🔍 DETAILED FAILURE/ERROR REPORT")
112
- print(f"{'='*60}")
113
- has_failures = True
114
-
115
- print(f"\n📋 {suite_name}:")
116
-
117
- for test, traceback in result.failures:
118
- print(f"\n❌ FAILURE: {test}")
119
- print("-" * 40)
120
- print(traceback)
121
-
122
- for test, traceback in result.errors:
123
- print(f"\n💥 ERROR: {test}")
124
- print("-" * 40)
125
- print(traceback)
126
-
127
-
128
- def main():
129
- """Run all PyBCSV tests and provide comprehensive report."""
130
- print("🚀 PyBCSV Comprehensive Test Suite")
131
- print("Testing Python BCSV implementation for:")
132
- print(" • Core functionality and data integrity")
133
- print(" • C++/Python interoperability")
134
- print(" • Error handling and edge cases")
135
- print(" • Pandas DataFrame integration")
136
- print(" • Performance and boundary conditions")
137
-
138
- # Test suites to run
139
- test_suites = [
140
- (TestBasicFunctionality, "Basic Functionality Tests"),
141
- (TestInteroperability, "C++/Python Interoperability Tests"),
142
- (TestErrorHandling, "Error Handling & Edge Cases"),
143
- (TestPandasIntegration, "Pandas Integration Tests"),
144
- (TestPerformanceEdgeCases, "Performance & Edge Cases"),
145
- ]
146
-
147
- # Run all test suites
148
- all_results = []
149
- total_start_time = time.time()
150
-
151
- for test_class, suite_name in test_suites:
152
- try:
153
- result, duration = run_test_suite(test_class, suite_name)
154
- all_results.append((suite_name, result, duration))
155
- except Exception as e:
156
- print(f"❌ Failed to run {suite_name}: {e}")
157
- continue
158
-
159
- total_end_time = time.time()
160
-
161
- # Calculate overall statistics
162
- total_tests = sum(result.testsRun for _, result, _ in all_results)
163
- total_failures = sum(len(result.failures) for _, result, _ in all_results)
164
- total_errors = sum(len(result.errors) for _, result, _ in all_results)
165
- total_skipped = sum(len(result.skipped) for _, result, _ in all_results)
166
- total_passed = total_tests - total_failures - total_errors - total_skipped
167
- total_time = total_end_time - total_start_time
168
-
169
- # Print overall summary
170
- print(f"\n{'='*60}")
171
- print("🎯 OVERALL TEST SUMMARY")
172
- print(f"{'='*60}")
173
- print(f"📊 Total Tests: {total_tests}")
174
- print(f"✅ Passed: {total_passed} ({total_passed/total_tests*100:.1f}%)" if total_tests > 0 else "✅ Passed: 0")
175
- print(f"❌ Failed: {total_failures}")
176
- print(f"💥 Errors: {total_errors}")
177
- print(f"⏭️ Skipped: {total_skipped}")
178
- print(f"⏱️ Total Time: {total_time:.2f}s")
179
-
180
- # Success rate
181
- if total_tests > 0:
182
- success_rate = (total_passed / total_tests) * 100
183
- if success_rate >= 95:
184
- print(f"🎉 Excellent! {success_rate:.1f}% success rate")
185
- elif success_rate >= 80:
186
- print(f"✅ Good! {success_rate:.1f}% success rate")
187
- elif success_rate >= 60:
188
- print(f"⚠️ Acceptable: {success_rate:.1f}% success rate")
189
- else:
190
- print(f"❌ Needs improvement: {success_rate:.1f}% success rate")
191
-
192
- # Per-suite breakdown
193
- print(f"\n📋 Per-Suite Results:")
194
- for suite_name, result, duration in all_results:
195
- total = result.testsRun
196
- passed = total - len(result.failures) - len(result.errors) - len(result.skipped)
197
- status = "✅" if len(result.failures) == 0 and len(result.errors) == 0 else "❌"
198
- print(f" {status} {suite_name}: {passed}/{total} passed ({duration:.2f}s)")
199
-
200
- # Print detailed failures if any
201
- print_detailed_failures(all_results)
202
-
203
- # Final recommendations
204
- print(f"\n{'='*60}")
205
- print("📝 RECOMMENDATIONS")
206
- print(f"{'='*60}")
207
-
208
- if total_failures == 0 and total_errors == 0:
209
- print("🎉 All tests passed! PyBCSV implementation is working correctly.")
210
- print("✅ The Python wrapper demonstrates:")
211
- print(" • Correct basic functionality")
212
- print(" • Proper error handling")
213
- print(" • C++/Python interoperability")
214
- print(" • Pandas integration")
215
- print(" • Good performance characteristics")
216
- else:
217
- print("⚠️ Some tests failed. Please review the detailed failure report above.")
218
- print("🔧 Recommended actions:")
219
- if total_errors > 0:
220
- print(" • Fix critical errors that prevent test execution")
221
- if total_failures > 0:
222
- print(" • Address test failures to ensure correct behavior")
223
- if total_skipped > 0:
224
- print(" • Review skipped tests and their skip reasons")
225
-
226
- # Return exit code
227
- exit_code = 0 if (total_failures == 0 and total_errors == 0) else 1
228
- return exit_code
229
-
230
-
231
- if __name__ == '__main__':
232
- exit_code = main()
233
- sys.exit(exit_code)
@@ -1,92 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test runner for the pybcsv test suite.
4
-
5
- This script runs all tests and provides a summary of results.
6
- """
7
-
8
- import unittest
9
- import sys
10
- import os
11
-
12
- # Add the pybcsv package to the path
13
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
14
-
15
- def run_tests():
16
- """Run all tests and return the result."""
17
- # Discover and run all tests
18
- loader = unittest.TestLoader()
19
- start_dir = os.path.dirname(__file__)
20
- suite = loader.discover(start_dir, pattern='test_*.py')
21
-
22
- # Run tests with verbose output
23
- runner = unittest.TextTestRunner(verbosity=2, buffer=True)
24
- result = runner.run(suite)
25
-
26
- return result
27
-
28
- def main():
29
- """Main test runner function."""
30
- print("=" * 60)
31
- print("Running pybcsv test suite")
32
- print("=" * 60)
33
-
34
- try:
35
- import pybcsv
36
- print(f"✓ pybcsv version {pybcsv.__version__} loaded successfully")
37
- except ImportError as e:
38
- print(f"❌ Failed to import pybcsv: {e}")
39
- print("Make sure pybcsv is installed: pip install -e .")
40
- return 1
41
-
42
- # Check optional dependencies
43
- try:
44
- import pandas
45
- print(f"✓ pandas {pandas.__version__} available")
46
- except ImportError:
47
- print("⚠ pandas not available - some tests will be skipped")
48
-
49
- try:
50
- import numpy
51
- print(f"✓ numpy {numpy.__version__} available")
52
- except ImportError:
53
- print("⚠ numpy not available - some tests may fail")
54
-
55
- print("\n" + "=" * 60)
56
- print("Starting tests...")
57
- print("=" * 60)
58
-
59
- result = run_tests()
60
-
61
- print("\n" + "=" * 60)
62
- print("Test Summary")
63
- print("=" * 60)
64
-
65
- if result.wasSuccessful():
66
- print(f"✅ All tests passed!")
67
- print(f" Tests run: {result.testsRun}")
68
- if result.skipped:
69
- print(f" Tests skipped: {len(result.skipped)}")
70
- return 0
71
- else:
72
- print(f"❌ Some tests failed")
73
- print(f" Tests run: {result.testsRun}")
74
- print(f" Failures: {len(result.failures)}")
75
- print(f" Errors: {len(result.errors)}")
76
- if result.skipped:
77
- print(f" Tests skipped: {len(result.skipped)}")
78
-
79
- if result.failures:
80
- print("\nFailures:")
81
- for test, traceback in result.failures:
82
- print(f" - {test}")
83
-
84
- if result.errors:
85
- print("\nErrors:")
86
- for test, traceback in result.errors:
87
- print(f" - {test}")
88
-
89
- return 1
90
-
91
- if __name__ == '__main__':
92
- sys.exit(main())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes