hanlp-haystack 2.1.1__tar.gz → 3.0.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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/hanlp-v2.2.0] - 2026-04-22
4
+
5
+ ### 📚 Documentation
6
+
7
+ - Simplify pydoc configs (#2855)
8
+
9
+ ### 🧪 Testing
10
+
11
+ - Track test coverage for all integrations (#3065)
12
+
13
+ ### 🧹 Chores
14
+
15
+ - Add ANN type annotations to google_genai, hanlp, jina, langfuse, lara (#2990)
16
+ - Enforce ruff docstring rules (D102/D103/D205/D209/D213/D417/D419) in integrations 11-20 (#3009)
17
+ - Increase lower pins for 3.14 support in some integrations + test with 3.14 (#3033)
18
+ - Hanlp - avoid mutating documents and add tests (#3206)
19
+
20
+
21
+ ## [integrations/hanlp-v2.1.1] - 2026-01-27
22
+
23
+ ### 🧹 Chores
24
+
25
+ - Pin transformers<5 for HanLP integration (#2785)
26
+
27
+
3
28
  ## [integrations/hanlp-v2.1.0] - 2026-01-14
4
29
 
5
30
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: hanlp-haystack
3
- Version: 2.1.1
3
+ Version: 3.0.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
@@ -11,16 +11,16 @@ License-File: LICENSE.txt
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: Programming Language :: Python
14
- Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
17
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
19
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.11
21
21
  Requires-Dist: hanlp>=2.1.1
22
22
  Requires-Dist: haystack-ai>=2.22.0
23
- Requires-Dist: tokenizers>=0.20.2
23
+ Requires-Dist: tokenizers>=0.21.0
24
24
  Requires-Dist: transformers<5,>=4.1.1
25
25
  Description-Content-Type: text/markdown
26
26
 
@@ -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
@@ -7,7 +7,7 @@ name = "hanlp-haystack"
7
7
  dynamic = ["version"]
8
8
  description = 'An integration of Han Language Processing - HanLP as a ChineseDocumentSplitter component.'
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  license = "Apache-2.0"
12
12
  keywords = []
13
13
  authors = [{ name = "deepset GmbH", email = "info@deepset.ai" }]
@@ -15,10 +15,10 @@ classifiers = [
15
15
  "License :: OSI Approved :: Apache Software License",
16
16
  "Development Status :: 4 - Beta",
17
17
  "Programming Language :: Python",
18
- "Programming Language :: Python :: 3.10",
19
18
  "Programming Language :: Python :: 3.11",
20
19
  "Programming Language :: Python :: 3.12",
21
20
  "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
22
  "Programming Language :: Python :: Implementation :: CPython",
23
23
  "Programming Language :: Python :: Implementation :: PyPy",
24
24
  ]
@@ -27,8 +27,8 @@ dependencies = [
27
27
  "hanlp>=2.1.1",
28
28
  # the following pins can be removed once HanLP supports transformers 5
29
29
  "transformers>=4.1.1,<5",
30
- # tokenizers is a dependency of transformers. The following version is the first one which supports Python 3.13.
31
- "tokenizers>=0.20.2",
30
+ # tokenizers is a dependency of transformers. The following version is the first one which supports Python 3.14.
31
+ "tokenizers>=0.21.0",
32
32
  ]
33
33
 
34
34
  [project.urls]
@@ -52,7 +52,7 @@ installer = "uv"
52
52
  dependencies = ["haystack-pydoc-tools", "ruff"]
53
53
 
54
54
  [tool.hatch.envs.default.scripts]
55
- docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
55
+ docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
56
56
  fmt = "ruff check --fix {args}; ruff format {args}"
57
57
  fmt-check = "ruff check {args} && ruff format --check {args}"
58
58
 
@@ -70,7 +70,8 @@ dependencies = [
70
70
  unit = 'pytest -m "not integration" {args:tests}'
71
71
  integration = 'pytest -m "integration" {args:tests}'
72
72
  all = 'pytest {args:tests}'
73
- cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
73
+ unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
74
+ integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
74
75
  types = """mypy -p haystack_integrations.components.preprocessors.hanlp {args}"""
75
76
 
76
77
  [tool.mypy]
@@ -96,9 +97,17 @@ line-length = 120
96
97
  [tool.ruff.lint]
97
98
  select = [
98
99
  "A",
100
+ "ANN",
99
101
  "ARG",
100
102
  "B",
101
103
  "C",
104
+ "D102", # Missing docstring in public method
105
+ "D103", # Missing docstring in public function
106
+ "D205", # 1 blank line required between summary line and description
107
+ "D209", # Closing triple quotes go to new line
108
+ "D213", # summary lines must be positioned on the second physical line of the docstring
109
+ "D417", # Missing argument descriptions in the docstring
110
+ "D419", # Docstring is empty
102
111
  "DTZ",
103
112
  "E",
104
113
  "EM",
@@ -133,12 +142,15 @@ ignore = [
133
142
  "PLR0912",
134
143
  "PLR0913",
135
144
  "PLR0915",
145
+ "PLR0917",
136
146
  # Ignore unused params
137
147
  "ARG001",
138
148
  "ARG002",
139
149
  "ARG005",
140
150
  "RUF001",
141
151
  "RUF002",
152
+ # Allow `Any` - used legitimately for dynamic types and SDK boundaries
153
+ "ANN401",
142
154
  ]
143
155
 
144
156
  [tool.ruff.lint.isort]
@@ -149,11 +161,12 @@ ban-relative-imports = "parents"
149
161
 
150
162
  [tool.ruff.lint.per-file-ignores]
151
163
  # Tests can use magic values, assertions, and relative imports
152
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
164
+ "tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
153
165
 
154
166
  [tool.coverage.run]
155
167
  source = ["haystack_integrations"]
156
168
  branch = true
169
+ relative_files = true
157
170
  parallel = false
158
171
 
159
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(
@@ -0,0 +1,11 @@
1
+ import pytest
2
+
3
+
4
+ @pytest.fixture(autouse=True)
5
+ def allow_deserialization_of_test_modules(monkeypatch):
6
+ """
7
+ haystack-ai >= 3.0 refuses to deserialize classes and callables from modules outside its
8
+ trusted-module allowlist. Tools and callbacks defined in the test modules live outside that
9
+ allowlist, so trust them explicitly; haystack-ai 2.x ignores this environment variable.
10
+ """
11
+ monkeypatch.setenv("HAYSTACK_DESERIALIZATION_ALLOWLIST", "tests,test_*")
@@ -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