From 3c68127cf65dc4a071e03187fdcf723406395301 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Mon, 8 Sep 2025 13:37:55 +0200 Subject: [PATCH 01/12] Add windows-compatible compiler options when compiling on windows --- setup.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index c140e07..72cb4ea 100644 --- a/setup.py +++ b/setup.py @@ -1,46 +1,55 @@ +import sys from distutils.extension import Extension import numpy from setuptools import setup, find_packages from Cython.Build import cythonize + +is_windows = sys.platform.startswith('win') + +if is_windows: + cythonize_args = dict( + extra_compile_args=['/std:c++20', '/O2'], + extra_link_args=['/std:c++20'], + ) +else: + cythonize_args = dict( + extra_compile_args=['-std=c++11', '-O3'], + extra_link_args=['-std=c++11'], + ) + ext_modules = cythonize( [ Extension( 'meeteval.wer.matching.cy_orc_matching', ['meeteval/wer/matching/cy_orc_matching.pyx'], - extra_compile_args=['-std=c++11'], - extra_link_args=['-std=c++11'], + **cythonize_args, ), Extension( 'meeteval.wer.matching.cy_mimo_matching', ['meeteval/wer/matching/cy_mimo_matching.pyx'], - extra_compile_args=['-std=c++11'], - extra_link_args=['-std=c++11'], + **cythonize_args, ), Extension( 'meeteval.wer.matching.cy_levenshtein', ['meeteval/wer/matching/cy_levenshtein.pyx'], - extra_compile_args=['-std=c++11'], - extra_link_args=['-std=c++11'], + **cythonize_args, ), Extension( 'meeteval.wer.matching.cy_time_constrained_orc_matching', ['meeteval/wer/matching/cy_time_constrained_orc_matching.pyx'], - extra_compile_args=['-std=c++11', '-O3'], - extra_link_args=['-std=c++11'], + **cythonize_args, ), Extension( 'meeteval.wer.matching.cy_greedy_combination_matching', ['meeteval/wer/matching/cy_greedy_combination_matching.pyx'], - extra_compile_args=['-std=c++11', '-O3'], - extra_link_args=['-std=c++11'], + **cythonize_args, ), Extension( 'meeteval.wer.matching.cy_time_constrained_mimo_matching', ['meeteval/wer/matching/cy_time_constrained_mimo_matching.pyx'], - extra_compile_args=['-std=c++11'], - extra_link_args=['-std=c++11'], + **cythonize_args, ), ] ) From b1901f4e0fa841919996a2c9f40963840011c0e6 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Mon, 8 Sep 2025 13:44:08 +0200 Subject: [PATCH 02/12] Add install test for windows --- .github/workflows/noneeditable.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/noneeditable.yml b/.github/workflows/noneeditable.yml index 574b350..654f8ac 100644 --- a/.github/workflows/noneeditable.yml +++ b/.github/workflows/noneeditable.yml @@ -11,11 +11,16 @@ on: jobs: build: - - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + runs-on: + - ubuntu-latest + - windows-latest + runs-on: ${{ matrix.runs-on }} steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: python-version: '3.11' From ceed6469f576539c29fbd253c7b53606ee0b735d Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Mon, 8 Sep 2025 14:16:37 +0200 Subject: [PATCH 03/12] Split Ubuntu and Windows install jobs --- .github/workflows/noneeditable.yml | 33 ++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/noneeditable.yml b/.github/workflows/noneeditable.yml index 654f8ac..8e1f1d8 100644 --- a/.github/workflows/noneeditable.yml +++ b/.github/workflows/noneeditable.yml @@ -10,14 +10,8 @@ on: branches: [ main ] jobs: - build: - strategy: - fail-fast: false - matrix: - runs-on: - - ubuntu-latest - - windows-latest - runs-on: ${{ matrix.runs-on }} + build-ubuntu: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.11 @@ -45,3 +39,26 @@ jobs: - name: Test with pytest run: | python -m pytest tests/test_cli.py # Cli test are here enough + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade cython + python -m pip install --upgrade setuptools + python -m pip install flake8 + # https://github.com/pypa/pip/issues/12030#issuecomment-1546344047 + python -m pip install wheel + pip install -r requirements.txt + python -m pip list + python -m pip install '.[test]' + - name: Test with pytest + run: | + python -m pytest tests/test_cli.py # Cli test are here enough From 203c2582ca2d9b3771a2a9c5ee6b9ba2a8d75a4b Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Tue, 9 Sep 2025 08:06:12 +0200 Subject: [PATCH 04/12] Skip some CLI tests on Windows - Bash syntax does not work on Windows - Perl is not installed on runner by default -> Skip md_eval tests for now --- tests/test_cli.py | 72 +++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3f5506b..3ee5134 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import sys import subprocess from pathlib import Path import shutil @@ -5,8 +6,15 @@ example_files = (Path(__file__).parent.parent / 'example_files').absolute() +on_windows = sys.platform.startswith('win') def run(cmd, cwd=example_files): + is_windows = sys.platform.startswith('win') + + # Translate file paths for windows + if is_windows: + cmd = cmd.replace('/', '\\') + cp = subprocess.run( cmd, shell=True, @@ -30,18 +38,10 @@ def run(cmd, cwd=example_files): f'\n\nstdout:\n{cp.stdout}' f'\n\nstderr:\n{cp.stderr}' ) - -def test_burn_orc(): - # Normal test with stm files - run(f'python -m meeteval.wer orcwer -h hyp.stm -r ref.stm') - # assert (example_files / 'hyp_orc.json').exists() - # assert (example_files / 'hyp_orc_per_reco.json').exists() - run(f'meeteval-wer orcwer -h hyp.stm -r ref.stm') - - # Multiple stm files - run(f"python -m meeteval.wer orcwer -h hypA.stm -h hypB.stm -r refA.stm -r refB.stm") - run(f"python -m meeteval.wer orcwer -h hyp.stm -h hypA.stm hypB.stm -r refA.stm refB.stm") +@pytest.mark.skipif(on_windows, reason='Bash features do not work on Windows.') +def test_bash(): + """Tests Bash-related features like piping and globbing.""" # Test with glob (backwards compatibility). Note: '?' and '*' are escaped. # Be careful, that the glob only matches the desired files. # The 'hyp*.stm' will here match 'hyp.stm', 'hypA.stm' and 'hypB.stm'. @@ -52,17 +52,10 @@ def test_burn_orc(): run(f"python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm") run(f"python -m meeteval.wer orcwer -h hyp?.stm -r ref?.stm") - # Test with ctm files - run(f'python -m meeteval.wer orcwer -h hyp1.ctm -h hyp2.ctm -r ref.stm') - run(f"python -m meeteval.wer orcwer -h 'hyp*.ctm' -r ref.stm") - run(f'python -m meeteval.wer orcwer -h hyp1.ctm -r ref.stm') + # Test path pattern completion + run("python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm --average-out {parent}/{stem}-average-out.yaml") - # Test output formats - run(f"python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm --average-out average-out.json") - # assert (example_files / 'average-out.json').exists() - run("python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm --average-out '{parent}/{stem}-average-out.yaml'") - # assert (example_files / 'hyp-average-out.yaml').exists() - # Output to stdout. Specifying the format requires = + # Test output to stdout. Specifying the format requires = run(f"python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm --average-out -") run(f"python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm --average-out=-.yaml") run(f"python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm --average-out=-.json") @@ -70,6 +63,23 @@ def test_burn_orc(): # Test with pipes. Makes "--average-out" file and "--per-reco-out" file # mandatory. run(f'python -m meeteval.wer orcwer -h <(cat hypA.stm hypB.stm) -r <(cat refA.stm refB.stm) --average-out hyp_orc.json --per-reco-out hyp_orc_per_reco.json') + + # Test output formats + run(f"python -m meeteval.wer orcwer -h hyp*.stm -r ref*.stm --average-out average-out.json") + +def test_burn_orc(): + # Normal test with stm files + run(f'python -m meeteval.wer orcwer -h hyp.stm -r ref.stm') + run(f'meeteval-wer orcwer -h hyp.stm -r ref.stm') + + # Multiple stm files + run(f"python -m meeteval.wer orcwer -h hypA.stm -h hypB.stm -r refA.stm -r refB.stm") + run(f"python -m meeteval.wer orcwer -h hyp.stm -h hypA.stm hypB.stm -r refA.stm refB.stm") + + # Test with ctm files + run(f'python -m meeteval.wer orcwer -h hyp1.ctm -h hyp2.ctm -r ref.stm') + run(f"python -m meeteval.wer orcwer -h hyp1.ctm -h hyp2.ctm -r ref.stm") + run(f'python -m meeteval.wer orcwer -h hyp1.ctm -r ref.stm') # Test with files in SegLST format run(f'python -m meeteval.wer orcwer -h hyp.seglst.json -r ref.seglst.json') @@ -105,7 +115,6 @@ def test_burn_greedy_ditcp(): def test_burn_mimo(): run(f'python -m meeteval.wer mimower -h hyp.stm -r ref.stm') - run(f"python -m meeteval.wer mimower -h 'hyp?.stm' -r 'ref?.stm'") run(f'python -m meeteval.wer mimower -h hyp.seglst.json -r ref.seglst.json') run('python -m meeteval.wer mimower -h hyp.stm -r ref.stm --reference-sort "segment" --hypothesis-sort "false"') @@ -116,25 +125,27 @@ def test_burn_tcmimo(): run(f'python -m meeteval.wer tcmimower -h hyp.seglst.json -r ref.seglst.json --collar 5') run(f'python -m meeteval.wer tcmimower -h hyp.stm -r ref.stm --hypothesis-sort true --collar 5') - def test_burn_cp(): run(f'python -m meeteval.wer cpwer -h hyp.stm -r ref.stm') - run(f"python -m meeteval.wer cpwer -h 'hyp?.stm' -r 'ref?.stm'") run(f'python -m meeteval.wer cpwer -h hyp.seglst.json -r ref.seglst.json') run('python -m meeteval.wer cpwer -h hyp.stm -r ref.stm --reference-sort "segment" --hypothesis-sort "false"') + + # Test UEM file run(f'python -m meeteval.wer cpwer -h hyp.stm -r ref.stm --uem uem.uem') def test_burn_tcp(): - # run(f'python -m meeteval.wer tcpwer -h hyp.stm -r ref.stm') # Mar 2025: Disabled, because default collar=0 is too a too special case to be default run(f'python -m meeteval.wer tcpwer -h hyp.stm -r ref.stm --collar 5') run(f'python -m meeteval.wer tcpwer -h hyp.stm -r ref.stm --hyp-pseudo-word-timing equidistant_points --collar 5') run(f'python -m meeteval.wer tcpwer -h hyp.seglst.json -r ref.seglst.json --collar 5') run(f'python -m meeteval.wer tcpwer -h hyp.stm -r ref.stm --reference-sort word --hypothesis-sort true --collar 5') + # Test that the collar option is mandatory + with pytest.raises(Exception, match='.*the following arguments are required: --collar.*'): + run(f'python -m meeteval.wer tcpwer -h hyp.stm -r ref.stm') + def test_burn_tcorc(): - # run(f'python -m meeteval.wer tcorcwer -h hyp.stm -r ref.stm') # Mar 2025: Disabled, because default collar=0 is too a too special case to be default run(f'python -m meeteval.wer tcorcwer -h hyp.stm -r ref.stm --collar 5') run(f'python -m meeteval.wer tcorcwer -h hyp.stm -r ref.stm --hyp-pseudo-word-timing equidistant_points --collar 5') run(f'python -m meeteval.wer tcorcwer -h hyp.seglst.json -r ref.seglst.json --collar 5') @@ -142,13 +153,13 @@ def test_burn_tcorc(): def test_burn_greedy_tcorc(): - # run(f'python -m meeteval.wer greedy_tcorcwer -h hyp.stm -r ref.stm') # Mar 2025: Disabled, because default collar=0 is too a too special case to be default run(f'python -m meeteval.wer greedy_tcorcwer -h hyp.stm -r ref.stm --collar 5') run(f'python -m meeteval.wer greedy_tcorcwer -h hyp.stm -r ref.stm --hyp-pseudo-word-timing equidistant_points --collar 5') run(f'python -m meeteval.wer greedy_tcorcwer -h hyp.seglst.json -r ref.seglst.json --collar 5') run(f'python -m meeteval.wer greedy_tcorcwer -h hyp.stm -r ref.stm --hypothesis-sort true --collar 5') +@pytest.mark.skipif(on_windows, reason='Requires Perl') def test_burn_md_eval_22(): run(f'python -m meeteval.der md_eval_22 -h hyp.stm -r ref.stm') run(f'meeteval-der md_eval_22 -h hyp.stm -r ref.stm') @@ -164,6 +175,7 @@ def test_burn_md_eval_22(): # ToDo: Table 2 of https://arxiv.org/pdf/2312.04324.pdf lists collars for # datsets. Add them here. +@pytest.mark.skipif(on_windows, reason='Requires Perl') def test_burn_dscore(): run(f'python -m meeteval.der dscore -h hyp.stm -r ref.stm') run(f'meeteval-der dscore -h hyp.stm -r ref.stm') @@ -216,7 +228,8 @@ def test_viz_html(): run(f'python -m meeteval.viz html -h hyp.stm -r ref.stm --out=viz') run(f'python -m meeteval.viz html -h hyp.stm -r ref.stm --alignment cp tcorc') - # Test loading a precomputed assignment +def test_viz_precomputed(): + """Test loading a precomputed assignment""" run(f'python -m meeteval.wer cpwer -h hyp.stm -r ref.stm --per-reco-out hyp_cpwer_per_reco.json') run(f'python -m meeteval.wer tcorcwer -h hyp.stm -r ref.stm --per-reco-out hyp_tcorcwer_per_reco.json --collar 5') run(f'meeteval-viz html -h hyp.stm -r ref.stm --alignment cp tcorc --per-reco-file hyp_cpwer_per_reco.json hyp_tcorcwer_per_reco.json') @@ -231,11 +244,14 @@ def test_normalize(tmpdir): run(f'python -m meeteval.wer normalize hyp.stm -o - --normalizer="lower,rm(.?!,)"') run(f'python -m meeteval.wer normalize hyp.stm -o {tmpdir / "hyp_normalized.stm"} --normalizer="lower,rm([^a-z0-9 ])"') +@pytest.mark.skipif(on_windows, reason='Piping is not supported on Windows') +def test_normalize_piping(tmpdir): # Test that chaining normalizer and wer scripts is equal to using the normalizer option on the script chained = run('python -m meeteval.wer cpwer -r <(python -m meeteval.wer normalize ref.stm -o - --normalizer "lower,rm(.?!,)") -h <(python -m meeteval.wer normalize hyp.stm -o - --normalizer "lower,rm(.?!,)") --average-out - --per-reco-out -') option = run('python -m meeteval.wer cpwer -r ref.stm -h hyp.stm --average-out - --per-reco-out - --normalizer "lower,rm(.?!,)"') assert chained.stdout == option.stdout assert chained.stderr == option.stderr +@pytest.mark.skipif(on_windows, reason='Piping is not supported on Windows') def test_pipe_cli_commands(): run('cat hyp.stm | python -m meeteval.wer normalize - -o - | python -m meeteval.io stm2seglst -o -') From 69cee087f6465321c80f96fe6d2ddab485cbbcee Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Tue, 9 Sep 2025 08:26:52 +0200 Subject: [PATCH 05/12] Use the default shell on Windows for CLI tests --- tests/test_cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3ee5134..c62cf64 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,10 +9,9 @@ on_windows = sys.platform.startswith('win') def run(cmd, cwd=example_files): - is_windows = sys.platform.startswith('win') # Translate file paths for windows - if is_windows: + if on_windows: cmd = cmd.replace('/', '\\') cp = subprocess.run( @@ -23,7 +22,7 @@ def run(cmd, cwd=example_files): check=False, universal_newlines=True, cwd=cwd, - executable='bash', # echo "<(cat hyp.stm)" requires bash not sh. + executable=None if on_windows else 'bash', # echo "<(cat hyp.stm)" requires bash not sh. ) if cp.returncode == 0: From a3eaf8e38abbfd58ce9c0cce7761e897aefc6863 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Tue, 9 Sep 2025 09:45:57 +0200 Subject: [PATCH 06/12] Use absolute file paths when conversion to relative path fails --- meeteval/viz/__main__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/meeteval/viz/__main__.py b/meeteval/viz/__main__.py index 34d2e91..f90e26a 100644 --- a/meeteval/viz/__main__.py +++ b/meeteval/viz/__main__.py @@ -224,7 +224,14 @@ def resolve_system_names(avs): shutil.copy(av['absolute_path'], f_new) av['absolute_path'] = f_new - av['save_path'] = os.path.relpath(av['absolute_path'], out.parent) + try: + av['save_path'] = os.path.relpath(av['absolute_path'], out.parent) + except ValueError: + # Fails on windows when the files are located on different mount + # points. Use the absolute path then. The relative path is meant for + # scenarios where the folders are copied or moved to another + # location. This breaks with absolute paths! + av['save_path'] = av['absolute_path'] out = Path(out) out.parent.mkdir(parents=True, exist_ok=True) From 2b17d0922fae889c8923145774fe75084fe665c5 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Tue, 9 Sep 2025 10:22:01 +0200 Subject: [PATCH 07/12] Run pytest tests on Windows and remove flake8 from pytest job - flake8 is run in install job, we do not need to run it in every test configuration --- .github/workflows/pytest.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f6258c7..94aa58a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -11,13 +11,17 @@ on: jobs: build: - - runs-on: ubuntu-latest + runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false matrix: # Remember to update "classifiers" in setup.py when changing Python version - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] + python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] + runs-on: [ubuntu-latest] + # Run tests on Windows only for a single Python version + include: + - python-version: '3.13' + runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -34,16 +38,10 @@ jobs: python -m pip install flake8 # https://github.com/pypa/pip/issues/12030#issuecomment-1546344047 python -m pip install wheel - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -r requirements.txt python -m pip install pytest-cov codecov python -m pip list python -m pip install -e '.[all]' - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - python -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - python -m flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | python -m pytest From 0c2f67dfa160b4f39234a576e915dbf8de686f30 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Tue, 9 Sep 2025 11:26:32 +0200 Subject: [PATCH 08/12] Update tests for windows --- .github/workflows/pytest.yml | 2 +- meeteval/io/smart.py | 11 +++++++---- meeteval/viz/__main__.py | 7 ++++--- meeteval/viz/visualize.py | 4 +++- setup.py | 1 - tests/test_docs.py | 2 ++ tests/test_io_converters.py | 16 +++++++++++++--- 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 94aa58a..9ef461e 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -20,7 +20,7 @@ jobs: runs-on: [ubuntu-latest] # Run tests on Windows only for a single Python version include: - - python-version: '3.13' + - python-version: '3.12' runs-on: windows-latest steps: diff --git a/meeteval/io/smart.py b/meeteval/io/smart.py index f860c0b..0cd6f2d 100644 --- a/meeteval/io/smart.py +++ b/meeteval/io/smart.py @@ -75,15 +75,18 @@ def _guess_format(path: 'Path | io.TextIOBase'): 'keyed_text' >>> _guess_format(Path('hyp.json')) 'json' - >>> _guess_format(Path('/dev/fd/63')) - 'stm' - >>> _guess_format(Path('/proc/self/fd/63')) - 'stm' >>> from io import StringIO >>> example_files = (Path(__file__).parent.parent.parent / 'example_files').absolute() >>> with open(example_files / 'hyp.stm') as f: ... print(_guess_format(f)) stm + + Tests using special paths do not work on windows + >>> import sys + >>> _guess_format(Path('/dev/fd/63')) if not sys.platform.startswith('win') else 'stm' + 'stm' + >>> _guess_format(Path('/proc/self/fd/63')) if not sys.platform.startswith('win') else 'stm' + 'stm' """ if isinstance(path, io.TextIOBase): path = Path(path.name) diff --git a/meeteval/viz/__main__.py b/meeteval/viz/__main__.py index f90e26a..946ff27 100644 --- a/meeteval/viz/__main__.py +++ b/meeteval/viz/__main__.py @@ -112,7 +112,7 @@ def load_per_reco_file(alignment, f): # Make the save_path relative to the index.html file such that # the links work when moving the folder av.data['save_path'] = str(save_name) - av.data['absolute_path'] = save_path.absolute() + av.data['absolute_path'] = str(save_path.absolute()) avs.append(av.data) dump_overview_table(avs, out / 'index.html') @@ -225,13 +225,14 @@ def resolve_system_names(avs): av['absolute_path'] = f_new try: - av['save_path'] = os.path.relpath(av['absolute_path'], out.parent) + av['save_path'] = str(os.path.relpath(av['absolute_path'], out.parent)) except ValueError: # Fails on windows when the files are located on different mount # points. Use the absolute path then. The relative path is meant for # scenarios where the folders are copied or moved to another # location. This breaks with absolute paths! - av['save_path'] = av['absolute_path'] + av['save_path'] = str(av['absolute_path']) + av['absolute_path'] = str(av['absolute_path']) out = Path(out) out.parent.mkdir(parents=True, exist_ok=True) diff --git a/meeteval/viz/visualize.py b/meeteval/viz/visualize.py index dede148..ecc1e25 100644 --- a/meeteval/viz/visualize.py +++ b/meeteval/viz/visualize.py @@ -81,7 +81,9 @@ def nested_round(obj): if isinstance(path, io.IOBase): simplejson.dump(obj, path, indent=indent, - sort_keys=sort_keys, **kwargs) + sort_keys=sort_keys, + for_json=True, + **kwargs) elif isinstance(path, (str, Path)): path = Path(path).expanduser() diff --git a/setup.py b/setup.py index 72cb4ea..9f5d6f8 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,6 @@ 'aiohttp', 'soundfile', 'tqdm', # Used in meeteval.viz.__main__.py - 'yattag', # Used in meeteval.viz.__main__.py 'platformdirs', # Used in meeteval.viz.visualize.py ] extras_require['test'] = [ diff --git a/tests/test_docs.py b/tests/test_docs.py index 1892d01..82fbcaf 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -3,6 +3,8 @@ if sys.version_info < (3, 11): pytest.skip(reason='algorithms.md requires Python 3.11+', allow_module_level=True) +if sys.platform.startswith('win'): + pytest.skip(reason='Does not run on Windows', allow_module_level=True) import re from pathlib import Path diff --git a/tests/test_io_converters.py b/tests/test_io_converters.py index 5838938..bcdfbae 100644 --- a/tests/test_io_converters.py +++ b/tests/test_io_converters.py @@ -1,3 +1,4 @@ +import sys import itertools from pathlib import Path import pytest @@ -13,7 +14,14 @@ 'seglst': 'hyp.seglst.json', } -def run(cmd): +on_windows = sys.platform.startswith('win') + +def run(cmd, cwd=example_files): + + # Translate file paths for windows + if on_windows: + cmd = cmd.replace('/', '\\') + cp = subprocess.run( cmd, shell=True, @@ -21,8 +29,8 @@ def run(cmd): stderr=subprocess.PIPE, check=False, universal_newlines=True, - cwd=example_files, - executable='bash', # echo "<(cat hyp.stm)" requires bash not sh. + cwd=cwd, + executable=None if on_windows else 'bash', # echo "<(cat hyp.stm)" requires bash not sh. ) if cp.returncode == 0: @@ -62,6 +70,7 @@ def test_merge_ctm_speaker_arg(tmp_path): assert (tmp_path / "hyp.stm").exists() meeteval.io.load(tmp_path / "hyp.stm").to_seglst().unique('speaker') == {'spk-A'} +@pytest.mark.skipif(on_windows, reason='Piping does not work on Windows') def test_piping(tmp_path): run(f'cat {example_files / "hyp.stm"} | meeteval-io stm2rttm {tmp_path / "hyp.rttm"}') run(f'cat {example_files / "hyp.stm"} | meeteval-io stm2rttm -') @@ -92,6 +101,7 @@ def test_convert_file_exists(tmp_path): run(f'meeteval-io stm2rttm --force {example_files / "hyp.stm"} {tmp_path / "hyp.rttm"}') run(f'meeteval-io stm2rttm -f {example_files / "hyp.stm"} {tmp_path / "hyp.rttm"}') +@pytest.mark.skipif(on_windows, reason='Piping does not work on Windows') def test_ctm_piping(): run(f'cat {example_files / "hyp1.ctm"} | meeteval-io ctm2stm --speaker spk-A - > /dev/null') with pytest.raises(Exception, match='.*the following arguments are required: --speaker.*'): From 6b218e2a7eabfa3ee23f45f1e7c7cb5bd83bdbd1 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Fri, 12 Sep 2025 15:44:36 +0200 Subject: [PATCH 09/12] Fix viz test on Windows --- meeteval/viz/overview_table.py | 4 +++- tests/test_di_cp.py | 3 ++- tests/test_viz.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/meeteval/viz/overview_table.py b/meeteval/viz/overview_table.py index 836e4f3..3fd309d 100644 --- a/meeteval/viz/overview_table.py +++ b/meeteval/viz/overview_table.py @@ -62,8 +62,10 @@ def get_average_wer(data): f',\n{indent}]' ) html = (Path(__file__).parent / 'overview_table.html').read_text() - import re + + # Escape backslashes for windows paths. re.escape escapes too much + html_data = html_data.replace('\\', '\\\\') html, n = re.subn(f'// DATA START((.|\n)*)// DATA END', f'const data = {html_data};', html) assert n == 1, (html, n) return html diff --git a/tests/test_di_cp.py b/tests/test_di_cp.py index 468ad0e..ee5f118 100644 --- a/tests/test_di_cp.py +++ b/tests/test_di_cp.py @@ -1,9 +1,10 @@ from hypothesis import given, strategies as st, assume, settings import pytest import meeteval +from meeteval.io import SegLST seglst = st.builds( - meeteval.io.SegLST, + SegLST, st.lists( st.builds( lambda **x: { diff --git a/tests/test_viz.py b/tests/test_viz.py index efadcdc..59b2028 100644 --- a/tests/test_viz.py +++ b/tests/test_viz.py @@ -99,7 +99,7 @@ def get_index_html_data(path): import re import yaml content = path.read_text() - data = re.search('data = (\[\n(.|\n)*\n\s*]);', content).groups(1)[0] + data = re.search('data = (\[\n(.|\n)*\n\s*\]);', content).groups(1)[0] data = yaml.safe_load(data) # JSON complains about trailing comma return data From 7bfa241903608298601ef6b35c8dd6a4502551c5 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Fri, 12 Sep 2025 16:02:10 +0200 Subject: [PATCH 10/12] Disable doctests on Windows --- .github/workflows/pytest.yml | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 9ef461e..1226edb 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -10,19 +10,13 @@ on: branches: [ main ] jobs: - build: - runs-on: ${{ matrix.runs-on }} + test-ubuntu: + runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Remember to update "classifiers" in setup.py when changing Python version python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] - runs-on: [ubuntu-latest] - # Run tests on Windows only for a single Python version - include: - - python-version: '3.12' - runs-on: windows-latest - steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -45,3 +39,37 @@ jobs: - name: Test with pytest run: | python -m pytest + test-windows: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + # Remember to update "classifiers" in setup.py when changing Python version + python-version: ['3.12'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install Perl + uses: shogo82148/actions-setup-perl@v1 + with: + perl-version: "5.38" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade cython + python -m pip install --upgrade setuptools + python -m pip install flake8 + # https://github.com/pypa/pip/issues/12030#issuecomment-1546344047 + python -m pip install wheel + pip install -r requirements.txt + python -m pip install pytest-cov codecov + python -m pip list + python -m pip install -e '.[all]' + - name: Test with pytest + run: | + # Ignore doctests on Windows. They are hard to guard w.r.t. file paths + python -m pytest tests From f2ccddcba1964cc43ed1050256293fa79bce5ace Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Fri, 12 Sep 2025 17:12:20 +0200 Subject: [PATCH 11/12] Revert _guess_format doctest for windows --- meeteval/io/smart.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/meeteval/io/smart.py b/meeteval/io/smart.py index 0cd6f2d..f860c0b 100644 --- a/meeteval/io/smart.py +++ b/meeteval/io/smart.py @@ -75,18 +75,15 @@ def _guess_format(path: 'Path | io.TextIOBase'): 'keyed_text' >>> _guess_format(Path('hyp.json')) 'json' + >>> _guess_format(Path('/dev/fd/63')) + 'stm' + >>> _guess_format(Path('/proc/self/fd/63')) + 'stm' >>> from io import StringIO >>> example_files = (Path(__file__).parent.parent.parent / 'example_files').absolute() >>> with open(example_files / 'hyp.stm') as f: ... print(_guess_format(f)) stm - - Tests using special paths do not work on windows - >>> import sys - >>> _guess_format(Path('/dev/fd/63')) if not sys.platform.startswith('win') else 'stm' - 'stm' - >>> _guess_format(Path('/proc/self/fd/63')) if not sys.platform.startswith('win') else 'stm' - 'stm' """ if isinstance(path, io.TextIOBase): path = Path(path.name) From 50e51126bba2ab3b4284255bfd6f2367cff5a787 Mon Sep 17 00:00:00 2001 From: Thilo von Neumann Date: Fri, 12 Sep 2025 17:14:31 +0200 Subject: [PATCH 12/12] Enable DER tests on Windows --- tests/test_cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c62cf64..04725f0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -158,7 +158,6 @@ def test_burn_greedy_tcorc(): run(f'python -m meeteval.wer greedy_tcorcwer -h hyp.stm -r ref.stm --hypothesis-sort true --collar 5') -@pytest.mark.skipif(on_windows, reason='Requires Perl') def test_burn_md_eval_22(): run(f'python -m meeteval.der md_eval_22 -h hyp.stm -r ref.stm') run(f'meeteval-der md_eval_22 -h hyp.stm -r ref.stm') @@ -174,7 +173,6 @@ def test_burn_md_eval_22(): # ToDo: Table 2 of https://arxiv.org/pdf/2312.04324.pdf lists collars for # datsets. Add them here. -@pytest.mark.skipif(on_windows, reason='Requires Perl') def test_burn_dscore(): run(f'python -m meeteval.der dscore -h hyp.stm -r ref.stm') run(f'meeteval-der dscore -h hyp.stm -r ref.stm')