diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 263e360..37e11a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -# Run on push only for dev/sandbox +# Run on push only for ci/staging # Otherwise it may trigger concurrently `push & pull_request` on PRs. on: push: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a794f5e..ba50793 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,48 +1,33 @@ name: Main -# Run on push only for dev/sandbox -# Otherwise it may trigger concurrently `push & pull_request` on PRs. on: + pull_request: null push: branches: - master jobs: build: - name: Python ${{ matrix.python }} + name: Linux runs-on: ubuntu-latest - strategy: - matrix: - python: - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "3.14" - - "pypy3.11" - steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: setup python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: ${{ matrix.python }} + python-version: | + pypy3.11 + 3.10 + 3.11 + 3.12 + 3.13 + 3.14 - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -e . - pip install coveralls --upgrade - - name: Run flake8 - run: | - pip install flake8 --upgrade - flake8 --exclude=build --ignore=E501,F403,F401,E241,E225,E128 . - - name: Run pycodestyle - run: | - pip install pycodestyle --upgrade - pycodestyle --ignore=E128,E261,E225,E501,W605 slugify test.py setup.py + python -m pip install coveralls tox tox-uv - name: Run test run: | - coverage run --source=slugify test.py + tox - name: Coveralls run: coveralls --service=github env: diff --git a/CHANGELOG.md b/CHANGELOG.md index f062ed0..400399f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ - Support Python 3.14. - Drop support for Python 3.9 and lower. +- Use tox for local test runs and in CI. +- Test the project against both `unidecode` and `text_unidecode`. +- Fix type annotation issues identified by mypy. +- Run CI against pull requests. ## 8.0.4 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..de62727 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +# coverage +# -------- + +[tool.coverage.run] +relative_files = true +parallel = true +branch = true +source = [ + "slugify", + "test", +] + +[tool.coverage.paths] +source = [ + "src", + "*/site-packages", +] + +[tool.coverage.report] +skip_covered = true +fail_under = 97 + + +# mypy +# ---- + +[tool.mypy] +packages = "slugify" +strict = true +sqlite_cache = true + + +# pytest +# ------ + +[tool.pytest.ini_options] +testpaths = ["test.py"] +addopts = "--color=yes" +filterwarnings = [ + "error", +] diff --git a/slugify/__main__.py b/slugify/__main__.py index 4cc4616..4e6b3d9 100644 --- a/slugify/__main__.py +++ b/slugify/__main__.py @@ -47,7 +47,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.error("Input strings and --stdin cannot work together") if args.replacements: - def split_check(repl): + def split_check(repl: str) -> list[str]: SEP = '->' if SEP not in repl: parser.error("Replacements must be of the form: ORIGINAL{SEP}REPLACED".format(SEP=SEP)) @@ -82,7 +82,7 @@ def slugify_params(args: argparse.Namespace) -> dict[str, Any]: ) -def main(argv: list[str] | None = None): # pragma: no cover +def main(argv: list[str] | None = None) -> None: """ Run this program """ if argv is None: argv = sys.argv @@ -94,5 +94,5 @@ def main(argv: list[str] | None = None): # pragma: no cover sys.exit(-1) -if __name__ == '__main__': # pragma: no cover +if __name__ == '__main__': main() diff --git a/slugify/slugify.py b/slugify/slugify.py index 67b31c8..9b5f27f 100644 --- a/slugify/slugify.py +++ b/slugify/slugify.py @@ -8,7 +8,7 @@ try: import unidecode except ImportError: - import text_unidecode as unidecode + import text_unidecode as unidecode # type: ignore[import-untyped, no-redef] __all__ = ['slugify', 'smart_truncate'] @@ -67,7 +67,7 @@ def smart_truncate( else: if save_order: break - if not truncated: # pragma: no cover + if not truncated: truncated = string[:max_length] return truncated.strip(separator) diff --git a/test.py b/test.py index d13ef94..fcec4b6 100644 --- a/test.py +++ b/test.py @@ -653,5 +653,5 @@ def test_multivalued_options_with_text(self): self.assertEqual(params['stopwords'], ['the', 'in', 'a', 'hurry']) -if __name__ == '__main__': +if __name__ == '__main__': # pragma: nocover unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0c16f5e --- /dev/null +++ b/tox.ini @@ -0,0 +1,69 @@ +[tox] +env_list = + coverage-erase + py{3.10, 3.11, 3.12, 3.13, 3.14}-{unidecode, text_unidecode} + pypy{3.11}-{unidecode, text_unidecode} + coverage-report + coverage-html + mypy + pycodestyle + +[testenv] +depends = + py{3.10, 3.11, 3.12, 3.13, 3.14}-{unidecode, text_unidecode}: coverage-erase + pypy{3.11}-{unidecode, text_unidecode}: coverage-erase +deps = + coverage[toml] + pytest + unidecode: pip + unidecode: unidecode +commands_pre: + # If testing unidecode, ensure text_unidecode is unavailable. + unidecode: pip uninstall --yes text_unidecode +commands = + coverage run -m pytest test.py + +[testenv:coverage_base] +deps = + coverage[toml] + +[testenv:coverage-erase] +base = coverage_base +commands = + coverage erase + +[testenv:coverage-report] +base = coverage_base +depends = + py{3.10, 3.11, 3.12, 3.13, 3.14}-{unidecode, text_unidecode} + pypy{3.11}-{unidecode, text_unidecode} +commands_pre = + - coverage combine +commands = + coverage report + +[testenv:coverage-html] +base = coverage_base +depends = + coverage-report +commands = + coverage html --fail-under=0 + +[testenv:mypy] +deps = + mypy + unidecode +commands = + mypy + +[testenv:pycodestyle] +deps = + pycodestyle +commands = + pycodestyle --ignore=E128,E261,E225,E501,W605 slugify test.py setup.py + +[testenv:flake8] +deps = + flake8 +commands = + flake8 --ignore=E501,F403,F401,E241,E225,E128 slugify/ setup.py test.py