csvsmith 0.11.1__tar.gz → 0.13.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 (47) hide show
  1. {csvsmith-0.11.1/src/csvsmith.egg-info → csvsmith-0.13.0}/PKG-INFO +2 -24
  2. {csvsmith-0.11.1 → csvsmith-0.13.0}/pyproject.toml +4 -4
  3. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/__init__.py +10 -1
  4. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/cli.py +33 -0
  5. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/csv_viewer.py +23 -0
  6. csvsmith-0.13.0/src/csvsmith/tools/strip_bom.py +52 -0
  7. {csvsmith-0.11.1 → csvsmith-0.13.0/src/csvsmith.egg-info}/PKG-INFO +2 -24
  8. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith.egg-info/SOURCES.txt +3 -1
  9. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_cli.py +28 -0
  10. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_csv_viewer.py +52 -0
  11. csvsmith-0.13.0/tests/test_strip_bom.py +63 -0
  12. {csvsmith-0.11.1 → csvsmith-0.13.0}/LICENSE +0 -0
  13. {csvsmith-0.11.1 → csvsmith-0.13.0}/README.rst +0 -0
  14. {csvsmith-0.11.1 → csvsmith-0.13.0}/setup.cfg +0 -0
  15. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/__init__.py +0 -0
  16. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/classify.py +0 -0
  17. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/dense_csv.py +0 -0
  18. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/excel2csv.py +0 -0
  19. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/filter_rows.py +0 -0
  20. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/find_matches_in_csv.py +0 -0
  21. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/knapsack_csv.py +0 -0
  22. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/move_files.py +0 -0
  23. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/row_dedup.py +0 -0
  24. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/sample_csv.py +0 -0
  25. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/tools/strict_concat.py +0 -0
  26. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/utils/__init__.py +0 -0
  27. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/utils/clean_numeric.py +0 -0
  28. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/utils/distance.py +0 -0
  29. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/utils/io.py +0 -0
  30. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith/utils/normalize.py +0 -0
  31. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith.egg-info/dependency_links.txt +0 -0
  32. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith.egg-info/entry_points.txt +0 -0
  33. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith.egg-info/requires.txt +0 -0
  34. {csvsmith-0.11.1 → csvsmith-0.13.0}/src/csvsmith.egg-info/top_level.txt +0 -0
  35. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_classify.py +0 -0
  36. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_clean_numeric.py +0 -0
  37. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_dense_csv.py +0 -0
  38. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_excel2csv.py +0 -0
  39. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_filter_rows.py +0 -0
  40. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_find_matches_in_csv.py +0 -0
  41. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_knapsack_csv.py +0 -0
  42. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_move_files.py +0 -0
  43. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_normalize.py +0 -0
  44. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_row_dedup.py +0 -0
  45. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_sample_csv.py +0 -0
  46. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_strict_concat.py +0 -0
  47. {csvsmith-0.11.1 → csvsmith-0.13.0}/tests/test_string_distance.py +0 -0
@@ -1,37 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csvsmith
3
- Version: 0.11.1
3
+ Version: 0.13.0
4
4
  Summary: Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers.
5
5
  Author-email: Eiichi YAMAMOTO <info@yeiichi.com>
6
- License: MIT License
7
-
8
- Copyright (c) 2026 Eiichi YAMAMOTO
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in
18
- all copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
26
- IN THE SOFTWARE.
27
-
6
+ License-Expression: MIT
28
7
  Project-URL: Homepage, https://github.com/yeiichi/csvsmith
29
8
  Project-URL: Repository, https://github.com/yeiichi/csvsmith
30
9
  Project-URL: Documentation, https://csvsmith.readthedocs.io/en/latest/
31
10
  Keywords: csv,deduplication,data-filtering,file-organization,filtering
32
11
  Classifier: Programming Language :: Python :: 3
33
12
  Classifier: Programming Language :: Python :: 3 :: Only
34
- Classifier: License :: OSI Approved :: MIT License
35
13
  Classifier: Intended Audience :: Developers
36
14
  Classifier: Topic :: Software Development :: Libraries
37
15
  Classifier: Topic :: Utilities
@@ -1,14 +1,15 @@
1
1
  [build-system]
