hanlp-haystack 2.1.0__tar.gz → 2.2.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.
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/hanlp-v2.1.1] - 2026-01-27
4
+
5
+ ### 🧹 Chores
6
+
7
+ - Pin transformers<5 for HanLP integration (#2785)
8
+
9
+
10
+ ## [integrations/hanlp-v2.1.0] - 2026-01-14
11
+
12
+ ### 🚀 Features
13
+
14
+ - Update hanlp components to auto call run `warm_up` (#2749)
15
+
16
+
3
17
  ## [integrations/hanlp-v2.0.0] - 2026-01-12
4
18
 
5
19
  ### 🐛 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanlp-haystack
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: An integration of Han Language Processing - HanLP as a ChineseDocumentSplitter component.
5
5
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/hanlp#readme
6
6
  Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
@@ -15,11 +15,14 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
18
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
21
  Requires-Python: >=3.10
21
22
  Requires-Dist: hanlp>=2.1.1
22
23
  Requires-Dist: haystack-ai>=2.22.0
24
+ Requires-Dist: tokenizers>=0.21.0
25
+ Requires-Dist: transformers<5,>=4.1.1
23
26
  Description-Content-Type: text/markdown
24
27
 
25
28
  # hanlp-haystack
@@ -0,0 +1,13 @@
1
+ loaders:
2
+ - modules:
3
+ - haystack_integrations.components.preprocessors.hanlp.chinese_document_splitter
4
+ search_path: [../src]
5
+ processors:
6
+ - type: filter
7
+ documented_only: true
8
+ skip_empty_modules: true
9
+ renderer:
10
+ description: HanLP integration for Haystack
11
+ id: integrations-hanlp
12
+ filename: hanlp.md
13
+ title: HanLP
@@ -19,12 +19,17 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
22
23
  "Programming Language :: Python :: Implementation :: CPython",
23
24
  "Programming Language :: Python :: Implementation :: PyPy",
24
25
  ]
25
26
  dependencies = [
26
27
  "haystack-ai>=2.22.0",
27
- "hanlp>=2.1.1"
28
+ "hanlp>=2.1.1",
29
+ # the following pins can be removed once HanLP supports transformers 5
30
+ "transformers>=4.1.1,<5",
31
+ # tokenizers is a dependency of transformers. The following version is the first one which supports Python 3.14.
32
+ "tokenizers>=0.21.0",
28
33
  ]
29
34
 
30
35
  [project.urls]
@@ -48,7 +53,7 @@ installer = "uv"
48
53
  dependencies = ["haystack-pydoc-tools", "ruff"]
49
54
 
50
55
  [tool.hatch.envs.default.scripts]
51
- docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
56
+ docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
52
57
  fmt = "ruff check --fix {args}; ruff format {args}"
53
58
  fmt-check = "ruff check {args} && ruff format --check {args}"
54
59
 
@@ -66,7 +71,8 @@ dependencies = [
66
71
  unit = 'pytest -m "not integration" {args:tests}'
67
72
  integration = 'pytest -m "integration" {args:tests}'
68
73
  all = 'pytest {args:tests}'
69
- cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
74
+ unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
75
+ integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
70
76
  types = """mypy -p haystack_integrations.components.preprocessors.hanlp {args}"""
71
77
 
72
78
  [tool.mypy]
@@ -92,9 +98,17 @@ line-length = 120
92
98
  [tool.ruff.lint]
93
99
  select = [
94
100
  "A",
101
+ "ANN",
95
102
  "ARG",
96
103
  "B",
97
104
  "C",
105
+ "D102", # Missing docstring in public method
106
+ "D103", # Missing docstring in public function
107
+ "D205", # 1 blank line required between summary line and description
108
+ "D209", # Closing triple quotes go to new line
109
+ "D213", # summary lines must be positioned on the second physical line of the docstring
110
+ "D417", # Missing argument descriptions in the docstring
111
+ "D419", # Docstring is empty
98
112
  "DTZ",
99
113
  "E",
100
114
  "EM",
@@ -135,6 +149,8 @@ ignore = [
135
149
  "ARG005",
136
150
  "RUF001",
137
151
  "RUF002",
152
+ # Allow `Any` - used legitimately for dynamic types and SDK boundaries
153
+ "ANN401",
138
154
  ]
139
155
 
140
156
  [tool.ruff.lint.isort]
@@ -145,11 +161,12 @@ ban-relative-imports = "parents"
145
161
 
146
162
  [tool.ruff.lint.per-file-ignores]
147
163
  # Tests can use magic values, assertions, and relative imports
148
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
164
+ "tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
149
165
 
150
166
  [tool.coverage.run]
151
167
  source = ["haystack_integrations"]
152
168
  branch = true
169
+ relative_files = true
153
170
  parallel = false
154
171
 
155
172
  [tool.coverage.report]
@@ -4,6 +4,7 @@
4
4
 
5
5
  from collections.abc import Callable
6
6
  from copy import deepcopy
7
+ from dataclasses import replace
7
8
  from typing import Any, Literal
8
9
 
9
10
  from haystack import Document, component, logging
@@ -440,10 +441,7 @@ class ChineseDocumentSplitter:
440
441
  previous_doc_start_idx = splits_start_idxs[i - 1]
441
442
  self._add_split_overlap_information(doc, doc_start_idx, previous_doc, previous_doc_start_idx)
442
443
 
443
- for d in documents:
444
- if d.content is not None:
445
- d.content = d.content.replace(" ", "")
446
- return documents
444
+ return [replace(d, content=d.content.replace(" ", "")) if d.content is not None else d for d in documents]
447
445
 
448
446
  @staticmethod
449
447
  def _add_split_overlap_information(
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ from unittest.mock import MagicMock, patch
6
+
5
7
  import pytest
6
8
  from haystack import Document
7
9
  from haystack.utils import deserialize_callable, serialize_callable
@@ -90,6 +92,145 @@ class TestChineseDocumentSplitter:
90
92
  assert callable(splitter.splitting_function)
91
93
  assert splitter.splitting_function("a.b.c") == ["a", "b", "c"]
92
94
 
95
+ def test_validate_init_parameters(self):
96
+ ChineseDocumentSplitter._validate_init_parameters(
97
+ split_by="word",
98
+ split_length=1000,
99
+ split_overlap=200,
100
+ split_threshold=0,
101
+ granularity="coarse",
102
+ )
103
+
104
+ with pytest.raises(ValueError, match="split_length must be positive"):
105
+ ChineseDocumentSplitter._validate_init_parameters(split_length=0)
106
+
107
+ with pytest.raises(ValueError, match="split_overlap must be non-negative"):
108
+ ChineseDocumentSplitter._validate_init_parameters(split_overlap=-1)
109
+
110
+ with pytest.raises(ValueError, match="split_overlap must be less than split_length"):
111
+ ChineseDocumentSplitter._validate_init_parameters(split_overlap=1000, split_length=500)
112
+
113
+ with pytest.raises(ValueError, match="split_threshold must be non-negative"):
114
+ ChineseDocumentSplitter._validate_init_parameters(split_threshold=-1)
115
+
116
+ with pytest.raises(ValueError, match="split_threshold must be less than split_length"):
117
+ ChineseDocumentSplitter._validate_init_parameters(split_threshold=1001, split_length=1000)
118
+
119
+ with pytest.raises(
120
+ ValueError,
121
+ match="split_by must be one of 'word', 'sentence', 'passage', 'page', 'line', 'period', 'function'",
122
+ ):
123
+ ChineseDocumentSplitter._validate_init_parameters(split_by="invalid")
124
+
125
+ with pytest.raises(ValueError, match="granularity must be one of 'coarse', 'fine'"):
126
+ ChineseDocumentSplitter._validate_init_parameters(granularity="invalid")
127
+
128
+ def test_split_by_character_returns_empty_for_none_content(self):
129
+ splitter = ChineseDocumentSplitter(split_by="word")
130
+ assert splitter._split_by_character(Document(content=None)) == []
131
+
132
+ def test_split_by_hanlp_sentence_returns_empty_for_none_content(self):
133
+ splitter = ChineseDocumentSplitter(split_by="sentence")
134
+ assert splitter._split_by_hanlp_sentence(Document(content=None)) == []
135
+
136
+ def test_split_document_dispatches_to_function_splitter(self):
137
+ splitter = ChineseDocumentSplitter(split_by="function", splitting_function=custom_split)
138
+ doc = Document(content="a.b.c")
139
+ result = splitter._split_document(doc)
140
+ assert [d.content for d in result] == ["a", "b", "c"]
141
+
142
+ def test_split_by_function_returns_empty_for_none_content(self):
143
+ splitter = ChineseDocumentSplitter(split_by="function", splitting_function=custom_split)
144
+ assert splitter._split_by_function(Document(content=None)) == []
145
+
146
+ def test_split_by_function_raises_when_no_function_provided(self):
147
+ splitter = ChineseDocumentSplitter(split_by="function", splitting_function=custom_split)
148
+ splitter.splitting_function = None
149
+ with pytest.raises(ValueError, match=r"No splitting function provided\."):
150
+ splitter._split_by_function(Document(content="a.b"))
151
+
152
+ def test_split_by_function_raises_when_function_returns_non_list(self):
153
+ splitter = ChineseDocumentSplitter(split_by="function", splitting_function=lambda t: "not a list")
154
+ with pytest.raises(ValueError, match="must return a list of strings"):
155
+ splitter._split_by_function(Document(content="a.b"))
156
+
157
+ def test_split_by_function_produces_documents_with_metadata(self):
158
+ splitter = ChineseDocumentSplitter(split_by="function", splitting_function=custom_split)
159
+ doc = Document(content="a.b.c", meta={"name": "src"})
160
+ result = splitter._split_by_function(doc)
161
+ assert len(result) == 3
162
+ assert [d.content for d in result] == ["a", "b", "c"]
163
+ assert all(d.meta["source_id"] == doc.id for d in result)
164
+ assert all(d.meta["name"] == "src" for d in result)
165
+ assert [d.meta["split_id"] for d in result] == [0, 1, 2]
166
+
167
+ @pytest.mark.parametrize(
168
+ ("previous_content", "current_content"),
169
+ [(None, "abc"), ("abc", None)],
170
+ )
171
+ def test_add_split_overlap_information_skips_when_content_is_none(self, previous_content, current_content):
172
+ previous_doc = Document(content=previous_content, meta={"_split_overlap": []})
173
+ current_doc = Document(content=current_content, meta={"_split_overlap": []})
174
+ ChineseDocumentSplitter._add_split_overlap_information(current_doc, 0, previous_doc, 0)
175
+ assert current_doc.meta["_split_overlap"] == []
176
+ assert previous_doc.meta["_split_overlap"] == []
177
+
178
+ def test_add_split_overlap_information_skips_when_no_matching_prefix(self):
179
+ previous_doc = Document(content="abcdef", meta={"_split_overlap": []})
180
+ current_doc = Document(content="xyz", meta={"_split_overlap": []})
181
+ ChineseDocumentSplitter._add_split_overlap_information(current_doc, 2, previous_doc, 0)
182
+ assert current_doc.meta["_split_overlap"] == []
183
+ assert previous_doc.meta["_split_overlap"] == []
184
+
185
+ @pytest.mark.parametrize("elements", [[], ["", " ", "\t"]])
186
+ def test_concatenate_units_returns_empty_for_empty_or_whitespace(self, elements):
187
+ splitter = ChineseDocumentSplitter(split_by="word", split_length=5, split_overlap=1)
188
+ assert splitter._concatenate_units(elements, split_length=5, split_overlap=1, split_threshold=0) == ([], [], [])
189
+
190
+ def test_concatenate_units_returns_single_chunk_for_short_input(self):
191
+ splitter = ChineseDocumentSplitter(split_by="word", split_length=10, split_overlap=0)
192
+ text_splits, pages, start_idxs = splitter._concatenate_units(
193
+ ["a", "b", "c"], split_length=10, split_overlap=0, split_threshold=0
194
+ )
195
+ assert text_splits == ["abc"]
196
+ assert pages == [1]
197
+ assert start_idxs == [0]
198
+
199
+ def test_concatenate_units_applies_split_threshold(self):
200
+ splitter = ChineseDocumentSplitter(split_by="word", split_length=2, split_overlap=0)
201
+ text_splits, _, _ = splitter._concatenate_units(
202
+ ["a", "b", "c"], split_length=2, split_overlap=0, split_threshold=2
203
+ )
204
+ # the trailing "c" chunk is smaller than split_threshold=2 and gets attached to previous
205
+ assert text_splits == ["abc"]
206
+
207
+ def test_number_of_sentences_to_keep_returns_zero_when_overlap_is_zero(self):
208
+ splitter = ChineseDocumentSplitter(split_by="word", split_length=10, split_overlap=0)
209
+ assert splitter._number_of_sentences_to_keep(["s1", "s2", "s3"], split_length=10, split_overlap=0) == 0
210
+
211
+ def test_number_of_sentences_to_keep_stops_when_overlap_reached(self):
212
+ splitter = ChineseDocumentSplitter(split_by="word", split_length=10, split_overlap=2)
213
+ # one word per sentence, so we need at least 3 sentences to exceed split_overlap=2
214
+ splitter.chinese_tokenizer = MagicMock(side_effect=lambda s: [s])
215
+ result = splitter._number_of_sentences_to_keep(["s1", "s2", "s3", "s4"], split_length=10, split_overlap=2)
216
+ assert result == 3
217
+
218
+ def test_number_of_sentences_to_keep_stops_when_split_length_exceeded(self):
219
+ splitter = ChineseDocumentSplitter(split_by="word", split_length=2, split_overlap=1)
220
+ # each sentence tokenizes to 5 "words", so first iteration already exceeds split_length
221
+ splitter.chinese_tokenizer = MagicMock(return_value=["a", "b", "c", "d", "e"])
222
+ assert splitter._number_of_sentences_to_keep(["s1", "s2", "s3"], split_length=2, split_overlap=1) == 0
223
+
224
+ def test_warm_up_is_idempotent(self):
225
+ splitter = ChineseDocumentSplitter(split_by="word")
226
+ with patch(
227
+ "haystack_integrations.components.preprocessors.hanlp.chinese_document_splitter.hanlp"
228
+ ) as mock_hanlp:
229
+ mock_hanlp.load.return_value = MagicMock()
230
+ splitter.warm_up()
231
+ splitter.warm_up()
232
+ assert mock_hanlp.load.call_count == 2 # once for tokenizer, once for split_sent
233
+
93
234
  @pytest.mark.integration
94
235
  def test_empty_list(self):
95
236
  splitter = ChineseDocumentSplitter()
@@ -216,36 +357,3 @@ class TestChineseDocumentSplitter:
216
357
  f"Chunks {i} and {i + 1} do not overlap. "
217
358
  f"Tail (up to 20 chars): '{overlap_prev}' vs Head (up to 20 chars): '{overlap_curr}'"
218
359
  )
219
-
220
- def test_validate_init_parameters(self):
221
- ChineseDocumentSplitter._validate_init_parameters(
222
- split_by="word",
223
- split_length=1000,
224
- split_overlap=200,
225
- split_threshold=0,
226
- granularity="coarse",
227
- )
228
-
229
- with pytest.raises(ValueError, match="split_length must be positive"):
230
- ChineseDocumentSplitter._validate_init_parameters(split_length=0)
231
-
232
- with pytest.raises(ValueError, match="split_overlap must be non-negative"):
233
- ChineseDocumentSplitter._validate_init_parameters(split_overlap=-1)
234
-
235
- with pytest.raises(ValueError, match="split_overlap must be less than split_length"):
236
- ChineseDocumentSplitter._validate_init_parameters(split_overlap=1000, split_length=500)
237
-
238
- with pytest.raises(ValueError, match="split_threshold must be non-negative"):
239
- ChineseDocumentSplitter._validate_init_parameters(split_threshold=-1)
240
-
241
- with pytest.raises(ValueError, match="split_threshold must be less than split_length"):
242
- ChineseDocumentSplitter._validate_init_parameters(split_threshold=1001, split_length=1000)
243
-
244
- with pytest.raises(
245
- ValueError,
246
- match="split_by must be one of 'word', 'sentence', 'passage', 'page', 'line', 'period', 'function'",
247
- ):
248
- ChineseDocumentSplitter._validate_init_parameters(split_by="invalid")
249
-
250
- with pytest.raises(ValueError, match="granularity must be one of 'coarse', 'fine'"):
251
- ChineseDocumentSplitter._validate_init_parameters(granularity="invalid")
@@ -1,28 +0,0 @@
1
- loaders:
2
- - ignore_when_discovered:
3
- - __init__
4
- modules:
5
- - haystack_integrations.components.preprocessors.hanlp.chinese_document_splitter
6
- search_path:
7
- - ../src
8
- type: haystack_pydoc_tools.loaders.CustomPythonLoader
9
- processors:
10
- - do_not_filter_modules: false
11
- documented_only: true
12
- expression: null
13
- skip_empty_modules: true
14
- type: filter
15
- - type: smart
16
- - type: crossref
17
- renderer:
18
- description: HanLP integration for Haystack
19
- id: integrations-hanlp
20
- markdown:
21
- add_member_class_prefix: false
22
- add_method_class_prefix: true
23
- classdef_code_block: false
24
- descriptive_class_title: false
25
- descriptive_module_title: true
26
- filename: hanlp.md
27
- title: HanLP
28
- type: haystack_pydoc_tools.renderers.DocusaurusRenderer
File without changes