From 2230a8221718c4c526e8cf8984cf1c85f80547f9 Mon Sep 17 00:00:00 2001 From: Ting Zhang <181050363+GPT-AGI@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:52:17 +0800 Subject: [PATCH 1/2] fix(utils): remove distutils boolean parser Co-authored-by: Cursor --- funasr/utils/type_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/funasr/utils/type_utils.py b/funasr/utils/type_utils.py index 0731e46f88..d1d4bae053 100644 --- a/funasr/utils/type_utils.py +++ b/funasr/utils/type_utils.py @@ -1,4 +1,3 @@ -from distutils.util import strtobool from typing import Optional from typing import Tuple from typing import Union @@ -15,7 +14,12 @@ def str2bool(value: str) -> bool: Args: value: TODO. """ - return bool(strtobool(value)) + normalized = value.lower() + if normalized in ("y", "yes", "t", "true", "on", "1"): + return True + if normalized in ("n", "no", "f", "false", "off", "0"): + return False + raise ValueError(f"invalid truth value {value!r}") def remove_parenthesis(value: str): From 610aea3484722e6a80608c3ab31acbe12d4f3ccf Mon Sep 17 00:00:00 2001 From: zhifu gao Date: Wed, 11 Nov 2026 13:20:50 +0800 Subject: [PATCH 2/2] fix(utils): preserve normalized boolean errors and cover Python 3.12 import Keep legacy lowercased invalid-value messages, add 23 parser contracts, and include them in the existing read-only Python 3.10/3.12 text workflow. The submitted parser fails three error contracts; corrected parser passes all 23. CI-command private run: 47 passed, 6 optional-dependency skips. Signed-off-by: LauraGPT --- .../workflows/test-text-postprocessing.yml | 5 ++ funasr/utils/type_utils.py | 2 +- tests/test_type_utils_compat.py | 65 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/test_type_utils_compat.py diff --git a/.github/workflows/test-text-postprocessing.yml b/.github/workflows/test-text-postprocessing.yml index 55364def5d..c851661af3 100644 --- a/.github/workflows/test-text-postprocessing.yml +++ b/.github/workflows/test-text-postprocessing.yml @@ -3,6 +3,8 @@ name: Validate text postprocessing on: pull_request: paths: + - "funasr/utils/type_utils.py" + - "tests/test_type_utils_compat.py" - "funasr/utils/postprocess_hotwords.py" - "funasr/utils/postprocess_utils.py" - "runtime/python/onnxruntime/funasr_onnx/utils/postprocess_utils.py" @@ -14,6 +16,8 @@ on: push: branches: [main] paths: + - "funasr/utils/type_utils.py" + - "tests/test_type_utils_compat.py" - "funasr/utils/postprocess_hotwords.py" - "funasr/utils/postprocess_utils.py" - "runtime/python/onnxruntime/funasr_onnx/utils/postprocess_utils.py" @@ -52,6 +56,7 @@ jobs: - name: Check hotword and rich transcription behavior without model weights run: | python -m pytest -q \ + tests/test_type_utils_compat.py \ tests/test_postprocess_hotwords.py \ tests/test_rich_transcription_postprocess.py \ tests/test_sentence_postprocess_timestamps.py \ diff --git a/funasr/utils/type_utils.py b/funasr/utils/type_utils.py index d1d4bae053..2b2323c35e 100644 --- a/funasr/utils/type_utils.py +++ b/funasr/utils/type_utils.py @@ -19,7 +19,7 @@ def str2bool(value: str) -> bool: return True if normalized in ("n", "no", "f", "false", "off", "0"): return False - raise ValueError(f"invalid truth value {value!r}") + raise ValueError(f"invalid truth value {normalized!r}") def remove_parenthesis(value: str): diff --git a/tests/test_type_utils_compat.py b/tests/test_type_utils_compat.py new file mode 100644 index 0000000000..0be3cfbdac --- /dev/null +++ b/tests/test_type_utils_compat.py @@ -0,0 +1,65 @@ +"""Keep the command-line boolean parser compatible without distutils.""" +import importlib.util +from pathlib import Path +import subprocess +import sys + +import pytest + + +SOURCE = Path(__file__).resolve().parents[1] / "funasr/utils/type_utils.py" + + +@pytest.fixture(scope="module") +def parser(): + spec = importlib.util.spec_from_file_location("boolean_parser_under_test", SOURCE) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.str2bool + + +@pytest.mark.parametrize( + ("value", "expected"), + [("y", True), ("yes", True), ("t", True), ("true", True), ("on", True), + ("1", True), ("n", False), ("no", False), ("f", False), ("false", False), + ("off", False), ("0", False), ("YeS", True), ("TRUE", True), + ("nO", False), ("OFF", False)], +) +def test_boolean_argument_preserves_values_and_boolean_type(parser, value, expected): + assert parser(value) is expected + + +@pytest.mark.parametrize( + ("value", "message"), + [("MAYBE", "invalid truth value 'maybe'"), + ("FaLsE!", "invalid truth value 'false!'"), + (" TRUE ", "invalid truth value ' true '"), + ("", "invalid truth value ''"), + ("2", "invalid truth value '2'"), + ("false!", "invalid truth value 'false!'")], +) +def test_invalid_boolean_argument_preserves_normalized_error(parser, value, message): + with pytest.raises(ValueError) as caught: + parser(value) + assert str(caught.value) == message + + +def test_boolean_parser_imports_when_distutils_is_unavailable(): + code = """ +import importlib.abc +import runpy +import sys + +class MissingDistutils(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == 'distutils' or fullname.startswith('distutils.'): + raise ImportError('distutils is unavailable') + +sys.meta_path.insert(0, MissingDistutils()) +parse = runpy.run_path(sys.argv[1])['str2bool'] +assert parse('yes') is True +assert parse('no') is False +""" + result = subprocess.run([sys.executable, "-c", code, str(SOURCE)], + text=True, capture_output=True, timeout=20) + assert result.returncode == 0, result.stdout + result.stderr