Files
ragflow/test/unit_test/rag/conftest.py
S d4ceeee4ed refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary

Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:

- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`

The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.

## Changes

- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
  - `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.

## Acceptance criteria

- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).

## Rebase protocol

As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.

---------

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 17:07:14 +08:00

125 lines
4.1 KiB
Python

#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Shared fixtures for ``rag`` unit tests.
Also restores the real ``common.data_source`` package before importing rag
unit tests.
``test/unit_test/data_source/conftest.py`` registers a lightweight
``sys.modules["common.data_source"]`` stub so submodule imports skip the heavy
package ``__init__.py``. Pytest collection order visits ``data_source/`` before
``rag/``, so without this hook ``rag.svr.sync_data_source`` fails on
``from common.data_source import BlobStorageConnector``.
"""
from __future__ import annotations
import importlib
import logging
import sys
import types
import pytest
_LOG = logging.getLogger(__name__)
_PDF_PARSER_KEY = "deepdoc.parser.pdf_parser"
def _restore_common_data_source_package() -> None:
mod = sys.modules.get("common.data_source")
if mod is None:
return
# Stub is a bare types.ModuleType with __path__ and no __file__; real package has __init__.py.
if getattr(mod, "__file__", None) is not None:
return
if not isinstance(mod, types.ModuleType) or not getattr(mod, "__path__", None):
return
keys = [key for key in sys.modules if key == "common.data_source" or key.startswith("common.data_source.")]
for key in keys:
del sys.modules[key]
importlib.invalidate_caches()
try:
importlib.import_module("common.data_source")
except Exception as exc: # pragma: no cover
raise ImportError("conftest: failed to restore real common.data_source package") from exc
_restore_common_data_source_package()
def _make_pdf_parser_stub():
pdf_parser = types.ModuleType(_PDF_PARSER_KEY)
class _StubPdfParser:
@staticmethod
def remove_tag(text):
return text
pdf_parser.RAGFlowPdfParser = _StubPdfParser
return pdf_parser
def _install_pdf_parser_stub() -> None:
"""Install a lightweight stub so ``rag.nlp`` imports without deepdoc/infinity.
Must run at conftest import time: delimiter and naive_merge tests import
``rag.nlp`` at module scope, which is before any fixture runs.
"""
if _PDF_PARSER_KEY in sys.modules:
_LOG.debug(
"pdf_parser_stub: retaining existing module for %s",
_PDF_PARSER_KEY,
)
return
sys.modules[_PDF_PARSER_KEY] = _make_pdf_parser_stub()
_LOG.debug("pdf_parser_stub: installed stub for %s", _PDF_PARSER_KEY)
_install_pdf_parser_stub()
@pytest.fixture
def pdf_parser_stub():
"""Ensure the pdf_parser stub is installed for the duration of a test.
Saves and restores any pre-existing ``sys.modules`` entry so tests that
opt into this fixture do not permanently replace a real module loaded
earlier in the session.
"""
previous = sys.modules.get(_PDF_PARSER_KEY)
stub = _make_pdf_parser_stub()
sys.modules[_PDF_PARSER_KEY] = stub
_LOG.debug("pdf_parser_stub fixture: installed stub for %s", _PDF_PARSER_KEY)
try:
yield stub
finally:
if previous is None:
# Keep a stub in place so later module-level imports still work;
# only restore when a real prior module existed.
if sys.modules.get(_PDF_PARSER_KEY) is stub:
pass
_LOG.debug(
"pdf_parser_stub fixture: left stub in place for %s",
_PDF_PARSER_KEY,
)
else:
sys.modules[_PDF_PARSER_KEY] = previous
_LOG.debug(
"pdf_parser_stub fixture: restored prior module for %s",
_PDF_PARSER_KEY,
)