From 34e6dc9027b84c5400a2d47a64277b5114eb758f Mon Sep 17 00:00:00 2001 From: Kenneth Enevoldsen Date: Tue, 10 Oct 2023 20:23:26 +0200 Subject: [PATCH] fix: updated from cruft template --- .cookiecutter.json | 1 - .cruft.json | 3 +- .github/workflows/documentation.yml | 3 +- .github/workflows/pre-commit.yml | 2 +- .github/workflows/release.yml | 35 ++- .github/workflows/static_type_checks.yml | 97 +++++++ .github/workflows/tests.yml | 21 +- pyproject.toml | 52 ++-- tasks.py | 307 ++++++++++++++++------- tests/__init.py | 0 10 files changed, 394 insertions(+), 127 deletions(-) create mode 100644 .github/workflows/static_type_checks.yml create mode 100644 tests/__init.py diff --git a/.cookiecutter.json b/.cookiecutter.json index d9f0ebab..2665f074 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -3,7 +3,6 @@ "*.github" ], "_template": "https://github.com/MartinBernstorff/swift-python-cookiecutter", - "add_makefile": "y", "author": "Kenneth Enevoldsen", "copyright_year": "2023", "email": "kennethcenevoldsen@gmail.com", diff --git a/.cruft.json b/.cruft.json index 1502a5ab..0aba82e7 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/MartinBernstorff/swift-python-cookiecutter", - "commit": "691543da74e08c82a24f26eae89536b1c5581673", + "commit": "7fdb02999e8596c525377c208ca902645d134f97", "checkout": null, "context": { "cookiecutter": { @@ -12,7 +12,6 @@ "github_user": "centre-for-humanities-computing", "version": "2.4.2", "copyright_year": "2023", - "add_makefile": "y", "license": "MIT", "_copy_without_render": [ "*.github" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index f3761351..95d92816 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -16,6 +16,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 # otherwise, you will failed to push refs to dest repo + token: ${{ secrets.PAT }} - name: Install dependencies shell: bash @@ -32,5 +33,5 @@ jobs: if: ${{ github.event_name == 'push' }} uses: ad-m/github-push-action@v0.6.0 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.PAT }} branch: gh-pages diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index f2125b63..79671c4a 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -64,7 +64,7 @@ jobs: ๐ŸŽ๏ธ Get results locally by running `pre-commit run --all-files` ๐Ÿ•ต๏ธ Examine the results in the `Run pre-commit` section of this workflow `pre-commit` - We also strongly recommend setting up the `ruff` and `black` extensions to auto-format on save in your chosen editor. + We also recommend setting up the `ruff` and `black` extensions to auto-format on save in your chosen editor. - name: Commit formatting if: ${{ steps.pre_commit.outputs.pre_commit_failed == 1 && github.event_name == 'pull_request' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1c5b38e..886fd151 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,12 +8,21 @@ name: Release on: push: branches: [main] + workflow_run: + workflows: ["tests"] + types: + - completed + jobs: release: runs-on: ubuntu-latest concurrency: release + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing using PyPI + # a guide on how to set it up is available here: https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ + - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' && github.event.workflow_run.conclusion == 'success'}} steps: # Checkout action is required for token to persist - uses: actions/checkout@v3 @@ -22,14 +31,20 @@ jobs: token: ${{ secrets.PAT }} - name: Python Semantic Release - uses: relekang/python-semantic-release@v7.33.2 + uses: python-semantic-release/python-semantic-release@v8.0.4 + id: release + with: + github_token: ${{ secrets.PAT }} + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + if: steps.release.outputs.released == 'true' + # This action supports PyPI's trusted publishing implementation, which allows authentication to PyPI without a manually + # configured API token or username/password combination. To perform trusted publishing with this action, your project's + # publisher must already be configured on PyPI. + + - name: Publish package distributions to GitHub Releases + uses: python-semantic-release/upload-to-gh-release@main + if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.PAT }} - # Remember to copy the [tool.semantic_release] section from pyproject.toml - # as well - # To enable pypi, - # 1) Set upload_to_pypi to true in pyproject.toml and - # 2) Set the pypi_token in the repo - # 3) Uncomment the two lines below - repository_username: __token__ - repository_password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/static_type_checks.yml b/.github/workflows/static_type_checks.yml new file mode 100644 index 00000000..72ef703a --- /dev/null +++ b/.github/workflows/static_type_checks.yml @@ -0,0 +1,97 @@ +# We do not include static_type_checks as a pre-commit hook because pre-commit hooks +# are installed in their own virtual environment, so static_type_checks cannot +# use stubs from imports +name: static_type_checks + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + static_type_checks: + runs-on: ubuntu-latest + permissions: + pull-requests: write + concurrency: + group: "${{ github.workflow }} @ ${{ github.ref }}" + cancel-in-progress: true + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.9"] + steps: + - uses: actions/checkout@v3 + + - name: Cache tox + uses: actions/cache@v3.2.6 + id: cache_tox + with: + path: | + .tox + key: ${{ runner.os }}-${{ matrix.python-version }}-static-type-checks + + - name: Set up Python + uses: actions/setup-python@v4 + id: setup_python + with: + python-version: ${{ matrix.python-version}} + + - name: Install dependencies + shell: bash + run: | + pip install invoke tox + + - name: Run static type checker + id: pyright + continue-on-error: true + run: | + if inv static-type-checks; then + echo "pyright check passed" + echo "pyright_failed=0" >> $GITHUB_OUTPUT + else + echo "pyright check failed" + echo "pyright_failed=1" >> $GITHUB_OUTPUT + fi + + - name: Find Comment + uses: peter-evans/find-comment@v2 + id: find_comment + if: ${{github.event_name == 'pull_request'}} + continue-on-error: true + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: โœจ Looks like pyright failed โœจ + + - uses: mshick/add-pr-comment@v2 + if: ${{ steps.pyright.outputs.pyright_failed == 1 && github.event_name == 'pull_request'}} + id: add_comment + with: + message: | + โœจ Looks like pyright failed โœจ + + If you want to fix this, we recommend doing it locally by either: + + a) Enabling pyright in VSCode and going through the errors in the problems tab + + `VSCode settings > Python > Analysis: Type checking mode > "basic"` + + b) Debugging via the command line + + 1. Installing pyright, which is included in the dev dependencies: `pip install -e ".[dev]"` + 2. Diagnosing the errors by running `pyright .` + + - uses: mshick/add-pr-comment@v2 + if: ${{ steps.pyright.outputs.pyright_failed == 0 && steps.find_comment.outputs.comment-id != '' && github.event_name == 'pull_request'}} + with: + message-id: ${{ steps.find_comment.outputs.comment-id }} + message: | + ๐ŸŒŸ pyright succeeds! ๐ŸŒŸ + + - name: Show pyright output + id: fail_run + if: ${{steps.pyright.outputs.pyright_failed == 1}} + run: | + inv static-type-checks # Rerunning pyright isn't optimal computationally, but typically takes no more than a couple of seconds, and this ensures that the errors are in the failing step diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 88d1271d..21a13d1d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,9 +30,17 @@ jobs: steps: - uses: actions/checkout@v3 + + - name: Cache tox + uses: actions/cache@v3.2.6 + id: cache_tox + with: + path: | + .tox + key: ${{ runner.os }}-${{ matrix.python-version }}-tests + - name: Set up Python uses: actions/setup-python@v4 - id: setup_python with: python-version: ${{ matrix.python-version }} cache: 'pip' # caching pip dependencies @@ -40,13 +48,16 @@ jobs: - name: Install dependencies shell: bash run: | - pip install .[tests] + pip install invoke tox - name: Run and write pytest shell: bash run: | - python -m pytest --durations=0 -x --junitxml=pytest.xml --cov-report=term-missing --cov=src/ tests/ - + source .venv/bin/activate + pytest --durations=0 -n 2 -x --junitxml=pytest.xml --cov-report=term-missing --cov=src/ tests/ + # Specifying two sets of "--pytest-args" is required for invoke to parse it as a list + inv test --pytest-args="--durations=0" --pytest-args="--junitxml=pytest.xml --cov-report=term-missing --cov=src/" + - name: Test report on failures uses: EnricoMi/publish-unit-test-result-action@v2 id: test_report_with_annotations @@ -62,6 +73,6 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' && github.actor != 'dependabot[bot]' && github.event_name == 'pull_request' && (success() || failure()) }} with: create-new-comment: false - report-only-changed-files: true + report-only-changed-files: false pytest-coverage-path: pytest-coverage.txt junitxml-path: ./pytest.xml diff --git a/pyproject.toml b/pyproject.toml index 652d5ff7..615dc1c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,8 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] keywords = ["nlp", "danish", "spacy-universe"] @@ -46,13 +48,7 @@ repository = "https://github.com/centre-for-humanities-computing/DaCy" file = "LICENSE" name = "Apache License 2.0" [project.optional-dependencies] -dev = [ - "cruft", - "mypy", - "pre-commit>=2.20.0", - "ruff>=0.0.262", - "black[jupyter]>=23.3.0", -] +dev = ["cruft", "pre-commit>=2.20.0", "ruff>=0.0.262", "black[jupyter]>=23.3.0"] tests = [ "pytest>=7.1.2", "pytest-cov>=3.0.0", @@ -109,10 +105,9 @@ where = ["src"] [tool.coverage.run] omit = ["**/tests/*", "**/about.py", "**/dev/*"] -[tool.mypy] -ignore_missing_imports = true -no_implicit_optional = true -warn_unreachable = true +[tool.pyright] +exclude = [".*venv*", ".tox"] +pythonPlatform = "Darwin" [tool.ruff] # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. @@ -164,7 +159,6 @@ exclude = [ ".eggs", ".git", ".hg", - ".mypy_cache", ".nox", ".pants.d", ".pytype", @@ -206,10 +200,38 @@ max-complexity = 10 [tool.semantic_release] branch = "main" -version_variable = ["pyproject.toml:version"] -upload_to_pypi = true -upload_to_release = true +version_toml = ["pyproject.toml:project.version"] build_command = "python -m pip install build; python -m build" [tool.setuptools] include-package-data = true + + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py{39,310} + +[testenv] +description: run unit tests +extras = tests +use_develop = true +commands = + pytest -n auto {posargs:test} + +[testenv:type] +description: run type checks +extras = tests, dev +basepython = py39 # Setting these explicitly avoid recreating env if your shell is set to a different version +use_develop = true +commands = + pyright src/ + +[testenv:docs] +description: build docs +extras = docs +basepython = py39 # Setting these explicitly avoid recreating env if your shell is set to a different version +use_develop = true +commands = + sphinx-build -b html docs docs/_build/html +""" diff --git a/tasks.py b/tasks.py index 9a079515..eba651a3 100644 --- a/tasks.py +++ b/tasks.py @@ -18,79 +18,126 @@ import platform import re -from dataclasses import dataclass +import shutil from pathlib import Path -from typing import Optional +from typing import List, Optional from invoke import Context, Result, task +# Extract supported python versions from the pyproject.toml classifiers key +SUPPORTED_PYTHON_VERSIONS = [ + line.split("::")[-1].strip().replace('"', "").replace(",", "") + for line in Path("pyproject.toml").read_text().splitlines() + if "Programming Language :: Python ::" in line +] + +NOT_WINDOWS = platform.system() != "Windows" + def echo_header(msg: str): print(f"\n--- {msg} ---") -@dataclass -class Emo: - DO = "๐Ÿค–" - GOOD = "โœ…" - FAIL = "๐Ÿšจ" - WARN = "๐Ÿšง" - SYNC = "๐Ÿš‚" - PY = "๐Ÿ" - CLEAN = "๐Ÿงน" - TEST = "๐Ÿงช" - COMMUNICATE = "๐Ÿ“ฃ" - EXAMINE = "๐Ÿ”" +class MsgType: + # Emojis have to be encoded as bytes to not break the terminal on Windows + @property + def DOING(self) -> str: + return b"\xf0\x9f\xa4\x96".decode() if NOT_WINDOWS else "DOING:" + + @property + def GOOD(self) -> str: + return b"\xe2\x9c\x85".decode() if NOT_WINDOWS else "DONE:" + + @property + def FAIL(self) -> str: + return b"\xf0\x9f\x9a\xa8".decode() if NOT_WINDOWS else "FAILED:" + + @property + def WARN(self) -> str: + return b"\xf0\x9f\x9a\xa7".decode() if NOT_WINDOWS else "WARNING:" + + @property + def SYNC(self) -> str: + return b"\xf0\x9f\x9a\x82".decode() if NOT_WINDOWS else "SYNCING:" + + @property + def PY(self) -> str: + return b"\xf0\x9f\x90\x8d".decode() if NOT_WINDOWS else "" + + @property + def CLEAN(self) -> str: + return b"\xf0\x9f\xa7\xb9".decode() if NOT_WINDOWS else "CLEANING:" + + @property + def TEST(self) -> str: + return b"\xf0\x9f\xa7\xaa".decode() if NOT_WINDOWS else "TESTING:" + + @property + def COMMUNICATE(self) -> str: + return b"\xf0\x9f\x93\xa3".decode() if NOT_WINDOWS else "COMMUNICATING:" + + @property + def EXAMINE(self) -> str: + return b"\xf0\x9f\x94\x8d".decode() if NOT_WINDOWS else "VIEWING:" + + +msg_type = MsgType() def git_init(c: Context, branch: str = "main"): """Initialize a git repository if it does not exist yet.""" # If no .git directory exits if not Path(".git").exists(): - echo_header(f"{Emo.DO} Initializing Git repository") + echo_header(f"{msg_type.DOING} Initializing Git repository") c.run(f"git init -b {branch}") c.run("git add .") - c.run("git commit -m 'Initial commit'") - print(f"{Emo.GOOD} Git repository initialized") + c.run("git commit -m 'Init'") + print(f"{msg_type.GOOD} Git repository initialized") else: - print(f"{Emo.GOOD} Git repository already initialized") + print(f"{msg_type.GOOD} Git repository already initialized") def setup_venv( c: Context, - python_version: str, + python_path: str, + venv_name: Optional[str] = None, ) -> str: - venv_name = f'.venv{python_version.replace(".", "")}' + """Create a virtual environment if it does not exist yet. + + Args: + c: The invoke context. + python_path: The python executable to use. + venv_name: The name of the virtual environment. Defaults to ".venv". + """ + if venv_name is None: + venv_name = ".venv" if not Path(venv_name).exists(): echo_header( - f"{Emo.DO} Creating virtual environment for {Emo.PY}{python_version}", + f"{msg_type.DOING} Creating virtual environment using {msg_type.PY}:{python_path}", ) - c.run(f"python{python_version} -m venv {venv_name}") - print(f"{Emo.GOOD} Virtual environment created") + c.run(f"{python_path} -m venv {venv_name}") + print(f"{msg_type.GOOD} Virtual environment created") else: - print(f"{Emo.GOOD} Virtual environment already exists") - - c.run(f"source {venv_name}/bin/activate") - + print(f"{msg_type.GOOD} Virtual environment already exists") return venv_name def _add_commit(c: Context, msg: Optional[str] = None): - print("๐Ÿ”จ Adding and committing changes") + print(f"{msg_type.DOING} Adding and committing changes") c.run("git add .") if msg is None: msg = input("Commit message: ") - c.run(f'git commit -m "{msg}"', pty=True, hide=True) - print("\n๐Ÿค– Changes added and committed\n") + c.run(f'git commit -m "{msg}"', pty=NOT_WINDOWS, hide=True) + print(f"{msg_type.GOOD} Changes added and committed") def is_uncommitted_changes(c: Context) -> bool: git_status_result: Result = c.run( "git status --porcelain", - pty=True, + pty=NOT_WINDOWS, hide=True, ) @@ -103,12 +150,12 @@ def add_and_commit(c: Context, msg: Optional[str] = None): if is_uncommitted_changes(c): uncommitted_changes_descr = c.run( "git status --porcelain", - pty=True, + pty=NOT_WINDOWS, hide=True, ).stdout echo_header( - f"{Emo.WARN} Uncommitted changes detected", + f"{msg_type.WARN} Uncommitted changes detected", ) for line in uncommitted_changes_descr.splitlines(): @@ -129,7 +176,7 @@ def branch_exists_on_remote(c: Context) -> bool: def update_branch(c: Context): - echo_header(f"{Emo.SYNC} Syncing branch with remote") + echo_header(f"{msg_type.SYNC} Syncing branch with remote") if not branch_exists_on_remote(c): c.run("git push --set-upstream origin HEAD") @@ -143,12 +190,12 @@ def update_branch(c: Context): def create_pr(c: Context): c.run( "gh pr create --web", - pty=True, + pty=NOT_WINDOWS, ) def update_pr(c: Context): - echo_header(f"{Emo.COMMUNICATE} Syncing PR") + echo_header(f"{msg_type.COMMUNICATE} Syncing PR") # Get current branch name branch_name = Path(".git/HEAD").read_text().split("/")[-1].strip() pr_result: Result = c.run( @@ -162,7 +209,7 @@ def update_pr(c: Context): else: open_web = input("Open in browser? [y/n] ") if "y" in open_web.lower(): - c.run("gh pr view --web", pty=True) + c.run("gh pr view --web", pty=NOT_WINDOWS) def exit_if_error_in_stdout(result: Result): @@ -176,78 +223,150 @@ def exit_if_error_in_stdout(result: Result): exit(0) -def pre_commit(c: Context): +def pre_commit(c: Context, auto_fix: bool): """Run pre-commit checks.""" # Essential to have a clean working directory before pre-commit to avoid committing # heterogenous files under a "style: linting" commit if is_uncommitted_changes(c): print( - f"{Emo.WARN} Your git working directory is not clean. Stash or commit before running pre-commit.", + f"{msg_type.WARN} Your git working directory is not clean. Stash or commit before running pre-commit.", ) - exit(0) + exit(1) - echo_header(f"{Emo.CLEAN} Running pre-commit checks") + echo_header(f"{msg_type.CLEAN} Running pre-commit checks") pre_commit_cmd = "pre-commit run --all-files" - result = c.run(pre_commit_cmd, pty=True, warn=True) + result = c.run(pre_commit_cmd, pty=NOT_WINDOWS, warn=True) exit_if_error_in_stdout(result) - if "fixed" in result.stdout or "reformatted" in result.stdout: + if ("fixed" in result.stdout or "reformatted" in result.stdout) and auto_fix: _add_commit(c, msg="style: Auto-fixes from pre-commit") - print(f"{Emo.DO} Fixed errors, re-running pre-commit checks") - second_result = c.run(pre_commit_cmd, pty=True, warn=True) + print(f"{msg_type.DOING} Fixed errors, re-running pre-commit checks") + second_result = c.run(pre_commit_cmd, pty=NOT_WINDOWS, warn=True) exit_if_error_in_stdout(second_result) + else: + if result.return_code != 0: + print(f"{msg_type.FAIL} Pre-commit checks failed") + exit(1) -def mypy(c: Context): - echo_header(f"{Emo.CLEAN} Running mypy") - c.run("mypy .", pty=True) +@task +def static_type_checks(c: Context): + echo_header(f"{msg_type.CLEAN} Running static type checks") + c.run("tox -e type", pty=NOT_WINDOWS) @task -def install(c: Context): - echo_header(f"{Emo.DO} Installing project") - c.run("pip install -e '.[dev,tests,docs]'") +def install( + c: Context, + pip_args: str = "", + msg: bool = True, + venv_path: Optional[str] = None, +): + """Install the project in editable mode using pip install""" + if msg: + echo_header(f"{msg_type.DOING} Installing project") + extras = ".[dev,tests,docs]" if NOT_WINDOWS else ".[dev,tests,docs]" + install_cmd = f"pip install -e {extras} {pip_args}" + + if venv_path is not None and NOT_WINDOWS: + with c.prefix(f"source {venv_path}/bin/activate"): + c.run(install_cmd) + return + + c.run(install_cmd) + + +def get_python_path(preferred_version: str) -> Optional[str]: + """Get path to python executable.""" + preferred_version_path = shutil.which(f"python{preferred_version}") + + if preferred_version_path is not None: + return preferred_version_path -@task -def setup(c: Context, python_version: str = "3.9"): - git_init(c) - venv_name = setup_venv(c, python_version=python_version) print( - f"{Emo.DO} Activate your virtual environment by running: \n\n\t\t source {venv_name}/bin/activate \n", + f"{msg_type.WARN}: python{preferred_version} not found, continuing with default python version", ) - print(f"{Emo.DO} Then install the project by running: \n\n\t\t inv install\n") + return shutil.which("python") @task -def update(c: Context): - echo_header(f"{Emo.DO} Updating project") - c.run("pip install --upgrade -e '.[dev,tests,docs]'") +def setup(c: Context, python_path: Optional[str] = None): + """Confirm that a git repo exists and setup a virtual environment. + + Args: + c: Invoke context + python_path: Path to the python executable to use for the virtual environment. Uses the return value of `which python` if not provided. + """ + git_init(c) + + if python_path is None: + # get path to python executable + python_path = get_python_path(preferred_version="3.9") + if not python_path: + print(f"{msg_type.FAIL} Python executable not found") + exit(1) + venv_name = setup_venv(c, python_path=python_path) + + install(c, pip_args="--upgrade", msg=False, venv_path=venv_name) + + if venv_name is not None: + print( + f"{msg_type.DOING} Activate your virtual environment by running: \n\n\t\t source {venv_name}/bin/activate \n", + ) @task -def test(c: Context): - echo_header(f"{Emo.TEST} Running tests") +def update(c: Context): + """Update dependencies.""" + echo_header(f"{msg_type.DOING} Updating project") + install(c, pip_args="--upgrade", msg=False) + + +@task(iterable="pytest_args") +def test( + c: Context, + python_versions: List[str] = (SUPPORTED_PYTHON_VERSIONS[0],), # noqa # type: ignore + pytest_args: List[str] = [], # noqa +): + """Run tests""" + # Invoke requires lists as type hints, but does not support lists as default arguments. + # Hence this super weird type hint and default argument for the python_versions arg. + echo_header(f"{msg_type.TEST} Running tests") + + python_version_strings = [f"py{v.replace('.', '')}" for v in python_versions] + python_version_arg_string = ",".join(python_version_strings) + + if not pytest_args: + pytest_args = [ + "tests", + "-n auto", + "-rfE", + "--failed-first", + "-p no:cov", + "--disable-warnings", + "-q", + ] + + pytest_arg_str = " ".join(pytest_args) + test_result: Result = c.run( - "pytest -n auto -rfE --failed-first -p no:typeguard -p no:cov --disable-warnings -q", + f"tox -e {python_version_arg_string} -- {pytest_arg_str}", warn=True, - pty=True, + pty=NOT_WINDOWS, ) # If "failed" in the pytest results - if "failed" in test_result.stdout: + failed_tests = [line for line in test_result.stdout if line.startswith("FAILED")] + + if len(failed_tests) > 0: + print("\n\n\n") + echo_header("Failed tests") print("\n\n\n") echo_header("Failed tests") - - # Get lines with "FAILED" in them from the .pytest_results file - failed_tests = [ - line - for line in Path("tests/.pytest_results").read_text().splitlines() - if line.startswith("FAILED") - ] for line in failed_tests: # Remove from start of line until /test_ @@ -255,35 +374,38 @@ def test(c: Context): # Keep only that after :: line_sans_suffix = line_sans_prefix[line_sans_prefix.find("::") + 2 :] - print(f"FAILED {Emo.FAIL} #{line_sans_suffix} ") + print(f"FAILED {msg_type.FAIL} #{line_sans_suffix} ") if test_result.return_code != 0: - exit(0) + exit(test_result.return_code) -def test_for_rej(c: Context): - # Check if any file in current directory, or its subdirectories, has a .rej extension - # If so, exit - rej_files = c.run("find . -name '*.rej' -type f -print", hide=True) +def test_for_rej(): + # Get all paths in current directory or subdirectories that end in .rej + rej_files = list(Path(".").rglob("*.rej")) - if ".rej" in rej_files.stdout: - print(f"\n{Emo.FAIL} Found .rej files leftover from cruft update.") - print(f"{rej_files.stdout}") - exit(0) + if len(rej_files) > 0: + print(f"\n{msg_type.FAIL} Found .rej files leftover from cruft update.\n") + for file in rej_files: + print(f" /{file}") + print("\nResolve the conflicts and try again. \n") + exit(1) @task -def lint(c: Context): - """Lint the project using the pre-commit hooks and mypy.""" - pre_commit(c) - mypy(c) +def lint(c: Context, auto_fix: bool = False): + """Lint the project.""" + test_for_rej() + pre_commit(c=c, auto_fix=auto_fix) + static_type_checks(c) @task -def pr(c: Context): +def pr(c: Context, auto_fix: bool = False): + """Run all checks and update the PR.""" add_and_commit(c) - lint(c) - test(c) + lint(c, auto_fix=auto_fix) + test(c, python_versions=SUPPORTED_PYTHON_VERSIONS) update_branch(c) update_pr(c) @@ -294,10 +416,11 @@ def docs(c: Context, view: bool = False, view_only: bool = False): Build and view docs. If neither build or view are specified, both are run. """ if not view_only: - echo_header(f"{Emo.DO} Building docs") - c.run("sphinx-build -b html docs docs/_build/html") + echo_header(f"{msg_type.DOING}: Building docs") + c.run("tox -e docs") + if view or view_only: - echo_header(f"{Emo.EXAMINE} open docs in browser") + echo_header(f"{msg_type.EXAMINE}: Opening docs in browser") # check the OS and open the docs in the browser if platform.system() == "Windows": c.run("start docs/_build/html/index.html") diff --git a/tests/__init.py b/tests/__init.py new file mode 100644 index 00000000..e69de29b