From b6f7a881b28ee5664dacfa85ae0d741d89349aa7 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:35:54 +0200 Subject: [PATCH 01/35] Migrate code tooling to use pre-commit and ruff The state of docstring linting in Python is a mess. Pydocstyle and Darglint are both deprecated, and Ruff is currently in preview and doesn't provide 100% parity (and Pydoclint configuration is a bit of a mess, having to use the flake config). ANYWAY, let's just use Ruff without preview mode --- .pre-commit-config.yaml | 28 ++++++++++++ README.md | 12 ++--- codetools.sh | 39 ---------------- pyproject.toml | 44 +++++++++++++------ scripts/download_data.py | 3 +- .../make_realistic/adcp_make_realistic.py | 2 +- 6 files changed, 68 insertions(+), 60 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100755 codetools.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b52d46ee --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + exclude: .*\.zarr/ + - id: check-yaml + - id: check-ast + - id: check-json + types: [text] + files: \.(json|ipynb)$ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.5 + hooks: + - id: ruff + args: [--fix, --show-fixes] + - id: ruff + name: ruff (isort jupyter) + args: [--select, I, --fix] + types_or: [jupyter] + - id: ruff-format + types_or: [python, jupyter] + + # - repo: https://github.com/rbubley/mirrors-prettier # Update mirror as official mirror is deprecated + # rev: v3.3.3 + # hooks: + # - id: prettier diff --git a/README.md b/README.md index b78bfb6b..867453dc 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ # Virtual_ship_classroom Emma's work for the MSc student material -Please contact me at e.e.daniels1@uu.nl with any questions. +Please contact me at e.e.daniels1@uu.nl with any questions. -This is a python tool that will allow students to virtually sample the ocean as if the measurements were coming from an actual oceanographic mission. At the moment we mimic ADCP, CTD, and simple underwaydata measurements and allow surface drifters and argo float deployments. We might add gliders and meteorological data in the future. +This is a python tool that will allow students to virtually sample the ocean as if the measurements were coming from an actual oceanographic mission. At the moment we mimic ADCP, CTD, and simple underwaydata measurements and allow surface drifters and argo float deployments. We might add gliders and meteorological data in the future. ### Requirements To use the material please create an environment called Parcels with the command `conda env create -f environment.yml ### Input data -The scripts are written to work with A-grid ocean data, specifically that from CMEMS. -Data can be downloaded with the download_data.py script. For now a different conda env is needed for downloading, see comments in the script. +The scripts are written to work with A-grid ocean data, specifically that from CMEMS. +Data can be downloaded with the download_data.py script. For now a different conda env is needed for downloading, see comments in the script. ### Sailing the ship Fill in the accompanying JSON file and run virtualship.py to start measuring. You can also use Sail_the_ship.ipynb -### Ideas for improvements to be made +### Ideas for improvements to be made - ACDP #bins instead of max_depth - bug when argo(/drifter?) deployed at final location? depth=(len(time)) - documentation that ships sails great circle path - CTDs op land? -- Argo's/drifters that collide with land. Chance to break? \ No newline at end of file +- Argo's/drifters that collide with land. Chance to break? diff --git a/codetools.sh b/codetools.sh deleted file mode 100755 index 8e0a4acc..00000000 --- a/codetools.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/sh - -# Runs all codetools and attempts to apply fixes wherever possible. -# Not suitable for the CI as that should not make any changes. - -set -e - -# Set working directory to the directory of this script. -cd "$(dirname "$0")" - -PACKAGE=virtual_ship -TESTS=tests - -echo "--------------" -echo "flake8" -echo "--------------" -flake8 ./$PACKAGE ./$TESTS -# darglint is ran as a plugin for flake8. - -echo "--------------" -echo "pydocstyle" -echo "--------------" -pydocstyle ./$PACKAGE ./$TESTS - -echo "--------------" -echo "sort-all" -echo "--------------" -find ./$PACKAGE -type f -name '__init__.py' -print0 | xargs -0 sort-all - -echo "--------------" -echo "black" -echo "--------------" -black ./$PACKAGE ./$TESTS - -echo "--------------" -echo "isort" -echo "--------------" -isort ./$PACKAGE ./$TESTS - diff --git a/pyproject.toml b/pyproject.toml index f585c3f7..7cc8bf25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,13 +47,7 @@ local_scheme = "no-local-version" [project.optional-dependencies] dev = [ - "black == 24.4.0", - "darglint == 1.8.1", - "flake8 == 7.0.0", - "Flake8-pyproject == 1.2.3", - "isort == 5.13.2", - "pydocstyle == 6.3.0", - "sort-all == 1.2.0", + "pre-commit", "pytest == 8.2.0", "pytest-cov == 5.0.0", "codecov == 2.1.13", @@ -65,10 +59,34 @@ dev = [ [project.scripts] do_expedition = "virtual_ship.cli.do_expedition:main" -[tool.isort] -profile = "black" -skip_gitignore = true -[tool.flake8] -extend-ignore = "E501" # Don't check line length. -docstring_style = "sphinx" # Use sphinx docstring style for darglint plugin. +[tool.ruff.lint] +exclude = ["*.ipynb"] + +select = [ + "E", # Error + "F", # pyflakes + "I", # isort + "D", # pydocstyle + "DOC", # pydoclint + # "B", # Bugbear + # "UP", # pyupgrade + # "LOG", # logging + # "ICN", # import conventions + # "G", # logging-format + "RUF", # ruff + "ISC001", # single-line-implicit-string-concatenation +] +ignore = [ + # line too long (82 > 79 characters) + "E501", + # consider `[meta, header, *data]` instead of concatenation + "RUF005", + # multi-line-summary-first-line + "D212", + # one-blank-line-before-class + "D203", + + # TODO: Remove later + "D100", "D103" +] diff --git a/scripts/download_data.py b/scripts/download_data.py index d89edba8..d3034ee7 100644 --- a/scripts/download_data.py +++ b/scripts/download_data.py @@ -4,9 +4,10 @@ This is a very crude script, here just as long as we do not properly incorporate it into the library. """ -import copernicusmarine import datetime +import copernicusmarine + if __name__ == "__main__": datadir = "input_data" username = input("username: ") diff --git a/virtual_ship/make_realistic/adcp_make_realistic.py b/virtual_ship/make_realistic/adcp_make_realistic.py index 69c24790..45fd25e4 100644 --- a/virtual_ship/make_realistic/adcp_make_realistic.py +++ b/virtual_ship/make_realistic/adcp_make_realistic.py @@ -58,7 +58,7 @@ def _to_csv( meta = "# depths (m): " + ",".join([str(d) for d in depths]) header = f"time,lat,lon,{','.join(['u' + str(n) + ',v' + str(n) for n in range(len(depths))])}" data = [ - f"{str(time)},{lat},{lon},{','.join([str(u) + ',' + str(v) for u, v in zip(us, vs)])}" + f"{time!s},{lat},{lon},{','.join([str(u) + ',' + str(v) for u, v in zip(us, vs)])}" for time, lat, lon, us, vs in zip(times, lats, lons, all_us.T, all_vs.T) ] From d8542c308896958598b24662be9603aecbec92e3 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:36:47 +0200 Subject: [PATCH 02/35] Enable pyupgrade --- pyproject.toml | 8 +++----- tests/make_realistic/test_adcp_make_realistic.py | 2 +- virtual_ship/expedition/checkpoint.py | 2 +- virtual_ship/expedition/schedule.py | 2 +- virtual_ship/expedition/ship_config.py | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7cc8bf25..487cc18c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,11 +69,9 @@ select = [ "I", # isort "D", # pydocstyle "DOC", # pydoclint - # "B", # Bugbear - # "UP", # pyupgrade - # "LOG", # logging - # "ICN", # import conventions - # "G", # logging-format + "B", # Bugbear + "UP", # pyupgrade + "ICN", # import conventions "RUF", # ruff "ISC001", # single-line-implicit-string-concatenation ] diff --git a/tests/make_realistic/test_adcp_make_realistic.py b/tests/make_realistic/test_adcp_make_realistic.py index 4b23b660..e2db6f7a 100644 --- a/tests/make_realistic/test_adcp_make_realistic.py +++ b/tests/make_realistic/test_adcp_make_realistic.py @@ -10,7 +10,7 @@ def test_adcp_make_realistic(tmpdir: py.path.LocalPath) -> None: file = adcp_make_realistic("adcp.zarr", out_dir=tmpdir, prefix="ADCP") # check if CSV is ok and can be loaded - with open(file, mode="r", newline="") as csvfile: + with open(file, newline="") as csvfile: # ignore lines starting with #, we assume that's metadata or comments reader = csv.reader(line for line in csvfile if not line.startswith("#")) for _ in reader: diff --git a/virtual_ship/expedition/checkpoint.py b/virtual_ship/expedition/checkpoint.py index d3bb08a4..27a85657 100644 --- a/virtual_ship/expedition/checkpoint.py +++ b/virtual_ship/expedition/checkpoint.py @@ -46,6 +46,6 @@ def from_yaml(cls, file_path: str | Path) -> Checkpoint: :param file_path: Path to the file to load from. :returns: The checkpoint. """ - with open(file_path, "r") as file: + with open(file_path) as file: data = yaml.safe_load(file) return Checkpoint(**data) diff --git a/virtual_ship/expedition/schedule.py b/virtual_ship/expedition/schedule.py index a92ba55b..d3865483 100644 --- a/virtual_ship/expedition/schedule.py +++ b/virtual_ship/expedition/schedule.py @@ -39,6 +39,6 @@ def from_yaml(cls, file_path: str | Path) -> Schedule: :param file_path: Path to the file to load from. :returns: The schedule. """ - with open(file_path, "r") as file: + with open(file_path) as file: data = yaml.safe_load(file) return Schedule(**data) diff --git a/virtual_ship/expedition/ship_config.py b/virtual_ship/expedition/ship_config.py index 8e4564fc..73a040e6 100644 --- a/virtual_ship/expedition/ship_config.py +++ b/virtual_ship/expedition/ship_config.py @@ -151,6 +151,6 @@ def from_yaml(cls, file_path: str | Path) -> ShipConfig: :param file_path: Path to the file to load from. :returns: The config. """ - with open(file_path, "r") as file: + with open(file_path) as file: data = yaml.safe_load(file) return ShipConfig(**data) From 40c5dc04a504be2c012a3635642f1a39e0741403 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:52:55 +0200 Subject: [PATCH 03/35] Nox, documentation, codecoverage, updated workflows (publish and testing)... Add nox workflow running, documentation generation with ReadTheDocs, coverage support, updated CI with publish workflow and improved testing, contributing guidelines, dependabot, updated pyproject.toml config (pytest/coverage options, updated metadata) Done with reference to Scientific Python copier template. --- .github/CONTRIBUTING.md | 52 ++++++++++++++++ .github/dependabot.yml | 11 ++++ .github/release.yml | 5 ++ .github/workflows/cd.yml | 60 ++++++++++++++++++ .github/workflows/ci.yml | 57 +++++++++++++++++ .github/workflows/codetools.yml | 46 -------------- .readthedocs.yaml | 17 +++++ docs/conf.py | 64 +++++++++++++++++++ docs/index.md | 17 +++++ noxfile.py | 107 ++++++++++++++++++++++++++++++++ pyproject.toml | 39 ++++++++++-- 11 files changed, 425 insertions(+), 50 deletions(-) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/codetools.yml create mode 100644 .readthedocs.yaml create mode 100644 docs/conf.py create mode 100644 docs/index.md create mode 100644 noxfile.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..822c9c05 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Quick development + +The fastest way to start with development is to use nox. If you don't have nox, +you can use `pipx run nox` to run it without installing, or `pipx install nox`. +If you don't have pipx (pip for applications), then you can install with +`pip install pipx` (the only case were installing an application with regular +pip is reasonable). If you use macOS, then pipx and nox are both in brew, use +`brew install pipx nox`. + +To use, run `nox`. This will lint and test using every installed version of +Python on your system, skipping ones that are not installed. You can also run +specific jobs: + +```console +$ nox -s lint # Lint only +$ nox -s tests # Python tests +$ nox -s docs # Build and serve the documentation +$ nox -s build # Make an SDist and wheel +``` + +Nox handles everything for you, including setting up an temporary virtual +environment for each run. + +# Setting up a development environment manually + +You can set up a development environment by first setting up a virtual environment using: + +```bash +python3 -m venv .venv +source ./.venv/bin/activate +``` +or +```bash +conda create -n virtual_ship python=3.8 +conda activate virtual_ship +``` + +Then install the dependencies: + +```bash +pip install -v -e .[dev] +``` + +Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtual_ship` + + +# Pre-commit + +We use pre-commit to enforce code style and other checks. This can be run by the `nox -s lint` command above, or by installing pre-commit separately and running `pre-commit run --all-files`. If pre-commit is installed separately, you can also install the pre-commit hook into your git repository by running `pre-commit install` such that you don't need to manually run it (it will run when you make a commit). + +Either way, the repository is set up to automatically run pre-commit checks and fix errors on every commit, so you should not need to worry about it. + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..6c4b3695 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: + - "*" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..9d1e0987 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,5 @@ +changelog: + exclude: + authors: + - dependabot + - pre-commit-ci diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..efc7d061 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,60 @@ +name: CD + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + release: + types: + - published + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Many color libraries just need this to be set to any value, but at least + # one distinguishes color depth, where "3" -> "256-bit color". + FORCE_COLOR: 3 + +jobs: + dist: + name: Distribution build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: hynek/build-and-inspect-python-package@v2 + + publish: + needs: [dist] + name: Publish to PyPI + environment: pypi + permissions: + id-token: write + attestations: write + contents: read + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + + steps: + - uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Generate artifact attestation for sdist and wheel + uses: actions/attest-build-provenance@v1.4.1 + with: + subject-path: "dist/*" + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + # Remember to tell (test-)pypi about this repo before publishing + # Remove this line to publish to PyPI + repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..29673d56 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Many color libraries just need this to be set to any value, but at least + # one distinguishes color depth, where "3" -> "256-bit color". + FORCE_COLOR: 3 + +jobs: + # pylint: + # name: Format + # runs-on: ubuntu-latest + # steps: + # - name: Run PyLint + # run: pipx run nox -s pylint -- --output-format=github + + tests: + name: tests (${{ matrix.runs-on }} | Python ${{ matrix.python-version }}) + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.12"] + runs-on: [ubuntu-latest, windows-latest, macos-14] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Install package + run: python -m pip install .[dev] + + - name: Test package + run: >- + python -m pytest -ra --cov --cov-report=xml --cov-report=term + --durations=20 + + - name: Upload coverage report + uses: codecov/codecov-action@v4.5.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codetools.yml b/.github/workflows/codetools.yml deleted file mode 100644 index 7027fec2..00000000 --- a/.github/workflows/codetools.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: ci - -on: [push, pull_request] - -env: - PACKAGE: virtual_ship - TESTS: tests - -jobs: - codetools: - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5.1.0 - with: - python-version: ${{ matrix.python-version }} - - name: install - run: pip install ".[dev]" - - name: flake8 - run: flake8 ./$PACKAGE ./$TESTS - - name: pydocstyle - run: pydocstyle ./$PACKAGE - - name: sort-all - run: | - find ./$PACKAGE -type f -name '__init__.py' -print0 | xargs -0 sort-all - [[ -z $(git status -s) ]] - git checkout -- . - - name: black - run: black --diff --check ./$PACKAGE ./$TESTS - - name: isort - run: isort --check-only --diff ./$PACKAGE ./$TESTS - - tests: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5.1.0 - with: - python-version: 3.12 - - name: install - run: pip install ".[dev]" - - name: run_tests - run: pytest --cov=virtual_ship tests diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..67c194c8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + commands: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + - uv venv + - uv pip install .[docs] + - .venv/bin/python -m sphinx -T -b html -d docs/_build/doctrees -D + language=en docs $READTHEDOCS_OUTPUT/html diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..ec18a4a1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import importlib.metadata +from typing import Any + +project = "virtual_ship" +copyright = "2024, Emma Daniëls" +author = "Emma Daniëls" +version = release = importlib.metadata.version("virtual_ship") + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", + "sphinx_copybutton", +] + +source_suffix = [".rst", ".md"] +exclude_patterns = [ + "_build", + "**.ipynb_checkpoints", + "Thumbs.db", + ".DS_Store", + ".env", + ".venv", +] + +html_theme = "furo" + +html_theme_options: dict[str, Any] = { + "footer_icons": [ + { + "name": "GitHub", + "url": "https://github.com/OceanParcels/Virtual_ship_classroom", + "html": """ + + + + """, + "class": "", + }, + ], + "source_repository": "https://github.com/OceanParcels/Virtual_ship_classroom", + "source_branch": "main", + "source_directory": "docs/", +} + +myst_enable_extensions = [ + "colon_fence", +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + +nitpick_ignore = [ + ("py:class", "_io.StringIO"), + ("py:class", "_io.BytesIO"), +] + +always_document_param_types = True diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..67b9d382 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# virtual_ship + +```{toctree} +:maxdepth: 2 +:hidden: + +``` + +```{include} ../README.md +:start-after: +``` + +## Indices and tables + +- {ref}`genindex` +- {ref}`modindex` +- {ref}`search` diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..b2cf2d25 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import argparse +import shutil +from pathlib import Path + +import nox + +DIR = Path(__file__).parent.resolve() + +nox.needs_version = ">=2024.3.2" +nox.options.sessions = ["lint", "pylint", "tests"] +nox.options.default_venv_backend = "uv|virtualenv" + + +@nox.session +def lint(session: nox.Session) -> None: + """ + Run the linter. + """ + session.install("pre-commit") + session.run( + "pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs + ) + + +@nox.session +def pylint(session: nox.Session) -> None: + """ + Run PyLint. + """ + # This needs to be installed into the package environment, and is slower + # than a pre-commit check + session.install(".", "pylint>=3.2") + session.run("pylint", "virtual_ship", *session.posargs) + + +@nox.session +def tests(session: nox.Session) -> None: + """ + Run the unit and regular tests. + """ + session.install(".[dev]") + session.run("pytest", *session.posargs) + + +@nox.session(reuse_venv=True) +def docs(session: nox.Session) -> None: + """ + Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory. + """ + + parser = argparse.ArgumentParser() + parser.add_argument( + "-b", dest="builder", default="html", help="Build target (default: html)" + ) + parser.add_argument("output", nargs="?", help="Output directory") + args, posargs = parser.parse_known_args(session.posargs) + serve = args.builder == "html" and session.interactive + + session.install("-e.[docs]", "sphinx-autobuild") + + shared_args = ( + "-n", # nitpicky mode + "-T", # full tracebacks + f"-b={args.builder}", + "docs", + args.output or f"docs/_build/{args.builder}", + *posargs, + ) + + if serve: + session.run("sphinx-autobuild", "--open-browser", *shared_args) + else: + session.run("sphinx-build", "--keep-going", *shared_args) + + +@nox.session +def build_api_docs(session: nox.Session) -> None: + """ + Build (regenerate) API docs. + """ + + session.install("sphinx") + session.run( + "sphinx-apidoc", + "-o", + "docs/api/", + "--module-first", + "--no-toc", + "--force", + "src/virtual_ship", + ) + + +@nox.session +def build(session: nox.Session) -> None: + """ + Build an SDist and wheel. + """ + + build_path = DIR.joinpath("build") + if build_path.exists(): + shutil.rmtree(build_path) + + session.install("build") + session.run("python", "-m", "build") diff --git a/pyproject.toml b/pyproject.toml index 487cc18c..f3ccf7d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,9 @@ license = { file = "LICENSE" } classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", + "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -34,9 +36,11 @@ dependencies = [ ] [project.urls] -homepage = "https://oceanparcels.org/" -repository = "https://github.com/OceanParcels/Virtual_ship_classroom" -tracker = "https://github.com/OceanParcels/Virtual_ship_classroom/issues" +Homepage = "https://oceanparcels.org/" # TODO: Update this to just be repo? +Repository = "https://github.com/OceanParcels/Virtual_ship_classroom" +"Bug Tracker" = "https://github.com/OceanParcels/Virtual_ship_classroom/issues" +Changelog = "https://github.com/OceanParcels/Virtual_ship_classroom/releases" + [tool.setuptools] packages = ["virtual_ship"] @@ -47,7 +51,6 @@ local_scheme = "no-local-version" [project.optional-dependencies] dev = [ - "pre-commit", "pytest == 8.2.0", "pytest-cov == 5.0.0", "codecov == 2.1.13", @@ -56,10 +59,38 @@ dev = [ "sortedcontainers-stubs == 2.4.2", ] +docs = [ + "sphinx>=7.0", + "myst_parser>=0.13", + "sphinx_copybutton", + "sphinx_autodoc_typehints", + "furo>=2023.08.17", +] + [project.scripts] do_expedition = "virtual_ship.cli.do_expedition:main" +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] +xfail_strict = true +filterwarnings = [ + "error", +] +log_cli_level = "INFO" +testpaths = [ + "tests", +] + + +[tool.coverage] +run.source = ["virtual_ship"] +report.exclude_also = [ + '\.\.\.', + 'if typing.TYPE_CHECKING:', +] + [tool.ruff.lint] exclude = ["*.ipynb"] From b127f262af01b842ebba0b086032e6c7ccc40e6d Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:04:32 +0200 Subject: [PATCH 04/35] add version information --- virtual_ship/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/virtual_ship/__init__.py b/virtual_ship/__init__.py index 6e74f1c8..20865258 100644 --- a/virtual_ship/__init__.py +++ b/virtual_ship/__init__.py @@ -1,9 +1,18 @@ """Code for the Virtual Ship Classroom, where Marine Scientists can combine Copernicus Marine Data with an OceanParcels ship to go on a virtual expedition.""" +from importlib.metadata import version as _version + from .location import Location from .spacetime import Spacetime +try: + __version__ = _version("virtual_ship") +except Exception: + # Local copy or not installed with setuptools + __version__ = "unknown" + __all__ = [ "Location", "Spacetime", + "__version__", ] From 15fbb17a35998b01b10850560cfb0ce991b1d9f7 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:07:29 +0200 Subject: [PATCH 05/35] Add code of conduct --- .github/CODE_OF_CONDUCT.md | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..9570e7d1 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at E.vanSebille@uu.nl. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ From 5c714cf284fd7b9d58024d43d5b850e7d74cd088 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:12:40 +0200 Subject: [PATCH 06/35] Add logo --- README.md | 5 ++--- docs/_static/virtual_ship_logo.png | Bin 0 -> 40739 bytes docs/_static/virtual_ship_logo_inverted.png | Bin 0 -> 41287 bytes docs/conf.py | 9 ++++++++- docs/index.md | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 docs/_static/virtual_ship_logo.png create mode 100644 docs/_static/virtual_ship_logo_inverted.png diff --git a/README.md b/README.md index 867453dc..61f12548 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ Please contact me at e.e.daniels1@uu.nl with any questions. This is a python tool that will allow students to virtually sample the ocean as if the measurements were coming from an actual oceanographic mission. At the moment we mimic ADCP, CTD, and simple underwaydata measurements and allow surface drifters and argo float deployments. We might add gliders and meteorological data in the future. -### Requirements -To use the material please create an environment called Parcels with the command -`conda env create -f environment.yml +### Installation +Please follow the installation instructions detailed in the [contributing page](.github/CONTRIBUTING.md). ### Input data The scripts are written to work with A-grid ocean data, specifically that from CMEMS. diff --git a/docs/_static/virtual_ship_logo.png b/docs/_static/virtual_ship_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..23c68c325342aca86f0acdccc37c3984b3bd8b8f GIT binary patch literal 40739 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 literal 0 HcmV?d00001 diff --git a/docs/_static/virtual_ship_logo_inverted.png b/docs/_static/virtual_ship_logo_inverted.png new file mode 100644 index 0000000000000000000000000000000000000000..d3334a127870fc692c4ac72bb947ac28c0564878 GIT binary patch literal 41287 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 literal 0 HcmV?d00001 diff --git a/docs/conf.py b/docs/conf.py index ec18a4a1..37e9cc01 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,7 +3,7 @@ import importlib.metadata from typing import Any -project = "virtual_ship" +project = "Virtual Ship Parcels" copyright = "2024, Emma Daniëls" author = "Emma Daniëls" version = release = importlib.metadata.version("virtual_ship") @@ -48,6 +48,13 @@ "source_directory": "docs/", } +html_static_path = ["_static"] +html_theme_options = { + "light_logo": "virtual_ship_logo.png", + "dark_logo": "virtual_ship_logo_inverted.png", + "sidebar_hide_name": True, +} + myst_enable_extensions = [ "colon_fence", ] diff --git a/docs/index.md b/docs/index.md index 67b9d382..f0f29250 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# virtual_ship +# Virtual Ship Parcels ```{toctree} :maxdepth: 2 From 801f50ae1267f72c489884b2c9884c8b761e8197 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:21:55 +0000 Subject: [PATCH 07/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/CONTRIBUTING.md | 1 - noxfile.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 822c9c05..f853209f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -49,4 +49,3 @@ Now you can start with development. Unit tests can be run by typing the command We use pre-commit to enforce code style and other checks. This can be run by the `nox -s lint` command above, or by installing pre-commit separately and running `pre-commit run --all-files`. If pre-commit is installed separately, you can also install the pre-commit hook into your git repository by running `pre-commit install` such that you don't need to manually run it (it will run when you make a commit). Either way, the repository is set up to automatically run pre-commit checks and fix errors on every commit, so you should not need to worry about it. - diff --git a/noxfile.py b/noxfile.py index b2cf2d25..5299e2a8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -49,7 +49,6 @@ def docs(session: nox.Session) -> None: """ Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory. """ - parser = argparse.ArgumentParser() parser.add_argument( "-b", dest="builder", default="html", help="Build target (default: html)" @@ -80,7 +79,6 @@ def build_api_docs(session: nox.Session) -> None: """ Build (regenerate) API docs. """ - session.install("sphinx") session.run( "sphinx-apidoc", @@ -98,7 +96,6 @@ def build(session: nox.Session) -> None: """ Build an SDist and wheel. """ - build_path = DIR.joinpath("build") if build_path.exists(): shutil.rmtree(build_path) From 8413bdeec8bc4bddebb3861a9ea437f2b8333f72 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:31:52 +0200 Subject: [PATCH 08/35] Update supported Pythons Python 3.8/3.9 were already not supported since we used 3.10 features --- .github/CONTRIBUTING.md | 2 +- .github/workflows/ci.yml | 2 +- meta.yaml | 2 +- pyproject.toml | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f853209f..5fb24032 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -31,7 +31,7 @@ source ./.venv/bin/activate ``` or ```bash -conda create -n virtual_ship python=3.8 +conda create -n virtual_ship python=3.10 conda activate virtual_ship ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29673d56..0dc0483b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.12"] + python-version: ["3.10", "3.13"] runs-on: [ubuntu-latest, windows-latest, macos-14] steps: diff --git a/meta.yaml b/meta.yaml index 6073e7c7..735f8589 100644 --- a/meta.yaml +++ b/meta.yaml @@ -11,7 +11,7 @@ build: requirements: run: - - python >=3.8 + - python >=3.10 - parcels >=3,<4 - pyproj >=3,<4 - sortedcontainers ==2.4.0 diff --git a/pyproject.toml b/pyproject.toml index f3ccf7d1..041903ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Code for the Virtual Ship Classroom, where Marine Scientists can readme = "README.md" dynamic = ["version"] authors = [{ name = "oceanparcels.org team" }] -requires-python = ">=3.8" +requires-python = ">=3.10" license = { file = "LICENSE" } classifiers = [ "Development Status :: 3 - Alpha", @@ -16,11 +16,10 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Topic :: Scientific/Engineering", "Topic :: Education", From 0a4c053d382457d4efe3603f572dd739d2baa08a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:32:49 +0000 Subject: [PATCH 09/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- virtual_ship/expedition/verify_schedule.py | 4 ++-- virtual_ship/make_realistic/adcp_make_realistic.py | 6 ++++-- virtual_ship/make_realistic/ctd_make_realistic.py | 10 +++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/virtual_ship/expedition/verify_schedule.py b/virtual_ship/expedition/verify_schedule.py index 55c34587..908380fa 100644 --- a/virtual_ship/expedition/verify_schedule.py +++ b/virtual_ship/expedition/verify_schedule.py @@ -40,7 +40,7 @@ def verify_schedule( if not all( [ next.time >= cur.time - for cur, next in zip(timed_waypoints, timed_waypoints[1:]) + for cur, next in zip(timed_waypoints, timed_waypoints[1:], strict=False) ] ): raise PlanningError( @@ -90,7 +90,7 @@ def verify_schedule( # check that ship will arrive on time at each waypoint (in case no unexpected event happen) time = schedule.waypoints[0].time for wp_i, (wp, wp_next) in enumerate( - zip(schedule.waypoints, schedule.waypoints[1:]) + zip(schedule.waypoints, schedule.waypoints[1:], strict=False) ): if wp.instrument is InstrumentType.CTD: time += timedelta(minutes=20) diff --git a/virtual_ship/make_realistic/adcp_make_realistic.py b/virtual_ship/make_realistic/adcp_make_realistic.py index 45fd25e4..b90137de 100644 --- a/virtual_ship/make_realistic/adcp_make_realistic.py +++ b/virtual_ship/make_realistic/adcp_make_realistic.py @@ -58,8 +58,10 @@ def _to_csv( meta = "# depths (m): " + ",".join([str(d) for d in depths]) header = f"time,lat,lon,{','.join(['u' + str(n) + ',v' + str(n) for n in range(len(depths))])}" data = [ - f"{time!s},{lat},{lon},{','.join([str(u) + ',' + str(v) for u, v in zip(us, vs)])}" - for time, lat, lon, us, vs in zip(times, lats, lons, all_us.T, all_vs.T) + f"{time!s},{lat},{lon},{','.join([str(u) + ',' + str(v) for u, v in zip(us, vs, strict=False)])}" + for time, lat, lon, us, vs in zip( + times, lats, lons, all_us.T, all_vs.T, strict=False + ) ] lines = [meta, header] + data diff --git a/virtual_ship/make_realistic/ctd_make_realistic.py b/virtual_ship/make_realistic/ctd_make_realistic.py index 8b7bee24..d0d4741c 100644 --- a/virtual_ship/make_realistic/ctd_make_realistic.py +++ b/virtual_ship/make_realistic/ctd_make_realistic.py @@ -196,7 +196,15 @@ def _to_cnv( rows = [ f"{_i_col(13 + 24 * n)}{_f_col(time + random_time_offset, 3)}{_f_col(lat, 5)}{_f_col(lon, 5)}{_f_col(-depth, 3)}{_f_col(temp, 4)}{_f_col(sal, 4)}" for n, (time, temp, lat, lon, depth, sal) in enumerate( - zip(times, temperatures, latitudes, longitudes, depths, salinities) + zip( + times, + temperatures, + latitudes, + longitudes, + depths, + salinities, + strict=False, + ) ) ] From e5864b618260188e6e294a339ce8a0f2e738e49e Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:16:34 +0200 Subject: [PATCH 10/35] Ignore deprecation warning of pkg_resources Implement Ruff suggestions --- noxfile.py | 24 ++++++---------------- pyproject.toml | 1 + virtual_ship/expedition/verify_schedule.py | 6 ++---- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/noxfile.py b/noxfile.py index 5299e2a8..e899a8fc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,9 +15,7 @@ @nox.session def lint(session: nox.Session) -> None: - """ - Run the linter. - """ + """Run the linter.""" session.install("pre-commit") session.run( "pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs @@ -26,9 +24,7 @@ def lint(session: nox.Session) -> None: @nox.session def pylint(session: nox.Session) -> None: - """ - Run PyLint. - """ + """Run PyLint.""" # This needs to be installed into the package environment, and is slower # than a pre-commit check session.install(".", "pylint>=3.2") @@ -37,18 +33,14 @@ def pylint(session: nox.Session) -> None: @nox.session def tests(session: nox.Session) -> None: - """ - Run the unit and regular tests. - """ + """Run the unit and regular tests.""" session.install(".[dev]") session.run("pytest", *session.posargs) @nox.session(reuse_venv=True) def docs(session: nox.Session) -> None: - """ - Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory. - """ + """Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory.""" parser = argparse.ArgumentParser() parser.add_argument( "-b", dest="builder", default="html", help="Build target (default: html)" @@ -76,9 +68,7 @@ def docs(session: nox.Session) -> None: @nox.session def build_api_docs(session: nox.Session) -> None: - """ - Build (regenerate) API docs. - """ + """Build (regenerate) API docs.""" session.install("sphinx") session.run( "sphinx-apidoc", @@ -93,9 +83,7 @@ def build_api_docs(session: nox.Session) -> None: @nox.session def build(session: nox.Session) -> None: - """ - Build an SDist and wheel. - """ + """Build an SDist and wheel.""" build_path = DIR.joinpath("build") if build_path.exists(): shutil.rmtree(build_path) diff --git a/pyproject.toml b/pyproject.toml index 041903ef..f9611c9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] xfail_strict = true filterwarnings = [ "error", + "ignore:pkg_resources is deprecated as an API.*:DeprecationWarning" # seabird (used in one test) uses pkg_resources ] log_cli_level = "INFO" testpaths = [ diff --git a/virtual_ship/expedition/verify_schedule.py b/virtual_ship/expedition/verify_schedule.py index 908380fa..fcc77217 100644 --- a/virtual_ship/expedition/verify_schedule.py +++ b/virtual_ship/expedition/verify_schedule.py @@ -1,5 +1,6 @@ """verify_schedule function and supporting classes.""" +import itertools from datetime import timedelta import pyproj @@ -38,10 +39,7 @@ def verify_schedule( # check waypoint times are in ascending order timed_waypoints = [wp for wp in schedule.waypoints if wp.time is not None] if not all( - [ - next.time >= cur.time - for cur, next in zip(timed_waypoints, timed_waypoints[1:], strict=False) - ] + [next.time >= cur.time for cur, next in itertools.pairwise(timed_waypoints)] ): raise PlanningError( "Each waypoint should be timed after all previous waypoints" From e8596d17774cc7b6e2a884cd42580a5f31eda5d6 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:33:13 +0200 Subject: [PATCH 11/35] Remove deprecated dependency pylib --- tests/expedition/test_schedule.py | 4 +--- tests/instruments/test_adcp.py | 3 +-- tests/instruments/test_argo_float.py | 3 +-- tests/instruments/test_ctd.py | 3 +-- tests/instruments/test_drifter.py | 3 +-- tests/instruments/test_ship_underwater_st.py | 3 +-- tests/make_realistic/test_adcp_make_realistic.py | 4 +--- tests/make_realistic/test_ctd_make_realistic.py | 3 +-- virtual_ship/instruments/adcp.py | 5 +++-- virtual_ship/instruments/argo_float.py | 4 ++-- virtual_ship/instruments/ctd.py | 4 ++-- virtual_ship/instruments/drifter.py | 4 ++-- virtual_ship/instruments/ship_underwater_st.py | 5 +++-- .../make_realistic/adcp_make_realistic.py | 16 +++++++--------- .../make_realistic/ctd_make_realistic.py | 15 ++++++--------- 15 files changed, 33 insertions(+), 46 deletions(-) diff --git a/tests/expedition/test_schedule.py b/tests/expedition/test_schedule.py index 37ee4a47..b09f8ba3 100644 --- a/tests/expedition/test_schedule.py +++ b/tests/expedition/test_schedule.py @@ -1,12 +1,10 @@ from datetime import datetime, timedelta -import py - from virtual_ship import Location from virtual_ship.expedition import Schedule, Waypoint -def test_schedule(tmpdir: py.path.LocalPath) -> None: +def test_schedule(tmpdir) -> None: out_path = tmpdir.join("schedule.yaml") # arbitrary time for testing diff --git a/tests/instruments/test_adcp.py b/tests/instruments/test_adcp.py index d119f3e2..dfbdeb18 100644 --- a/tests/instruments/test_adcp.py +++ b/tests/instruments/test_adcp.py @@ -3,7 +3,6 @@ import datetime import numpy as np -import py import xarray as xr from parcels import FieldSet @@ -11,7 +10,7 @@ from virtual_ship.instruments.adcp import simulate_adcp -def test_simulate_adcp(tmpdir: py.path.LocalPath) -> None: +def test_simulate_adcp(tmpdir) -> None: # maximum depth the ADCP can measure MAX_DEPTH = -1000 # -1000 # minimum depth the ADCP can measure diff --git a/tests/instruments/test_argo_float.py b/tests/instruments/test_argo_float.py index b25e80e7..92037b42 100644 --- a/tests/instruments/test_argo_float.py +++ b/tests/instruments/test_argo_float.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta import numpy as np -import py import xarray as xr from parcels import FieldSet @@ -11,7 +10,7 @@ from virtual_ship.instruments.argo_float import ArgoFloat, simulate_argo_floats -def test_simulate_argo_floats(tmpdir: py.path.LocalPath) -> None: +def test_simulate_argo_floats(tmpdir) -> None: # arbitrary time offset for the dummy fieldset base_time = datetime.strptime("1950-01-01", "%Y-%m-%d") diff --git a/tests/instruments/test_ctd.py b/tests/instruments/test_ctd.py index eab53bf2..3ff2550b 100644 --- a/tests/instruments/test_ctd.py +++ b/tests/instruments/test_ctd.py @@ -8,7 +8,6 @@ from datetime import timedelta import numpy as np -import py import xarray as xr from parcels import Field, FieldSet @@ -16,7 +15,7 @@ from virtual_ship.instruments.ctd import CTD, simulate_ctd -def test_simulate_ctds(tmpdir: py.path.LocalPath) -> None: +def test_simulate_ctds(tmpdir) -> None: # arbitrary time offset for the dummy fieldset base_time = datetime.datetime.strptime("1950-01-01", "%Y-%m-%d") diff --git a/tests/instruments/test_drifter.py b/tests/instruments/test_drifter.py index b7e03d6b..0cefdafb 100644 --- a/tests/instruments/test_drifter.py +++ b/tests/instruments/test_drifter.py @@ -4,7 +4,6 @@ from datetime import timedelta import numpy as np -import py import xarray as xr from parcels import FieldSet @@ -12,7 +11,7 @@ from virtual_ship.instruments.drifter import Drifter, simulate_drifters -def test_simulate_drifters(tmpdir: py.path.LocalPath) -> None: +def test_simulate_drifters(tmpdir) -> None: # arbitrary time offset for the dummy fieldset base_time = datetime.datetime.strptime("1950-01-01", "%Y-%m-%d") diff --git a/tests/instruments/test_ship_underwater_st.py b/tests/instruments/test_ship_underwater_st.py index 54b8bdf0..85b741e4 100644 --- a/tests/instruments/test_ship_underwater_st.py +++ b/tests/instruments/test_ship_underwater_st.py @@ -3,7 +3,6 @@ import datetime import numpy as np -import py import xarray as xr from parcels import FieldSet @@ -11,7 +10,7 @@ from virtual_ship.instruments.ship_underwater_st import simulate_ship_underwater_st -def test_simulate_ship_underwater_st(tmpdir: py.path.LocalPath) -> None: +def test_simulate_ship_underwater_st(tmpdir) -> None: # depth at which the sampling will be done DEPTH = -2 diff --git a/tests/make_realistic/test_adcp_make_realistic.py b/tests/make_realistic/test_adcp_make_realistic.py index e2db6f7a..16ec4b91 100644 --- a/tests/make_realistic/test_adcp_make_realistic.py +++ b/tests/make_realistic/test_adcp_make_realistic.py @@ -1,11 +1,9 @@ import csv -import py - from virtual_ship.make_realistic import adcp_make_realistic -def test_adcp_make_realistic(tmpdir: py.path.LocalPath) -> None: +def test_adcp_make_realistic(tmpdir) -> None: # add noise and convert to CSV file = adcp_make_realistic("adcp.zarr", out_dir=tmpdir, prefix="ADCP") diff --git a/tests/make_realistic/test_ctd_make_realistic.py b/tests/make_realistic/test_ctd_make_realistic.py index dc266d95..553b68b9 100644 --- a/tests/make_realistic/test_ctd_make_realistic.py +++ b/tests/make_realistic/test_ctd_make_realistic.py @@ -1,10 +1,9 @@ -import py import seabird from virtual_ship.make_realistic import ctd_make_realistic -def test_ctd_make_realistic(tmpdir: py.path.LocalPath) -> None: +def test_ctd_make_realistic(tmpdir) -> None: # add noise and convert to cnv files = ctd_make_realistic("ctd.zarr", out_dir=tmpdir, prefix="CTD_") diff --git a/virtual_ship/instruments/adcp.py b/virtual_ship/instruments/adcp.py index bca4ab94..5563bdd2 100644 --- a/virtual_ship/instruments/adcp.py +++ b/virtual_ship/instruments/adcp.py @@ -1,7 +1,8 @@ """ADCP instrument.""" +from pathlib import Path + import numpy as np -import py from parcels import FieldSet, ParticleSet, ScipyParticle, Variable from ..spacetime import Spacetime @@ -24,7 +25,7 @@ def _sample_velocity(particle, fieldset, time): def simulate_adcp( fieldset: FieldSet, - out_path: str | py.path.LocalPath, + out_path: str | Path, max_depth: float, min_depth: float, num_bins: int, diff --git a/virtual_ship/instruments/argo_float.py b/virtual_ship/instruments/argo_float.py index ad4a4871..91b1e752 100644 --- a/virtual_ship/instruments/argo_float.py +++ b/virtual_ship/instruments/argo_float.py @@ -3,9 +3,9 @@ import math from dataclasses import dataclass from datetime import datetime, timedelta +from pathlib import Path import numpy as np -import py from parcels import ( AdvectionRK4, FieldSet, @@ -117,7 +117,7 @@ def _check_error(particle, fieldset, time): def simulate_argo_floats( fieldset: FieldSet, - out_path: str | py.path.LocalPath, + out_path: str | Path, argo_floats: list[ArgoFloat], outputdt: timedelta, endtime: datetime | None, diff --git a/virtual_ship/instruments/ctd.py b/virtual_ship/instruments/ctd.py index 0d017fa7..6f76b408 100644 --- a/virtual_ship/instruments/ctd.py +++ b/virtual_ship/instruments/ctd.py @@ -2,9 +2,9 @@ from dataclasses import dataclass from datetime import timedelta +from pathlib import Path import numpy as np -import py from parcels import FieldSet, JITParticle, ParticleSet, Variable from ..spacetime import Spacetime @@ -55,7 +55,7 @@ def _ctd_cast(particle, fieldset, time): def simulate_ctd( fieldset: FieldSet, - out_path: str | py.path.LocalPath, + out_path: str | Path, ctds: list[CTD], outputdt: timedelta, ) -> None: diff --git a/virtual_ship/instruments/drifter.py b/virtual_ship/instruments/drifter.py index 7854d4cb..b4db3b9a 100644 --- a/virtual_ship/instruments/drifter.py +++ b/virtual_ship/instruments/drifter.py @@ -2,9 +2,9 @@ from dataclasses import dataclass from datetime import datetime, timedelta +from pathlib import Path import numpy as np -import py from parcels import AdvectionRK4, FieldSet, JITParticle, ParticleSet, Variable from ..spacetime import Spacetime @@ -42,7 +42,7 @@ def _check_lifetime(particle, fieldset, time): def simulate_drifters( fieldset: FieldSet, - out_path: str | py.path.LocalPath, + out_path: str | Path, drifters: list[Drifter], outputdt: timedelta, dt: timedelta, diff --git a/virtual_ship/instruments/ship_underwater_st.py b/virtual_ship/instruments/ship_underwater_st.py index 3d2050d8..407055ad 100644 --- a/virtual_ship/instruments/ship_underwater_st.py +++ b/virtual_ship/instruments/ship_underwater_st.py @@ -1,7 +1,8 @@ """Ship salinity and temperature.""" +from pathlib import Path + import numpy as np -import py from parcels import FieldSet, ParticleSet, ScipyParticle, Variable from ..spacetime import Spacetime @@ -28,7 +29,7 @@ def _sample_temperature(particle, fieldset, time): def simulate_ship_underwater_st( fieldset: FieldSet, - out_path: str | py.path.LocalPath, + out_path: str | Path, depth: float, sample_points: list[Spacetime], ) -> None: diff --git a/virtual_ship/make_realistic/adcp_make_realistic.py b/virtual_ship/make_realistic/adcp_make_realistic.py index b90137de..9dbcbea9 100644 --- a/virtual_ship/make_realistic/adcp_make_realistic.py +++ b/virtual_ship/make_realistic/adcp_make_realistic.py @@ -1,15 +1,16 @@ """adcp_make_realistic function.""" +from pathlib import Path + import numpy as np -import py import xarray as xr def adcp_make_realistic( - zarr_path: str | py.path.LocalPath, - out_dir: str | py.path.LocalPath, + zarr_path: str | Path, + out_dir: str | Path, prefix: str, -) -> py.path.LocalPath: +) -> Path: """ Take simulated ADCP data, add noise, then save in (an inconvenient educational) CSV format. @@ -30,11 +31,8 @@ def adcp_make_realistic( all_us, all_vs = _add_noise(times, depths, all_us, all_vs) csv = _to_csv(times, depths, lats, lons, all_us, all_vs) - out_file = ( - out_dir.join(f"{prefix}.csv") - if isinstance(out_dir, py.path.LocalPath) - else f"{out_dir}/{prefix}.csv" - ) + out_file = Path(out_dir) / f"{prefix}.csv" + with open(out_file, "w") as out_cnv: out_cnv.write(csv) diff --git a/virtual_ship/make_realistic/ctd_make_realistic.py b/virtual_ship/make_realistic/ctd_make_realistic.py index d0d4741c..aeae2ab7 100644 --- a/virtual_ship/make_realistic/ctd_make_realistic.py +++ b/virtual_ship/make_realistic/ctd_make_realistic.py @@ -1,18 +1,18 @@ """ctd_make_realistic function.""" import random +from pathlib import Path import numpy as np import opensimplex -import py import xarray as xr def ctd_make_realistic( - zarr_path: str | py.path.LocalPath, - out_dir: str | py.path.LocalPath, + zarr_path: str | Path, + out_dir: str | Path, prefix: str, -) -> list[py.path.LocalPath]: +) -> list[Path]: """ Take simulated CTD data, add noise, then save in CNV format (1 file per CTD). @@ -36,11 +36,8 @@ def ctd_make_realistic( temperature = _add_temperature_noise(temperature, depth) salinity = _add_salinity_noise(salinity, depth) - out_file = ( - out_dir.join(f"{prefix}{ctd_i}.cnv") - if isinstance(out_dir, py.path.LocalPath) - else f"{out_dir}/{prefix}{ctd_i}.cnv" - ) + out_file = Path(out_dir) / f"{prefix}{ctd_i}.cnv" + files.append(out_file) cnv = _to_cnv( From 5501e8d18b48a6ab476b415e97f3e0a0123d1717 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:34:23 +0200 Subject: [PATCH 12/35] ignore pset empty warning --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f9611c9a..6ddf78bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,8 @@ addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] xfail_strict = true filterwarnings = [ "error", - "ignore:pkg_resources is deprecated as an API.*:DeprecationWarning" # seabird (used in one test) uses pkg_resources + "ignore:pkg_resources is deprecated as an API.*:DeprecationWarning", # seabird (used in one test) uses pkg_resources + "ignore:ParticleSet is empty.*:RuntimeWarning" # TODO: Probably should be ignored in the source code ] log_cli_level = "INFO" testpaths = [ From a8751a2a7426ed5365a30f5baa74dc7bc4b86243 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:31:53 +0200 Subject: [PATCH 13/35] Enable prettier --- .github/CONTRIBUTING.md | 3 +- .github/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 9 +++--- README.md | 5 ++++ tests/expedition/expedition_dir/schedule.yaml | 30 +++++++++---------- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5fb24032..7bb93b52 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -29,7 +29,9 @@ You can set up a development environment by first setting up a virtual environme python3 -m venv .venv source ./.venv/bin/activate ``` + or + ```bash conda create -n virtual_ship python=3.10 conda activate virtual_ship @@ -43,7 +45,6 @@ pip install -v -e .[dev] Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtual_ship` - # Pre-commit We use pre-commit to enforce code style and other checks. This can be run by the `nox -s lint` command above, or by installing pre-commit separately and running `pre-commit run --all-files`. If pre-commit is installed separately, you can also install the pre-commit hook into your git repository by running `pre-commit install` such that you don't need to manually run it (it will run when you make a commit). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dc0483b..b3f5a5fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: # run: pipx run nox -s pylint -- --output-format=github tests: - name: tests (${{ matrix.runs-on }} | Python ${{ matrix.python-version }}) + name: tests (${{ matrix.runs-on }} | Python ${{ matrix.python-version }}) runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b52d46ee..38daed3b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,8 +21,7 @@ repos: types_or: [jupyter] - id: ruff-format types_or: [python, jupyter] - - # - repo: https://github.com/rbubley/mirrors-prettier # Update mirror as official mirror is deprecated - # rev: v3.3.3 - # hooks: - # - id: prettier + - repo: https://github.com/rbubley/mirrors-prettier # Update mirror as official mirror is deprecated + rev: v3.3.3 + hooks: + - id: prettier diff --git a/README.md b/README.md index 61f12548..0cf7b858 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,25 @@ # Virtual_ship_classroom + Emma's work for the MSc student material Please contact me at e.e.daniels1@uu.nl with any questions. This is a python tool that will allow students to virtually sample the ocean as if the measurements were coming from an actual oceanographic mission. At the moment we mimic ADCP, CTD, and simple underwaydata measurements and allow surface drifters and argo float deployments. We might add gliders and meteorological data in the future. ### Installation + Please follow the installation instructions detailed in the [contributing page](.github/CONTRIBUTING.md). ### Input data + The scripts are written to work with A-grid ocean data, specifically that from CMEMS. Data can be downloaded with the download_data.py script. For now a different conda env is needed for downloading, see comments in the script. ### Sailing the ship + Fill in the accompanying JSON file and run virtualship.py to start measuring. You can also use Sail_the_ship.ipynb ### Ideas for improvements to be made + - ACDP #bins instead of max_depth - bug when argo(/drifter?) deployed at final location? depth=(len(time)) - documentation that ships sails great circle path diff --git a/tests/expedition/expedition_dir/schedule.yaml b/tests/expedition/expedition_dir/schedule.yaml index 6d55f6c5..0db1d2af 100644 --- a/tests/expedition/expedition_dir/schedule.yaml +++ b/tests/expedition/expedition_dir/schedule.yaml @@ -1,16 +1,16 @@ waypoints: -- instrument: CTD - location: - latitude: 0 - longitude: 0 - time: 2023-01-01 00:00:00 -- instrument: DRIFTER - location: - latitude: 0.01 - longitude: 0.01 - time: 2023-01-01 01:00:00 -- instrument: ARGO_FLOAT - location: - latitude: 0.02 - longitude: 0.02 - time: 2023-01-01 02:00:00 + - instrument: CTD + location: + latitude: 0 + longitude: 0 + time: 2023-01-01 00:00:00 + - instrument: DRIFTER + location: + latitude: 0.01 + longitude: 0.01 + time: 2023-01-01 01:00:00 + - instrument: ARGO_FLOAT + location: + latitude: 0.02 + longitude: 0.02 + time: 2023-01-01 02:00:00 From e41eb136faf4f2782083fb12eda8d54353699b0d Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:24:07 +0200 Subject: [PATCH 14/35] update readme --- .github/CONTRIBUTING.md | 2 +- .gitignore | 1 + README.md | 64 ++++++++++++++++++++++++++++++----------- noxfile.py | 2 +- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7bb93b52..ec50b9b0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,7 +19,7 @@ $ nox -s build # Make an SDist and wheel ``` Nox handles everything for you, including setting up an temporary virtual -environment for each run. +environment for each run. Run `nox --list` to see all available jobs. # Setting up a development environment manually diff --git a/.gitignore b/.gitignore index 924a28fe..c434aedc 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/api/ # PyBuilder .pybuilder/ diff --git a/README.md b/README.md index 0cf7b858..9a591988 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,57 @@ -# Virtual_ship_classroom +

