diff --git a/.containerignore b/.containerignore new file mode 100644 index 00000000..eb7d5ae1 --- /dev/null +++ b/.containerignore @@ -0,0 +1,7 @@ +Dockerfile +build/ +dist/ +.mypy_cache +.tox +.venv* +venv* diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 00000000..d0921df6 --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,39 @@ +// For format details, see https://aka.ms/devcontainer.json +{ + "name": "Python 3 Developer Container", + "build": { + "dockerfile": "Dockerfile", + "target": "build", + "context": ".", + "args": {} + }, + "remoteEnv": { + "DISPLAY": "${localEnv:DISPLAY}" + }, + // Set *default* container specific settings.json values on container create. + "settings": { + "python.defaultInterpreterPath": "/venv/bin/python", + "python.linting.enabled": true + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance" + ], + // Make sure the files we are mapping into the container exist on the host + "initializeCommand": "bash -c 'for i in $HOME/.inputrc; do [ -f $i ] || touch $i; done'", + "runArgs": [ + "--net=host", + "-v=${localEnv:HOME}/.ssh:/root/.ssh", + "-v=${localEnv:HOME}/.inputrc:/root/.inputrc" + ], + "mounts": [ + // map in home directory - not strictly necessary but useful + "source=${localEnv:HOME},target=${localEnv:HOME},type=bind,consistency=cached" + ], + // make the workspace folder the same inside and outside of the container + "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", + "workspaceFolder": "${localWorkspaceFolder}", + // After the container is created, install the python project in editable form + "postCreateCommand": "pip install $([ -f requirements_dev.txt ] && echo -r requirements_dev.txt ) -e .[dev]" +} \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 02eb847e..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# ideas from https://www.docker.com/blog/containerized-python-development-part-1/ - -# This file is for use as a .vscode devcontainer as well as a runtime -# container. The devcontainer should be rootful and use podman or docker -# with user namespaces. - -ARG BASE="mcr.microsoft.com/vscode/devcontainers/python:0-3.10-bullseye" -FROM ${BASE} as base - -# use root to pin where the packages will install -USER root -ENV PATH=/root/.local/bin:$PATH - -FROM base as developer - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - gcc python3-dev && \ - rm -rf /var/lib/apt/lists/* - -WORKDIR /workspace -COPY . . - -# install runtime from DIST if there is one -RUN mkdir -vp /root/.local && \ - if [ -d dist ] ; then \ - touch requirements.txt && \ - pip install --no-cache --user -r requirements.txt dist/*.whl ; \ - fi - -FROM base as runtime - -COPY --from=developer /root/.local /root/.local - -ENTRYPOINT ["demo"] -CMD [] - -EXPOSE 8080 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index af6666d9..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,68 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.231.6/containers/python-3 -{ - "name": "Python 3", - "build": { - "dockerfile": "Dockerfile", - "target": "developer", - "context": "..", - "args": {} - }, - "remoteEnv": { - "DISPLAY": "${localEnv:DISPLAY}" - }, - // Set *default* container specific settings.json values on container create. - "settings": { - "python.defaultInterpreterPath": "/usr/local/bin/python", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", - "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", - "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", - "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" - }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "streetsidesoftware.code-spell-checker", - "ryanluker.vscode-coverage-gutters", - "mhutchie.git-graph", - "eamodio.gitlens", - "gruntfuggly.todo-tree", - "redhat.vscode-yaml", - "nsd.vscode-epics", - "alefragnani.bookmarks" - ], - "features": { - //"docker-from-docker": "20.10", - "git": "os-provided" - }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - // "remoteUser": "vscode", - // Make sure the files we are mapping into the container exist on the host - "initializeCommand": "bash -c 'for i in $HOME/.inputrc $HOME/.bashrc_dev; do [ -f $i ] || touch $i; done'", - "runArgs": [ - "--privileged", - "--net=host", - "-v=${localEnv:HOME}/.ssh:/root/.ssh", - "-v=${localEnv:HOME}/.bashrc_dev:/root/.bashrc", - "-v=${localEnv:HOME}/.inputrc:/root/.inputrc" - ], - "mounts": [ - // map in home directory - not strictly necessary but may be useful - "source=${localEnv:HOME},target=${localEnv:HOME},type=bind,consistency=cached" - ], - "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", - "workspaceFolder": "/workspace", - // After the container is created, install the python project in editable form - // This installs into the system python of the container - "postCreateCommand": "pip install $([ -f requirements_dev.txt ] && echo -r requirements_dev.txt ) -e .[dev]" -} \ No newline at end of file diff --git a/.devcontainer/local_build.sh b/.devcontainer/local_build.sh deleted file mode 100755 index ed538139..00000000 --- a/.devcontainer/local_build.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# locally build a runtime container for testing - -# first make sure a wheel is built -( - cd .. - pip install build - rm -r dist - python -m build --wheel -) - -# make the container name the same as the root folder name of this clone -container_name=$(cd ..; basename $(realpath .)) -echo building "${container_name} ..." - -# run the build with required build-args for a runtime build -ln -s ../dist . -docker build --build-arg BASE=python:3.10-slim -t "${container_name}" .. --file ./Dockerfile -unlink dist diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 00000000..092a75da --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.containerignore \ No newline at end of file diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst new file mode 100644 index 00000000..19ab494f --- /dev/null +++ b/.github/CONTRIBUTING.rst @@ -0,0 +1,35 @@ +Contributing to the project +=========================== + +Contributions and issues are most welcome! All issues and pull requests are +handled through GitHub_. Also, please check for any existing issues before +filing a new one. If you have a great idea but it involves big changes, please +file a ticket before making a pull request! We want to make sure you don't spend +your time coding something that might not fit the scope of the project. + +.. _GitHub: https://github.com/DiamondLightSource/python3-pip-skeleton/issues + +Issue or Discussion? +-------------------- + +Github also offers discussions_ as a place to ask questions and share ideas. If +your issue is open ended and it is not obvious when it can be "closed", please +raise it as a discussion instead. + +.. _discussions: https://github.com/DiamondLightSource/python3-pip-skeleton/discussions + +Code coverage +------------- + +While 100% code coverage does not make a library bug-free, it significantly +reduces the number of easily caught bugs! Please make sure coverage remains the +same or is improved by a pull request! + +Developer guide +--------------- + +The `Developer Guide`_ contains information on setting up a development +environment, running the tests and what standards the code and documentation +should follow. + +.. _Developer Guide: https://diamondlightsource.github.io/python3-pip-skeleton/main/developer/how-to/contribute.html diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..fb7c6ee6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/pages/index.html b/.github/pages/index.html index bd856272..80f0a009 100644 --- a/.github/pages/index.html +++ b/.github/pages/index.html @@ -2,10 +2,10 @@ - Redirecting to main branch - - - + Redirecting to main branch + + + \ No newline at end of file diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py new file mode 100755 index 00000000..39c12772 --- /dev/null +++ b/.github/pages/make_switcher.py @@ -0,0 +1,99 @@ +import json +import logging +from argparse import ArgumentParser +from pathlib import Path +from subprocess import CalledProcessError, check_output +from typing import List, Optional + + +def report_output(stdout: bytes, label: str) -> List[str]: + ret = stdout.decode().strip().split("\n") + print(f"{label}: {ret}") + return ret + + +def get_branch_contents(ref: str) -> List[str]: + """Get the list of directories in a branch.""" + stdout = check_output(["git", "ls-tree", "-d", "--name-only", ref]) + return report_output(stdout, "Branch contents") + + +def get_sorted_tags_list() -> List[str]: + """Get a list of sorted tags in descending order from the repository.""" + stdout = check_output(["git", "tag", "-l", "--sort=-v:refname"]) + return report_output(stdout, "Tags list") + + +def get_versions(ref: str, add: Optional[str], remove: Optional[str]) -> List[str]: + """Generate the file containing the list of all GitHub Pages builds.""" + # Get the directories (i.e. builds) from the GitHub Pages branch + try: + builds = set(get_branch_contents(ref)) + except CalledProcessError: + builds = set() + logging.warning(f"Cannot get {ref} contents") + + # Add and remove from the list of builds + if add: + builds.add(add) + if remove: + assert remove in builds, f"Build '{remove}' not in {sorted(builds)}" + builds.remove(remove) + + # Get a sorted list of tags + tags = get_sorted_tags_list() + + # Make the sorted versions list from main branches and tags + versions: List[str] = [] + for version in ["master", "main"] + tags: + if version in builds: + versions.append(version) + builds.remove(version) + + # Add in anything that is left to the bottom + versions += sorted(builds) + print(f"Sorted versions: {versions}") + return versions + + +def write_json(path: Path, repository: str, versions: str): + org, repo_name = repository.split("/") + struct = [ + dict(version=version, url=f"https://{org}.github.io/{repo_name}/{version}/") + for version in versions + ] + text = json.dumps(struct, indent=2) + print(f"JSON switcher:\n{text}") + path.write_text(text) + + +def main(args=None): + parser = ArgumentParser( + description="Make a versions.txt file from gh-pages directories" + ) + parser.add_argument( + "--add", + help="Add this directory to the list of existing directories", + ) + parser.add_argument( + "--remove", + help="Remove this directory from the list of existing directories", + ) + parser.add_argument( + "repository", + help="The GitHub org and repository name: ORG/REPO", + ) + parser.add_argument( + "output", + type=Path, + help="Path of write switcher.json to", + ) + args = parser.parse_args(args) + + # Write the versions file + versions = get_versions("origin/gh-pages", args.add, args.remove) + write_json(args.output, args.repository, versions) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 08f6d7f4..23ec9462 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -2,150 +2,173 @@ name: Code CI on: push: - branches: - # Restricting to these branches and tags stops duplicate jobs on internal - # PRs but stops CI running on internal branches without a PR. Delete the - # next 5 lines to restore the original behaviour - - master - - main - tags: - - "*" pull_request: schedule: # Run every Monday at 8am to check latest versions of dependencies - - cron: "0 8 * * MON" + - cron: "0 8 * * WED" jobs: lint: - runs-on: "ubuntu-latest" + # pull requests are a duplicate of a branch push if within the same repo. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + steps: - - name: checkout - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v3 - - name: setup python 3.10 - uses: actions/setup-python@v2 + - name: Setup python + uses: actions/setup-python@v4 with: python-version: "3.10" - - name: lint + - name: Lint run: | - pip install --user .[dev] + touch requirements_dev.txt requirements.txt + pip install -r requirements.txt -r requirements_dev.txt -e .[dev] tox -e pre-commit,mypy - wheel: + test: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - os: ["ubuntu-latest"] + os: ["ubuntu-latest"] # can add windows-latest, macos-latest python: ["3.10"] runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 + env: + # https://github.com/pytest-dev/pytest/issues/2042 + PY_IGNORE_IMPORTMISMATCH: "1" - - name: Create Sdist and Wheel - run: | - pipx run build --sdist --wheel + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Upload Wheel and Sdist as artifacts - uses: actions/upload-artifact@v2 + - name: Setup python ${{ matrix.python }} + uses: actions/setup-python@v4 with: - name: dist - path: dist + python-version: ${{ matrix.python }} - test: - strategy: - fail-fast: false - matrix: - os: ["ubuntu-latest"] - # keep matrix tight for now to avoid HTTP 429 errors - # TODO: restore macos and 3.9 and 3.10 - python: ["3.10"] - lock: [false] + - name: Install with latest dependencies + run: pip install .[dev] - include: - # Add an extra Python3.10 runner to use the lockfile - - os: "ubuntu-latest" - python: "3.10" - lock: true + - name: Run tests + run: pytest tests - runs-on: ${{ matrix.os }} - env: - # https://github.com/pytest-dev/pytest/issues/2042 - PY_IGNORE_IMPORTMISMATCH: "1" - # enable QT tests with no X Display - QT_QPA_PLATFORM: "offscreen" + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + name: ${{ matrix.python }}/${{ matrix.os }} + files: cov.xml + + container: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - - name: checkout - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v3 with: fetch-depth: 0 - - name: setup python ${{ matrix.python }} - uses: actions/setup-python@v2 + - name: Log in to GitHub Docker Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 with: - python-version: ${{ matrix.python }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: install with locked dependencies - if: matrix.lock - run: | - touch requirements.txt requirements_dev.txt - pip install -r requirements.txt -e . - pip freeze --exclude-editable > requirements.txt - pip install -r requirements_dev.txt -e .[dev] - pip freeze --exclude-editable > requirements_dev.txt + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=ref,event=tag - - name: install with latest dependencies - if: ${{ ! matrix.lock }} - run: pip install -e .[dev] + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 - - name: run tests - run: pytest tests + - name: Build developer image for testing + uses: docker/build-push-action@v3 + with: + tags: build:latest + context: . + target: build + load: true - - name: create requirements_dev.txt + - name: Run tests in the container locked with requirements_dev.txt run: | - pip freeze --exclude-editable > requirements_dev.txt + docker run -v /var/run/docker.sock:/var/run/docker.sock --privileged \ + --init --name tests --net host -v /tmp:/tmp \ + build bash /project/.github/workflows/container_tests.sh + docker cp tests:/project/dist . + docker cp tests:/project/cov.xml . + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + name: 3.10-locked/ubuntu-latest + files: cov.xml - - name: Upload lockfiles - if: matrix.lock - uses: actions/upload-artifact@v2 + - name: Build runtime image + uses: docker/build-push-action@v3 with: - name: lockfiles - path: | - requirements.txt - requirements_dev.txt + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + context: . + labels: ${{ steps.meta.outputs.labels }} + + - name: Test cli works in runtime image + # check that the first tag can run with --version parameter + run: docker run $(echo ${{ steps.meta.outputs.tags }} | head -1) --version - - name: Run codacy-coverage-reporter - uses: codacy/codacy-coverage-reporter-action@v1 + - name: Setup python 3.10 + uses: actions/setup-python@v4 with: - api-token: ${{ secrets.CODACY_GK }} - coverage-reports: cov.xml + python-version: "3.10" + + - name: Test cli works in sdist installed in local python + # ${GITHUB_REPOSITORY##*/} is the repo name without org + # Replace this with the cli command if different to the repo name + run: pip install dist/*.gz && ${GITHUB_REPOSITORY##*/} --version + + - name: Upload build files + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist release: - needs: [lint, wheel, test] - runs-on: ubuntu-latest # upload to PyPI and make a release on every tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: container + runs-on: ubuntu-latest + steps: - - uses: actions/download-artifact@v2 - with: - path: artifacts + - uses: actions/download-artifact@v3 - name: Github Release # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 with: - files: | - artifacts/dist/* - artifacts/lockfiles/* + prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} + files: dist/* generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.pypi_token }} - run: pipx run twine upload artifacts/dist/* + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/container_tests.sh b/.github/workflows/container_tests.sh new file mode 100644 index 00000000..5f921597 --- /dev/null +++ b/.github/workflows/container_tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -x + +cd /project +source /venv/bin/activate + +touch requirements_dev.txt +pip install -r requirements_dev.txt -e .[dev] +pip freeze --exclude-editable > dist/requirements_dev.txt + +pipdeptree + +pytest tests diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1aec8c50..f0e7ebb6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,18 +2,14 @@ name: Docs CI on: push: - branches: - - main - tags: - - "*" pull_request: jobs: docs: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository strategy: fail-fast: false matrix: - os: ["ubuntu-latest"] python: ["3.10"] runs-on: ubuntu-latest @@ -24,7 +20,7 @@ jobs: run: sleep 60 - name: Install python version - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} @@ -33,7 +29,7 @@ jobs: run: sudo apt-get install graphviz - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -45,12 +41,14 @@ jobs: - name: Build docs run: tox -e docs + - name: Sanitize ref name for docs version + run: echo "DOCS_VERSION=${GITHUB_REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV + - name: Move to versioned directory - # e.g. master or 0.1.2 - run: mv build/html ".github/pages/${GITHUB_REF##*/}" + run: mv build/html .github/pages/$DOCS_VERSION - - name: Write versions.txt - run: sphinx_rtd_theme_github_versions .github/pages + - name: Write switcher.json + run: python .github/pages/make_switcher.py --add $DOCS_VERSION ${{ github.repository }} .github/pages/switcher.json - name: Publish Docs to gh-pages if: github.event_name == 'push' diff --git a/.github/workflows/docs_clean.yml b/.github/workflows/docs_clean.yml new file mode 100644 index 00000000..d5425e42 --- /dev/null +++ b/.github/workflows/docs_clean.yml @@ -0,0 +1,43 @@ +name: Docs Cleanup CI + +# delete branch documentation when a branch is deleted +# also allow manually deleting a documentation version +on: + delete: + workflow_dispatch: + inputs: + version: + description: "documentation version to DELETE" + required: true + type: string + +jobs: + remove: + if: github.event.ref_type == 'branch' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v3 + with: + ref: gh-pages + + - name: removing documentation for branch ${{ github.event.ref }} + if: ${{ github.event_name != 'workflow_dispatch' }} + run: echo "REF_NAME=${{ github.event.ref }}" >> $GITHUB_ENV + + - name: manually removing documentation version ${{ github.event.inputs.version }} + if: ${{ github.event_name == 'workflow_dispatch' }} + run: echo "REF_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV + + - name: Sanitize ref name for docs version + run: echo "DOCS_VERSION=${REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV + + - name: update index and push changes + run: | + rm -r ${{ env.DOCS_VERSION }} + python make_switcher.py --remove $DOCS_VERSION ${{ github.repository }} switcher.json + git config --global user.name 'GitHub Actions Docs Cleanup CI' + git config --global user.email 'GithubActionsCleanup@noreply.github.com' + git commit -am "Removing redundant docs version $DOCS_VERSION" + git push diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 1ea071ca..e6838560 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -3,30 +3,25 @@ name: Link Check on: schedule: # Run every Monday at 8am to check URL links still resolve - - cron: "0 8 * * MON" + - cron: "0 8 * * WED" jobs: docs: strategy: fail-fast: false matrix: - os: ["ubuntu-latest"] python: ["3.10"] runs-on: ubuntu-latest steps: - - name: Avoid git conflicts when tag and branch pushed at same time - if: startsWith(github.ref, 'refs/tags') - run: sleep 60 - - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install python version - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} diff --git a/.gitignore b/.gitignore index 79009ed4..e0fba46a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,14 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -*$py.class # C extensions *.so # Distribution / packaging .Python +env/ +.venv build/ develop-eggs/ dist/ @@ -19,13 +20,10 @@ lib64/ parts/ sdist/ var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST +**/_version.py # PyInstaller # Usually these files are written by a python script from a template @@ -40,16 +38,14 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml -*.cover -*.py,cover -.hypothesis/ +cov.xml .pytest_cache/ +.mypy_cache/ # Translations *.mo @@ -57,16 +53,6 @@ coverage.xml # Django stuff: *.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy # Sphinx documentation docs/_build/ @@ -74,67 +60,8 @@ docs/_build/ # PyBuilder target/ -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -cov.xml -.coverage - -# These are generated at build time -# to commit them remove this line -requirements* - -src/mciwb/_version.py +# likely venv names .venv* +venv* + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 027b9fd6..5e270b08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,3 @@ repos: language: system entry: flake8 types: [python] - - - diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 734f215e..041f8944 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ + "ms-vscode-remote.remote-containers", "ms-python.vscode-pylance", "ms-python.python", "ryanluker.vscode-coverage-gutters" diff --git a/.vscode/launch.json b/.vscode/launch.json index 93d17932..f8fcdb4f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,7 @@ // Cannot have coverage and debugging at the same time. // https://github.com/microsoft/vscode-python/issues/693 "PYTEST_ADDOPTS": "--no-cov" - } + }, } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8debfe33..946e69d4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,12 +5,12 @@ "tasks": [ { "type": "shell", - "label": "Tests with coverage", - "command": "tox -e tests", + "label": "Tests, lint and docs", + "command": "tox -p", "options": { "cwd": "${workspaceRoot}" }, - "problemMatcher": [] + "problemMatcher": [], } ] } \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index b2060ac0..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,159 +0,0 @@ -Contributing -============ - -Contributions and issues are most welcome! All issues and pull requests are -handled through GitHub_. Also, please check for any existing issues before -filing a new one. If you have a great idea but it involves big changes, please -file a ticket before making a pull request! We want to make sure you don't spend -your time coding something that might not fit the scope of the project. - -.. _GitHub: https://github.com/gilesknap/mciwb/issues - -Running the tests ------------------ - -To run in a container -~~~~~~~~~~~~~~~~~~~~~ - -Use vscode devcontainer as follows:: - - $ git clone git://github.com/gilesknap/mciwb.git - $ vscode mciwb - Click on 'Reopen in Container' when prompted - In a vscode Terminal: - $ tox -p - - -To run locally -~~~~~~~~~~~~~~ - -Get the source source code and run the unit tests directly -on your workstation as follows:: - - $ git clone git://github.com/gilesknap/mciwb.git - $ cd mciwb - $ virtualenv .venv - $ source .venv/bin/activate - $ pip install -e .[dev] - $ tox -p - -In both cases tox -p runs in parallel the following checks: - - - Build Spinx Documentation - - run pytest on all tests in ./tests - - run mypy linting on all files in ./src ./tests - - run pre-commit checks: - - - run flake8 style checks against all source - - run black formatting checks against all source - -While 100% code coverage does not make a library bug-free, it significantly -reduces the number of easily caught bugs! Please make sure coverage remains the -same or is improved by a pull request! - -Code Styling ------------- - -The code in this repository conforms to standards set by the following tools: - -- black_ for code formatting -- flake8_ for style checks -- isort_ for import ordering -- mypy_ for static type checking - -flake8 and black be run by pre-commit_. You can run the above checks on -all files with this command:: - - $ tox -e pre-commit,mypy - -Or you can install a pre-commit hook that will run each time you do a ``git -commit`` on just the files that have changed. Note that mypy is not in -the pre-commit because it is a little slow :: - - $ pre-commit install - -.. _black: https://github.com/psf/black -.. _flake8: https://flake8.pycqa.org/en/latest/ -.. _isort: https://github.com/PyCQA/isort -.. _mypy: https://github.com/python/mypy -.. _pre-commit: https://pre-commit.com/ - -Docstrings are pre-processed using the Sphinx Napoleon extension. As such, -google-style_ is considered as standard for this repository. Please use type -hints in the function signature for types. For example:: - - def func(arg1: str, arg2: int) -> bool: - """Summary line. - - Extended description of function. - - Args: - arg1: Description of arg1 - arg2: Description of arg2 - - Returns: - Description of return value - """ - return True - -.. _google-style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#google-vs-numpy - -Documentation -------------- - -Documentation is contained in the ``docs`` directory and extracted from -docstrings of the API. - -Docs follow the underlining convention:: - - Headling 1 (page title) - ======================= - - Heading 2 - --------- - - Heading 3 - ~~~~~~~~~ - -You can build the docs from the project directory by running:: - - $ tox -e docs - $ firefox build/html/index.html - -Release Process ---------------- - -To make a new release, please follow this checklist: - -- Choose a new PEP440 compliant release number -- Git tag the version -- Push to GitHub and the actions will make a release on pypi -- Push to internal gitlab and do a dls-release.py of the tag -- Check and edit for clarity the autogenerated GitHub release_ - -.. _release: https://github.com/gilesknap/mciwb/releases - - -Checking Dependencies ---------------------- - -To see a graph of the python package dependency tree type:: - - pipdeptree - -Updating the tools ------------------- - -This module is merged with the python3-pip-skeleton_. This is a generic -Python project structure which provides a means to keep tools and -techniques in sync between multiple Python projects. To update to the -latest version of the skeleton, run:: - - $ git pull https://github.com/epics-containers/python3-pip-skeleton skeleton - -Any merge conflicts will indicate an area where something has changed that -conflicts with the setup of the current module. Check the `closed pull requests -`_ -of the skeleton module for more details. - -.. _python3-pip-skeleton: https://dls-controls.github.io/dls-python3-skeleton diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c8616501 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# This file is for use as a devcontainer and a runtime container +# +# The devcontainer should use the build target and run as root with podman +# or docker with user namespaces. +# +FROM python:3.10 as build + +# Add any system dependencies for the developer/build environment here +RUN apt-get update && apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + build-essential \ + busybox \ + git \ + net-tools \ + vim \ + && rm -rf /var/lib/apt/lists/* \ + && busybox --install + +COPY . /project + +# set up a virtual environment and put it in PATH +RUN python -m venv /venv +ENV PATH=/venv/bin:$PATH +ENV TOX_DIRECT=1 + +# build the source distribution and wheel +RUN cd /project && \ + pip install --upgrade pip build && \ + export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) && \ + python -m build --sdist --wheel && \ + touch requirements.txt + +# add the docker cli only +RUN docker_url=https://download.docker.com/linux/static/stable/x86_64 && \ + docker_version=20.10.9 && \ + curl -fsSL $docker_url/docker-$docker_version.tgz | \ + tar zxvf - --strip 1 -C /usr/bin docker/docker + +# install the wheel and generate the requirements file +RUN cd /project && \ + pip install -r requirements.txt dist/*.whl && \ + pip freeze > dist/requirements.txt && \ + # we don't want to include our own wheel in requirements - remove with sed + # and replace with a comment to avoid a zero length asset upload later + sed -i '/file:/s/^/# Requirements for /' dist/requirements.txt + +FROM python:3.10-slim as runtime + +# Add apt-get system dependecies for runtime here if needed + +COPY --from=build /venv/ /venv/ +ENV PATH=/venv/bin:$PATH + +# change this entrypoint if it is not the same as the repo +ENTRYPOINT ["mciwb"] +CMD ["--version"] diff --git a/LICENSE b/LICENSE index 261eeb9e..8dada3ed 100644 --- a/LICENSE +++ b/LICENSE @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.rst b/README.rst index f20fc3db..073363c7 100644 --- a/README.rst +++ b/README.rst @@ -1,27 +1,45 @@ -|code_ci| |docs_ci| |coverage| |quality| |pypi_version| |license| +|code_ci| |docs_ci| |coverage| |pypi_version| |license| Minecraft Interactive World Builder =================================== - + This project is intended as a fun way to learn the Python Programming Language. -Experienced Python programmers can also use this library to create +Experienced Python programmers can also use this library to create Minecraft worlds with interactive Python features. -============== ============================================================== -PyPI ``pip install mciwb`` -Source code https://github.com/gilesknap/mciwb -Documentation https://gilesknap.github.io/mciwb -Releases https://github.com/gilesknap/mciwb/releases -============== ============================================================== +Quick Start +----------- + +To get started learning Python, see +`Introduction `_ + +For existing Python developers, see +`API documentation `_ + +Example Build +------------- + +The pagoda and castle with working portcullis were all created programmatically +with Python and the current version of MCIWB. + +.. figure:: user/images/castle.png + :alt: pylance + :align: center + :width: 600px + + Example Build + +Goals +----- Minecraft Interactive World Builder's goals are: - - Use Python to create anything inside of Minecraft worlds. + - Use Python to create anything inside of Minecraft worlds. - Call Python code when events occur inside the world. - Use an iPython prompt to interact with the world by typing Python commands - - Have your player inside the world execute Python code by placing command + - Have your player inside the world execute Python code by placing command signs or activating switches/levers with Python actions. All instructions on how to set up a Minecraft Server and the Python developer @@ -29,14 +47,8 @@ environment are included. There are step by step tutorials to introduce Python Programming and the features of Minecraft Interactive World Builder. -To get started learning Python, see -`Introduction `_ - -For existing Python developers, see -`API documentation `_ - .. note:: - + This is a work in progress. There are now enough tutorials to learn the fundamentals of Python and enough functionality to have some fun interacting with Minecraft from Python code. @@ -50,33 +62,26 @@ Credits This project would not be possible without `mcipc `_, which provides the client library for Minecraft RCON. -Example Build -------------- - -The pagoda and castle with working portcullis were all created programmatically -with Python and the current version of MCIWB. - -.. figure:: https://raw.githubusercontent.com/gilesknap/mciwb/main/docs/images/castle.png - :alt: pylance - :align: center - :width: 900px +Links +----- - Example Build +============== ============================================================== +PyPI ``pip install mciwb`` +Source code https://github.com/gilesknap/mciwb +Documentation https://gilesknap.github.io/mciwb +Releases https://github.com/gilesknap/mciwb/releases +============== ============================================================== -.. |code_ci| image:: https://github.com/gilesknap/mciwb/workflows/Code%20CI/badge.svg?branch=main - :target: https://github.com/gilesknap/mciwb/actions?query=workflow%3A%22Code+CI%22 +.. |code_ci| image:: https://github.com/gilesknap/mciwb/actions/workflows/code.yml/badge.svg?branch=main + :target: https://github.com/gilesknap/mciwb/actions/workflows/code.yml :alt: Code CI -.. |docs_ci| image:: https://github.com/gilesknap/mciwb/workflows/Docs%20CI/badge.svg?branch=main - :target: https://github.com/gilesknap/mciwb/actions?query=workflow%3A%22Docs+CI%22 +.. |docs_ci| image:: https://github.com/gilesknap/mciwb/actions/workflows/docs.yml/badge.svg?branch=main + :target: https://github.com/gilesknap/mciwb/actions/workflows/docs.yml :alt: Docs CI -.. |quality| image:: https://app.codacy.com/project/badge/Grade/4c514b64299e4ccd8c569d3e787245c7 - :target: https://www.codacy.com/gh/gilesknap/mciwb/dashboard?utm_source=github.com&utm_medium=referral&utm_content=gilesknap/mciwb&utm_campaign=Badge_Grade - :alt: Code Quality - -.. |coverage| image:: https://app.codacy.com/project/badge/Coverage/4c514b64299e4ccd8c569d3e787245c7 +.. |coverage| image:: https://app.codacy.com/project/badge/Coverage/4c514b64299e4ccd8c569d3e787245c7 :target: https://www.codacy.com/gh/gilesknap/mciwb/dashboard?utm_source=github.com&utm_medium=referral&utm_content=gilesknap/mciwb&utm_campaign=Badge_Coverage :alt: Test Coverage @@ -88,8 +93,6 @@ with Python and the current version of MCIWB. :target: https://opensource.org/licenses/Apache-2.0 :alt: Apache License - - .. Anything below this line is used when viewing README.rst and will be replaced when included in index.rst diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css deleted file mode 100644 index 5fd9b721..00000000 --- a/docs/_static/theme_overrides.css +++ /dev/null @@ -1,34 +0,0 @@ -/* override table width restrictions */ -@media screen and (min-width: 639px) { - .wy-table-responsive table td { - /* !important prevents the common CSS stylesheets from - overriding this as on RTD they are loaded after this stylesheet */ - white-space: normal !important; - } -} - -/* override table padding */ -.rst-content table.docutils th, .rst-content table.docutils td { - padding: 4px 6px; -} - -/* Add two-column option */ -@media only screen and (min-width: 1000px) { - .columns { - padding-left: 10px; - padding-right: 10px; - float: left; - width: 50%; - min-height: 145px; - } -} - -.endcolumns { - clear: both -} - -/* Hide toctrees within columns and captions from all toctrees. - This is what makes the include trick in index.rst work */ -.columns .toctree-wrapper, .toctree-wrapper .caption-text { - display: none; -} diff --git a/docs/conf.py b/docs/conf.py index 53edc9c4..e026cc2b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,7 +2,10 @@ # # This file only contains a selection of the most common options. For a full # list see the documentation: -# https://www.sphinx-doc.org/en/main/usage/configuration.html +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +from pathlib import Path +from subprocess import check_output import mciwb @@ -16,8 +19,10 @@ # The short X.Y version. if "+" in release: - # Not on a tag - version = "main" + # Not on a tag, use branch name + root = Path(__file__).absolute().parent.parent + git_branch = check_output("git branch --show-current".split(), cwd=root) + version = git_branch.decode().strip() else: version = release @@ -32,6 +37,10 @@ "sphinx.ext.viewcode", # Adds the inheritance-diagram generation directive "sphinx.ext.inheritance_diagram", + # Add a copy button to each code block + "sphinx_copybutton", + # For the card element + "sphinx_design", ] # If true, Sphinx will warn about all references where the target cannot @@ -42,7 +51,16 @@ # generating warnings in "nitpicky mode". Note that type should include the # domain name if present. Example entries would be ('py:func', 'int') or # ('envvar', 'LD_LIBRARY_PATH'). -nitpick_ignore = [("py:func", "int", "py:class")] +nitpick_ignore = [ + ("py:class", "NoneType"), + ("py:class", "'str'"), + ("py:class", "'float'"), + ("py:class", "'int'"), + ("py:class", "'bool'"), + ("py:class", "'object'"), + ("py:class", "'id'"), + ("py:class", "typing_extensions.Literal"), +] # Both the class’ and the __init__ method’s docstring are concatenated and # inserted into the main body of the autoclass directive @@ -82,24 +100,67 @@ # A dictionary of graphviz graph attributes for inheritance diagrams. inheritance_graph_attrs = dict(rankdir="TB") - -# Ignore localhost links for period check that links in docs are valid +# Common links that should be available on every page +rst_epilog = """ +.. _Diamond Light Source: http://www.diamond.ac.uk +.. _black: https://github.com/psf/black +.. _flake8: https://flake8.pycqa.org/en/latest/ +.. _isort: https://github.com/PyCQA/isort +.. _mypy: http://mypy-lang.org/ +.. _pre-commit: https://pre-commit.com/ +""" + +# Ignore localhost links for periodic check that links in docs are valid linkcheck_ignore = [r"http://localhost:\d+/"] +# Set copy-button to ignore python and bash prompts +# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#using-regexp-prompt-identifiers +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " +copybutton_prompt_is_regexp = True + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme_github_versions" - -# Options for the sphinx rtd theme, use DLS blue -html_theme_options = dict(style_nav_header_background="rgb(7, 43, 93)") - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +html_theme = "pydata_sphinx_theme" +github_repo = project +github_user = "gilesknap" + +# Theme options for pydata_sphinx_theme +html_theme_options = dict( + logo=dict( + text=project, + ), + use_edit_page_button=True, + github_url=f"https://github.com/{github_user}/{github_repo}", + icon_links=[ + dict( + name="PyPI", + url=f"https://pypi.org/project/{project}", + icon="fas fa-cube", + ) + ], + switcher=dict( + json_url=f"https://{github_user}.github.io/{github_repo}/switcher.json", + version_match=version, + ), + navbar_end=["theme-switcher", "icon-links", "version-switcher"], + external_links=[ + dict( + name="Release Notes", + url=f"https://github.com/{github_user}/{github_repo}/releases", + ) + ], +) + +# A dictionary of values to pass into the template engine’s context for all pages +html_context = dict( + github_user=github_user, + github_repo=project, + github_version=version, + doc_path="docs", +) # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False @@ -107,9 +168,6 @@ # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = False -# Add some CSS classes for columns and other tweaks in a custom css file -html_css_files = ["theme_overrides.css"] - # Logo html_logo = "images/logo.png" html_favicon = "images/logo.png" diff --git a/docs/developer/explanations/decisions.rst b/docs/developer/explanations/decisions.rst new file mode 100644 index 00000000..5841e6ea --- /dev/null +++ b/docs/developer/explanations/decisions.rst @@ -0,0 +1,17 @@ +.. This Source Code Form is subject to the terms of the Mozilla Public +.. License, v. 2.0. If a copy of the MPL was not distributed with this +.. file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Architectural Decision Records +============================== + +We record major architectural decisions in Architecture Decision Records (ADRs), +as `described by Michael Nygard +`_. +Below is the list of our current ADRs. + +.. toctree:: + :maxdepth: 1 + :glob: + + decisions/* \ No newline at end of file diff --git a/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst b/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst new file mode 100644 index 00000000..b2d3d0fe --- /dev/null +++ b/docs/developer/explanations/decisions/0001-record-architecture-decisions.rst @@ -0,0 +1,26 @@ +1. Record architecture decisions +================================ + +Date: 2022-02-18 + +Status +------ + +Accepted + +Context +------- + +We need to record the architectural decisions made on this project. + +Decision +-------- + +We will use Architecture Decision Records, as `described by Michael Nygard +`_. + +Consequences +------------ + +See Michael Nygard's article, linked above. To create new ADRs we will copy and +paste from existing ones. diff --git a/docs/developer/how-to/build-docs.rst b/docs/developer/how-to/build-docs.rst new file mode 100644 index 00000000..79e3f780 --- /dev/null +++ b/docs/developer/how-to/build-docs.rst @@ -0,0 +1,38 @@ +Build the docs using sphinx +=========================== + +You can build the `sphinx`_ based docs from the project directory by running:: + + $ tox -e docs + +This will build the static docs on the ``docs`` directory, which includes API +docs that pull in docstrings from the code. + +.. seealso:: + + `documentation_standards` + +The docs will be built into the ``build/html`` directory, and can be opened +locally with a web browse:: + + $ firefox build/html/index.html + +Autobuild +--------- + +You can also run an autobuild process, which will watch your ``docs`` +directory for changes and rebuild whenever it sees changes, reloading any +browsers watching the pages:: + + $ tox -e docs autobuild + +You can view the pages at localhost:: + + $ firefox http://localhost:8000 + +If you are making changes to source code too, you can tell it to watch +changes in this directory too:: + + $ tox -e docs autobuild -- --watch src + +.. _sphinx: https://www.sphinx-doc.org/ \ No newline at end of file diff --git a/docs/developer/how-to/contribute.rst b/docs/developer/how-to/contribute.rst new file mode 100644 index 00000000..65b992f0 --- /dev/null +++ b/docs/developer/how-to/contribute.rst @@ -0,0 +1 @@ +.. include:: ../../../.github/CONTRIBUTING.rst diff --git a/docs/developer/how-to/lint.rst b/docs/developer/how-to/lint.rst new file mode 100644 index 00000000..1086c3c4 --- /dev/null +++ b/docs/developer/how-to/lint.rst @@ -0,0 +1,38 @@ +Run linting using pre-commit +============================ + +Code linting is handled by black_, flake8_ and isort_ run under pre-commit_. + +Running pre-commit +------------------ + +You can run the above checks on all files with this command:: + + $ tox -e pre-commit + +Or you can install a pre-commit hook that will run each time you do a ``git +commit`` on just the files that have changed:: + + $ pre-commit install + +Fixing issues +------------- + +If black reports an issue you can tell it to reformat all the files in the +repository:: + + $ black . + +Likewise with isort:: + + $ isort . + +If you get any flake8 issues you will have to fix those manually. + +VSCode support +-------------- + +The ``.vscode/settings.json`` will run black and isort formatters as well as +flake8 checking on save. Issues will be highlighted in the editor window. + + diff --git a/docs/developer/how-to/make-release.rst b/docs/developer/how-to/make-release.rst new file mode 100644 index 00000000..ba1b30ba --- /dev/null +++ b/docs/developer/how-to/make-release.rst @@ -0,0 +1,16 @@ +Make a release +============== + +To make a new release, please follow this checklist: + +- Choose a new PEP440 compliant release number (see https://peps.python.org/pep-0440/) +- Go to the GitHub release_ page +- Choose ``Draft New Release`` +- Click ``Choose Tag`` and supply the new tag you chose (click create new tag) +- Click ``Generate release notes``, review and edit these notes +- Choose a title and click ``Publish Release`` + +Note that tagging and pushing to the main branch has the same effect except that +you will not get the option to edit the release notes. + +.. _release: https://github.com/gilesknap/mciwb/releases diff --git a/docs/developer/how-to/run-tests.rst b/docs/developer/how-to/run-tests.rst new file mode 100644 index 00000000..d2e03644 --- /dev/null +++ b/docs/developer/how-to/run-tests.rst @@ -0,0 +1,12 @@ +Run the tests using pytest +========================== + +Testing is done with pytest_. It will find functions in the project that `look +like tests`_, and run them to check for errors. You can run it with:: + + $ tox -e pytest + +It will also report coverage to the commandline and to ``cov.xml``. + +.. _pytest: https://pytest.org/ +.. _look like tests: https://docs.pytest.org/explanation/goodpractices.html#test-discovery diff --git a/docs/developer/how-to/static-analysis.rst b/docs/developer/how-to/static-analysis.rst new file mode 100644 index 00000000..065920e1 --- /dev/null +++ b/docs/developer/how-to/static-analysis.rst @@ -0,0 +1,8 @@ +Run static analysis using mypy +============================== + +Static type analysis is done with mypy_. It checks type definition in source +files without running them, and highlights potential issues where types do not +match. You can run it with:: + + $ tox -e mypy diff --git a/docs/developer/how-to/update-tools.rst b/docs/developer/how-to/update-tools.rst new file mode 100644 index 00000000..0b313226 --- /dev/null +++ b/docs/developer/how-to/update-tools.rst @@ -0,0 +1,16 @@ +Update the tools +================ + +This module is merged with the python3-pip-skeleton_. This is a generic +Python project structure which provides a means to keep tools and +techniques in sync between multiple Python projects. To update to the +latest version of the skeleton, run:: + + $ git pull git@github.com:DiamondLightSource/python3-pip-skeleton.git + +Any merge conflicts will indicate an area where something has changed that +conflicts with the setup of the current module. Check the `closed pull requests +`_ +of the skeleton module for more details. + +.. _python3-pip-skeleton: https://DiamondLightSource.github.io/python3-pip-skeleton diff --git a/docs/developer/index.rst b/docs/developer/index.rst new file mode 100644 index 00000000..6f8bf1bc --- /dev/null +++ b/docs/developer/index.rst @@ -0,0 +1,63 @@ +Developer Guide +=============== + +Documentation is split into four categories, also accessible from links in the +side-bar. + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-regular:`directions_run;3em` + + .. toctree:: + :caption: Tutorials + :maxdepth: 1 + + tutorials/dev-install + + +++ + + Tutorials for getting up and running as a developer. + + .. grid-item-card:: :material-regular:`task;3em` + + .. toctree:: + :caption: How-to Guides + :maxdepth: 1 + + how-to/contribute + how-to/build-docs + how-to/run-tests + how-to/static-analysis + how-to/lint + how-to/update-tools + how-to/make-release + + +++ + + Practical step-by-step guides for day-to-day dev tasks. + + .. grid-item-card:: :material-regular:`apartment;3em` + + .. toctree:: + :caption: Explanations + :maxdepth: 1 + + explanations/decisions + + +++ + + Explanations of how and why the architecture is why it is. + + .. grid-item-card:: :material-regular:`description;3em` + + .. toctree:: + :caption: Reference + :maxdepth: 1 + + reference/standards + reference/contributing + + +++ + + Technical reference material on standards in use. diff --git a/docs/developer/reference/contributing.rst b/docs/developer/reference/contributing.rst new file mode 100644 index 00000000..30f80aef --- /dev/null +++ b/docs/developer/reference/contributing.rst @@ -0,0 +1,2 @@ +.. include:: ../../../.github/CONTRIBUTING.rst + \ No newline at end of file diff --git a/docs/developer/reference/standards.rst b/docs/developer/reference/standards.rst new file mode 100644 index 00000000..b78a719e --- /dev/null +++ b/docs/developer/reference/standards.rst @@ -0,0 +1,64 @@ +Standards +========= + +This document defines the code and documentation standards used in this +repository. + +Code Standards +-------------- + +The code in this repository conforms to standards set by the following tools: + +- black_ for code formatting +- flake8_ for style checks +- isort_ for import ordering +- mypy_ for static type checking + +.. seealso:: + + How-to guides `../how-to/lint` and `../how-to/static-analysis` + +.. _documentation_standards: + +Documentation Standards +----------------------- + +Docstrings are pre-processed using the Sphinx Napoleon extension. As such, +google-style_ is considered as standard for this repository. Please use type +hints in the function signature for types. For example: + +.. code:: python + + def func(arg1: str, arg2: int) -> bool: + """Summary line. + + Extended description of function. + + Args: + arg1: Description of arg1 + arg2: Description of arg2 + + Returns: + Description of return value + """ + return True + +.. _google-style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#google-vs-numpy + +Documentation is contained in the ``docs`` directory and extracted from +docstrings of the API. + +Docs follow the underlining convention:: + + Headling 1 (page title) + ======================= + + Heading 2 + --------- + + Heading 3 + ~~~~~~~~~ + +.. seealso:: + + How-to guide `../how-to/build-docs` \ No newline at end of file diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst new file mode 100644 index 00000000..34ac053f --- /dev/null +++ b/docs/developer/tutorials/dev-install.rst @@ -0,0 +1,60 @@ +Developer install +================= + +These instructions will take you through the minimal steps required to get a dev +environment setup, so you can run the tests locally. + +Clone the repository +-------------------- + +First clone the repository locally using `Git +`_:: + + $ git clone git://github.com/gilesknap/mciwb.git + +Install dependencies +-------------------- + +You can choose to either develop on the host machine using a `venv` (which +requires python 3.8 or later) or to run in a container under `VSCode +`_ + +.. tab-set:: + + .. tab-item:: Local virtualenv + + .. code:: + + $ cd mciwb + $ python3 -m venv venv + $ source venv/bin/activate + $ pip install -e .[dev] + + .. tab-item:: VSCode devcontainer + + .. code:: + + $ vscode mciwb + # Click on 'Reopen in Container' when prompted + # Open a new terminal + +See what was installed +---------------------- + +To see a graph of the python package dependency tree type:: + + $ pipdeptree + +Build and test +-------------- + +Now you have a development environment you can run the tests in a terminal:: + + $ tox -p + +This will run in parallel the following checks: + +- `../how-to/build-docs` +- `../how-to/run-tests` +- `../how-to/static-analysis` +- `../how-to/lint` diff --git a/docs/explanations.rst b/docs/explanations.rst deleted file mode 100644 index 20285520..00000000 --- a/docs/explanations.rst +++ /dev/null @@ -1,16 +0,0 @@ -:orphan: - -Explanations -============ - -Explanation of how the library works and why it works that way. - -.. toctree:: - :caption: Explanations - - explain/scope - explain/server-config - explain/coordinates - explain/mcipc - - diff --git a/docs/genindex.rst b/docs/genindex.rst new file mode 100644 index 00000000..93eb8b29 --- /dev/null +++ b/docs/genindex.rst @@ -0,0 +1,5 @@ +API Index +========= + +.. + https://stackoverflow.com/a/42310803 diff --git a/docs/how-to.rst b/docs/how-to.rst deleted file mode 100644 index 8254b5b5..00000000 --- a/docs/how-to.rst +++ /dev/null @@ -1,20 +0,0 @@ -:orphan: - -How-to Guides -============= - -Practical step-by-step guides for the more experienced user. - -.. toctree:: - :caption: How-to Guides - - - how-to/startup - how-to/completion - how-to/extensions - how-to/troubleshooting - how-to/backup - how-to/wsl2 - how-to/coordinates - - diff --git a/docs/index.rst b/docs/index.rst index 9bde8adf..d31105b2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,48 +1,29 @@ +:html_theme.sidebar_secondary.remove: + .. include:: ../README.rst :end-before: when included in index.rst - How the documentation is structured ----------------------------------- -Documentation is split into four categories, also accessible from links in the -side-bar. - -.. rst-class:: columns - -`tutorials` -~~~~~~~~~~~ - -.. include:: tutorials.rst - :start-after: ========= - -.. rst-class:: columns - -`how-to` -~~~~~~~~ - -.. include:: how-to.rst - :start-after: ============= - -.. rst-class:: columns - -`explanations` -~~~~~~~~~~~~~~ +The documentation is split into 2 sections: -.. include:: explanations.rst - :start-after: ============ +.. grid:: 2 -.. rst-class:: columns + .. grid-item-card:: :material-regular:`person;4em` + :link: user/index + :link-type: doc -`reference` -~~~~~~~~~~~ + The User Guide contains documentation on how to install and use mciwb. -.. include:: reference.rst - :start-after: ========= + .. grid-item-card:: :material-regular:`code;4em` + :link: developer/index + :link-type: doc -.. rst-class:: endcolumns + The Developer Guide contains documentation on how to develop and contribute changes back to mciwb. -About the documentation -~~~~~~~~~~~~~~~~~~~~~~~ +.. toctree:: + :hidden: -`Why is the documentation structured this way? `_ + user/index + developer/index diff --git a/docs/reference.rst b/docs/reference.rst deleted file mode 100644 index a639a100..00000000 --- a/docs/reference.rst +++ /dev/null @@ -1,20 +0,0 @@ -:orphan: - -Reference -========= - -Detailed information including autogenerated code documentation. - -.. toctree:: - :caption: Reference - - reference/index - reference/contributing - reference/api - reference/demo - Releases - Index - -.. - Index link above is a hack to make genindex.html a relative link - https://stackoverflow.com/a/31820846 diff --git a/docs/reference/contributing.rst b/docs/reference/contributing.rst deleted file mode 100644 index 3a1c1d69..00000000 --- a/docs/reference/contributing.rst +++ /dev/null @@ -1,2 +0,0 @@ -.. include:: ../../CONTRIBUTING.rst - \ No newline at end of file diff --git a/docs/tutorials.rst b/docs/tutorials.rst deleted file mode 100644 index fdc2fd8d..00000000 --- a/docs/tutorials.rst +++ /dev/null @@ -1,22 +0,0 @@ -:orphan: - -Tutorials -========= - -Tutorials for installation, library and commandline usage. New users start here. - -.. toctree:: - :caption: Tutorials - - tutorials/00-prereq - tutorials/01-setup - tutorials/02-orientation - tutorials/03-variables - tutorials/04-loops - tutorials/05-functions - tutorials/05a-modules - tutorials/06-if-then - tutorials/07-lists - tutorials/08-classes - tutorials/09-dictionaries - \ No newline at end of file diff --git a/docs/explain/coordinates.rst b/docs/user/explanations/coordinates.rst similarity index 100% rename from docs/explain/coordinates.rst rename to docs/user/explanations/coordinates.rst diff --git a/docs/user/explanations/docs-structure.rst b/docs/user/explanations/docs-structure.rst new file mode 100644 index 00000000..f25a09ba --- /dev/null +++ b/docs/user/explanations/docs-structure.rst @@ -0,0 +1,18 @@ +About the documentation +----------------------- + + :material-regular:`format_quote;2em` + + The Grand Unified Theory of Documentation + + -- David Laing + +There is a secret that needs to be understood in order to write good software +documentation: there isn't one thing called *documentation*, there are four. + +They are: *tutorials*, *how-to guides*, *technical reference* and *explanation*. +They represent four different purposes or functions, and require four different +approaches to their creation. Understanding the implications of this will help +improve most documentation - often immensely. + +`More information on this topic. `_ diff --git a/docs/explain/mcipc.rst b/docs/user/explanations/mcipc.rst similarity index 100% rename from docs/explain/mcipc.rst rename to docs/user/explanations/mcipc.rst diff --git a/docs/explain/scope.rst b/docs/user/explanations/scope.rst similarity index 100% rename from docs/explain/scope.rst rename to docs/user/explanations/scope.rst diff --git a/docs/explain/server-config.rst b/docs/user/explanations/server-config.rst similarity index 100% rename from docs/explain/server-config.rst rename to docs/user/explanations/server-config.rst diff --git a/docs/how-to/backup.rst b/docs/user/how-to/backup.rst similarity index 100% rename from docs/how-to/backup.rst rename to docs/user/how-to/backup.rst diff --git a/docs/how-to/completion.rst b/docs/user/how-to/completion.rst similarity index 100% rename from docs/how-to/completion.rst rename to docs/user/how-to/completion.rst diff --git a/docs/how-to/coordinates.rst b/docs/user/how-to/coordinates.rst similarity index 100% rename from docs/how-to/coordinates.rst rename to docs/user/how-to/coordinates.rst diff --git a/docs/how-to/extensions.rst b/docs/user/how-to/extensions.rst similarity index 100% rename from docs/how-to/extensions.rst rename to docs/user/how-to/extensions.rst diff --git a/docs/user/how-to/run-container.rst b/docs/user/how-to/run-container.rst new file mode 100644 index 00000000..bb70db20 --- /dev/null +++ b/docs/user/how-to/run-container.rst @@ -0,0 +1,15 @@ +Run in a container +================== + +Pre-built containers with mciwb and its dependencies already +installed are available on `Github Container Registry +`_. + +Starting the container +---------------------- + +To pull the container from github container registry and run:: + + $ docker run ghcr.io/gilesknap/mciwb:main --version + +To get a released version, use a numbered release instead of ``main``. diff --git a/docs/how-to/startup.rst b/docs/user/how-to/startup.rst similarity index 100% rename from docs/how-to/startup.rst rename to docs/user/how-to/startup.rst diff --git a/docs/how-to/troubleshooting.rst b/docs/user/how-to/troubleshooting.rst similarity index 100% rename from docs/how-to/troubleshooting.rst rename to docs/user/how-to/troubleshooting.rst diff --git a/docs/how-to/wsl2.rst b/docs/user/how-to/wsl2.rst similarity index 82% rename from docs/how-to/wsl2.rst rename to docs/user/how-to/wsl2.rst index 499a5c59..83e2ed17 100644 --- a/docs/how-to/wsl2.rst +++ b/docs/user/how-to/wsl2.rst @@ -10,6 +10,12 @@ Windows Subsystem for Linux 2 (or WSL2) should already have been installed as part of the Docker Desktop install that was done in `docker`. +To make sure the correct version of WSL2 is installed you can open a +CMD window in Administrator mode and run the following commands: + + wsl --install + wsl --set-version Ubuntu-22.04 2 + WSL2 is a Linux distribution that runs inside of Windows. It allows us to make the tutorials look exactly the same for Windows, Linux and MacOS. The linux command line in Ubuntu Linux inside WSL2 is called ``bash`` and @@ -55,6 +61,10 @@ On the left bar of VSCode there is a remote connections Icon. Continue with tutorial ---------------------- +IMPORTANT: close all VSCode windows except for the one that is connected to +WSL2. This means that when you finish this tutorial and close VSCode it will +remember to open the WSL2 connection next time. + You can now continue with the main tutorial. If you do everything through vscode then the tutorials will work exactly the same for Windows as for a linux computer. diff --git a/docs/images/castle.png b/docs/user/images/castle.png similarity index 100% rename from docs/images/castle.png rename to docs/user/images/castle.png diff --git a/docs/images/compass.png b/docs/user/images/compass.png similarity index 100% rename from docs/images/compass.png rename to docs/user/images/compass.png diff --git a/docs/images/compass.png:Zone.Identifier b/docs/user/images/compass.png:Zone.Identifier similarity index 100% rename from docs/images/compass.png:Zone.Identifier rename to docs/user/images/compass.png:Zone.Identifier diff --git a/docs/images/coords.excalidraw.png b/docs/user/images/coords.excalidraw.png similarity index 100% rename from docs/images/coords.excalidraw.png rename to docs/user/images/coords.excalidraw.png diff --git a/docs/images/coords.excalidraw.png:Zone.Identifier b/docs/user/images/coords.excalidraw.png:Zone.Identifier similarity index 100% rename from docs/images/coords.excalidraw.png:Zone.Identifier rename to docs/user/images/coords.excalidraw.png:Zone.Identifier diff --git a/docs/images/golem.png b/docs/user/images/golem.png similarity index 100% rename from docs/images/golem.png rename to docs/user/images/golem.png diff --git a/docs/images/launcher.png b/docs/user/images/launcher.png similarity index 100% rename from docs/images/launcher.png rename to docs/user/images/launcher.png diff --git a/docs/images/pagoda.png b/docs/user/images/pagoda.png similarity index 100% rename from docs/images/pagoda.png rename to docs/user/images/pagoda.png diff --git a/docs/images/portal.png b/docs/user/images/portal.png similarity index 100% rename from docs/images/portal.png rename to docs/user/images/portal.png diff --git a/docs/images/prompt.excalidraw.png b/docs/user/images/prompt.excalidraw.png similarity index 100% rename from docs/images/prompt.excalidraw.png rename to docs/user/images/prompt.excalidraw.png diff --git a/docs/images/pylance.png b/docs/user/images/pylance.png similarity index 100% rename from docs/images/pylance.png rename to docs/user/images/pylance.png diff --git a/docs/images/remote_ubuntu.excalidraw.png b/docs/user/images/remote_ubuntu.excalidraw.png similarity index 100% rename from docs/images/remote_ubuntu.excalidraw.png rename to docs/user/images/remote_ubuntu.excalidraw.png diff --git a/docs/images/remote_wsl2.excalidraw.png b/docs/user/images/remote_wsl2.excalidraw.png similarity index 100% rename from docs/images/remote_wsl2.excalidraw.png rename to docs/user/images/remote_wsl2.excalidraw.png diff --git a/docs/images/server_address.png b/docs/user/images/server_address.png similarity index 100% rename from docs/images/server_address.png rename to docs/user/images/server_address.png diff --git a/docs/images/split_term.png b/docs/user/images/split_term.png similarity index 100% rename from docs/images/split_term.png rename to docs/user/images/split_term.png diff --git a/docs/images/table_json.png b/docs/user/images/table_json.png similarity index 100% rename from docs/images/table_json.png rename to docs/user/images/table_json.png diff --git a/docs/images/terminals.png b/docs/user/images/terminals.png similarity index 100% rename from docs/images/terminals.png rename to docs/user/images/terminals.png diff --git a/docs/images/trust.png b/docs/user/images/trust.png similarity index 100% rename from docs/images/trust.png rename to docs/user/images/trust.png diff --git a/docs/images/typewriter.jpg b/docs/user/images/typewriter.jpg similarity index 100% rename from docs/images/typewriter.jpg rename to docs/user/images/typewriter.jpg diff --git a/docs/images/vscode_hello.excalidraw.png b/docs/user/images/vscode_hello.excalidraw.png similarity index 100% rename from docs/images/vscode_hello.excalidraw.png rename to docs/user/images/vscode_hello.excalidraw.png diff --git a/docs/user/index.rst b/docs/user/index.rst new file mode 100644 index 00000000..bfd34ae7 --- /dev/null +++ b/docs/user/index.rst @@ -0,0 +1,81 @@ +User Guide +========== + +Documentation is split into four categories, also accessible from links in the +side-bar. + +.. grid:: 2 + :gutter: 4 + + .. grid-item-card:: :material-regular:`directions_walk;3em` + + .. toctree:: + :caption: Tutorials + :maxdepth: 1 + + tutorials/00-prereq + tutorials/01-setup + tutorials/02-orientation + tutorials/03-variables + tutorials/04-loops + tutorials/05-functions + tutorials/05a-modules + tutorials/06-if-then + tutorials/07-lists + tutorials/08-classes + tutorials/09-dictionaries + + +++ + + Tutorials for installation and typical usage. New users start here. + + .. grid-item-card:: :material-regular:`directions;3em` + + .. toctree:: + :caption: How-to Guides + :maxdepth: 1 + + how-to/startup + how-to/completion + how-to/extensions + how-to/troubleshooting + how-to/backup + how-to/wsl2 + how-to/coordinates + how-to/run-container + + +++ + + Practical step-by-step guides for the more experienced user. + + .. grid-item-card:: :material-regular:`info;3em` + + .. toctree:: + :caption: Explanations + :maxdepth: 1 + + explanations/docs-structure + explanations/scope + explanations/server-config + explanations/coordinates + explanations/mcipc + + + +++ + + Explanations of how the library works and why it works that way. + + .. grid-item-card:: :material-regular:`menu_book;3em` + + .. toctree:: + :caption: Reference + :maxdepth: 1 + + reference/index + reference/api + reference/demo + ../genindex + + +++ + + Technical reference material including APIs and release notes. diff --git a/docs/reference/api.rst b/docs/user/reference/api.rst similarity index 100% rename from docs/reference/api.rst rename to docs/user/reference/api.rst diff --git a/docs/reference/demo.rst b/docs/user/reference/demo.rst similarity index 100% rename from docs/reference/demo.rst rename to docs/user/reference/demo.rst diff --git a/docs/reference/index.rst b/docs/user/reference/index.rst similarity index 100% rename from docs/reference/index.rst rename to docs/user/reference/index.rst diff --git a/docs/tutorials/00-prereq.rst b/docs/user/tutorials/00-prereq.rst similarity index 100% rename from docs/tutorials/00-prereq.rst rename to docs/user/tutorials/00-prereq.rst diff --git a/docs/tutorials/01-setup.rst b/docs/user/tutorials/01-setup.rst similarity index 92% rename from docs/tutorials/01-setup.rst rename to docs/user/tutorials/01-setup.rst index 225c224e..b81dff02 100644 --- a/docs/tutorials/01-setup.rst +++ b/docs/user/tutorials/01-setup.rst @@ -194,18 +194,31 @@ Python The Python Programming Language is also free and open-source. -This is essential for working with MCIWB. On Linux you will likely already -have it installed. Note that this project has been tested with -Python 3.9 and 3.10 but other versions may also be OK. +This is essential for working with MCIWB. +Python 3.10 because the underlying RCON library requires it. This is the +latest version of Python at the time of writing. -These commands will install the necessary packages to make Python 3.9 +These commands will install the necessary packages to make Python 3.10 available on your system:: sudo apt update - sudo apt install -y python3.9 - sudo apt install -y python3.9-venv + sudo apt install -y python3.10 + sudo apt install -y python3.10-venv sudo apt install -y python3-pip +If you have an older version of Ubuntu it may not have Python 3.10 available. +In this case you need to use the ``deadsnakes`` PPA to get the latest version as +follows:: + + sudo apt install -y software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt install -y python3.10 + sudo apt install -y python3.10-venv + sudo apt install -y python3-pip + +Virtual Environment +------------------- + We are going to create a **Virtual Environment** for our Python packages to install in. This is a way to keep the packages you install (like mciwb) separate from the packages you install for other projects. @@ -213,7 +226,7 @@ separate from the packages you install for other projects. First create the Virtual Environment by typing (note the leading "." in the folder name ".venv"):: - python3.9 -m venv .venv + python3.10 -m venv .venv Then activate it by typing the following. This command will need to be repeated each time you restart your terminal or open a new terminal:: diff --git a/docs/tutorials/02-orientation.rst b/docs/user/tutorials/02-orientation.rst similarity index 100% rename from docs/tutorials/02-orientation.rst rename to docs/user/tutorials/02-orientation.rst diff --git a/docs/tutorials/03-variables.rst b/docs/user/tutorials/03-variables.rst similarity index 99% rename from docs/tutorials/03-variables.rst rename to docs/user/tutorials/03-variables.rst index 6974d6af..ba59e5e7 100644 --- a/docs/tutorials/03-variables.rst +++ b/docs/user/tutorials/03-variables.rst @@ -43,7 +43,7 @@ computer program. They have the following properties: .. note:: **Scope** is a moderately advanced concept which you can ignore for the moment. Advanced topics like this will be covered - in the `../explanations` section. I suggest skipping anything in the + in the ``explanations`` section. I suggest skipping anything in the explanations section on your first read through the tutorials and then come back later when you have a better understanding of the fundamentals. diff --git a/docs/tutorials/04-loops.rst b/docs/user/tutorials/04-loops.rst similarity index 100% rename from docs/tutorials/04-loops.rst rename to docs/user/tutorials/04-loops.rst diff --git a/docs/tutorials/05-functions.rst b/docs/user/tutorials/05-functions.rst similarity index 98% rename from docs/tutorials/05-functions.rst rename to docs/user/tutorials/05-functions.rst index 3ede5450..0b7006e3 100644 --- a/docs/tutorials/05-functions.rst +++ b/docs/user/tutorials/05-functions.rst @@ -97,7 +97,7 @@ tutorial we will look into the detail of how this function works. Try copying and pasting this function into your Python console. -.. literalinclude :: ../../src/demo/pagoda.py +.. literalinclude :: ../../../src/demo/pagoda.py :language: python :lines: 4- diff --git a/docs/tutorials/05a-modules.rst b/docs/user/tutorials/05a-modules.rst similarity index 99% rename from docs/tutorials/05a-modules.rst rename to docs/user/tutorials/05a-modules.rst index 38fb39d0..526675be 100644 --- a/docs/tutorials/05a-modules.rst +++ b/docs/user/tutorials/05a-modules.rst @@ -71,7 +71,7 @@ Now we can paste our pagoda function into the editor window and save it with Menu -> File -> Save (or Ctrl+S is a shortcut to save the current file). Use this slightly modified version of the pagoda function: -.. literalinclude :: ../../src/demo/pagoda.py +.. literalinclude :: ../../../src/demo/pagoda.py :language: python To try using this function you can now type the following command in the diff --git a/docs/tutorials/06-if-then.rst b/docs/user/tutorials/06-if-then.rst similarity index 99% rename from docs/tutorials/06-if-then.rst rename to docs/user/tutorials/06-if-then.rst index 1151f706..358d6848 100644 --- a/docs/tutorials/06-if-then.rst +++ b/docs/user/tutorials/06-if-then.rst @@ -149,7 +149,7 @@ Here is a reminder of how to do that using your bash prompt: Paste this code into gate.py and save it. -.. literalinclude :: ../../src/demo/gate.py +.. literalinclude :: ../../../src/demo/gate.py :language: python Note that the ``build_gate`` function has a default value for position. I @@ -179,7 +179,7 @@ called ``open_close``. This function will be called when the lever is activated. By defining ``open_close`` *inside* of ``make_gate`` we are able to use the variable ``position`` which is needed when moving the -portcullis. (If you want to understand more about this see `../explain/scope`) +portcullis. (If you want to understand more about this see `../explanations/scope`) This code always creates a gate facing north. ``position`` represents the bottom WEST corner of the portcullis. diff --git a/docs/tutorials/07-lists.rst b/docs/user/tutorials/07-lists.rst similarity index 74% rename from docs/tutorials/07-lists.rst rename to docs/user/tutorials/07-lists.rst index 26897b40..cc90390d 100644 --- a/docs/tutorials/07-lists.rst +++ b/docs/user/tutorials/07-lists.rst @@ -4,5 +4,5 @@ Lists TODO: Introduce Python lists and demonstrate by building castle walls around our village using this code: -.. literalinclude :: ../../src/demo/walls.py +.. literalinclude :: ../../../src/demo/walls.py :language: python diff --git a/docs/tutorials/08-classes.rst b/docs/user/tutorials/08-classes.rst similarity index 71% rename from docs/tutorials/08-classes.rst rename to docs/user/tutorials/08-classes.rst index 84ef5459..c8a33db5 100644 --- a/docs/tutorials/08-classes.rst +++ b/docs/user/tutorials/08-classes.rst @@ -4,11 +4,11 @@ Classes TODO: Introduce classes and demonstrate building all of the buildings inside the castle walls with this code: -.. literalinclude :: ../../src/demo/house.py +.. literalinclude :: ../../../src/demo/house.py :language: python Note that this code can now build a complete castle from previous steps. -.. literalinclude :: ../../src/demo/village.py +.. literalinclude :: ../../../src/demo/village.py :language: python \ No newline at end of file diff --git a/docs/tutorials/09-dictionaries.rst b/docs/user/tutorials/09-dictionaries.rst similarity index 100% rename from docs/tutorials/09-dictionaries.rst rename to docs/user/tutorials/09-dictionaries.rst diff --git a/pyproject.toml b/pyproject.toml index 7741c4ca..77a92854 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,5 @@ [build-system] -# To get a reproducible wheel, wheel must be pinned to the same version as in -# dls-python3, and setuptools must produce the same dist-info. Cap setuptools -# to the last version that didn't add License-File to METADATA -requires = ["setuptools<57", "wheel==0.33.1"] +requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] diff --git a/run-tests.sh b/run-tests.sh new file mode 100644 index 00000000..6eb8801e --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# This script runs the tests inside the build container +# useful for verifying that the CI will work as this is how it runs the tests + +podman run --rm -v /tmp:/tmp -v \ + /run/user/1000/podman/podman.sock:/var/run/docker.sock \ + --privileged --init --net host build bash \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 5ac998a5..cc54bfe7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,15 +9,12 @@ long_description = file: README.rst long_description_content_type = text/x-rst classifiers = License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 -plaforrms = - Linux - Windows - Mac + Programming Language :: Python :: 3.10 [options] -python_requires = >=3.9 +python_requires = >=3.10 packages = find: # =src is interpreted as {"": "src"} # as per recommendation here https://hynek.me/articles/testing-packaging/ @@ -37,17 +34,21 @@ install_requires = [options.extras_require] # For development tests/docs dev = - black==22.3.0 + black==22.10.0 flake8-isort isort>5.0 mypy + pipdeptree pre-commit + pydata-sphinx-theme < 0.10.1 pytest-cov - sphinx-rtd-theme-github-versions - tox setuptools_scm[toml]>=6.2 - mock - types-setuptools + sphinx-autobuild + sphinx-copybutton + sphinx-design + tox + tox-direct + types-mock [options.packages.find] where = src @@ -55,10 +56,10 @@ where = src exclude = tests -# If you want to include data files in packages, -# either define [options.package_data] or -# set this to True and include a MANIFEST.in file. -include_package_data = False +# Specify any package data to be included in the wheel below. +# [options.package_data] +# mciwb = +# subpackage/*.yaml [options.entry_points] # Include a command line script @@ -96,32 +97,41 @@ filterwarnings = # its difficult to ensure all sockets are closed in tests so ignore ignore:.*socket.*:ResourceWarning ignore::DeprecationWarning - # Doctest python code in docs, python code in src docstrings, test functions in tests testpaths = - tests + docs src tests [coverage:run] -data_file = /tmp/gphotomciwbs_sync.coverage +data_file = /tmp/mciwb.coverage + +[coverage:paths] +# Tests are run from installed location, map back to the src directory +source = + src + **/site-packages/ + # Use tox to provide parallel linting and testing # NOTE that we pre-install all tools in the dev dependencies (including tox). # Hence the use of allowlist_externals instead of using the tox virtualenvs. # This ensures a match between developer time tools in the IDE and tox tools. +# Setting TOX_DIRECT=1 in the environment will make this even faster [tox:tox] -minversion = 3.7 -skipsdist=true -skipinstall=true - -[testenv:{pre-commit,mypy,pytest,docs}] -setenv = - QT_QPA_PLATFORM = offscreen -allowlist_externals = - pytest - pre-commit - mypy +skipsdist = True + +[testenv:pytest] +allowlist_externals = pytest +commands = pytest {posargs} + +[testenv:mypy] +allowlist_externals = mypy +commands = mypy src tests {posargs} + +[testenv:pre-commit] +allowlist_externals = pre-commit +commands = pre-commit run --all-files {posargs} + +[testenv:docs] +allowlist_externals = sphinx-build -commands = - pytest: pytest tests {posargs} - mypy: mypy src tests {posargs} - pre-commit: pre-commit run --all-files {posargs} - docs: sphinx-build -EWT --keep-going docs build/html {posargs} + sphinx-autobuild +commands = sphinx-{posargs:build -EW --keep-going} -T docs build/html diff --git a/src/demo/arrows.py b/src/demo/arrows.py index 6f14ce87..34b7da82 100644 --- a/src/demo/arrows.py +++ b/src/demo/arrows.py @@ -28,6 +28,8 @@ def explosion_test(): c.kill(f"@e[{arrow_entity}, {vec2params(pos)}]") for i in range(power): c.summon(str(Item.TNT), pos + Direction.UP * (i - power // 2)) + c.summon(str(Item.TNT), pos + Direction.EAST * (i - power // 2)) + c.summon(str(Item.TNT), pos + Direction.NORTH * (i - power // 2)) Monitor.stop_named(exploding) Monitor(explosion_test, name=exploding) diff --git a/src/demo/bat.py b/src/demo/bat.py new file mode 100644 index 00000000..d87c44d0 --- /dev/null +++ b/src/demo/bat.py @@ -0,0 +1,23 @@ +""" +Make a pet bat follow you around floating over your head +""" + +from mciwb.imports import Direction, Monitor, get_client, get_world + + +def enable_bat(): + name = "bat" + Monitor.stop_named(name) + Monitor(do_bat, name=name, poll_rate=0.1) + + +def do_bat(): + c = get_client() + + w = get_world() + + c.teleport( + targets="@e[limit=1, type=bat]", + location=w.player.pos_f + Direction.UP * 2.5, + rotation=w.player.rotation, + ) diff --git a/src/demo/snowball.py b/src/demo/snowball.py new file mode 100644 index 00000000..7bc483ac --- /dev/null +++ b/src/demo/snowball.py @@ -0,0 +1,59 @@ +""" +Implement exploding snowballs. + +Note the snowball disappears when it hits something so we need to monitor +all snowballs in flight and generate a BOOM at the last known position as +they disappear. +""" + +from time import sleep + +from mciwb.imports import Item, Monitor, Vec3, get_client, parse_nbt + +exploding = "exploding_snowballs" + + +def enable_snowballs(): + Monitor.stop_named(exploding) + Monitor(monitor_snowballs, name=exploding, poll_rate=0.1) + + +def uuid_str(nbt: dict): + # need to parse uuid because of the I; and default str(list) has extraneous quotes + # TODO replace parse_nbt with a class that handles all conversions + uuid_str = ( + f"UUID:" + f'[I; {nbt["UUID"][0]}, {nbt["UUID"][1]},' + f' {nbt["UUID"][2]}, {nbt["UUID"][3]}]' + ) + return uuid_str + + +def monitor_snowballs(): + c = get_client() + # Note that snowballs have a default Air of 300 (on MC 1.19.2 anyway) + result = c.data.get(entity="@e[limit=1, type=snowball, nbt={Air:300s}]") + if "Snowball" in result: + nbt = parse_nbt(result) + if isinstance(nbt, dict): + # change Air property so monitor_snowballs no longer sees this snowball + entity = f"@e[limit=1, type=snowball, nbt={{{uuid_str(nbt)}}}]" + c.data.merge(entity=entity, nbt="{Air:301}") + + Monitor(track_snowball, once=True, params=(entity, result)) + + +def track_snowball(entity: str, result: str): + c = get_client() + + # track for 20 secs only + for i in range(200): + sleep(0.1) + last_result = result + result = c.data.get(entity=entity) + if "Snowball" not in result: + nbt = parse_nbt(last_result) + if isinstance(nbt, dict): + pos = Vec3(*nbt["Pos"]).with_ints() + c.summon(str(Item.TNT), pos) + break diff --git a/src/demo/test.py b/src/demo/test.py new file mode 100644 index 00000000..e1672250 --- /dev/null +++ b/src/demo/test.py @@ -0,0 +1,43 @@ +import threading +from time import sleep + +from mcipc.rcon.je import Client + +from mciwb.nbt import parse_nbt + + +def get_player_pos(name: str, wait: float): + with Client("nuc2", port=30555, passwd="spider") as client: + for i in range(100): + x = client.data.get(entity="@p", path="Pos") + print(name, parse_nbt(x)) + sleep(wait) + + +def get_player_pos2(name: str, wait: float): + client = Client("nuc2", port=30555, passwd="spider") + client.connect(True) + for i in range(100): + x = client.data.get(entity="@p", path="Pos") + print(name, parse_nbt(x)) + sleep(wait) + + +client = Client("nuc2", port=30555, passwd="spider") +client.connect(True) + + +def get_player_pos3(name: str, wait: float): + # share the connection between threads + for i in range(100): + x = client.data.get(entity="@p", path="Pos") + print(name, parse_nbt(x)) + sleep(wait) + + +def go(): + test = get_player_pos2 + t1 = threading.Thread(target=test, args=("t1", 0.01)) + t2 = threading.Thread(target=test, args=("t2", 0.05)) + t1.start() + t2.start() diff --git a/src/mciwb/__init__.py b/src/mciwb/__init__.py index 18be7cca..0fe6655f 100644 --- a/src/mciwb/__init__.py +++ b/src/mciwb/__init__.py @@ -3,14 +3,10 @@ from setuptools_scm import get_version # Warning: If the install is nested to the same depth, this will always succeed - tmp_version = get_version(root="../../", relative_to=__file__) + __version__ = get_version(root="../../", relative_to=__file__) del get_version except (ImportError, LookupError): # Use installed version - from ._version import version as __version__ # type: ignore -else: - __version__ = tmp_version + from ._version import __version__ -# imports here are used to bring commonly used classes into a single namespace -# for ease of use for novice programmers __all__ = ["__version__"] diff --git a/src/mciwb/__main__.py b/src/mciwb/__main__.py index be8b6ba3..4aa2932b 100644 --- a/src/mciwb/__main__.py +++ b/src/mciwb/__main__.py @@ -16,7 +16,7 @@ from mciwb.server import ( HOST, MinecraftServer, - backup_folder, + backup_folder_default, def_pass, def_port, def_world_type, @@ -108,6 +108,7 @@ def start( world_type: str = def_world_type, server_name: str = server_name, folder: Path = default_server_folder, + backup_folder: Path = backup_folder_default, port: int = def_port, debug: bool = False, ): @@ -126,7 +127,9 @@ def start( else: log.info(f"Creating new Minecraft server in {folder}") - server = MinecraftServer(server_name, port, password, folder, world_type) + server = MinecraftServer( + server_name, port, password, folder, world_type, backup_folder=backup_folder + ) server.create() @@ -148,7 +151,7 @@ def stop( def backup( folder: Path = default_server_folder, backup_name: str = "", - backup_folder: Path = backup_folder, + backup_folder: Path = backup_folder_default, debug: bool = False, ): """ @@ -165,7 +168,7 @@ def restore( backup_name: str = typer.Argument(""), debug: bool = False, folder: Path = default_server_folder, - backup_folder: Path = backup_folder, + backup_folder: Path = backup_folder_default, server_name: str = server_name, port: int = def_port, ): @@ -177,7 +180,9 @@ def restore( stop(server_name=server_name) backup = Backup(world_folder=folder / "world", backup_folder=backup_folder) backup.restore(name=backup_name) - start(folder=folder, port=port, server_name=server_name) + start( + folder=folder, port=port, server_name=server_name, backup_folder=backup_folder + ) if __name__ == "__main__": diff --git a/src/mciwb/backup.py b/src/mciwb/backup.py index 47b6ebec..80f4a7b4 100644 --- a/src/mciwb/backup.py +++ b/src/mciwb/backup.py @@ -6,7 +6,7 @@ from zipfile import ZIP_DEFLATED, ZipFile from mciwb.logging import log -from mciwb.server import backup_folder, default_server_folder +from mciwb.server import backup_folder_default, default_server_folder from mciwb.threads import get_client @@ -16,7 +16,7 @@ class Backup: def __init__( self, world_folder: Path = default_server_folder / "world", - backup_folder: Path = backup_folder, + backup_folder: Path = backup_folder_default, ): self.world_folder = Path(world_folder) self.backup_folder = Path(backup_folder) @@ -73,7 +73,7 @@ def restore(self, name=None, backup=False): else: if not name.endswith(".zip"): name += ".zip" - rfile = backup_folder / self.re_valid_filename.sub("_", name) + rfile = self.backup_folder / self.re_valid_filename.sub("_", name) if not rfile.exists(): raise ValueError(f"{rfile} not found") diff --git a/src/mciwb/nbt.py b/src/mciwb/nbt.py index a468f381..349ba5b6 100644 --- a/src/mciwb/nbt.py +++ b/src/mciwb/nbt.py @@ -1,6 +1,8 @@ import json import re +from mciwb.logging import log + # extract preamble from string responses to commands (benign for raw SNBT) preamble_re = re.compile(r"[^\[{]*(.*)") # extract list type identifiers @@ -24,12 +26,16 @@ def parse_nbt(s_nbt_text: str) -> object: See https://minecraft.fandom.com/wiki/NBT_format """ - text = preamble_re.sub(r"\1", s_nbt_text) - text = list_types_re.sub(r"", text) - text = unquoted_re.sub(r'"\1"', text).replace("'", "") - text = no_decimal_floats_re.sub(r"\1.0", text) - text = floats_re.sub(r"\1", text) - text = integers_re.sub(r"\1", text) - text = text.replace('"true"', '"True"').replace('"false"', '"False"') + try: + text = preamble_re.sub(r"\1", s_nbt_text) + text = list_types_re.sub(r"", text) + text = unquoted_re.sub(r'"\1"', text).replace("'", "") + text = no_decimal_floats_re.sub(r"\1.0", text) + text = floats_re.sub(r"\1", text) + text = integers_re.sub(r"\1", text) + text = text.replace('"true"', '"True"').replace('"false"', '"False"') - return json.loads(text) + return json.loads(text) + except Exception as e: + log.error(f"Error parsing NBT text: {s_nbt_text} \n\n ERROR: {e}") + return None diff --git a/src/mciwb/player.py b/src/mciwb/player.py index d766838b..346ab646 100644 --- a/src/mciwb/player.py +++ b/src/mciwb/player.py @@ -5,7 +5,7 @@ import math import re from time import sleep -from typing import List, Match, Pattern +from typing import List, Match, Pattern, Tuple from mcwb.types import Direction, Vec3 from mcwb.volume import Volume @@ -14,7 +14,9 @@ from mciwb.threads import get_client regex_coord = re.compile(r"\[(-?\d+.?\d*)d, *(-?\d+.?\d*)d, *(-?\d+.?\d*)d\]") -regex_angle = re.compile(r"-?\ *[0-9]+\.?[0-9]*(?:[Ee]\ *-?\ *[0-9]+)?") +float_reg = r"(-?\ *[0-9]+\.?[0-9]*(?:[Ee]\ *-?\ *[0-9]+)?)f" +regex_angle = re.compile(float_reg) +regex_rot = re.compile(r"\[(-?\d+.?\d*)f, *(-?\d+.?\d*)f\]") class PlayerNotInWorld(Exception): @@ -58,15 +60,19 @@ def inventory(self) -> List[str]: return get_client().data.get(entity=self.name, path="Inventory") @property - def pos(self) -> Vec3: + def pos_f(self) -> Vec3: """ - Return the player's position + Return the player's precise position """ match = self._get_entity_data("Pos", regex_coord) - pos = Vec3( - float(match.group(1)), float(match.group(2)), float(match.group(3)) - ).with_ints() - return pos + return Vec3(float(match.group(1)), float(match.group(2)), float(match.group(3))) + + @property + def pos(self) -> Vec3: + """ + Return the player's block position + """ + return self.pos_f.with_ints() @property def facing(self) -> Vec3: @@ -80,11 +86,19 @@ def facing(self) -> Vec3: WEST = Vec3(-1, 0, 0) """ match = self._get_entity_data("Rotation", regex_angle) - angle = float(match.group(0)) + angle = float(match.group(1)) index = int(((math.floor(angle) + 45) % 360) / 90) return Direction.cardinals[index] + @property + def rotation(self) -> Tuple[float, float]: + """ + Get the player's rotation in degrees + """ + match = self._get_entity_data("Rotation", regex_rot) + return float(match.group(1)), float(match.group(2)) + def player_in(self, volume: Volume) -> bool: """ Check if the player is inside the Volume diff --git a/src/mciwb/server.py b/src/mciwb/server.py index efd9dd09..85f82a45 100644 --- a/src/mciwb/server.py +++ b/src/mciwb/server.py @@ -16,7 +16,7 @@ HOST = "localhost" # the default locally mapped backup folder for minecraft data -backup_folder = Path.home() / "mciwb-backups" +backup_folder_default = Path.home() / "mciwb-backups" server_name = "mciwb-server" default_server_folder = Path.home() / server_name @@ -45,6 +45,7 @@ def __init__( password: str, server_folder: Path, world_type: str, + backup_folder: Path = None, # type: ignore keep: bool = True, test=False, ) -> None: @@ -57,6 +58,7 @@ def __init__( self.name = name self.password = password self.server_folder = server_folder + self.backup_folder = backup_folder or backup_folder_default self.world = self.server_folder / "world" self.world_type = world_type self.container = None @@ -204,18 +206,21 @@ def create(self, world_zip=None, force=False) -> None: shutil.rmtree(self.server_folder) self.server_folder.mkdir(parents=True) - if not backup_folder.exists(): - backup_folder.mkdir(parents=True) + if not self.backup_folder.exists(): + self.backup_folder.mkdir(parents=True) container = docker_client.containers.run( - "itzg/minecraft-server", + "docker.io/itzg/minecraft-server", detach=True, environment=env, ports={f"{self.rcon}/tcp": self.rcon, f"{self.port}": self.port}, - restart_policy={"Name": "always" if self.keep else "no"}, + restart_policy={"Name": "unless-stopped" if self.keep else "no"}, volumes={ str(self.server_folder): {"bind": "/data", "mode": "rw"}, - str(backup_folder): {"bind": str(backup_folder), "mode": "rw"}, + str(self.backup_folder): { + "bind": str(self.backup_folder), + "mode": "rw", + }, }, name=self.name, ) diff --git a/src/mciwb/threads.py b/src/mciwb/threads.py index 59fa8c35..6e2add60 100644 --- a/src/mciwb/threads.py +++ b/src/mciwb/threads.py @@ -13,23 +13,24 @@ thread_local = threading.local() +def _enter_thread(client, target, name): + # make new client connection for this thread + # Note use of type(client). When testing the type may be MockClient + # TODO would prefer to use a context for client here as the cleanup + # works - but it gets an error running under PyTest (huh?) + new_client = type(client)(client.host, client.port, passwd=client.passwd) + new_client.connect(True) + set_client(new_client) + + target() + + def new_thread(client: Client, target, name: str) -> threading.Thread: """ Create a thread with its own RCON Client connection stored in thread local storage """ - def _enter_thread(client, target, name): - # make new client connection for this thread - new_client = type(client)(client.host, client.port, passwd=client.passwd) - new_client.connect(True) - - # save our new client in the thread local storage - set_client(new_client) - thread_local.name = name - - target() - - # Start a new thread and use _enter_thread to set it up with a RCON client + # Start a new thread and use _enter_thread to set it up with a new RCON client new_thread = threading.Thread(target=_enter_thread, args=(client, target, name)) new_thread.start() diff --git a/tests/conftest.py b/tests/conftest.py index 78ecc3a4..e72048cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,7 @@ ENTITY_POS = Vec3(0, -60, 0) test_server_name = "mciwb-test-server" +test_backup_name = "mciwb-backup-server" servers_folder = Path(gettempdir()) / "test-mc-servers" logging.basicConfig( @@ -48,6 +49,7 @@ def minecraft_container(request: pytest.FixtureRequest): keep=KEEP_SERVER, world_type="flat", server_folder=servers_folder / test_server_name, + backup_folder=servers_folder / test_backup_name, test=True, ) mc.create() diff --git a/tests/test_mciwb.py b/tests/test_mciwb.py new file mode 100644 index 00000000..ede268b8 --- /dev/null +++ b/tests/test_mciwb.py @@ -0,0 +1,9 @@ +import subprocess +import sys + +from mciwb import __version__ + + +def test_cli_version(): + cmd = [sys.executable, "-m", "mciwb", "--version"] + assert subprocess.check_output(cmd).decode().strip() == __version__ diff --git a/tests/test_system/test_cli.py b/tests/test_system/test_cli.py index 60f42cb6..67ff8c71 100644 --- a/tests/test_system/test_cli.py +++ b/tests/test_system/test_cli.py @@ -48,6 +48,7 @@ def test_backup(tmp_path): server_folder = Path(tempfile.gettempdir()) / "test-backup-server" server_folder2 = Path(tempfile.gettempdir()) / "test-backup-server2" backup_folder = Path(tempfile.gettempdir()) / "test-backup-backup" + backup_folder_tmp = Path(tempfile.gettempdir()) / "test-backup-backup" result = run_cli( "start", @@ -57,6 +58,8 @@ def test_backup(tmp_path): "test", "--port", "20200", + "--backup-folder", + backup_folder_tmp, ) assert not any(err in result.stdout for err in checks) diff --git a/tests/test_unit/test_monitor.py b/tests/test_unit/test_monitor.py index 3ee7cc80..894923fa 100644 --- a/tests/test_unit/test_monitor.py +++ b/tests/test_unit/test_monitor.py @@ -9,10 +9,14 @@ def test_func(): count += 1 count = 0 + time = 0 mon = Monitor(test_func, once=True) while mon._polling: sleep(0.1) + time += 1 + if time > 10: + break assert count == 1 @@ -23,10 +27,15 @@ def test_func(): count += 1 count = 0 + time = 0 mon = Monitor(test_func, poll_rate=0.0001) while count < 100: sleep(0.1) + time += 1 + if time > 10: + break + mon.remove_poller_func(test_func) Monitor.stop_all()