diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3ca9d6b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,51 @@ +name: tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: ${{ matrix.os }} / Python ${{ matrix.python }} / ${{ matrix.numpy }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + numpy: ["numpy"] + include: + - os: ubuntu-latest + python: "3.12" + numpy: "numpy<2" + - os: macos-latest + python: "3.13" + numpy: "numpy" + - os: windows-latest + python: "3.13" + numpy: "numpy" + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + # Install from the sdist, in an isolated build: this checks MANIFEST.in + # and the build requirements of pyproject.toml as well. + - name: Build the sdist + run: | + python -m pip install --upgrade pip build + python -m build --sdist --outdir dist + - name: Install the sdist + shell: bash + run: python -m pip install "${{ matrix.numpy }}" scipy pytest dist/*.tar.gz + # Run outside the checkout, so that the installed (compiled) module is + # the one imported. + - name: Test + working-directory: ${{ runner.temp }} + shell: bash + run: python -m pytest -q --import-mode=importlib "$GITHUB_WORKSPACE/tests" diff --git a/setup.py b/setup.py index c09bf17..4914350 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ setup( name = 'svmloader', - version = '0.5.1', + version = '0.5.2', description = DESCRIPTION, long_description = LONG_DESCRIPTION, long_description_content_type = 'text/markdown', diff --git a/svmloader.pyx b/svmloader.pyx index 9c14a76..0f322f8 100644 --- a/svmloader.pyx +++ b/svmloader.pyx @@ -69,7 +69,7 @@ cdef _load_svmfile(fp, dtype, ltype, bint zero_based, bint multilabels): # process the line last_idx = -1 - while s[0] != '#' and s[0] != '\n' and s[0] != 0: + while s[0] != '#' and s[0] != '\n' and s[0] != '\r' and s[0] != 0: # '\r': CRLF line ending # get index idx = strtol(s, &end, 10) if s == end: @@ -225,7 +225,8 @@ cdef _save_svmfile(filename, mat, labels, bint zero_based, cdef Py_ssize_t i cdef Py_ssize_t j cdef Py_ssize_t offset = 0 if zero_based else 1 - with open(filename, 'w') as fp: + # '\n' line endings on every platform, as libsvm files have + with open(filename, 'w', newline='\n') as fp: for i in range(indptr.shape[0] - 1): if labels is not None: fp.write('%s ' % labels[i]) diff --git a/tests/test_svmloader.py b/tests/test_svmloader.py index da5b101..31054b6 100644 --- a/tests/test_svmloader.py +++ b/tests/test_svmloader.py @@ -154,3 +154,22 @@ def test_load_compressed_file(tmp_path, suffix): np.testing.assert_array_equal(y_comp, y_plain) np.testing.assert_allclose(X_comp.toarray(), X_plain.toarray()) + + +def test_load_crlf_file(tmp_path): + path = tmp_path / "crlf.svm" + path.write_bytes(DATA.replace("\n", "\r\n").encode()) + + X, y = svmloader.load_svmfile(str(path), zero_based=False) + + assert y.tolist() == [1, 0] + np.testing.assert_allclose(X.toarray(), [[0.5, 0.0, 2.0], [0.0, 1.0, 0.0]]) + + +def test_save_svmfile_writes_lf_line_endings(tmp_path): + X = sp.csr_matrix(np.array([[0.0, 1.5], [2.0, 0.0]])) + path = tmp_path / "out.svm" + + svmloader.save_svmfile(str(path), X, np.array([1, 0])) + + assert path.read_bytes() == b"1 2:1.5\n0 1:2\n"