2
- requires = ["setuptools>=68.0", "wheel"]
2
+ requires = ["setuptools>=77.0", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "csvsmith"
7
- version = "0.11.1"
7
+ version = "0.13.0"
8
8
  description = "Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers."
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.10"
11
- license = { file = "LICENSE" }
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
12
13
 
13
14
  authors = [
14
15
  { name = "Eiichi YAMAMOTO", email = "info@yeiichi.com" }
@@ -25,7 +26,6 @@ keywords = [
25
26
  classifiers = [
26
27
  "Programming Language :: Python :: 3",
27
28
  "Programming Language :: Python :: 3 :: Only",
28
- "License :: OSI Approved :: MIT License",
29
29
  "Intended Audience :: Developers",
30
30
  "Topic :: Software Development :: Libraries",
31
31
  "Topic :: Utilities",
@@ -21,6 +21,7 @@ Public API:
21
21
  - concentrate_csv
22
22
  - rehydrate_csv
23
23
  - create_sample_csv
24
+ - strip_utf8_bom
24
25
  - DataFrame
25
26
  - build_filter
26
27
  - infer_type
@@ -42,7 +43,12 @@ Submodules:
42
43
  - csvsmith.cli (CLI entrypoint)
43
44
  """
44
45
 
45
- __version__ = "0.8.0"
46
+ from importlib.metadata import PackageNotFoundError, version
47
+
48
+ try:
49
+ __version__ = version("csvsmith")
50
+ except PackageNotFoundError:
51
+ __version__ = "0.0.0"
46
52
 
47
53
  from .tools.classify import CSVClassifier
48
54
  from .tools.csv_viewer import DataFrame, build_filter, infer_type
@@ -67,6 +73,7 @@ from .tools.row_dedup import (
67
73
  dedupe_with_report,
68
74
  )
69
75
  from .tools.sample_csv import SampleCSVResult, create_sample_csv
76
+ from .tools.strip_bom import StripBomResult, strip_utf8_bom
70
77
  from .tools.strict_concat import save_csv, strict_concat_rows
71
78
  from .utils.clean_numeric import clean_numeric
72
79
  from .utils.distance import StringDistance, Relation, Result, analyze_pair
@@ -101,6 +108,8 @@ __all__ = [
101
108
  "rehydrate_csv",
102
109
  "SampleCSVResult",
103
110
  "create_sample_csv",
111
+ "StripBomResult",
112
+ "strip_utf8_bom",
104
113
  "DataFrame",
105
114
  "build_filter",
106
115
  "infer_type",
@@ -24,6 +24,7 @@ from .tools.sample_csv import (
24
24
  DEFAULT_ROW_COUNT,
25
25
  create_sample_csv,
26
26
  )
27
+ from .tools.strip_bom import strip_utf8_bom
27
28
  from .tools.strict_concat import save_csv, strict_concat_rows
28
29
  from .tools.find_matches_in_csv import find_matches_in_csv
29
30
  from .tools.knapsack_csv import mark_knapsack_csv
@@ -204,6 +205,18 @@ def cmd_find_matches(args: argparse.Namespace) -> int:
204
205
  return 0
205
206
 
206
207
 
208
+ def cmd_strip_bom(args: argparse.Namespace) -> int:
209
+ try:
210
+ result = strip_utf8_bom(args.input, args.output, in_place=args.in_place)
211
+ except (OSError, ValueError) as e:
212
+ print(f"Error: {e}", file=sys.stderr)
213
+ return 1
214
+
215
+ status = "removed BOM" if result.removed else "no BOM found"
216
+ print(f"Wrote CSV to: {result.output_path} ({status})")
217
+ return 0
218
+
219
+
207
220
  def cmd_strict_concat(args: argparse.Namespace) -> int:
208
221
  input_dir = Path(args.input_dir)
209
222
  if not input_dir.is_dir():
@@ -388,6 +401,25 @@ def _add_find_matches_parser(subparsers) -> None:
388
401
  parser.set_defaults(func=cmd_find_matches)
389
402
 
390
403
 
404
+ def _add_strip_bom_parser(subparsers) -> None:
405
+ parser = subparsers.add_parser(
406
+ "strip-bom",
407
+ help="Remove a leading UTF-8 BOM from a CSV file.",
408
+ )
409
+ parser.add_argument("input", help="Input CSV file.")
410
+ parser.add_argument(
411
+ "-o",
412
+ "--output",
413
+ help="Output CSV file (default: <input-stem>.no-bom.csv).",
414
+ )
415
+ parser.add_argument(
416
+ "--in-place",
417
+ action="store_true",
418
+ help="Rewrite the input file instead of writing a separate output file.",
419
+ )
420
+ parser.set_defaults(func=cmd_strip_bom)
421
+
422
+
391
423
  def _add_strict_concat_parser(subparsers) -> None:
392
424
  parser = subparsers.add_parser(
393
425
  "strict-concat",
@@ -664,6 +696,7 @@ def build_parser() -> argparse.ArgumentParser:
664
696
 
665
697
  _add_row_duplicates_parser(subparsers)
666
698
  _add_find_matches_parser(subparsers)
699
+ _add_strip_bom_parser(subparsers)
667
700
  _add_strict_concat_parser(subparsers)
668
701
  _add_concentrate_parser(subparsers)
669
702
  _add_rehydrate_parser(subparsers)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import csv
4
+ import io
4
5
  import operator
5
6
  from pathlib import Path
6
7
  from typing import Any, Callable, TextIO
@@ -88,6 +89,28 @@ class DataFrame:
88
89
  """Return the first ``n`` rows formatted as a text table."""
89
90
  return self.render(end=n)
90
91
 
92
+ def to_csv(
93
+ self,
94
+ filepath: str | Path | None = None,
95
+ *,
96
+ encoding: str = "utf-8",
97
+ ) -> str | Path:
98
+ """Return the ``DataFrame`` as CSV, or write it to ``filepath``."""
99
+ output = io.StringIO()
100
+ writer = csv.writer(output)
101
+ writer.writerow(self.columns)
102
+ for index in range(len(self)):
103
+ writer.writerow([self._data[column][index] for column in self.columns])
104
+ csv_text = output.getvalue()
105
+
106
+ if filepath is None:
107
+ return csv_text
108
+
109
+ path = Path(filepath)
110
+ path.parent.mkdir(parents=True, exist_ok=True)
111
+ path.write_text(csv_text, encoding=encoding, newline="")
112
+ return path
113
+
91
114
  def select(self, columns: list[str]) -> DataFrame:
92
115
  """Return a new ``DataFrame`` with the selected columns."""
93
116
  missing = [column for column in columns if column not in self._data]
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+
7
+ UTF8_BOM = b"\xef\xbb\xbf"
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class StripBomResult:
12
+ input_path: Path
13
+ output_path: Path
14
+ removed: bool
15
+
16
+
17
+ def default_output_path(csv_path: str | Path) -> Path:
18
+ """Return the default output path for a BOM-stripped CSV."""
19
+ path = Path(csv_path)
20
+ return path.with_name(f"{path.stem}.no-bom{path.suffix}")
21
+
22
+
23
+ def strip_utf8_bom(
24
+ csv_path: str | Path,
25
+ output_path: str | Path | None = None,
26
+ *,
27
+ in_place: bool = False,
28
+ ) -> StripBomResult:
29
+ """Remove a leading UTF-8 BOM from a CSV file while preserving all other bytes."""
30
+ input_path = Path(csv_path)
31
+ if in_place and output_path is not None:
32
+ raise ValueError("--in-place cannot be used with --output")
33
+
34
+ if in_place:
35
+ resolved_output_path = input_path
36
+ elif output_path:
37
+ resolved_output_path = Path(output_path)
38
+ else:
39
+ resolved_output_path = default_output_path(input_path)
40
+
41
+ data = input_path.read_bytes()
42
+ removed = data.startswith(UTF8_BOM)
43
+ output_data = data[len(UTF8_BOM) :] if removed else data
44
+
45
+ resolved_output_path.parent.mkdir(parents=True, exist_ok=True)
46
+ resolved_output_path.write_bytes(output_data)
47
+
48
+ return StripBomResult(
49
+ input_path=input_path,
50
+ output_path=resolved_output_path,
51
+ removed=removed,
52
+ )
@@ -1,37 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csvsmith
3
- Version: 0.11.1
3
+ Version: 0.13.0
4
4
  Summary: Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers.
5
5
  Author-email: Eiichi YAMAMOTO <info@yeiichi.com>
6
- License: MIT License
7
-
8
- Copyright (c) 2026 Eiichi YAMAMOTO
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in
18
- all copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
26
- IN THE SOFTWARE.
27
-
6
+ License-Expression: MIT
28
7
  Project-URL: Homepage, https://github.com/yeiichi/csvsmith
29
8
  Project-URL: Repository, https://github.com/yeiichi/csvsmith
30
9
  Project-URL: Documentation, https://csvsmith.readthedocs.io/en/latest/
31
10
  Keywords: csv,deduplication,data-filtering,file-organization,filtering
32
11
  Classifier: Programming Language :: Python :: 3
33
12
  Classifier: Programming Language :: Python :: 3 :: Only
34
- Classifier: License :: OSI Approved :: MIT License
35
13
  Classifier: Intended Audience :: Developers
36
14
  Classifier: Topic :: Software Development :: Libraries
37
15
  Classifier: Topic :: Utilities
@@ -21,6 +21,7 @@ src/csvsmith/tools/move_files.py
21
21
  src/csvsmith/tools/row_dedup.py
22
22
  src/csvsmith/tools/sample_csv.py
23
23
  src/csvsmith/tools/strict_concat.py
24
+ src/csvsmith/tools/strip_bom.py
24
25
  src/csvsmith/utils/__init__.py
25
26
  src/csvsmith/utils/clean_numeric.py
26
27
  src/csvsmith/utils/distance.py
@@ -40,4 +41,5 @@ tests/test_normalize.py
40
41
  tests/test_row_dedup.py
41
42
  tests/test_sample_csv.py
42
43
  tests/test_strict_concat.py
43
- tests/test_string_distance.py
44
+ tests/test_string_distance.py
45
+ tests/test_strip_bom.py
@@ -1,7 +1,15 @@
1
+ from importlib.metadata import version
2
+
1
3
  import pytest
4
+
5
+ import csvsmith
2
6
  from csvsmith.cli import build_parser, main
3
7
 
4
8
 
9
+ def test_package_version_matches_metadata():
10
+ assert csvsmith.__version__ == version("csvsmith")
11
+
12
+
5
13
  def test_main_help():
6
14
  with pytest.raises(SystemExit) as excinfo:
7
15
  main(["--help"])
@@ -190,6 +198,26 @@ def test_cli_parses_find_matches_command():
190
198
  assert args.no_nfkc is True
191
199
 
192
200
 
201
+ def test_cli_parses_strip_bom_command():
202
+ parser = build_parser()
203
+ args = parser.parse_args(["strip-bom", "input.csv", "-o", "output.csv"])
204
+
205
+ assert args.command == "strip-bom"
206
+ assert args.input == "input.csv"
207
+ assert args.output == "output.csv"
208
+ assert args.in_place is False
209
+
210
+
211
+ def test_cli_parses_strip_bom_command_in_place():
212
+ parser = build_parser()
213
+ args = parser.parse_args(["strip-bom", "input.csv", "--in-place"])
214
+
215
+ assert args.command == "strip-bom"
216
+ assert args.input == "input.csv"
217
+ assert args.output is None
218
+ assert args.in_place is True
219
+
220
+
193
221
  def test_cli_parses_strict_concat_command():
194
222
  parser = build_parser()
195
223
  args = parser.parse_args(["strict-concat", "input_dir", "-o", "output.csv"])
@@ -70,6 +70,58 @@ def test_dataframe_filter_supports_numeric_comparisons(tmp_path):
70
70
  assert df["name"] == ["Alice"]
71
71
 
72
72
 
73
+ def test_dataframe_to_csv_returns_csv_string(tmp_path):
74
+ source = tmp_path / "sample.csv"
75
+ write_csv(
76
+ source,
77
+ [
78
+ ["name", "score", "note"],
79
+ ["Alice", "10", "Hello, Tokyo"],
80
+ ["Bob", "8", "Line\nbreak"],
81
+ ],
82
+ )
83
+
84
+ df = DataFrame.from_csv(source).filter(build_filter("score", ">", "7"))
85
+
86
+ assert (
87
+ df.to_csv()
88
+ == 'name,score,note\r\nAlice,10,"Hello, Tokyo"\r\nBob,8,"Line\nbreak"\r\n'
89
+ )
90
+
91
+
92
+ def test_dataframe_to_csv_writes_csv_file(tmp_path):
93
+ source = tmp_path / "sample.csv"
94
+ output = tmp_path / "exports" / "filtered.csv"
95
+ write_csv(
96
+ source,
97
+ [
98
+ ["name", "score", "note"],
99
+ ["Alice", "10", "Hello, Tokyo"],
100
+ ["Bob", "8", "Line\nbreak"],
101
+ ],
102
+ )
103
+
104
+ df = DataFrame.from_csv(source).filter(build_filter("score", ">", "8"))
105
+
106
+ result = df.to_csv(output)
107
+
108
+ assert result == output
109
+ assert output.read_text(encoding="utf-8") == "name,score,note\nAlice,10,\"Hello, Tokyo\"\n"
110
+
111
+
112
+ def test_dataframe_to_csv_round_trips_with_from_csv(tmp_path):
113
+ source = tmp_path / "sample.csv"
114
+ output = tmp_path / "output.csv"
115
+ write_csv(source, [["id", "name"], ["001", "Alice"], ["002", "Bob"]])
116
+
117
+ DataFrame.from_csv(source, convert_types=False).to_csv(output)
118
+
119
+ round_tripped = DataFrame.from_csv(output, convert_types=False)
120
+ assert round_tripped.columns == ["id", "name"]
121
+ assert round_tripped["id"] == ["001", "002"]
122
+ assert round_tripped["name"] == ["Alice", "Bob"]
123
+
124
+
73
125
  def test_view_command_filters_and_selects_columns(tmp_path, capsys):
74
126
  source = tmp_path / "sample.csv"
75
127
  write_csv(
@@ -0,0 +1,63 @@
1
+ import pytest
2
+
3
+ from csvsmith.cli import main
4
+ from csvsmith.tools.strip_bom import UTF8_BOM, default_output_path, strip_utf8_bom
5
+
6
+
7
+ def test_strip_utf8_bom_writes_default_output_without_bom(tmp_path):
8
+ source = tmp_path / "input.csv"
9
+ source.write_bytes(UTF8_BOM + b"id,name\r\n1,Alice\r\n")
10
+
11
+ result = strip_utf8_bom(source)
12
+
13
+ assert result.output_path == tmp_path / "input.no-bom.csv"
14
+ assert result.removed is True
15
+ assert result.output_path.read_bytes() == b"id,name\r\n1,Alice\r\n"
16
+ assert source.read_bytes().startswith(UTF8_BOM)
17
+
18
+
19
+ def test_strip_utf8_bom_preserves_file_without_bom(tmp_path):
20
+ source = tmp_path / "input.csv"
21
+ output = tmp_path / "output.csv"
22
+ source.write_bytes(b"id,name\n1,Alice\n")
23
+
24
+ result = strip_utf8_bom(source, output)
25
+
26
+ assert result.output_path == output
27
+ assert result.removed is False
28
+ assert output.read_bytes() == b"id,name\n1,Alice\n"
29
+
30
+
31
+ def test_strip_utf8_bom_can_rewrite_in_place(tmp_path):
32
+ source = tmp_path / "input.csv"
33
+ source.write_bytes(UTF8_BOM + b"id,name\n1,Alice\n")
34
+
35
+ result = strip_utf8_bom(source, in_place=True)
36
+
37
+ assert result.output_path == source
38
+ assert result.removed is True
39
+ assert source.read_bytes() == b"id,name\n1,Alice\n"
40
+
41
+
42
+ def test_strip_utf8_bom_rejects_in_place_with_output(tmp_path):
43
+ source = tmp_path / "input.csv"
44
+ source.write_bytes(UTF8_BOM + b"id,name\n")
45
+
46
+ with pytest.raises(ValueError, match="--in-place"):
47
+ strip_utf8_bom(source, tmp_path / "output.csv", in_place=True)
48
+
49
+
50
+ def test_default_output_path_handles_multi_dot_names():
51
+ assert default_output_path("report.final.csv").name == "report.final.no-bom.csv"
52
+
53
+
54
+ def test_strip_bom_cli_writes_requested_output(tmp_path, capsys):
55
+ source = tmp_path / "input.csv"
56
+ output = tmp_path / "clean.csv"
57
+ source.write_bytes(UTF8_BOM + b"id,name\n1,Alice\n")
58
+
59
+ exit_code = main(["strip-bom", str(source), "-o", str(output)])
60
+
61
+ assert exit_code == 0
62
+ assert output.read_bytes() == b"id,name\n1,Alice\n"
63
+ assert "removed BOM" in capsys.readouterr().out
File without changes
File without changes
File without changes