mirror of
https://github.com/modelscope/FunASR.git
synced 2026-10-12 02:19:18 +00:00
fix: support NumPy 2 with verified scientific-stack compatibility (#3674)
Retain the contributor alias cleanup and remove the NumPy cap with verified CPU dependency, frontend, masking and clustering coverage. NumPy 1.26.4 and 2.4.0 each pass 23 exact-head CI tests and 77 local tests plus 184 subtests. Existing EEND mel calls now use keyword arguments for modern librosa. The related installation issue remains open pending published-package and reporter re-test. Co-authored-by: linhongyu510 <[email protected]> Signed-off-by: zhifu gao <[email protected]>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
name: Validate NumPy compatibility
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "setup.py"
|
||||
- "funasr/__init__.py"
|
||||
- "funasr/frontends/**"
|
||||
- "funasr/models/emotion2vec/**"
|
||||
- "funasr/models/campplus/**"
|
||||
- "funasr/models/eend/utils/feature.py"
|
||||
- "tests/test_numpy_compatibility.py"
|
||||
- "tests/test_numpy_cluster_compatibility.py"
|
||||
- "tests/test_eend_feature_compatibility.py"
|
||||
- "tests/test_pypi_metadata.py"
|
||||
- ".github/workflows/test-numpy-compatibility.yml"
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "setup.py"
|
||||
- "funasr/__init__.py"
|
||||
- "funasr/frontends/**"
|
||||
- "funasr/models/emotion2vec/**"
|
||||
- "funasr/models/campplus/**"
|
||||
- "funasr/models/eend/utils/feature.py"
|
||||
- "tests/test_numpy_compatibility.py"
|
||||
- "tests/test_numpy_cluster_compatibility.py"
|
||||
- "tests/test_eend_feature_compatibility.py"
|
||||
- "tests/test_pypi_metadata.py"
|
||||
- ".github/workflows/test-numpy-compatibility.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
numpy-compatibility:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
numpy: ["1.26.4", "2.4.0"]
|
||||
env:
|
||||
OMP_NUM_THREADS: "1"
|
||||
MKL_NUM_THREADS: "1"
|
||||
NUMBA_NUM_THREADS: "1"
|
||||
HF_HUB_OFFLINE: "1"
|
||||
NUMPY_VERSION: ${{ matrix.numpy }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
- name: Install CPU runtime and requested NumPy version
|
||||
run: |
|
||||
python -m pip install torch==2.10.0 torchaudio==2.10.0 --index-url https://download.pytorch.org/whl/cpu
|
||||
python -m pip install "numpy==$NUMPY_VERSION" -e . pytest einops pytorch-wpe --report install-report.json
|
||||
- name: Record resolved environment and validate dependencies
|
||||
run: |
|
||||
git rev-parse HEAD | tee source-commit.txt
|
||||
python -c "import numpy, os; assert numpy.__version__ == os.environ['NUMPY_VERSION']"
|
||||
python -m pip freeze | tee resolved-dependencies.txt
|
||||
python -m pip check | tee pip-check.txt
|
||||
- name: Exercise real frontend, masking, import and array boundaries
|
||||
run: |
|
||||
python -m pytest -q \
|
||||
tests/test_numpy_compatibility.py \
|
||||
tests/test_numpy_cluster_compatibility.py \
|
||||
tests/test_eend_feature_compatibility.py \
|
||||
tests/test_pypi_metadata.py \
|
||||
--junitxml=numpy-results.xml
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: numpy-${{ matrix.numpy }}-evidence
|
||||
path: |
|
||||
resolved-dependencies.txt
|
||||
pip-check.txt
|
||||
numpy-results.xml
|
||||
source-commit.txt
|
||||
install-report.json
|
||||
@@ -411,6 +411,6 @@ class MultiChannelFrontend(nn.Module):
|
||||
rescale_line = line_item[3 : (len(line_item) - 1)]
|
||||
vars_list = list(rescale_line)
|
||||
continue
|
||||
means = np.array(means_list).astype(np.float)
|
||||
vars = np.array(vars_list).astype(np.float)
|
||||
means = np.array(means_list).astype(np.float64)
|
||||
vars = np.array(vars_list).astype(np.float64)
|
||||
return means, vars
|
||||
|
||||
@@ -18,7 +18,7 @@ def transform(Y, dtype=np.float32):
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
mel_basis = librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
mean = np.mean(Y, axis=0)
|
||||
|
||||
@@ -35,7 +35,7 @@ def transform(Y, transform_type=None, dtype=np.float32):
|
||||
|
||||
Args:
|
||||
Y: STFT
|
||||
(n_frames, n_bins)-shaped np.complex array
|
||||
(n_frames, n_bins)-shaped complex array
|
||||
transform_type:
|
||||
None, "log"
|
||||
dtype: output data type
|
||||
@@ -52,21 +52,21 @@ def transform(Y, transform_type=None, dtype=np.float32):
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 16000
|
||||
n_mels = 40
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
mel_basis = librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
elif transform_type == "logmel23":
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
mel_basis = librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
elif transform_type == "logmel23_mn":
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
mel_basis = librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
mean = np.mean(Y, axis=0)
|
||||
@@ -75,7 +75,7 @@ def transform(Y, transform_type=None, dtype=np.float32):
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
mel_basis = librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
# b = np.ones(300)/300
|
||||
@@ -92,7 +92,7 @@ def transform(Y, transform_type=None, dtype=np.float32):
|
||||
n_fft = 2 * (Y.shape[1] - 1)
|
||||
sr = 8000
|
||||
n_mels = 23
|
||||
mel_basis = librosa.filters.mel(sr, n_fft, n_mels)
|
||||
mel_basis = librosa.filters.mel(sr=sr, n_fft=n_fft, n_mels=n_mels)
|
||||
Y = np.dot(Y**2, mel_basis.T)
|
||||
Y = np.log10(np.maximum(Y, 1e-10))
|
||||
mean = np.mean(Y, axis=0)
|
||||
|
||||
@@ -324,7 +324,7 @@ def compute_mask_indices(
|
||||
for length in sorted(lengths, reverse=True):
|
||||
lens = np.fromiter(
|
||||
(e - s if e - s >= length + min_space else 0 for s, e in parts),
|
||||
np.int,
|
||||
np.int64,
|
||||
)
|
||||
l_sum = np.sum(lens)
|
||||
if l_sum == 0:
|
||||
|
||||
@@ -14,8 +14,9 @@ requirements = {
|
||||
"scipy>=1.4.1",
|
||||
"librosa",
|
||||
"soundfile>=0.12.1",
|
||||
# Keep the scientific stack ABI-compatible with the supported SciPy wheels.
|
||||
"numpy<2",
|
||||
# numpy 2.x is supported; deprecated aliases (np.float, np.int) were
|
||||
# removed from call sites so the package imports and runs on both 1.x and 2.x.
|
||||
"numpy",
|
||||
"PyYAML>=5.1.2",
|
||||
"tqdm",
|
||||
"requests",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Check EEND feature values across the supported scientific-stack matrix."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spectra():
|
||||
amplitudes = np.array([1, 10, 100, 1000], dtype=np.float32)
|
||||
return amplitudes[:, None] * np.full((4, 257), 1 + 1j, dtype=np.complex64)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode,bands", [("logmel", 40), ("logmel23", 23)])
|
||||
def test_eend_logmel_preserves_power_ratios(spectra, mode, bands):
|
||||
from funasr.models.eend.utils.feature import transform
|
||||
|
||||
result = transform(spectra, transform_type=mode)
|
||||
assert result.dtype == np.float32 and result.shape == (4, bands)
|
||||
assert np.isfinite(result).all()
|
||||
# A tenfold amplitude increase adds 2 to log10 power in every mel band.
|
||||
np.testing.assert_allclose(np.diff(result, axis=0), 2.0, atol=2e-6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode,expected",
|
||||
[
|
||||
("logmel23_mn", [-3, -1, 1, 3]),
|
||||
("logmel23_mvn", np.array([-3, -1, 1, 3]) / np.sqrt(5)),
|
||||
("logmel23_swn", [-5, -3, -1, 1]),
|
||||
],
|
||||
)
|
||||
def test_eend_normalization_values(spectra, mode, expected):
|
||||
from funasr.models.eend.utils.feature import transform
|
||||
|
||||
original = spectra.copy()
|
||||
result = transform(spectra, transform_type=mode)
|
||||
assert result.dtype == np.float32
|
||||
np.testing.assert_allclose(
|
||||
result, np.repeat(np.asarray(expected)[:, None], 23, axis=1), atol=2e-6
|
||||
)
|
||||
np.testing.assert_array_equal(spectra, original)
|
||||
|
||||
|
||||
def test_eend_ola_mean_normalization(spectra):
|
||||
from funasr.frontends.eend_ola_feature import transform
|
||||
|
||||
result = transform(spectra)
|
||||
np.testing.assert_allclose(
|
||||
result, np.repeat(np.array([-3, -1, 1, 3])[:, None], 23, axis=1), atol=2e-6
|
||||
)
|
||||
assert result.dtype == np.float32
|
||||
|
||||
|
||||
def test_mel23_waveform_frontend_returns_finite_padded_features():
|
||||
from funasr.frontends.wav_frontend import WavFrontendMel23
|
||||
|
||||
wave = np.sin(2 * np.pi * 440 * np.arange(1600) / 8000).astype(np.float32)
|
||||
samples = torch.from_numpy(np.stack([wave, wave]))
|
||||
frontend = WavFrontendMel23(
|
||||
fs=8000, frame_length=256, frame_shift=80, lfr_m=0, lfr_n=1
|
||||
)
|
||||
features, lengths = frontend(samples, torch.tensor([1600, 1200]))
|
||||
assert features.shape == (2, 20, 23)
|
||||
assert lengths.tolist() == [20, 15]
|
||||
assert features.dtype == torch.float32 and torch.isfinite(features).all()
|
||||
assert torch.count_nonzero(features[1, 15:]) == 0
|
||||
assert torch.count_nonzero(features[0]) > 0
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Model-free compatibility checks for the production speaker clustering paths."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from funasr.models.campplus.cluster_backend import ClusterBackend, UmapHdbscan
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def preserve_numpy_random_state():
|
||||
# The production sklearn/UMAP calls use NumPy's global random state.
|
||||
state = np.random.get_state()
|
||||
np.random.seed(17)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
np.random.set_state(state)
|
||||
|
||||
|
||||
def _speaker_embeddings(num_speakers, dtype, samples_per_speaker=24):
|
||||
rng = np.random.default_rng(20260909)
|
||||
expected = np.repeat(np.arange(num_speakers), samples_per_speaker)
|
||||
centers = np.eye(num_speakers, 8)
|
||||
embeddings = centers[expected] + rng.normal(0.0, 0.015, (len(expected), 8))
|
||||
embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
|
||||
order = rng.permutation(len(expected))
|
||||
return embeddings[order].astype(dtype), expected[order]
|
||||
|
||||
|
||||
def _assert_same_partition(labels, expected):
|
||||
labels = np.asarray(labels)
|
||||
assert labels.shape == expected.shape
|
||||
assert np.issubdtype(labels.dtype, np.integer)
|
||||
assert np.all(labels >= 0), "Separated speakers must not be classified as noise"
|
||||
np.testing.assert_array_equal(
|
||||
labels[:, None] == labels[None, :],
|
||||
expected[:, None] == expected[None, :],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
|
||||
@pytest.mark.parametrize("num_speakers", [2, 3])
|
||||
def test_spectral_partition_survives_dtype_and_input_order(dtype, num_speakers):
|
||||
embeddings, expected = _speaker_embeddings(num_speakers, dtype)
|
||||
original = embeddings.copy()
|
||||
# This range selects the real spectral branch, not the <20 zero-label path.
|
||||
assert 20 <= len(embeddings) < 2048
|
||||
backend = ClusterBackend()
|
||||
labels = backend(embeddings, oracle_num=num_speakers)
|
||||
_assert_same_partition(labels, expected)
|
||||
|
||||
permutation = np.random.default_rng(91).permutation(len(embeddings))
|
||||
permuted_labels = backend(embeddings[permutation], oracle_num=num_speakers)
|
||||
_assert_same_partition(permuted_labels, expected[permutation])
|
||||
np.testing.assert_array_equal(embeddings, original)
|
||||
|
||||
|
||||
def test_spectral_oracle_can_override_two_natural_speakers():
|
||||
embeddings, expected = _speaker_embeddings(2, np.float32)
|
||||
assert np.unique(expected).size == 2
|
||||
labels = ClusterBackend()(embeddings, oracle_num=1)
|
||||
_assert_same_partition(labels, np.zeros_like(expected))
|
||||
|
||||
|
||||
def test_umap_hdbscan_preserves_separated_speakers():
|
||||
import numba
|
||||
|
||||
embeddings, expected = _speaker_embeddings(2, np.float32, samples_per_speaker=32)
|
||||
original = embeddings.copy()
|
||||
# Exercise the real wrapper on a small fixture, not the >=2048 dispatcher.
|
||||
cluster = UmapHdbscan(
|
||||
n_neighbors=10, n_components=2, min_samples=3, min_cluster_size=12
|
||||
)
|
||||
previous_threads = numba.get_num_threads()
|
||||
numba.set_num_threads(1)
|
||||
try:
|
||||
labels = cluster(embeddings)
|
||||
finally:
|
||||
numba.set_num_threads(previous_threads)
|
||||
_assert_same_partition(labels, expected)
|
||||
np.testing.assert_array_equal(embeddings, original)
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Exercise FunASR's NumPy boundaries without downloading model weights."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cmvn_path(tmp_path):
|
||||
path = tmp_path / "frontend.cmvn"
|
||||
path.write_text(
|
||||
"<AddShift>\n<LearnRateCoef> 0 0 "
|
||||
+ " ".join(["0.1"] * 8)
|
||||
+ " </LearnRateCoef>\n</AddShift>\n"
|
||||
+ "<Rescale>\n<LearnRateCoef> 0 0 "
|
||||
+ " ".join(["1.25"] * 8)
|
||||
+ " </LearnRateCoef>\n</Rescale>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def test_cmvn_load_returns_float64_arrays(cmvn_path):
|
||||
from funasr.frontends.default import MultiChannelFrontend
|
||||
|
||||
means, scales = MultiChannelFrontend._load_cmvn(None, cmvn_path)
|
||||
assert means.dtype == scales.dtype == np.float64
|
||||
np.testing.assert_array_equal(means, np.full(8, 0.1, dtype=np.float64))
|
||||
np.testing.assert_array_equal(scales, np.full(8, 1.25, dtype=np.float64))
|
||||
|
||||
|
||||
def test_frontend_applies_cmvn_and_preserves_padding(cmvn_path):
|
||||
from funasr.frontends.default import MultiChannelFrontend
|
||||
|
||||
kwargs = dict(
|
||||
fs=16000,
|
||||
n_fft=512,
|
||||
frame_length=25,
|
||||
frame_shift=10,
|
||||
n_mels=8,
|
||||
center=False,
|
||||
mc=False,
|
||||
)
|
||||
plain = MultiChannelFrontend(**kwargs).eval()
|
||||
normalized = MultiChannelFrontend(**kwargs, cmvn_file=str(cmvn_path)).eval()
|
||||
# Initialization crosses the NumPy -> Torch boundary in production.
|
||||
assert normalized.mean.dtype == normalized.std.dtype == torch.float64
|
||||
np.testing.assert_array_equal(normalized.mean.numpy(), np.full(8, 0.1))
|
||||
|
||||
wave = np.sin(2 * np.pi * 440 * np.arange(1600) / 16000).astype(np.float32)
|
||||
samples = torch.from_numpy(np.stack([wave, wave]))
|
||||
samples[1, 1200:] = 0
|
||||
lengths = torch.tensor([1600, 1200])
|
||||
with torch.no_grad():
|
||||
before, before_lengths = plain(samples, lengths)
|
||||
after, after_lengths = normalized(samples, lengths)
|
||||
torch.testing.assert_close(after_lengths, before_lengths)
|
||||
assert after.shape == before.shape and after.shape[-1] == 8
|
||||
assert torch.isfinite(after).all()
|
||||
for row, length in enumerate(after_lengths.tolist()):
|
||||
assert length > 0
|
||||
torch.testing.assert_close(
|
||||
after[row, :length], (before[row, :length] + 0.1) * 1.25
|
||||
)
|
||||
assert torch.count_nonzero(after[row, length:]) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", [0, 1, 7])
|
||||
def test_emotion2vec_no_overlap_respects_padding_and_spacing(seed):
|
||||
from funasr.models.emotion2vec.fairseq_modules import compute_mask_indices
|
||||
|
||||
padding = torch.zeros((2, 80), dtype=torch.bool)
|
||||
padding[1, 53:] = True
|
||||
previous = np.random.get_state()
|
||||
try:
|
||||
np.random.seed(seed)
|
||||
mask = compute_mask_indices(
|
||||
(2, 80),
|
||||
padding,
|
||||
mask_prob=0.45,
|
||||
mask_length=4,
|
||||
no_overlap=True,
|
||||
min_space=2,
|
||||
require_same_masks=False,
|
||||
)
|
||||
finally:
|
||||
np.random.set_state(previous)
|
||||
assert mask.shape == (2, 80) and mask.dtype == np.bool_
|
||||
assert not mask[padding.numpy()].any()
|
||||
for row in mask:
|
||||
indices = np.flatnonzero(row)
|
||||
assert len(indices) > 0
|
||||
spans = np.split(indices, np.flatnonzero(np.diff(indices) > 1) + 1)
|
||||
assert all(len(span) == 4 for span in spans)
|
||||
assert all(
|
||||
right[0] - left[-1] - 1 >= 2 for left, right in zip(spans, spans[1:])
|
||||
)
|
||||
# Convert the real mask, not a duplicate np.fromiter expression.
|
||||
tensor_mask = torch.from_numpy(mask)
|
||||
assert tensor_mask.dtype == torch.bool
|
||||
np.testing.assert_array_equal(tensor_mask.numpy(), mask)
|
||||
|
||||
|
||||
def test_relevant_modules_import_in_a_fresh_process():
|
||||
modules = [
|
||||
"funasr.frontends.default",
|
||||
"funasr.frontends.wav_frontend",
|
||||
"funasr.models.emotion2vec.fairseq_modules",
|
||||
"funasr.models.eend.utils.feature",
|
||||
"funasr.auto.auto_model",
|
||||
]
|
||||
code = """
|
||||
import importlib, json, pathlib, sys
|
||||
root = pathlib.Path(sys.argv[1]).resolve()
|
||||
names = json.loads(sys.argv[2])
|
||||
loaded = {}
|
||||
for name in names:
|
||||
module = importlib.import_module(name)
|
||||
path = pathlib.Path(module.__file__).resolve()
|
||||
assert path.is_relative_to(root), (name, str(path))
|
||||
loaded[name] = str(path)
|
||||
import funasr
|
||||
errors = funasr.get_import_errors()
|
||||
assert not {name: errors[name] for name in names if name in errors}, errors
|
||||
print('NUMPY_IMPORT_RESULT=' + json.dumps(loaded))
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env.update({"PYTHONPATH": str(ROOT), "HF_HUB_OFFLINE": "1", "OMP_NUM_THREADS": "1"})
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code, str(ROOT), json.dumps(modules)],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
payload = next(
|
||||
line.removeprefix("NUMPY_IMPORT_RESULT=")
|
||||
for line in result.stdout.splitlines()
|
||||
if line.startswith("NUMPY_IMPORT_RESULT=")
|
||||
)
|
||||
assert set(json.loads(payload)) == set(modules)
|
||||
@@ -34,7 +34,29 @@ def test_package_keywords_surface_deployment_discovery_terms():
|
||||
assert keyword in text
|
||||
|
||||
|
||||
def test_core_dependencies_prevent_numpy_two_abi_mismatches():
|
||||
def test_core_dependencies_allow_numpy_two():
|
||||
"""numpy must not be pinned to <2; the codebase is compatible with both 1.x and 2.x."""
|
||||
text = _setup_text()
|
||||
|
||||
assert '"numpy<2"' in text
|
||||
assert '"numpy<2"' not in text
|
||||
assert '"numpy"' in text
|
||||
|
||||
|
||||
def test_no_removed_numpy_aliases_in_source():
|
||||
"""np.float / np.int / etc. were removed in numpy 1.24 and must not appear in funasr code."""
|
||||
import re
|
||||
|
||||
removed_aliases = re.compile(
|
||||
r"\bnp\.(float|int|bool|object|str|complex|long|unicode)\b(?!\d|_)"
|
||||
)
|
||||
source_files = list((ROOT / "funasr").rglob("*.py"))
|
||||
offenders: list[str] = []
|
||||
for path in source_files:
|
||||
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
||||
# Skip docstrings/comments: only flag lines that are actual code
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#") or stripped.startswith('"') or stripped.startswith("'"):
|
||||
continue
|
||||
if removed_aliases.search(line):
|
||||
offenders.append(f"{path.relative_to(ROOT)}:{lineno}: {stripped}")
|
||||
assert not offenders, f"Removed numpy aliases found:\n" + "\n".join(offenders)
|
||||
|
||||
Reference in New Issue
Block a user