+ + + VirtualShipParcels logo' + +

-Emma's work for the MSc student material -Please contact me at e.e.daniels1@uu.nl with any questions. + -This is a python tool that will allow students to virtually sample the ocean as if the measurements were coming from an actual oceanographic mission. At the moment we mimic ADCP, CTD, and simple underwaydata measurements and allow surface drifters and argo float deployments. We might add gliders and meteorological data in the future. +--- -### Installation + + + + + + + + + + + +
Project OwnerEmma Daniels (e.e.daniels1@uu.nl)
Development statusAlpha
-Please follow the installation instructions detailed in the [contributing page](.github/CONTRIBUTING.md). + -### Input data +VirtualShipParcels is a command line simulator allowing students to plan and conduct a virtual research expedition, receiving measurements as if they were coming from actual oceanographic instruments including: -The scripts are written to work with A-grid ocean data, specifically that from CMEMS. -Data can be downloaded with the download_data.py script. For now a different conda env is needed for downloading, see comments in the script. +- ADCP (for currents) +- CTD (for conductivity, and temperature) +- underwater measurements (salinity and temperature) +- surface drifters +- argo float deployments -### Sailing the ship + -Fill in the accompanying JSON file and run virtualship.py to start measuring. You can also use Sail_the_ship.ipynb +## Installation -### Ideas for improvements to be made +For a normal installation do: -- ACDP #bins instead of max_depth -- bug when argo(/drifter?) deployed at final location? depth=(len(time)) -- documentation that ships sails great circle path -- CTDs op land? -- Argo's/drifters that collide with land. Chance to break? +```bash +conda create -n my_env python=3.12 +conda activate my_env +conda install -c conda-forge virtualship +``` + +For a development installation, please follow the instructions detailed in the [contributing page](.github/CONTRIBUTING.md). + +## Usage + +Copy the expedition directory, and fill in the accompanying YAML file. Then run `virtualship run ` to start measuring. + + + +## Input data + +The scripts are written to work with A-grid ocean data from CMEMS. diff --git a/noxfile.py b/noxfile.py index e899a8fc..2426e0bb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -77,7 +77,7 @@ def build_api_docs(session: nox.Session) -> None: "--module-first", "--no-toc", "--force", - "src/virtual_ship", + "virtual_ship", ) From 47248f9a74a288f45af3d9a42927cf1dd3a5c881 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:32:47 +0200 Subject: [PATCH 15/35] update name to virtualship --- .github/CONTRIBUTING.md | 6 +++--- .gitignore | 2 +- docs/conf.py | 2 +- meta.yaml | 6 +++--- noxfile.py | 4 ++-- pyproject.toml | 16 ++++++++-------- tests.sh | 5 ----- tests/expedition/test_do_expedition.py | 2 +- tests/expedition/test_schedule.py | 4 ++-- tests/expedition/test_simulate_schedule.py | 6 +++--- tests/instruments/test_adcp.py | 4 ++-- tests/instruments/test_argo_float.py | 4 ++-- tests/instruments/test_ctd.py | 4 ++-- tests/instruments/test_drifter.py | 4 ++-- tests/instruments/test_ship_underwater_st.py | 4 ++-- tests/make_realistic/test_adcp_make_realistic.py | 2 +- tests/make_realistic/test_ctd_make_realistic.py | 2 +- {virtual_ship => virtualship}/__init__.py | 2 +- {virtual_ship => virtualship}/cli/__init__.py | 0 .../cli/do_expedition.py | 2 +- .../expedition/__init__.py | 0 .../expedition/checkpoint.py | 0 .../expedition/do_expedition.py | 0 .../expedition/expedition_cost.py | 0 .../expedition/input_data.py | 0 .../expedition/instrument_type.py | 0 .../expedition/schedule.py | 0 .../expedition/ship_config.py | 0 .../expedition/simulate_measurements.py | 0 .../expedition/simulate_schedule.py | 0 .../expedition/verify_schedule.py | 0 .../expedition/waypoint.py | 0 .../instruments/__init__.py | 0 .../instruments/adcp.py | 0 .../instruments/argo_float.py | 0 {virtual_ship => virtualship}/instruments/ctd.py | 0 .../instruments/drifter.py | 0 .../instruments/ship_underwater_st.py | 0 {virtual_ship => virtualship}/location.py | 0 .../make_realistic/__init__.py | 0 .../make_realistic/adcp_make_realistic.py | 0 .../make_realistic/ctd_make_realistic.py | 0 {virtual_ship => virtualship}/spacetime.py | 0 43 files changed, 38 insertions(+), 43 deletions(-) delete mode 100755 tests.sh rename {virtual_ship => virtualship}/__init__.py (91%) rename {virtual_ship => virtualship}/cli/__init__.py (100%) rename {virtual_ship => virtualship}/cli/do_expedition.py (91%) rename {virtual_ship => virtualship}/expedition/__init__.py (100%) rename {virtual_ship => virtualship}/expedition/checkpoint.py (100%) rename {virtual_ship => virtualship}/expedition/do_expedition.py (100%) rename {virtual_ship => virtualship}/expedition/expedition_cost.py (100%) rename {virtual_ship => virtualship}/expedition/input_data.py (100%) rename {virtual_ship => virtualship}/expedition/instrument_type.py (100%) rename {virtual_ship => virtualship}/expedition/schedule.py (100%) rename {virtual_ship => virtualship}/expedition/ship_config.py (100%) rename {virtual_ship => virtualship}/expedition/simulate_measurements.py (100%) rename {virtual_ship => virtualship}/expedition/simulate_schedule.py (100%) rename {virtual_ship => virtualship}/expedition/verify_schedule.py (100%) rename {virtual_ship => virtualship}/expedition/waypoint.py (100%) rename {virtual_ship => virtualship}/instruments/__init__.py (100%) rename {virtual_ship => virtualship}/instruments/adcp.py (100%) rename {virtual_ship => virtualship}/instruments/argo_float.py (100%) rename {virtual_ship => virtualship}/instruments/ctd.py (100%) rename {virtual_ship => virtualship}/instruments/drifter.py (100%) rename {virtual_ship => virtualship}/instruments/ship_underwater_st.py (100%) rename {virtual_ship => virtualship}/location.py (100%) rename {virtual_ship => virtualship}/make_realistic/__init__.py (100%) rename {virtual_ship => virtualship}/make_realistic/adcp_make_realistic.py (100%) rename {virtual_ship => virtualship}/make_realistic/ctd_make_realistic.py (100%) rename {virtual_ship => virtualship}/spacetime.py (100%) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ec50b9b0..1544b0cf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -33,8 +33,8 @@ source ./.venv/bin/activate or ```bash -conda create -n virtual_ship python=3.10 -conda activate virtual_ship +conda create -n virtualship python=3.10 +conda activate virtualship ``` Then install the dependencies: @@ -43,7 +43,7 @@ Then install the dependencies: pip install -v -e .[dev] ``` -Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtual_ship` +Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtualship` # Pre-commit diff --git a/.gitignore b/.gitignore index c434aedc..6c296b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -172,6 +172,6 @@ cython_debug/ #.idea/ # Auto generated by setuptools scm -virtual_ship/_version_setup.py +virtualship/_version_setup.py .vscode/ diff --git a/docs/conf.py b/docs/conf.py index 37e9cc01..f0ae5981 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,7 +6,7 @@ project = "Virtual Ship Parcels" copyright = "2024, Emma Daniëls" author = "Emma Daniëls" -version = release = importlib.metadata.version("virtual_ship") +version = release = importlib.metadata.version("virtualship") extensions = [ "myst_parser", diff --git a/meta.yaml b/meta.yaml index 735f8589..21df09f1 100644 --- a/meta.yaml +++ b/meta.yaml @@ -1,13 +1,13 @@ package: - name: virtual_ship + name: virtualship version: 0.0.2 source: - path: virtual_ship + path: virtualship build: entry_points: - - do_expedition = virtual_ship.cli.do_expedition:main + - do_expedition = virtualship.cli.do_expedition:main requirements: run: diff --git a/noxfile.py b/noxfile.py index 2426e0bb..9c53d392 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,7 +28,7 @@ def pylint(session: nox.Session) -> None: # This needs to be installed into the package environment, and is slower # than a pre-commit check session.install(".", "pylint>=3.2") - session.run("pylint", "virtual_ship", *session.posargs) + session.run("pylint", "virtualship", *session.posargs) @nox.session @@ -77,7 +77,7 @@ def build_api_docs(session: nox.Session) -> None: "--module-first", "--no-toc", "--force", - "virtual_ship", + "virtualship", ) diff --git a/pyproject.toml b/pyproject.toml index 6ddf78bd..98daa37f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools >= 61.0", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" [project] -name = "virtual_ship" +name = "virtualship" description = "Code for the Virtual Ship Classroom, where Marine Scientists can combine Copernicus Marine Data with an OceanParcels ship to go on a virtual expedition." readme = "README.md" dynamic = ["version"] @@ -36,16 +36,16 @@ dependencies = [ [project.urls] Homepage = "https://oceanparcels.org/" # TODO: Update this to just be repo? -Repository = "https://github.com/OceanParcels/Virtual_ship_classroom" -"Bug Tracker" = "https://github.com/OceanParcels/Virtual_ship_classroom/issues" -Changelog = "https://github.com/OceanParcels/Virtual_ship_classroom/releases" +Repository = "https://github.com/OceanParcels/virtualship" +"Bug Tracker" = "https://github.com/OceanParcels/virtualship/issues" +Changelog = "https://github.com/OceanParcels/virtualship/releases" [tool.setuptools] -packages = ["virtual_ship"] +packages = ["virtualship"] [tool.setuptools_scm] -write_to = "virtual_ship/_version_setup.py" +write_to = "virtualship/_version_setup.py" local_scheme = "no-local-version" [project.optional-dependencies] @@ -67,7 +67,7 @@ docs = [ ] [project.scripts] -do_expedition = "virtual_ship.cli.do_expedition:main" +do_expedition = "virtualship.cli.do_expedition:main" [tool.pytest.ini_options] @@ -86,7 +86,7 @@ testpaths = [ [tool.coverage] -run.source = ["virtual_ship"] +run.source = ["virtualship"] report.exclude_also = [ '\.\.\.', 'if typing.TYPE_CHECKING:', diff --git a/tests.sh b/tests.sh deleted file mode 100755 index a6203336..00000000 --- a/tests.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -# Runs the tests and creates a code coverage report. - -pytest --cov=virtual_ship --cov-report=html tests diff --git a/tests/expedition/test_do_expedition.py b/tests/expedition/test_do_expedition.py index 11e6b862..055764af 100644 --- a/tests/expedition/test_do_expedition.py +++ b/tests/expedition/test_do_expedition.py @@ -1,6 +1,6 @@ from pytest import CaptureFixture -from virtual_ship.expedition import do_expedition +from virtualship.expedition import do_expedition def test_do_expedition(capfd: CaptureFixture) -> None: diff --git a/tests/expedition/test_schedule.py b/tests/expedition/test_schedule.py index b09f8ba3..fd1ed959 100644 --- a/tests/expedition/test_schedule.py +++ b/tests/expedition/test_schedule.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta -from virtual_ship import Location -from virtual_ship.expedition import Schedule, Waypoint +from virtualship import Location +from virtualship.expedition import Schedule, Waypoint def test_schedule(tmpdir) -> None: diff --git a/tests/expedition/test_simulate_schedule.py b/tests/expedition/test_simulate_schedule.py index e27fed3f..8f92c678 100644 --- a/tests/expedition/test_simulate_schedule.py +++ b/tests/expedition/test_simulate_schedule.py @@ -2,9 +2,9 @@ import pyproj -from virtual_ship import Location -from virtual_ship.expedition import Schedule, ShipConfig, Waypoint -from virtual_ship.expedition.simulate_schedule import ( +from virtualship import Location +from virtualship.expedition import Schedule, ShipConfig, Waypoint +from virtualship.expedition.simulate_schedule import ( ScheduleOk, ScheduleProblem, simulate_schedule, diff --git a/tests/instruments/test_adcp.py b/tests/instruments/test_adcp.py index dfbdeb18..118ac21f 100644 --- a/tests/instruments/test_adcp.py +++ b/tests/instruments/test_adcp.py @@ -6,8 +6,8 @@ import xarray as xr from parcels import FieldSet -from virtual_ship import Location, Spacetime -from virtual_ship.instruments.adcp import simulate_adcp +from virtualship import Location, Spacetime +from virtualship.instruments.adcp import simulate_adcp def test_simulate_adcp(tmpdir) -> None: diff --git a/tests/instruments/test_argo_float.py b/tests/instruments/test_argo_float.py index 92037b42..31aafe53 100644 --- a/tests/instruments/test_argo_float.py +++ b/tests/instruments/test_argo_float.py @@ -6,8 +6,8 @@ import xarray as xr from parcels import FieldSet -from virtual_ship import Location, Spacetime -from virtual_ship.instruments.argo_float import ArgoFloat, simulate_argo_floats +from virtualship import Location, Spacetime +from virtualship.instruments.argo_float import ArgoFloat, simulate_argo_floats def test_simulate_argo_floats(tmpdir) -> None: diff --git a/tests/instruments/test_ctd.py b/tests/instruments/test_ctd.py index 3ff2550b..12619edf 100644 --- a/tests/instruments/test_ctd.py +++ b/tests/instruments/test_ctd.py @@ -11,8 +11,8 @@ import xarray as xr from parcels import Field, FieldSet -from virtual_ship import Location, Spacetime -from virtual_ship.instruments.ctd import CTD, simulate_ctd +from virtualship import Location, Spacetime +from virtualship.instruments.ctd import CTD, simulate_ctd def test_simulate_ctds(tmpdir) -> None: diff --git a/tests/instruments/test_drifter.py b/tests/instruments/test_drifter.py index 0cefdafb..b49b510b 100644 --- a/tests/instruments/test_drifter.py +++ b/tests/instruments/test_drifter.py @@ -7,8 +7,8 @@ import xarray as xr from parcels import FieldSet -from virtual_ship import Location, Spacetime -from virtual_ship.instruments.drifter import Drifter, simulate_drifters +from virtualship import Location, Spacetime +from virtualship.instruments.drifter import Drifter, simulate_drifters def test_simulate_drifters(tmpdir) -> None: diff --git a/tests/instruments/test_ship_underwater_st.py b/tests/instruments/test_ship_underwater_st.py index 85b741e4..89a37227 100644 --- a/tests/instruments/test_ship_underwater_st.py +++ b/tests/instruments/test_ship_underwater_st.py @@ -6,8 +6,8 @@ import xarray as xr from parcels import FieldSet -from virtual_ship import Location, Spacetime -from virtual_ship.instruments.ship_underwater_st import simulate_ship_underwater_st +from virtualship import Location, Spacetime +from virtualship.instruments.ship_underwater_st import simulate_ship_underwater_st def test_simulate_ship_underwater_st(tmpdir) -> None: diff --git a/tests/make_realistic/test_adcp_make_realistic.py b/tests/make_realistic/test_adcp_make_realistic.py index 16ec4b91..d5635f15 100644 --- a/tests/make_realistic/test_adcp_make_realistic.py +++ b/tests/make_realistic/test_adcp_make_realistic.py @@ -1,6 +1,6 @@ import csv -from virtual_ship.make_realistic import adcp_make_realistic +from virtualship.make_realistic import adcp_make_realistic def test_adcp_make_realistic(tmpdir) -> None: diff --git a/tests/make_realistic/test_ctd_make_realistic.py b/tests/make_realistic/test_ctd_make_realistic.py index 553b68b9..dc96393a 100644 --- a/tests/make_realistic/test_ctd_make_realistic.py +++ b/tests/make_realistic/test_ctd_make_realistic.py @@ -1,6 +1,6 @@ import seabird -from virtual_ship.make_realistic import ctd_make_realistic +from virtualship.make_realistic import ctd_make_realistic def test_ctd_make_realistic(tmpdir) -> None: diff --git a/virtual_ship/__init__.py b/virtualship/__init__.py similarity index 91% rename from virtual_ship/__init__.py rename to virtualship/__init__.py index 20865258..877fe4e1 100644 --- a/virtual_ship/__init__.py +++ b/virtualship/__init__.py @@ -6,7 +6,7 @@ from .spacetime import Spacetime try: - __version__ = _version("virtual_ship") + __version__ = _version("virtualship") except Exception: # Local copy or not installed with setuptools __version__ = "unknown" diff --git a/virtual_ship/cli/__init__.py b/virtualship/cli/__init__.py similarity index 100% rename from virtual_ship/cli/__init__.py rename to virtualship/cli/__init__.py diff --git a/virtual_ship/cli/do_expedition.py b/virtualship/cli/do_expedition.py similarity index 91% rename from virtual_ship/cli/do_expedition.py rename to virtualship/cli/do_expedition.py index 868a1aa3..d9c5b397 100644 --- a/virtual_ship/cli/do_expedition.py +++ b/virtualship/cli/do_expedition.py @@ -7,7 +7,7 @@ import argparse from pathlib import Path -from virtual_ship.expedition.do_expedition import do_expedition +from virtualship.expedition.do_expedition import do_expedition def main() -> None: diff --git a/virtual_ship/expedition/__init__.py b/virtualship/expedition/__init__.py similarity index 100% rename from virtual_ship/expedition/__init__.py rename to virtualship/expedition/__init__.py diff --git a/virtual_ship/expedition/checkpoint.py b/virtualship/expedition/checkpoint.py similarity index 100% rename from virtual_ship/expedition/checkpoint.py rename to virtualship/expedition/checkpoint.py diff --git a/virtual_ship/expedition/do_expedition.py b/virtualship/expedition/do_expedition.py similarity index 100% rename from virtual_ship/expedition/do_expedition.py rename to virtualship/expedition/do_expedition.py diff --git a/virtual_ship/expedition/expedition_cost.py b/virtualship/expedition/expedition_cost.py similarity index 100% rename from virtual_ship/expedition/expedition_cost.py rename to virtualship/expedition/expedition_cost.py diff --git a/virtual_ship/expedition/input_data.py b/virtualship/expedition/input_data.py similarity index 100% rename from virtual_ship/expedition/input_data.py rename to virtualship/expedition/input_data.py diff --git a/virtual_ship/expedition/instrument_type.py b/virtualship/expedition/instrument_type.py similarity index 100% rename from virtual_ship/expedition/instrument_type.py rename to virtualship/expedition/instrument_type.py diff --git a/virtual_ship/expedition/schedule.py b/virtualship/expedition/schedule.py similarity index 100% rename from virtual_ship/expedition/schedule.py rename to virtualship/expedition/schedule.py diff --git a/virtual_ship/expedition/ship_config.py b/virtualship/expedition/ship_config.py similarity index 100% rename from virtual_ship/expedition/ship_config.py rename to virtualship/expedition/ship_config.py diff --git a/virtual_ship/expedition/simulate_measurements.py b/virtualship/expedition/simulate_measurements.py similarity index 100% rename from virtual_ship/expedition/simulate_measurements.py rename to virtualship/expedition/simulate_measurements.py diff --git a/virtual_ship/expedition/simulate_schedule.py b/virtualship/expedition/simulate_schedule.py similarity index 100% rename from virtual_ship/expedition/simulate_schedule.py rename to virtualship/expedition/simulate_schedule.py diff --git a/virtual_ship/expedition/verify_schedule.py b/virtualship/expedition/verify_schedule.py similarity index 100% rename from virtual_ship/expedition/verify_schedule.py rename to virtualship/expedition/verify_schedule.py diff --git a/virtual_ship/expedition/waypoint.py b/virtualship/expedition/waypoint.py similarity index 100% rename from virtual_ship/expedition/waypoint.py rename to virtualship/expedition/waypoint.py diff --git a/virtual_ship/instruments/__init__.py b/virtualship/instruments/__init__.py similarity index 100% rename from virtual_ship/instruments/__init__.py rename to virtualship/instruments/__init__.py diff --git a/virtual_ship/instruments/adcp.py b/virtualship/instruments/adcp.py similarity index 100% rename from virtual_ship/instruments/adcp.py rename to virtualship/instruments/adcp.py diff --git a/virtual_ship/instruments/argo_float.py b/virtualship/instruments/argo_float.py similarity index 100% rename from virtual_ship/instruments/argo_float.py rename to virtualship/instruments/argo_float.py diff --git a/virtual_ship/instruments/ctd.py b/virtualship/instruments/ctd.py similarity index 100% rename from virtual_ship/instruments/ctd.py rename to virtualship/instruments/ctd.py diff --git a/virtual_ship/instruments/drifter.py b/virtualship/instruments/drifter.py similarity index 100% rename from virtual_ship/instruments/drifter.py rename to virtualship/instruments/drifter.py diff --git a/virtual_ship/instruments/ship_underwater_st.py b/virtualship/instruments/ship_underwater_st.py similarity index 100% rename from virtual_ship/instruments/ship_underwater_st.py rename to virtualship/instruments/ship_underwater_st.py diff --git a/virtual_ship/location.py b/virtualship/location.py similarity index 100% rename from virtual_ship/location.py rename to virtualship/location.py diff --git a/virtual_ship/make_realistic/__init__.py b/virtualship/make_realistic/__init__.py similarity index 100% rename from virtual_ship/make_realistic/__init__.py rename to virtualship/make_realistic/__init__.py diff --git a/virtual_ship/make_realistic/adcp_make_realistic.py b/virtualship/make_realistic/adcp_make_realistic.py similarity index 100% rename from virtual_ship/make_realistic/adcp_make_realistic.py rename to virtualship/make_realistic/adcp_make_realistic.py diff --git a/virtual_ship/make_realistic/ctd_make_realistic.py b/virtualship/make_realistic/ctd_make_realistic.py similarity index 100% rename from virtual_ship/make_realistic/ctd_make_realistic.py rename to virtualship/make_realistic/ctd_make_realistic.py diff --git a/virtual_ship/spacetime.py b/virtualship/spacetime.py similarity index 100% rename from virtual_ship/spacetime.py rename to virtualship/spacetime.py From 6b03f9df3ce1fcf95b057dc203b25ec0af9a7adc Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:16:44 +0200 Subject: [PATCH 16/35] Update CLI to use click --- .github/CONTRIBUTING.md | 6 ++-- README.md | 50 ++++++++++++++++++++++++++++++-- pyproject.toml | 3 +- virtualship/cli/commands.py | 40 +++++++++++++++++++++++++ virtualship/cli/compat.py | 24 +++++++++++++++ virtualship/cli/do_expedition.py | 30 ------------------- virtualship/cli/main.py | 16 ++++++++++ 7 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 virtualship/cli/commands.py create mode 100644 virtualship/cli/compat.py delete mode 100644 virtualship/cli/do_expedition.py create mode 100644 virtualship/cli/main.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 1544b0cf..091bab10 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -33,14 +33,14 @@ source ./.venv/bin/activate or ```bash -conda create -n virtualship python=3.10 -conda activate virtualship +conda create -n ship python=3.10 +conda activate ship ``` Then install the dependencies: ```bash -pip install -v -e .[dev] +pip install -v -e ".[dev]" ``` Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtualship` diff --git a/README.md b/README.md index 9a591988..b63fbd5b 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,55 @@ For a development installation, please follow the instructions detailed in the [ ## Usage -Copy the expedition directory, and fill in the accompanying YAML file. Then run `virtualship run ` to start measuring. +```console +$ virtualship --help +Usage: virtualship [OPTIONS] COMMAND [ARGS]... - +Options: + --help Show this message and exit. + +Commands: + fetch Download the relevant data specified in an expedition directory... + init Initialize a directory for a new expedition, with an example... + run Do the expedition. +``` + +```console +$ virtualship init --help +Usage: virtualship init [OPTIONS] PATH + + Initialize a directory for a new expedition, with an example configuration. + +Options: + --help Show this message and exit. +``` + +```console + +$ virtualship fetch --help +Usage: virtualship fetch [OPTIONS] PATH + + Download the relevant data specified in an expedition directory (i.e., by + the expedition config). + +Options: + --help Show this message and exit. +``` + +```console +$ virtualship run --help +Usage: virtualship run [OPTIONS] PATH + + Do the expedition. + +Options: + --help Show this message and exit. + +``` + +For examples, see LINK_TO_TURORIALS. + + ## Input data diff --git a/pyproject.toml b/pyproject.toml index 98daa37f..b8426dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Intended Audience :: Education", ] dependencies = [ + "click", "parcels >= 3, < 4", "pyproj >= 3, < 4", "sortedcontainers == 2.4.0", @@ -67,7 +68,7 @@ docs = [ ] [project.scripts] -do_expedition = "virtualship.cli.do_expedition:main" +virtualshi = "virtualship.cli.main:cli" # We probably need to switch to a source layout since the folder name is colliding with this. [tool.pytest.ini_options] diff --git a/virtualship/cli/commands.py b/virtualship/cli/commands.py new file mode 100644 index 00000000..0764bb3a --- /dev/null +++ b/virtualship/cli/commands.py @@ -0,0 +1,40 @@ +from pathlib import Path + +import click + +from virtualship.expedition.do_expedition import do_expedition + + +@click.command( + help="Initialize a directory for a new expedition, with an example configuration." +) +@click.argument( + "path", + type=click.Path(exists=False, file_okay=False, dir_okay=True), + # help="Expedition directory", +) +def init(path): + """Entrypoint for the tool.""" + raise NotImplementedError("Not implemented yet.") + + +@click.command( + help="Download the relevant data specified in an expedition directory (i.e., by the expedition config)." +) +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True), +) +def fetch(path): + """Entrypoint for the tool.""" + raise NotImplementedError("Not implemented yet.") + + +@click.command(help="Do the expedition.") +@click.argument( + "path", + type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True), +) +def run(path): + """Entrypoint for the tool.""" + do_expedition(Path(path)) diff --git a/virtualship/cli/compat.py b/virtualship/cli/compat.py new file mode 100644 index 00000000..567516ed --- /dev/null +++ b/virtualship/cli/compat.py @@ -0,0 +1,24 @@ +"""Compatibility tooling.""" + +import os +from typing import Literal + + +def detect_shell() -> Literal["bash", "zsh", "cmd", "powershell", "unknown"]: + shell = os.environ.get("SHELL") + if shell: + if "bash" in shell: + return "bash" + elif "zsh" in shell: + return "zsh" + elif os.environ.get("COMSPEC"): + return "cmd" + elif os.environ.get("PSModulePath"): + return "powershell" + return "unknown" + + +if detect_shell() in ["bash", "zsh"]: + DELETE_COMMAND_FMT = "rm -r {path}" +else: + DELETE_COMMAND_FMT = None diff --git a/virtualship/cli/do_expedition.py b/virtualship/cli/do_expedition.py deleted file mode 100644 index d9c5b397..00000000 --- a/virtualship/cli/do_expedition.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Command line interface tool for virtualship.expedition.do_expedition:do_expedition function. - -See --help for usage. -""" - -import argparse -from pathlib import Path - -from virtualship.expedition.do_expedition import do_expedition - - -def main() -> None: - """Entrypoint for the tool.""" - parser = argparse.ArgumentParser( - prog="do_expedition", - description="Perform an expedition based on a provided schedule.", - ) - parser.add_argument( - "dir", - type=str, - help="Directory for the expedition. This should contain all required configuration files, and the result will be saved here as well.", - ) - args = parser.parse_args() - - do_expedition(Path(args.dir)) - - -if __name__ == "__main__": - main() diff --git a/virtualship/cli/main.py b/virtualship/cli/main.py new file mode 100644 index 00000000..da8e4a07 --- /dev/null +++ b/virtualship/cli/main.py @@ -0,0 +1,16 @@ +import click + +from . import commands + + +@click.group() +def cli(): + pass + + +cli.add_command(commands.init) +cli.add_command(commands.fetch) +cli.add_command(commands.run) + +if __name__ == "__main__": + cli() From c50fed220334372b8c987e0b3a27002e1227f0db Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:21:09 +0200 Subject: [PATCH 17/35] switch to src layout --- .gitignore | 2 +- docs/conf.py | 4 ++-- noxfile.py | 2 +- pyproject.toml | 8 ++++---- {virtualship => src/virtualship}/__init__.py | 0 {virtualship => src/virtualship}/cli/__init__.py | 0 {virtualship => src/virtualship}/cli/commands.py | 0 {virtualship => src/virtualship}/cli/compat.py | 0 {virtualship => src/virtualship}/cli/main.py | 0 {virtualship => src/virtualship}/expedition/__init__.py | 0 {virtualship => src/virtualship}/expedition/checkpoint.py | 0 .../virtualship}/expedition/do_expedition.py | 0 .../virtualship}/expedition/expedition_cost.py | 0 {virtualship => src/virtualship}/expedition/input_data.py | 0 .../virtualship}/expedition/instrument_type.py | 0 {virtualship => src/virtualship}/expedition/schedule.py | 0 .../virtualship}/expedition/ship_config.py | 0 .../virtualship}/expedition/simulate_measurements.py | 0 .../virtualship}/expedition/simulate_schedule.py | 0 .../virtualship}/expedition/verify_schedule.py | 0 {virtualship => src/virtualship}/expedition/waypoint.py | 0 {virtualship => src/virtualship}/instruments/__init__.py | 0 {virtualship => src/virtualship}/instruments/adcp.py | 0 .../virtualship}/instruments/argo_float.py | 0 {virtualship => src/virtualship}/instruments/ctd.py | 0 {virtualship => src/virtualship}/instruments/drifter.py | 0 .../virtualship}/instruments/ship_underwater_st.py | 0 {virtualship => src/virtualship}/location.py | 0 .../virtualship}/make_realistic/__init__.py | 0 .../virtualship}/make_realistic/adcp_make_realistic.py | 0 .../virtualship}/make_realistic/ctd_make_realistic.py | 0 {virtualship => src/virtualship}/spacetime.py | 0 32 files changed, 8 insertions(+), 8 deletions(-) rename {virtualship => src/virtualship}/__init__.py (100%) rename {virtualship => src/virtualship}/cli/__init__.py (100%) rename {virtualship => src/virtualship}/cli/commands.py (100%) rename {virtualship => src/virtualship}/cli/compat.py (100%) rename {virtualship => src/virtualship}/cli/main.py (100%) rename {virtualship => src/virtualship}/expedition/__init__.py (100%) rename {virtualship => src/virtualship}/expedition/checkpoint.py (100%) rename {virtualship => src/virtualship}/expedition/do_expedition.py (100%) rename {virtualship => src/virtualship}/expedition/expedition_cost.py (100%) rename {virtualship => src/virtualship}/expedition/input_data.py (100%) rename {virtualship => src/virtualship}/expedition/instrument_type.py (100%) rename {virtualship => src/virtualship}/expedition/schedule.py (100%) rename {virtualship => src/virtualship}/expedition/ship_config.py (100%) rename {virtualship => src/virtualship}/expedition/simulate_measurements.py (100%) rename {virtualship => src/virtualship}/expedition/simulate_schedule.py (100%) rename {virtualship => src/virtualship}/expedition/verify_schedule.py (100%) rename {virtualship => src/virtualship}/expedition/waypoint.py (100%) rename {virtualship => src/virtualship}/instruments/__init__.py (100%) rename {virtualship => src/virtualship}/instruments/adcp.py (100%) rename {virtualship => src/virtualship}/instruments/argo_float.py (100%) rename {virtualship => src/virtualship}/instruments/ctd.py (100%) rename {virtualship => src/virtualship}/instruments/drifter.py (100%) rename {virtualship => src/virtualship}/instruments/ship_underwater_st.py (100%) rename {virtualship => src/virtualship}/location.py (100%) rename {virtualship => src/virtualship}/make_realistic/__init__.py (100%) rename {virtualship => src/virtualship}/make_realistic/adcp_make_realistic.py (100%) rename {virtualship => src/virtualship}/make_realistic/ctd_make_realistic.py (100%) rename {virtualship => src/virtualship}/spacetime.py (100%) diff --git a/.gitignore b/.gitignore index 6c296b9c..ca9a1bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -172,6 +172,6 @@ cython_debug/ #.idea/ # Auto generated by setuptools scm -virtualship/_version_setup.py +src/virtualship/_version_setup.py .vscode/ diff --git a/docs/conf.py b/docs/conf.py index f0ae5981..93fd8e28 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,7 +34,7 @@ "footer_icons": [ { "name": "GitHub", - "url": "https://github.com/OceanParcels/Virtual_ship_classroom", + "url": "https://github.com/OceanParcels/virtualship", "html": """ @@ -43,7 +43,7 @@ "class": "", }, ], - "source_repository": "https://github.com/OceanParcels/Virtual_ship_classroom", + "source_repository": "https://github.com/OceanParcels/virtualship", "source_branch": "main", "source_directory": "docs/", } diff --git a/noxfile.py b/noxfile.py index 9c53d392..fcb9f970 100644 --- a/noxfile.py +++ b/noxfile.py @@ -77,7 +77,7 @@ def build_api_docs(session: nox.Session) -> None: "--module-first", "--no-toc", "--force", - "virtualship", + "src/virtualship", ) diff --git a/pyproject.toml b/pyproject.toml index b8426dda..1e4456b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,12 +41,12 @@ Repository = "https://github.com/OceanParcels/virtualship" "Bug Tracker" = "https://github.com/OceanParcels/virtualship/issues" Changelog = "https://github.com/OceanParcels/virtualship/releases" +[tool.setuptools.packages.find] +where = ["src"] -[tool.setuptools] -packages = ["virtualship"] [tool.setuptools_scm] -write_to = "virtualship/_version_setup.py" +version_file = "src/virtualship/_version_setup.py" local_scheme = "no-local-version" [project.optional-dependencies] @@ -68,7 +68,7 @@ docs = [ ] [project.scripts] -virtualshi = "virtualship.cli.main:cli" # We probably need to switch to a source layout since the folder name is colliding with this. +virtualship = "virtualship.cli.main:cli" [tool.pytest.ini_options] diff --git a/virtualship/__init__.py b/src/virtualship/__init__.py similarity index 100% rename from virtualship/__init__.py rename to src/virtualship/__init__.py diff --git a/virtualship/cli/__init__.py b/src/virtualship/cli/__init__.py similarity index 100% rename from virtualship/cli/__init__.py rename to src/virtualship/cli/__init__.py diff --git a/virtualship/cli/commands.py b/src/virtualship/cli/commands.py similarity index 100% rename from virtualship/cli/commands.py rename to src/virtualship/cli/commands.py diff --git a/virtualship/cli/compat.py b/src/virtualship/cli/compat.py similarity index 100% rename from virtualship/cli/compat.py rename to src/virtualship/cli/compat.py diff --git a/virtualship/cli/main.py b/src/virtualship/cli/main.py similarity index 100% rename from virtualship/cli/main.py rename to src/virtualship/cli/main.py diff --git a/virtualship/expedition/__init__.py b/src/virtualship/expedition/__init__.py similarity index 100% rename from virtualship/expedition/__init__.py rename to src/virtualship/expedition/__init__.py diff --git a/virtualship/expedition/checkpoint.py b/src/virtualship/expedition/checkpoint.py similarity index 100% rename from virtualship/expedition/checkpoint.py rename to src/virtualship/expedition/checkpoint.py diff --git a/virtualship/expedition/do_expedition.py b/src/virtualship/expedition/do_expedition.py similarity index 100% rename from virtualship/expedition/do_expedition.py rename to src/virtualship/expedition/do_expedition.py diff --git a/virtualship/expedition/expedition_cost.py b/src/virtualship/expedition/expedition_cost.py similarity index 100% rename from virtualship/expedition/expedition_cost.py rename to src/virtualship/expedition/expedition_cost.py diff --git a/virtualship/expedition/input_data.py b/src/virtualship/expedition/input_data.py similarity index 100% rename from virtualship/expedition/input_data.py rename to src/virtualship/expedition/input_data.py diff --git a/virtualship/expedition/instrument_type.py b/src/virtualship/expedition/instrument_type.py similarity index 100% rename from virtualship/expedition/instrument_type.py rename to src/virtualship/expedition/instrument_type.py diff --git a/virtualship/expedition/schedule.py b/src/virtualship/expedition/schedule.py similarity index 100% rename from virtualship/expedition/schedule.py rename to src/virtualship/expedition/schedule.py diff --git a/virtualship/expedition/ship_config.py b/src/virtualship/expedition/ship_config.py similarity index 100% rename from virtualship/expedition/ship_config.py rename to src/virtualship/expedition/ship_config.py diff --git a/virtualship/expedition/simulate_measurements.py b/src/virtualship/expedition/simulate_measurements.py similarity index 100% rename from virtualship/expedition/simulate_measurements.py rename to src/virtualship/expedition/simulate_measurements.py diff --git a/virtualship/expedition/simulate_schedule.py b/src/virtualship/expedition/simulate_schedule.py similarity index 100% rename from virtualship/expedition/simulate_schedule.py rename to src/virtualship/expedition/simulate_schedule.py diff --git a/virtualship/expedition/verify_schedule.py b/src/virtualship/expedition/verify_schedule.py similarity index 100% rename from virtualship/expedition/verify_schedule.py rename to src/virtualship/expedition/verify_schedule.py diff --git a/virtualship/expedition/waypoint.py b/src/virtualship/expedition/waypoint.py similarity index 100% rename from virtualship/expedition/waypoint.py rename to src/virtualship/expedition/waypoint.py diff --git a/virtualship/instruments/__init__.py b/src/virtualship/instruments/__init__.py similarity index 100% rename from virtualship/instruments/__init__.py rename to src/virtualship/instruments/__init__.py diff --git a/virtualship/instruments/adcp.py b/src/virtualship/instruments/adcp.py similarity index 100% rename from virtualship/instruments/adcp.py rename to src/virtualship/instruments/adcp.py diff --git a/virtualship/instruments/argo_float.py b/src/virtualship/instruments/argo_float.py similarity index 100% rename from virtualship/instruments/argo_float.py rename to src/virtualship/instruments/argo_float.py diff --git a/virtualship/instruments/ctd.py b/src/virtualship/instruments/ctd.py similarity index 100% rename from virtualship/instruments/ctd.py rename to src/virtualship/instruments/ctd.py diff --git a/virtualship/instruments/drifter.py b/src/virtualship/instruments/drifter.py similarity index 100% rename from virtualship/instruments/drifter.py rename to src/virtualship/instruments/drifter.py diff --git a/virtualship/instruments/ship_underwater_st.py b/src/virtualship/instruments/ship_underwater_st.py similarity index 100% rename from virtualship/instruments/ship_underwater_st.py rename to src/virtualship/instruments/ship_underwater_st.py diff --git a/virtualship/location.py b/src/virtualship/location.py similarity index 100% rename from virtualship/location.py rename to src/virtualship/location.py diff --git a/virtualship/make_realistic/__init__.py b/src/virtualship/make_realistic/__init__.py similarity index 100% rename from virtualship/make_realistic/__init__.py rename to src/virtualship/make_realistic/__init__.py diff --git a/virtualship/make_realistic/adcp_make_realistic.py b/src/virtualship/make_realistic/adcp_make_realistic.py similarity index 100% rename from virtualship/make_realistic/adcp_make_realistic.py rename to src/virtualship/make_realistic/adcp_make_realistic.py diff --git a/virtualship/make_realistic/ctd_make_realistic.py b/src/virtualship/make_realistic/ctd_make_realistic.py similarity index 100% rename from virtualship/make_realistic/ctd_make_realistic.py rename to src/virtualship/make_realistic/ctd_make_realistic.py diff --git a/virtualship/spacetime.py b/src/virtualship/spacetime.py similarity index 100% rename from virtualship/spacetime.py rename to src/virtualship/spacetime.py From 051471ee4ea95015724e75ecf28a19edb1860ce6 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:43:06 +0200 Subject: [PATCH 18/35] Remove compat.py not relevant to current development. --- src/virtualship/cli/compat.py | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 src/virtualship/cli/compat.py diff --git a/src/virtualship/cli/compat.py b/src/virtualship/cli/compat.py deleted file mode 100644 index 567516ed..00000000 --- a/src/virtualship/cli/compat.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Compatibility tooling.""" - -import os -from typing import Literal - - -def detect_shell() -> Literal["bash", "zsh", "cmd", "powershell", "unknown"]: - shell = os.environ.get("SHELL") - if shell: - if "bash" in shell: - return "bash" - elif "zsh" in shell: - return "zsh" - elif os.environ.get("COMSPEC"): - return "cmd" - elif os.environ.get("PSModulePath"): - return "powershell" - return "unknown" - - -if detect_shell() in ["bash", "zsh"]: - DELETE_COMMAND_FMT = "rm -r {path}" -else: - DELETE_COMMAND_FMT = None From fe09226d389e26e2d8fe776dc318df12c1d1409b Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:18:27 +0200 Subject: [PATCH 19/35] Update doc website with new pages Add nbsphinx support (not 100%, need to switch docs backend to conda so that we can support pandoc) --- .github/CONTRIBUTING.md | 8 +++++--- docs/conf.py | 1 + docs/contributing.md | 3 +++ docs/index.md | 6 +++++- docs/quickstart.md | 5 +++++ docs/tutorials/index.md | 5 +++++ docs/tutorials/my_tutorial.ipynb | 33 ++++++++++++++++++++++++++++++++ pyproject.toml | 3 +++ 8 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 docs/contributing.md create mode 100644 docs/quickstart.md create mode 100644 docs/tutorials/index.md create mode 100644 docs/tutorials/my_tutorial.ipynb diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 091bab10..351e69be 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,4 +1,6 @@ -# Quick development +# Contributing + +## Quick development The fastest way to start with development is to use nox. If you don't have nox, you can use `pipx run nox` to run it without installing, or `pipx install nox`. @@ -21,7 +23,7 @@ $ nox -s build # Make an SDist and wheel Nox handles everything for you, including setting up an temporary virtual environment for each run. Run `nox --list` to see all available jobs. -# Setting up a development environment manually +## Setting up a development environment manually You can set up a development environment by first setting up a virtual environment using: @@ -45,7 +47,7 @@ pip install -v -e ".[dev]" Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtualship` -# Pre-commit +## Pre-commit We use pre-commit to enforce code style and other checks. This can be run by the `nox -s lint` command above, or by installing pre-commit separately and running `pre-commit run --all-files`. If pre-commit is installed separately, you can also install the pre-commit hook into your git repository by running `pre-commit install` such that you don't need to manually run it (it will run when you make a commit). diff --git a/docs/conf.py b/docs/conf.py index 93fd8e28..d9cfa43f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,6 +10,7 @@ extensions = [ "myst_parser", + "nbsphinx", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..01fbe425 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,3 @@ +```{include} ../.github/CONTRIBUTING.md + +``` diff --git a/docs/index.md b/docs/index.md index f0f29250..63c04ec4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,9 +1,13 @@ # Virtual Ship Parcels ```{toctree} -:maxdepth: 2 +:maxdepth: 1 :hidden: +Home +quickstart +tutorials/index +contributing ``` ```{include} ../README.md diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..dfaa4a78 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,5 @@ +# Quickstart + +```{note} +We don't yet have a quickstart guide. Please refer to the tutorials. If you would like to work on a quickstart guide, please open an issue to discuss. +``` diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 00000000..2fb50d36 --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,5 @@ +# Tutorials + +```{nbgallery} +my_tutorial.ipynb +``` diff --git a/docs/tutorials/my_tutorial.ipynb b/docs/tutorials/my_tutorial.ipynb new file mode 100644 index 00000000..10167fd3 --- /dev/null +++ b/docs/tutorials/my_tutorial.ipynb @@ -0,0 +1,33 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Awesome Example Tutorial" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Still a work in progress\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ship", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pyproject.toml b/pyproject.toml index 1e4456b9..5547492f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,9 @@ dev = [ docs = [ "sphinx>=7.0", "myst_parser>=0.13", + "nbsphinx", + "ipykernel", + "pandoc", "sphinx_copybutton", "sphinx_autodoc_typehints", "furo>=2023.08.17", From 24bf89a02c0d460286a62020f25b2d7a2ad22787 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:58:07 +0200 Subject: [PATCH 20/35] Update docs environment to conda --- .github/CONTRIBUTING.md | 12 +++++++++++- docs/conda_requirements.txt | 12 ++++++++++++ noxfile.py | 7 +++++-- pyproject.toml | 11 ----------- 4 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 docs/conda_requirements.txt diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 351e69be..d10a797a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,7 +21,7 @@ $ nox -s build # Make an SDist and wheel ``` Nox handles everything for you, including setting up an temporary virtual -environment for each run. Run `nox --list` to see all available jobs. +environment for each run. Run `nox --list` to see all available jobs. The docs environment is re-used between runs, so if adding new dependencies, you may need to run `nox -s docs --reuse-venv=no` to ensure the environment is up to date. ## Setting up a development environment manually @@ -47,6 +47,16 @@ pip install -v -e ".[dev]" Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtualship` +--- + +For documentation, we use a conda environment. Due to limitations with `nox` being unable to read YAML files, we use a `conda_requirements.txt` file instead. + +```bash +conda create -n ship-docs python=3.10 +conda activate ship-docs +conda install --file docs/conda_requirements.txt +``` + ## Pre-commit We use pre-commit to enforce code style and other checks. This can be run by the `nox -s lint` command above, or by installing pre-commit separately and running `pre-commit run --all-files`. If pre-commit is installed separately, you can also install the pre-commit hook into your git repository by running `pre-commit install` such that you don't need to manually run it (it will run when you make a commit). diff --git a/docs/conda_requirements.txt b/docs/conda_requirements.txt new file mode 100644 index 00000000..17bbdfc0 --- /dev/null +++ b/docs/conda_requirements.txt @@ -0,0 +1,12 @@ +# Text file with conda dependencies +# to install manually you can do `conda install -f docs/conda_requirements.txt -c conda-forge` +# nox doesn't support environment.yml files :( +sphinx>=7.0 +myst-parser>=0.13 +nbsphinx +ipykernel +pandoc +sphinx-copybutton +sphinx-autodoc-typehints +furo +sphinx-autobuild diff --git a/noxfile.py b/noxfile.py index fcb9f970..72b6c544 100644 --- a/noxfile.py +++ b/noxfile.py @@ -38,7 +38,7 @@ def tests(session: nox.Session) -> None: session.run("pytest", *session.posargs) -@nox.session(reuse_venv=True) +@nox.session(venv_backend="conda", reuse_venv=True) def docs(session: nox.Session) -> None: """Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory.""" parser = argparse.ArgumentParser() @@ -49,7 +49,10 @@ def docs(session: nox.Session) -> None: args, posargs = parser.parse_known_args(session.posargs) serve = args.builder == "html" and session.interactive - session.install("-e.[docs]", "sphinx-autobuild") + session.conda_install( + "--file", "docs/conda_requirements.txt", channel="conda-forge" + ) + session.install("-e.") shared_args = ( "-n", # nitpicky mode diff --git a/pyproject.toml b/pyproject.toml index 5547492f..768e047b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,17 +59,6 @@ dev = [ "sortedcontainers-stubs == 2.4.2", ] -docs = [ - "sphinx>=7.0", - "myst_parser>=0.13", - "nbsphinx", - "ipykernel", - "pandoc", - "sphinx_copybutton", - "sphinx_autodoc_typehints", - "furo>=2023.08.17", -] - [project.scripts] virtualship = "virtualship.cli.main:cli" From c7cd60cbf674eb3c52509d48628759aa326eabf8 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:28:58 +0200 Subject: [PATCH 21/35] doc: maintainer release checklist --- .github/CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d10a797a..482255c1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -62,3 +62,16 @@ conda install --file docs/conda_requirements.txt We use pre-commit to enforce code style and other checks. This can be run by the `nox -s lint` command above, or by installing pre-commit separately and running `pre-commit run --all-files`. If pre-commit is installed separately, you can also install the pre-commit hook into your git repository by running `pre-commit install` such that you don't need to manually run it (it will run when you make a commit). Either way, the repository is set up to automatically run pre-commit checks and fix errors on every commit, so you should not need to worry about it. + +--- + +--- + +## For maintainers + +### Release checklist + +- Go to GitHub, draft new release. Enter name of version and "create new tag" if it doesn't already exist. Click "Generate Release Notes". Currate release notes as needed. Look at a previous version release to match the format (title, header, section organisation etc.) +- Go to [conda-forge/virtualship-feedstock](https://github.com/conda-forge/virtualship-feedstock), create a new issue (select the "Bot Commands" issue from the menu) with title `@conda-forge-admin, please update version`. This will prompt a build, otherwise there can be a delay in the build. + - Approve PR and merge on green +- Check "publish to PyPI" workflow succeeded From 059b5a95b76fa89f778065fa99daafec76bb4e47 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:09:46 +0200 Subject: [PATCH 22/35] migrate to conda for dev environment --- .github/CONTRIBUTING.md | 68 ++++++++------------------ .github/workflows/ci.yml | 10 ++-- .readthedocs.yaml | 17 ++++--- docs/conda_requirements.txt | 12 ----- environment.yml | 33 +++++++++++++ noxfile.py | 95 ------------------------------------- pyproject.toml | 10 ---- 7 files changed, 66 insertions(+), 179 deletions(-) delete mode 100644 docs/conda_requirements.txt create mode 100644 environment.yml delete mode 100644 noxfile.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 482255c1..432f9de9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,67 +1,41 @@ # Contributing -## Quick development - -The fastest way to start with development is to use nox. If you don't have nox, -you can use `pipx run nox` to run it without installing, or `pipx install nox`. -If you don't have pipx (pip for applications), then you can install with -`pip install pipx` (the only case were installing an application with regular -pip is reasonable). If you use macOS, then pipx and nox are both in brew, use -`brew install pipx nox`. - -To use, run `nox`. This will lint and test using every installed version of -Python on your system, skipping ones that are not installed. You can also run -specific jobs: - -```console -$ nox -s lint # Lint only -$ nox -s tests # Python tests -$ nox -s docs # Build and serve the documentation -$ nox -s build # Make an SDist and wheel -``` - -Nox handles everything for you, including setting up an temporary virtual -environment for each run. Run `nox --list` to see all available jobs. The docs environment is re-used between runs, so if adding new dependencies, you may need to run `nox -s docs --reuse-venv=no` to ensure the environment is up to date. - -## Setting up a development environment manually +All contributions are welcome no matter your background or experience! We collaborate on GitHub using issues to track bugs, features, and discuss future development. We use pull requests to collaborate on changes to the codebase (and modifications to the tutorials). -You can set up a development environment by first setting up a virtual environment using: +## For developers -```bash -python3 -m venv .venv -source ./.venv/bin/activate -``` +### Development installation -or +We use `conda` to manage our development installation. Make sure you have `conda` installed by following [the instructions here](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html) and then run the following commands: ```bash conda create -n ship python=3.10 conda activate ship +conda env update --file environment.yml +pip install -e . --no-deps --no-build-isolation ``` -Then install the dependencies: - -```bash -pip install -v -e ".[dev]" -``` - -Now you can start with development. Unit tests can be run by typing the command `pytest`, and coverage can be seen by running `pytest --cov=virtualship` +This creates an environment, and installs all the dependencies that you need for development, including: ---- +- core dependencies +- development dependencies (e.g., for testing) +- documentation dependencies -For documentation, we use a conda environment. Due to limitations with `nox` being unable to read YAML files, we use a `conda_requirements.txt` file instead. +then installs the package in editable mode. -```bash -conda create -n ship-docs python=3.10 -conda activate ship-docs -conda install --file docs/conda_requirements.txt -``` +### Useful commands -## Pre-commit +The following commands are useful for local development: -We use pre-commit to enforce code style and other checks. This can be run by the `nox -s lint` command above, or by installing pre-commit separately and running `pre-commit run --all-files`. If pre-commit is installed separately, you can also install the pre-commit hook into your git repository by running `pre-commit install` such that you don't need to manually run it (it will run when you make a commit). +- `pytest` to run tests +- `pre-commit run --all-files` to run pre-commit checks +- `pre-commit install` (optional) to install pre-commit hooks + - this means that every time you commit, pre-commit checks will run on the files you changed +- `sphinx-autobuild docs docs/_build` to build and serve the documentation +- `sphinx-apidoc -o docs/api/ --module-first --no-toc --force src/virtualship` (optional) to generate the API documentation +- `sphinx-build -b linkcheck docs/ _build/linkcheck` to check for broken links in the documentation -Either way, the repository is set up to automatically run pre-commit checks and fix errors on every commit, so you should not need to worry about it. +The running of these commands is useful for local development and quick iteration, but not _vital_ as they will be run automatically in the CI pipeline (`pre-commit` by pre-commit.ci, `pytest` by GitHub Actions, and `sphinx` by ReadTheDocs). --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3f5a5fd..7a4e96e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,13 +38,11 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: conda-incubator/setup-miniconda@v3 with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - - name: Install package - run: python -m pip install .[dev] + environment-file: environment.yml + python-version: ${{ matrix.python-version }}} + channels: conda-forge - name: Test package run: >- diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 67c194c8..8bbb0d95 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,12 +6,11 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.12" - commands: - - asdf plugin add uv - - asdf install uv latest - - asdf global uv latest - - uv venv - - uv pip install .[docs] - - .venv/bin/python -m sphinx -T -b html -d docs/_build/doctrees -D - language=en docs $READTHEDOCS_OUTPUT/html + python: mambaforge-22.9 + jobs: + pre_build: + - sphinx-build -b linkcheck docs/ _build/linkcheck + - sphinx-apidoc -o docs/api/ --module-first --no-toc --force src/virtualship + +conda: + environment: environment.yml diff --git a/docs/conda_requirements.txt b/docs/conda_requirements.txt deleted file mode 100644 index 17bbdfc0..00000000 --- a/docs/conda_requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Text file with conda dependencies -# to install manually you can do `conda install -f docs/conda_requirements.txt -c conda-forge` -# nox doesn't support environment.yml files :( -sphinx>=7.0 -myst-parser>=0.13 -nbsphinx -ipykernel -pandoc -sphinx-copybutton -sphinx-autodoc-typehints -furo -sphinx-autobuild diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..14389240 --- /dev/null +++ b/environment.yml @@ -0,0 +1,33 @@ +name: ship +channels: + - defaults + - conda-forge +dependencies: + - click + - parcels >= 3, < 4 + - pyproj >= 3, < 4 + - sortedcontainers == 2.4.0 + - opensimplex == 0.4.5 + - numpy >=1, < 2 + - pydantic >=2, <3 + + # linting + - pre-commit + + # Testing + - pytest + - pytest-cov + - codecov + - seabird + - setuptools + + # Docs + - sphinx>=7.0 + - myst-parser>=0.13 + - nbsphinx + - ipykernel + - pandoc + - sphinx-copybutton + - sphinx-autodoc-typehints + - furo + - sphinx-autobuild diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 72b6c544..00000000 --- a/noxfile.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import annotations - -import argparse -import shutil -from pathlib import Path - -import nox - -DIR = Path(__file__).parent.resolve() - -nox.needs_version = ">=2024.3.2" -nox.options.sessions = ["lint", "pylint", "tests"] -nox.options.default_venv_backend = "uv|virtualenv" - - -@nox.session -def lint(session: nox.Session) -> None: - """Run the linter.""" - session.install("pre-commit") - session.run( - "pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs - ) - - -@nox.session -def pylint(session: nox.Session) -> None: - """Run PyLint.""" - # This needs to be installed into the package environment, and is slower - # than a pre-commit check - session.install(".", "pylint>=3.2") - session.run("pylint", "virtualship", *session.posargs) - - -@nox.session -def tests(session: nox.Session) -> None: - """Run the unit and regular tests.""" - session.install(".[dev]") - session.run("pytest", *session.posargs) - - -@nox.session(venv_backend="conda", reuse_venv=True) -def docs(session: nox.Session) -> None: - """Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory.""" - parser = argparse.ArgumentParser() - parser.add_argument( - "-b", dest="builder", default="html", help="Build target (default: html)" - ) - parser.add_argument("output", nargs="?", help="Output directory") - args, posargs = parser.parse_known_args(session.posargs) - serve = args.builder == "html" and session.interactive - - session.conda_install( - "--file", "docs/conda_requirements.txt", channel="conda-forge" - ) - session.install("-e.") - - shared_args = ( - "-n", # nitpicky mode - "-T", # full tracebacks - f"-b={args.builder}", - "docs", - args.output or f"docs/_build/{args.builder}", - *posargs, - ) - - if serve: - session.run("sphinx-autobuild", "--open-browser", *shared_args) - else: - session.run("sphinx-build", "--keep-going", *shared_args) - - -@nox.session -def build_api_docs(session: nox.Session) -> None: - """Build (regenerate) API docs.""" - session.install("sphinx") - session.run( - "sphinx-apidoc", - "-o", - "docs/api/", - "--module-first", - "--no-toc", - "--force", - "src/virtualship", - ) - - -@nox.session -def build(session: nox.Session) -> None: - """Build an SDist and wheel.""" - build_path = DIR.joinpath("build") - if build_path.exists(): - shutil.rmtree(build_path) - - session.install("build") - session.run("python", "-m", "build") diff --git a/pyproject.toml b/pyproject.toml index 768e047b..59f91400 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,16 +49,6 @@ where = ["src"] version_file = "src/virtualship/_version_setup.py" local_scheme = "no-local-version" -[project.optional-dependencies] -dev = [ - "pytest == 8.2.0", - "pytest-cov == 5.0.0", - "codecov == 2.1.13", - "seabird == 0.12.0", - "setuptools == 70.0.0", - "sortedcontainers-stubs == 2.4.2", -] - [project.scripts] virtualship = "virtualship.cli.main:cli" From 6a3c4a1425f6451a0139b09a149c02980c72f84a Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:11:22 +0200 Subject: [PATCH 23/35] update contributing guidelines --- .github/CONTRIBUTING.md | 52 +-------------------------------------- docs/contributing.md | 54 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 432f9de9..b58b5b32 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,51 +1 @@ -# Contributing - -All contributions are welcome no matter your background or experience! We collaborate on GitHub using issues to track bugs, features, and discuss future development. We use pull requests to collaborate on changes to the codebase (and modifications to the tutorials). - -## For developers - -### Development installation - -We use `conda` to manage our development installation. Make sure you have `conda` installed by following [the instructions here](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html) and then run the following commands: - -```bash -conda create -n ship python=3.10 -conda activate ship -conda env update --file environment.yml -pip install -e . --no-deps --no-build-isolation -``` - -This creates an environment, and installs all the dependencies that you need for development, including: - -- core dependencies -- development dependencies (e.g., for testing) -- documentation dependencies - -then installs the package in editable mode. - -### Useful commands - -The following commands are useful for local development: - -- `pytest` to run tests -- `pre-commit run --all-files` to run pre-commit checks -- `pre-commit install` (optional) to install pre-commit hooks - - this means that every time you commit, pre-commit checks will run on the files you changed -- `sphinx-autobuild docs docs/_build` to build and serve the documentation -- `sphinx-apidoc -o docs/api/ --module-first --no-toc --force src/virtualship` (optional) to generate the API documentation -- `sphinx-build -b linkcheck docs/ _build/linkcheck` to check for broken links in the documentation - -The running of these commands is useful for local development and quick iteration, but not _vital_ as they will be run automatically in the CI pipeline (`pre-commit` by pre-commit.ci, `pytest` by GitHub Actions, and `sphinx` by ReadTheDocs). - ---- - ---- - -## For maintainers - -### Release checklist - -- Go to GitHub, draft new release. Enter name of version and "create new tag" if it doesn't already exist. Click "Generate Release Notes". Currate release notes as needed. Look at a previous version release to match the format (title, header, section organisation etc.) -- Go to [conda-forge/virtualship-feedstock](https://github.com/conda-forge/virtualship-feedstock), create a new issue (select the "Bot Commands" issue from the menu) with title `@conda-forge-admin, please update version`. This will prompt a build, otherwise there can be a delay in the build. - - Approve PR and merge on green -- Check "publish to PyPI" workflow succeeded +See the following [contributing guide](../docs/contributing.md). diff --git a/docs/contributing.md b/docs/contributing.md index 01fbe425..85aeabba 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,3 +1,55 @@ -```{include} ../.github/CONTRIBUTING.md +# Contributing +All contributions are welcome no matter your background or experience! We collaborate on GitHub using issues to track bugs, features, and discuss future development. We use pull requests to collaborate on changes to the codebase (and modifications to the tutorials). + +## For developers + +### Development installation + +We use `conda` to manage our development installation. Make sure you have `conda` installed by following [the instructions here](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html) and then run the following commands: + +```bash +conda create -n ship python=3.10 +conda activate ship +conda env update --file environment.yml +pip install -e . --no-deps --no-build-isolation ``` + +This creates an environment, and installs all the dependencies that you need for development, including: + +- core dependencies +- development dependencies (e.g., for testing) +- documentation dependencies + +then installs the package in editable mode. + +### Useful commands + +The following commands are useful for local development: + +- `pytest` to run tests +- `pre-commit run --all-files` to run pre-commit checks +- `pre-commit install` (optional) to install pre-commit hooks + - this means that every time you commit, pre-commit checks will run on the files you changed +- `sphinx-autobuild docs docs/_build` to build and serve the documentation +- `sphinx-apidoc -o docs/api/ --module-first --no-toc --force src/virtualship` (optional) to generate the API documentation +- `sphinx-build -b linkcheck docs/ _build/linkcheck` to check for broken links in the documentation + +The running of these commands is useful for local development and quick iteration, but not _vital_ as they will be run automatically in the CI pipeline (`pre-commit` by pre-commit.ci, `pytest` by GitHub Actions, and `sphinx` by ReadTheDocs). + +## For maintainers + +### Release checklist + +- Go to GitHub, draft new release. Enter name of version and "create new tag" if it doesn't already exist. Click "Generate Release Notes". Currate release notes as needed. Look at a previous version release to match the format (title, header, section organisation etc.) +- Go to [conda-forge/virtualship-feedstock](https://github.com/conda-forge/virtualship-feedstock), create a new issue (select the "Bot Commands" issue from the menu) with title `@conda-forge-admin, please update version`. This will prompt a build, otherwise there can be a delay in the build. + - Approve PR and merge on green +- Check "publish to PyPI" workflow succeeded + +### Adding dependencies + +When adding a dependency, make sure to modify the following files where relevant: + +- `environment.yml` for core and development dependencies (important for the development environment, and CI) +- `pyproject.toml` for core dependencies (important for the pypi package) +- `recipe/meta.yml` in the conda-forge feedstock for core dependencies From f1975a2a4ab7e1d2928601772e5f33547be44178 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:16:39 +0200 Subject: [PATCH 24/35] cli add version option --- README.md | 1 + src/virtualship/cli/main.py | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index b63fbd5b..8602e7f4 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ $ virtualship --help Usage: virtualship [OPTIONS] COMMAND [ARGS]... Options: + --version Show the version and exit. --help Show this message and exit. Commands: diff --git a/src/virtualship/cli/main.py b/src/virtualship/cli/main.py index da8e4a07..6d0aa258 100644 --- a/src/virtualship/cli/main.py +++ b/src/virtualship/cli/main.py @@ -4,6 +4,7 @@ @click.group() +@click.version_option() def cli(): pass From c4144248ef20ba4b93547019ffc6c5fe7dc892ea Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:29:30 +0200 Subject: [PATCH 25/35] update CD to publish to pypi --- .github/workflows/cd.yml | 7 ++----- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index efc7d061..79c2dbd1 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -53,8 +53,5 @@ jobs: with: subject-path: "dist/*" - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - # Remember to tell (test-)pypi about this repo before publishing - # Remove this line to publish to PyPI - repository-url: https://test.pypi.org/legacy/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a4e96e1..585d4bd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: environment-file: environment.yml - python-version: ${{ matrix.python-version }}} + python-version: ${{ matrix.python-version }} channels: conda-forge - name: Test package From 16fcc7935f2b547eced092ba0ca2dba15b546aa7 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:53:54 +0200 Subject: [PATCH 26/35] remove py3.13 support release got pushed back by a week :P --- .github/workflows/ci.yml | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 585d4bd8..c806c55f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.13"] + python-version: ["3.10", "3.12"] runs-on: [ubuntu-latest, windows-latest, macos-14] steps: diff --git a/pyproject.toml b/pyproject.toml index 59f91400..2269c573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Topic :: Scientific/Engineering", "Topic :: Education", From fd44ed3ab8d5c1b7560f7417d6933d9346db7214 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:14:46 +0200 Subject: [PATCH 27/35] update contributing --- docs/contributing.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/contributing.md b/docs/contributing.md index 85aeabba..fe57599d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -51,5 +51,4 @@ The running of these commands is useful for local development and quick iteratio When adding a dependency, make sure to modify the following files where relevant: - `environment.yml` for core and development dependencies (important for the development environment, and CI) -- `pyproject.toml` for core dependencies (important for the pypi package) -- `recipe/meta.yml` in the conda-forge feedstock for core dependencies +- `pyproject.toml` for core dependencies (important for the pypi package, this should propogate through automatically to `recipe/meta.yml` in the conda-forge feedstock) From d91d3e3fcccb8dd7c6942d885eeb1c78b7794e32 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:15:54 +0200 Subject: [PATCH 28/35] update ci and warnings ignore --- .github/workflows/ci.yml | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c806c55f..47ae5735 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash -el {0} + env: # Many color libraries just need this to be set to any value, but at least # one distinguishes color depth, where "3" -> "256-bit color". @@ -44,6 +48,8 @@ jobs: python-version: ${{ matrix.python-version }} channels: conda-forge + - run: pip install . + - name: Test package run: >- python -m pytest -ra --cov --cov-report=xml --cov-report=term diff --git a/pyproject.toml b/pyproject.toml index 2269c573..a6164e01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] xfail_strict = true filterwarnings = [ "error", - "ignore:pkg_resources is deprecated as an API.*:DeprecationWarning", # seabird (used in one test) uses pkg_resources + "ignore:.*pkg_resources.*:DeprecationWarning", # seabird (used in one test) uses pkg_resources "ignore:ParticleSet is empty.*:RuntimeWarning" # TODO: Probably should be ignored in the source code ] log_cli_level = "INFO" From c48777ea73e7844efae38a35bd04aa39b31b9570 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:44:28 +0200 Subject: [PATCH 29/35] Update deprecation warning handling --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6164e01..e99c5b22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,8 @@ addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] xfail_strict = true filterwarnings = [ "error", - "ignore:.*pkg_resources.*:DeprecationWarning", # seabird (used in one test) uses pkg_resources + "default::DeprecationWarning", + "error::DeprecationWarning:virtualship", "ignore:ParticleSet is empty.*:RuntimeWarning" # TODO: Probably should be ignored in the source code ] log_cli_level = "INFO" From afc6cf109b02f02f6d2fc095bf1a4708e823652e Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:00:40 +0200 Subject: [PATCH 30/35] patch pip install . --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47ae5735..01597ed2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: python-version: ${{ matrix.python-version }} channels: conda-forge - - run: pip install . + - run: pip install . --no-deps --no-build-isolation - name: Test package run: >- From 1b39b9197e13163825afc3b84caa83ce271856bc Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:24:57 +0200 Subject: [PATCH 31/35] switch to micromamba ci --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01597ed2..4cf39a99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,12 +41,12 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - - uses: conda-incubator/setup-miniconda@v3 + - uses: mamba-org/setup-micromamba@v1 with: + environment-name: ship environment-file: environment.yml - python-version: ${{ matrix.python-version }} - channels: conda-forge + create-args: >- + python=${{matrix.python-version}} - run: pip install . --no-deps --no-build-isolation From 46fd185e268f42bab98202236ea76a626b392d59 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:58:48 +0200 Subject: [PATCH 32/35] update code of conduct Project team contact details already in readme --- .github/CODE_OF_CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 9570e7d1..0e2522dd 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at E.vanSebille@uu.nl. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. From e66c2f5818ae97ef95ae97bd55aea359fe5f74f5 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:42:40 +0200 Subject: [PATCH 33/35] Update README.md Co-authored-by: Erik van Sebille --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8602e7f4..aa901dfe 100644 --- a/README.md +++ b/README.md @@ -101,4 +101,4 @@ For examples, see LINK_TO_TURORIALS. ## Input data -The scripts are written to work with A-grid ocean data from CMEMS. +The scripts are written to work with [A-grid ocean data from the Copernicus Marine Service](https://data.marine.copernicus.eu/product/GLOBAL_ANALYSISFORECAST_PHY_001_024/description). From 1d54956f861ddb4515a85d2942caa30a02d03ec0 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:43:58 +0200 Subject: [PATCH 34/35] Update docs/quickstart.md Co-authored-by: Erik van Sebille --- docs/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index dfaa4a78..cc947599 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,5 +1,5 @@ # Quickstart ```{note} -We don't yet have a quickstart guide. Please refer to the tutorials. If you would like to work on a quickstart guide, please open an issue to discuss. +We don't yet have a quickstart guide. Please refer to the tutorials. If you would like to work on a quickstart guide, please [open an Issue](https://github.com/OceanParcels/virtualship/issues/new) to discuss. ``` From 705973ee30f8b88e65a83ca6be6672e80a2cc269 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:46:07 +0200 Subject: [PATCH 35/35] review feedback --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aa901dfe..17846c1e 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,12 @@ VirtualShipParcels is a command line simulator allowing students to plan and con For a normal installation do: ```bash -conda create -n my_env python=3.12 -conda activate my_env -conda install -c conda-forge virtualship +conda create -n ship -c conda-forge virtualship +conda activate ship ``` +which creates an environment named `ship` with the latest version of `virtualship`. You can replace `ship` with any name you like. + For a development installation, please follow the instructions detailed in the [contributing page](.github/CONTRIBUTING.md). ## Usage