diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..945d1e9 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,18 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_commit: v0.14.4 +_src_path: gl:openscm/copier-core-python-repository +conda_release: true +email: florence.bockting@tu-dortmund.de +include_cli: false +name: Florence Bockting +notebook_based_docs: true +package_manager: uv +pandas_doctests: true +plot_dependencies: true +project_description_short: Learning prior distributions for model parameters in a + Bayesian model based on expert information. +project_name_human: PyMC elicito +project_name_pip: pymc-elicito +project_name_python: pymc_elicito +project_url: https://github.com/pymc-devs/pymc-elicito +track_lock_file: true diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..ebc3616 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Report a bug +title: '' +labels: bug +assignees: '' + +--- + +## Describe the bug + + +## Failing Test + + +## Expected behavior + + +## Screenshots + + +## System + + + - OS: [e.g. Windows, Linux, macOS] + - Python version [e.g. Python 3.11] + +## Additional context + diff --git a/.github/ISSUE_TEMPLATE/default.md b/.github/ISSUE_TEMPLATE/default.md new file mode 100644 index 0000000..8b7025e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/default.md @@ -0,0 +1,21 @@ +--- +name: Default +about: Report an issue or problem +title: '' +labels: triage +assignees: '' + +--- + +## The problem + + +## Definition of "done" + + +## Additional context + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f297ebb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: Feature Request +about: Request a feature or suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +## The motivation + + + +## The proposed solution + + + +## Alternatives + + + +## Additional context + + diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..4fd0ef1 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,29 @@ +name: "Setup Python and uv" +description: "setup Python and uv" + +inputs: + python-version: + description: "Python version to use" + required: true + uv-dependency-install-flags: + description: "Flags to pass to uv when running `uv install`" + required: true + run-uv-install: + description: "Should we run the uv install steps" + required: false + default: true + +runs: + using: "composite" + steps: + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.8.8" + python-version: ${{ inputs.python-version }} + - name: Install dependencies + shell: bash + if: ${{ (inputs.run-uv-install == 'true') }} + run: | + uv sync ${{ inputs.uv-dependency-install-flags }} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7ff7065 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +## Description + +## Checklist + +Please confirm that this pull request has done the following: + +- [ ] Tests added +- [ ] Documentation added (where applicable) +- [ ] Changelog item added to `changelog/` diff --git a/.github/release_template.md b/.github/release_template.md new file mode 100644 index 0000000..73c69de --- /dev/null +++ b/.github/release_template.md @@ -0,0 +1,3 @@ +## Announcements + +* Announcement 1 diff --git a/.github/workflows/bump.yaml b/.github/workflows/bump.yaml new file mode 100644 index 0000000..8db1e1a --- /dev/null +++ b/.github/workflows/bump.yaml @@ -0,0 +1,78 @@ +name: Bump version + +on: + workflow_dispatch: + inputs: + bump_rule: + type: choice + description: How to bump the project's version (see https://docs.astral.sh/uv/reference/cli/#uv-version) + options: + - patch + - minor + - major + - stable + - alpha + - beta + - rc + - post + - dev + required: true + +jobs: + bump_version: + name: "Bump version and create changelog" + if: "!startsWith(github.event.head_commit.message, 'bump:')" + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + env: + CI_COMMIT_EMAIL: "ci-runner@pymc-elicito.invalid" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" + + - uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--all-extras --group dev" + + - name: Create bump and changelog + run: | + git config --global user.name "$GITHUB_ACTOR" + git config --global user.email "$CI_COMMIT_EMAIL" + + BASE_VERSION=`sed -ne 's/^version = "\([0-9\.post]*\)"/\1/p' pyproject.toml` + echo "Bumping from version $BASE_VERSION" + + # Bump + uv version --bump ${{ github.event.inputs.bump_rule }} + + NEW_VERSION=`sed -ne 's/^version = "\([0-9\.]*\)"/\1/p' pyproject.toml` + echo "Bumping to version $NEW_VERSION" + + # Build CHANGELOG + uv run towncrier build --yes --version v$NEW_VERSION + + # Commit, tag and push + git commit -a -m "bump: version $BASE_VERSION -> $NEW_VERSION" + git tag v$NEW_VERSION + git push && git push --tags + + # Bump to alpha (so that future commits do not have the same + # version as the tagged commit) + BASE_VERSION=`sed -ne 's/^version = "\([0-9\.]*\)"/\1/p' pyproject.toml` + + # Bump to pre-release of next version + uv version --bump post + + NEW_VERSION=`sed -ne 's/^version = "\([0-9\.post]*\)"/\1/p' pyproject.toml` + echo "Bumping version $BASE_VERSION > $NEW_VERSION" + + # Commit and push + git commit -a -m "bump(pre-release): version $BASE_VERSION > $NEW_VERSION" + git push diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..0af8028 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,260 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + tags: ['v*'] + +jobs: + mypy: + if: ${{ !github.event.pull_request.draft }} + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.9" ] + runs-on: "${{ matrix.os }}" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--all-extras --group dev" + - name: mypy + run: | + MYPYPATH=stubs uv run mypy src + + docs: + if: ${{ !github.event.pull_request.draft }} + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--all-extras --group docs" + - name: docs + run: | + uv run mkdocs build --strict + - uses: ./.github/actions/setup + with: + python-version: "3.11" + uv-dependency-install-flags: "--all-extras --group docs --group dev" + - name: docs-with-changelog + run: | + # Check CHANGELOG will build too + uv run towncrier build --yes + uv run mkdocs build --strict + # Just in case, undo the staged changes + git restore --staged . && git restore . + + urls: + if: ${{ !github.event.pull_request.draft }} + runs-on: "ubuntu-latest" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: check-urls-are-valid + uses: lycheeverse/lychee-action@v2 + with: + # Exclude local links + # and the template link in pyproject.toml + args: "--exclude 'file://' --exclude '^https://github\\.com/pymc-devs/pymc-elicito/pull/\\{issue\\}$' ." + + tests: + strategy: + fail-fast: false + matrix: + os: [ "ubuntu-latest" ] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows + # and doesn't seem to affect unix-based systems so we include it. + # If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + # When running the tests, install with all optional dependencies + # to get maximum test coverage. + # If we find that we're getting failures + # when people try to run without installing optional dependencies, + # we should add a CI step that runs the tests without optional dependencies too. + # We don't have that right now, because we're not sure this pain point exists. + uv-dependency-install-flags: "--all-extras --group tests" + - name: Run tests + run: | + uv run pytest -r a -v src tests --doctest-modules --doctest-report ndiff --cov=src --cov-report=term-missing --cov-report=xml + uv run coverage report + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v4.2.0 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + tests-resolution-strategies: + strategy: + fail-fast: false + matrix: + # Only test on ubuntu here for now. + # We could consider doing this on different platforms too, + # although that probably belongs better with the PyPI tests. + os: [ "ubuntu-latest" ] + # Tests with lowest direct resolution. + # We don't do lowest because that is essentially testing + # whether downstream dependencies + # have set their minimum support dependencies correctly, + # which isn't our problem to solve. + resolution-strategy: [ "lowest-direct" ] + # Only test against the oldest supported python version + # because python is itself a direct dependency + # (so we're testing against the lowest direct python too). + python-version: [ "3.9" ] + runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows + # and doesn't seem to affect unix-based systems so we include it. + # If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.8.8" + python-version: ${{ matrix.python-version }} + - name: Create venv + run: | + uv venv --seed + - name: Install dependencies + run: | + uv pip install --requirements requirements-only-tests-locked.txt + uv pip compile --python ${{ matrix.python-version }} --resolution ${{ matrix.resolution-strategy }} --all-extras pyproject.toml -o requirements-tmp.txt + uv pip install --requirements requirements-tmp.txt . + - name: Run tests + run: | + uv run --no-sync pytest tests -r a -v + + tests-without-extras: + # Run the tests without installing extras. + # This is just a test to make sure to avoid + # breaking our test PyPI install workflow. + strategy: + fail-fast: false + matrix: + os: [ "ubuntu-latest" ] + # Just test against one Python version, this is just a helper. + # The real work happens in the test PyPI install + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows + # and doesn't seem to affect unix-based systems so we include it. + # If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Python "${{ matrix.python-version }}" + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: "${{ matrix.python-version }}" + - name: Install + run: | + pip install --upgrade pip wheel + pip install --no-deps . + pip install -r requirements-locked.txt + pip install -r requirements-only-tests-min-locked.txt + - name: Run tests + run: | + pytest tests -r a -vv tests + + imports-without-extras: + strategy: + fail-fast: false + matrix: + os: [ "ubuntu-latest" ] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + runs-on: "${{ matrix.os }}" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Python "${{ matrix.python-version }}" + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: "${{ matrix.python-version }}" + - name: Install + run: | + pip install --upgrade pip wheel + pip install . + - name: Check importable without extras + run: python scripts/test-install.py + + check-build: + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.8.8" + python-version: ${{ matrix.python-version }} + - name: Build package + run: | + uv run python scripts/add-locked-targets-to-pyproject-toml.py + cat pyproject.toml + uv build + # Just in case, undo the changes to `pyproject.toml` + git restore --staged . && git restore . + - name: Check build + run: | + tar -tvf dist/pymc_elicito-*.tar.gz --wildcards '*pymc_elicito/py.typed' + tar -tvf dist/pymc_elicito-*.tar.gz --wildcards 'pymc_elicito-*/LICENCE' + + check-dependency-licences: + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + steps: + - name: Check out repository + uses: actions/checkout@v4 + - uses: ./.github/actions/setup + with: + python-version: ${{ matrix.python-version }} + uv-dependency-install-flags: "--group dev" + - name: Check licences of dependencies + shell: bash + run: | + TEMP_FILE=$(mktemp) + uv export --no-dev > $TEMP_FILE + uv run liccheck -r $TEMP_FILE -R licence-check.txt + cat licence-check.txt diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..e31361c --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,43 @@ +name: Deploy + +on: + release: + types: [published] + +defaults: + run: + shell: bash + +jobs: + deploy-pypi: + name: Deploy to PyPI + # Having an environment for deployment is strongly recommend by PyPI + # https://docs.pypi.org/trusted-publishers/adding-a-publisher/#github-actions + # You can comment this line out if you don't want it. + environment: deploy + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + permissions: + # this permission is mandatory for trusted publishing with PyPI + id-token: write + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.8.8" + python-version: ${{ matrix.python-version }} + - name: Publish to PyPI + run: | + uv run python scripts/add-locked-targets-to-pyproject-toml.py + uv build + uv publish + # Just in case, undo the changes to `pyproject.toml` + git restore --staged . && git restore . diff --git a/.github/workflows/install-conda.yaml b/.github/workflows/install-conda.yaml new file mode 100644 index 0000000..3368f73 --- /dev/null +++ b/.github/workflows/install-conda.yaml @@ -0,0 +1,81 @@ +# Test installation of the latest version from conda/mamba works. +# We make sure that we run the tests that apply to the version we installed, +# rather than the latest tests in main. +# The reason we do this, is that we want this workflow to test +# that installing from conda/mamba leads to a correct installation. +# If we tested against main, the tests could fail +# because the tests from main require the new features in main to pass. +name: Test installation conda + +on: + workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + # This means At 03:00 on Wednesday. + # see https://crontab.guru/#0_0_*_*_3 + - cron: '0 3 * * 3' + +jobs: + test-micromamba-installation: + name: Test (micro)mamba install ${{ matrix.install-target }} (${{ matrix.python-version }}, ${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + # Check both 'library' install and the 'application' (i.e. locked) install + install-target: ["pymc-elicito", "pymc-elicito-locked"] + runs-on: "${{ matrix.os }}" + steps: + - name: Setup (micro)mamba and install package + uses: mamba-org/setup-micromamba@v1 + with: + environment-name: test-mamba-install + create-args: >- + python=${{ matrix.python-version }} + -c conda-forge ${{ matrix.install-target }} + init-shell: bash + - name: Get version + shell: bash -leo pipefail {0} + run: | + INSTALLED_VERSION=`python -c 'import pymc_elicito; print(f"v{pymc_elicito.__version__}")'` + echo $INSTALLED_VERSION + echo "INSTALLED_VERSION=$INSTALLED_VERSION" >> $GITHUB_ENV + - name: Check installed version environment variable + shell: bash -leo pipefail {0} + run: | + echo "${{ env.INSTALLED_VERSION }}" + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ env.INSTALLED_VERSION }} + - name: Test installation + shell: bash -leo pipefail {0} + run: | + which python + python scripts/test-install.py + - name: Install pytest and other test dependencies + shell: bash -leo pipefail {0} + run: | + pip install -r requirements-only-tests-min-locked.txt + # micromamba install pytest pytest-regressions + - name: Run tests + shell: bash -leo pipefail {0} + run: | + # Can't run doctests here because the paths are different. + # This only runs with minimum test dependencies installed. + # So this is really just a smoke test, + # rather than a super thorough integration test. + # You will have to make sure that your tests run + # without all the extras installed for this to pass. + pytest tests -r a -vv tests + - name: Install all test dependencies + shell: bash -leo pipefail {0} + run: | + pip install -r requirements-only-tests-locked.txt + - name: Run tests + shell: bash -leo pipefail {0} + run: | + # Can't run doctests here because the paths are different. + pytest tests -r a -vv tests diff --git a/.github/workflows/install-pypi.yaml b/.github/workflows/install-pypi.yaml new file mode 100644 index 0000000..3ed3376 --- /dev/null +++ b/.github/workflows/install-pypi.yaml @@ -0,0 +1,87 @@ +# Test installation of the latest version from PyPI works. +# We make sure that we run the tests that apply to the version we installed, +# rather than the latest tests in main. +# The reason we do this, is that we want this workflow to test +# that installing from PyPI leads to a correct installation. +# If we tested against main, the tests could fail +# because the tests from main require the new features in main to pass. +name: Test installation PyPI + +on: + workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + # This means At 03:00 on Wednesday. + # see https://crontab.guru/#0_0_*_*_3 + - cron: '0 3 * * 3' + +jobs: + test-pypi-install: + name: Test PyPI install ${{ matrix.install-target }} (${{ matrix.python-version }}, ${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest"] + # Test against all security and bugfix versions: https://devguide.python.org/versions/ + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + # Check both 'library' install and the 'application' (i.e. locked) install + install-target: ["pymc-elicito", "pymc-elicito[locked]"] + runs-on: "${{ matrix.os }}" + steps: + - name: Set up Python "${{ matrix.python-version }}" + id: setup-python + uses: actions/setup-python@v4 + with: + python-version: "${{ matrix.python-version }}" + - name: Install + run: | + pip install --upgrade pip wheel + pip install "${{ matrix.install-target }}" 2>stderr.txt + - name: Check no warnings + if: matrix.os != 'windows-latest' + run: | + if grep -q "WARN" stderr.txt; then echo "Warnings in pip install output" && cat stderr.txt && exit 1; else exit 0; fi + - name: Get version non-windows + if: matrix.os != 'windows-latest' + run: | + INSTALLED_VERSION=`python -c 'import pymc_elicito; print(f"v{pymc_elicito.__version__}")'` + echo $INSTALLED_VERSION + echo "INSTALLED_VERSION=$INSTALLED_VERSION" >> $GITHUB_ENV + - name: Get version windows + if: matrix.os == 'windows-latest' + run: | + chcp 65001 # use utf-8 + python -c 'import pymc_elicito; f = open("version.txt", "w"); f.write(f"INSTALLED_VERSION=v{pymc_elicito.__version__}"); f.close()' + echo "Showing version.txt" + type version.txt + type version.txt >> $env:GITHUB_ENV + - name: Check installed version environment variable + run: | + echo "${{ env.INSTALLED_VERSION }}" + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ env.INSTALLED_VERSION }} + - name: Test installation + run: | + which python + python scripts/test-install.py + - name: Install min test dependencies + run: | + pip install -r requirements-only-tests-min-locked.txt + - name: Run tests + run: | + # Can't run doctests here because the paths are different. + # This only runs with minimum test dependencies installed. + # So this is really just a smoke test, + # rather than a super thorough integration test. + # You will have to make sure that your tests run + # without all the extras installed for this to pass. + pytest tests -r a -vv tests + - name: Install all test dependencies + run: | + pip install -r requirements-only-tests-locked.txt + - name: Run tests with extra test dependencies installed + run: | + # Can't run doctests here because the paths are different. + pytest tests -r a -vv tests diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..e3d2d59 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,59 @@ +name: Release + +on: + push: + tags: ['v*'] + +defaults: + run: + shell: bash + +jobs: + draft-release: + name: Create draft release + strategy: + matrix: + os: [ "ubuntu-latest" ] + python-version: [ "3.11" ] + runs-on: "${{ matrix.os }}" + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.8.8" + python-version: ${{ matrix.python-version }} + - name: Add version to environment + run: | + PROJECT_VERSION=`sed -ne 's/^version = "\([0-9\.]*\)"/\1/p' pyproject.toml` + echo "PROJECT_VERSION=$PROJECT_VERSION" >> $GITHUB_ENV + - name: Build package for PyPI + run: | + uv run python scripts/add-locked-targets-to-pyproject-toml.py + uv build + # Just in case, undo the changes to `pyproject.toml` + git restore --staged . && git restore . + - name: Generate Release Notes + run: | + echo "" >> ".github/release_template.md" + echo "## Changelog" >> ".github/release_template.md" + echo "" >> ".github/release_template.md" + uv add typer + uv run python scripts/changelog-to-release-template.py >> ".github/release_template.md" + echo "" >> ".github/release_template.md" + echo "## Changes" >> ".github/release_template.md" + echo "" >> ".github/release_template.md" + git log $(git describe --tags --abbrev=0 HEAD^)..HEAD --pretty='format:* %h %s' --no-merges >> ".github/release_template.md" + - name: Create Release Draft + uses: softprops/action-gh-release@v2 + with: + body_path: ".github/release_template.md" + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" + draft: true + files: | + dist/pymc_elicito-${{ env.PROJECT_VERSION }}-py3-none-any.whl + dist/pymc_elicito-${{ env.PROJECT_VERSION }}.tar.gz diff --git a/.github/workflows/test-upstream-latest.yaml b/.github/workflows/test-upstream-latest.yaml new file mode 100644 index 0000000..0d51ee4 --- /dev/null +++ b/.github/workflows/test-upstream-latest.yaml @@ -0,0 +1,58 @@ +name: Test against upstream latest + +on: + workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + # This means At 03:00 on Wednesday. + # see https://crontab.guru/#0_0_*_*_3 + - cron: '0 3 * * 3' + +jobs: + tests-upstream-latest: + strategy: + fail-fast: false + matrix: + # Only test on ubuntu here for now. + # We could consider doing this on different platforms too, + # but this is mainly a warning for us of what is coming, + # rather than a super robust dive. + os: [ "ubuntu-latest" ] + # Test against all bugfix versions: https://devguide.python.org/versions/ + # as they are latest and ones most likely to support new features + python-version: [ "3.12", "3.13" ] + runs-on: "${{ matrix.os }}" + defaults: + run: + # This might be needed for Windows + # and doesn't seem to affect unix-based systems so we include it. + # If you have better proof of whether this is needed or not, + # feel free to update. + shell: bash + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Setup uv + id: setup-uv + uses: astral-sh/setup-uv@v4 + with: + version: "0.8.8" + python-version: ${{ matrix.python-version }} + # Often you need a step like this for e.g. numpy, scipy, pandas + - name: Setup compilation dependencies + run: | + echo "python${{ matrix.python-version }}-dev" + sudo add-apt-repository ppa:deadsnakes/ppa -y + sudo apt update + sudo apt install -y "python${{ matrix.python-version }}-dev" + - name: Create venv + run: | + uv venv --seed + - name: Install dependencies + run: | + uv pip install --requirements requirements-only-tests-locked.txt --requirements requirements-only-tests-min-locked.txt + uv pip install --requirements pyproject.toml --all-extras . + uv pip install --requirements requirements-upstream-dev.txt + - name: Run tests + run: | + uv run --no-sync pytest tests -r a -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4577fd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,157 @@ +# Notebooks +*.ipynb + +# Auto-generated docs and helper files +docs/api/* +!docs/api/.gitkeep + +# pdm stuff +.pdm-python + +# Databases +*.db + +# Jupyter cache +.jupyter_cache + +# IDE stuff +.idea/ + +# Ruff cache +.ruff_cache + +# Licence check +licence-check.txt + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Mac stuff +*.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3b00037 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,65 @@ +# See https://pre-commit.com for more information +ci: + autofix_prs: false + autoupdate_schedule: quarterly + autoupdate_branch: pre-commit-autoupdate + # Currently network access isn't supported in the pre-commit CI product. + skip: [uv-sync, uv-lock, uv-export] + +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v6.0.0" + hooks: + #- id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: trailing-whitespace + - repo: local + hooks: + # Prevent committing .rej files + - id: forbidden-files + name: forbidden files + entry: found Copier update rejection files; review them and remove them + language: fail + files: "\\.rej$" + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: "v0.8.4" + hooks: + - id: ruff + args: [ --fix, --exit-non-zero-on-fix ] + - id: ruff-format + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.5.21 + hooks: + - id: uv-sync + - id: uv-lock + name: uv-lock-check + args: ["--check"] + # Put requirements.txt files in the repo too + - id: uv-export + name: export-requirements + args: ["-o", "requirements-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project"] + - id: uv-export + name: export-requirements-optional + args: ["-o", "requirements-incl-optional-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--all-extras"] + - id: uv-export + name: export-requirements-docs + args: ["-o", "requirements-docs-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--all-extras", "--group", "docs"] + - id: uv-export + name: export-requirements-only-tests-min + args: ["-o", "requirements-only-tests-min-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--only-group", "tests-min"] + - id: uv-export + name: export-requirements-only-tests + args: ["-o", "requirements-only-tests-locked.txt", "--no-hashes", "--no-dev", "--no-emit-project", "--only-group", "tests"] + # # Not released yet + # - id: uv-sync diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..9cc8034 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,24 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + jobs: + post_install: + # RtD seems to be not happy with pdm installs, + # hence use pip directly instead. + - python -m pip install -r requirements-docs-locked.txt + - python -m pip list + pre_build: + - pip install --no-deps . + +mkdocs: + configuration: mkdocs.yml + fail_on_warning: true diff --git a/LICENSE b/LICENCE similarity index 100% rename from LICENSE rename to LICENCE diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5382d7b --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +# Makefile to help automate key steps + +.DEFAULT_GOAL := help +# Will likely fail on Windows, but Makefiles are in general not Windows +# compatible so we're not too worried +TEMP_FILE := $(shell mktemp) + +# A helper script to get short descriptions of each target in the Makefile +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([\$$\(\)a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-30s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + + +.PHONY: help +help: ## print short description of each target + @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +.PHONY: checks +checks: ## run all the linting checks of the codebase + @echo "=== pre-commit ==="; uv run pre-commit run --all-files || echo "--- pre-commit failed ---" >&2; \ + echo "=== mypy ==="; MYPYPATH=stubs uv run mypy src || echo "--- mypy failed ---" >&2; \ + echo "======" + +.PHONY: ruff-fixes +ruff-fixes: ## fix the code using ruff + # format before and after checking so that the formatted stuff is checked and + # the fixed stuff is formatted + uv run ruff format src tests scripts docs + uv run ruff check src tests scripts docs --fix + uv run ruff format src tests scripts docs + +.PHONY: test +test: ## run the tests + uv run pytest src tests -r a -v --doctest-modules --doctest-report ndiff --cov=src + +# Note on code coverage and testing: +# You must specify cov=src. +# Otherwise, funny things happen when doctests are involved. +# If you want to debug what is going on with coverage, +# we have found that adding COVERAGE_DEBUG=trace +# to the front of the below command +# can be very helpful as it shows you +# if coverage is tracking the coverage +# of all of the expected files or not. +# We are sure that the coverage maintainers would appreciate a PR +# that improves the coverage handling when there are doctests +# and a `src` layout like ours. + +.PHONY: docs +docs: ## build the docs + uv run mkdocs build + +.PHONY: docs-strict +docs-strict: ## build the docs strictly (e.g. raise an error on warnings, this most closely mirrors what we do in the CI) + uv run mkdocs build --strict + +.PHONY: docs-serve +docs-serve: ## serve the docs locally + uv run mkdocs serve + +.PHONY: changelog-draft +changelog-draft: ## compile a draft of the next changelog + uv run towncrier build --draft --version draft + +.PHONY: licence-check +licence-check: ## Check that licences of the dependencies are suitable + # Will likely fail on Windows, but Makefiles are in general not Windows + # compatible so we're not too worried + uv export --no-dev > $(TEMP_FILE) + uv run liccheck -r $(TEMP_FILE) -R licence-check.txt + rm -f $(TEMP_FILE) + +.PHONY: virtual-environment +virtual-environment: ## update virtual environment, create a new one if it doesn't already exist + uv sync --all-extras --group all-dev + uv run pre-commit install diff --git a/README.md b/README.md index 559619a..ad352f5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,174 @@ -# pymc-elicito -Expert prior elicitation with PyMC + +# PyMC elicito + +Learning prior distributions for parameters in a Bayesian model based on expert information. + +**Key info :** +[![Docs](https://readthedocs.org/projects/pymc-elicito/badge/?version=latest)](https://pymc-elicito.readthedocs.io) +[![Main branch: supported Python versions](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fpymc-devs%2Fpymc-elicito%2Fmain%2Fpyproject.toml)](https://github.com/pymc-devs/pymc-elicito/blob/main/pyproject.toml) +[![Licence](https://img.shields.io/badge/license-Apache%20License%202.0-blue)](https://github.com/pymc-devs/pymc-elicito/blob/main/LICENCE) + +**PyPI :** +in progress + + +**Conda :** +in progress + + +**Tests :** +[![CI](https://github.com/pymc-devs/pymc-elicito/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/pymc-devs/pymc-elicito/actions/workflows/ci.yaml) +[![Coverage](https://codecov.io/gh/pymc-devs/pymc-elicito/branch/main/graph/badge.svg)](https://codecov.io/gh/pymc-devs/pymc-elicito) + +**Other info :** +[![Last Commit](https://img.shields.io/github/last-commit/pymc-devs/pymc-elicito.svg)](https://github.com/pymc-devs/pymc-elicito/commits/main) +[![Contributors](https://img.shields.io/github/contributors/pymc-devs/pymc-elicito.svg)](https://github.com/pymc-devs/pymc-elicito/graphs/contributors) + +## Status + + + +**Prototype**: +This project is just starting up and the code is all prototype. +Pymc-elicito re-implements the Python package [`elicito`](https://github.com/florence-bockting/elicito) using pymc instead of tensorflow(-probability) as dependency. + + + +Full documentation can be found at: +[pymc-elicito.readthedocs.io](https://pymc-elicito.readthedocs.io/en/latest/). +We recommend reading the docs there because the internal documentation links +don't render correctly on GitHub's viewer. + +## Installation + + +### As an application + +If you want to use PyMC elicito as an application, +then we recommend using the 'locked' version of the package. +This version pins the version of all dependencies too, +which reduces the chance of installation issues +because of breaking updates to dependencies. + +The locked version of PyMC elicito can be installed with + +=== "mamba" + ```sh + mamba install -c conda-forge pymc-elicito-locked + ``` + +=== "conda" + ```sh + conda install -c conda-forge pymc-elicito-locked + ``` + +=== "pip" + ```sh + pip install 'pymc-elicito[locked]' + ``` + +### As a library + +If you want to use PyMC elicito as a library, +for example you want to use it +as a dependency in another package/application that you're building, +then we recommend installing the package with the commands below. +This method provides the loosest pins possible of all dependencies. +This gives you, the package/application developer, +as much freedom as possible to set the versions of different packages. +However, the tradeoff with this freedom is that you may install +incompatible versions of PyMC elicito's dependencies +(we cannot test all combinations of dependencies, +particularly ones which haven't been released yet!). +Hence, you may run into installation issues. +If you believe these are because of a problem in PyMC elicito, +please [raise an issue](https://github.com/pymc-devs/pymc-elicito/issues). + +The (non-locked) version of PyMC elicito can be installed with + +=== "mamba" + ```sh + mamba install -c conda-forge pymc-elicito + ``` + +=== "conda" + ```sh + conda install -c conda-forge pymc-elicito + ``` + +=== "pip" + ```sh + pip install pymc-elicito + ``` + +Additional dependencies can be installed using + +=== "mamba" + If you are installing with mamba, we recommend + installing the extras by hand because there is no stable + solution yet (see [conda issue #7502](https://github.com/conda/conda/issues/7502)) + +=== "conda" + If you are installing with conda, we recommend + installing the extras by hand because there is no stable + solution yet (see [conda issue #7502](https://github.com/conda/conda/issues/7502)) + +=== "pip" + ```sh + # To add plotting dependencies + pip install 'pymc-elicito[plots]' + + # To add all optional dependencies + pip install 'pymc-elicito[full]' + ``` + +### For developers + +For development, we rely on [uv](https://docs.astral.sh/uv/) +for all our dependency management. +To get started, you will need to make sure that uv is installed +([instructions here](https://docs.astral.sh/uv/getting-started/installation/) +(we found that the self-managed install was best, +particularly for upgrading uv later). + +For all of our work, we use our `Makefile`. +You can read the instructions out and run the commands by hand if you wish, +but we generally discourage this because it can be error prone. +In order to create your environment, run `make virtual-environment`. + +If there are any issues, the messages from the `Makefile` should guide you through. +If not, please raise an issue in the +[issue tracker](https://github.com/pymc-devs/pymc-elicito/issues). + +For the rest of our developer docs, please see [development][development]. + + + +## Original template + +This project was generated from this template: +[copier core python repository](https://gitlab.com/openscm/copier-core-python-repository). +[copier](https://copier.readthedocs.io/en/stable/) is used to manage and +distribute this template. diff --git a/changelog/1.improvement.md b/changelog/1.improvement.md new file mode 100644 index 0000000..31baa60 --- /dev/null +++ b/changelog/1.improvement.md @@ -0,0 +1,4 @@ ++ setting up the overall structure with working CI for the new repository ++ update licence to Apache 2.0 ++ add test function to ensure CI passes ++ add requirement files (created by running uv run pre-commit install) diff --git a/changelog/README.md b/changelog/README.md new file mode 100644 index 0000000..d59222a --- /dev/null +++ b/changelog/README.md @@ -0,0 +1,47 @@ +# CHANGELOG + +This directory contains "news fragments", +i.e. short files that contain a small markdown-formatted bit of text +that will be added to the CHANGELOG when it is next compiled. + +The CHANGELOG will be read by users, +so this description should be aimed to PyMC elicito users +instead of describing internal changes which are only relevant to developers. +Merge requests in combination with our git history +provide additional developer-centric information. + +Make sure to use phrases in the past tense and use punctuation, examples: + +``` +Improved verbose diff output with sequences. + +Terminal summary statistics now use multiple colors. +``` + +Each file should have a name of the form `..md`, where `` is the merge request number, and `` is one of: + +* `feature`: new user facing features, like new command-line options and new behaviour. +* `improvement`: improvement of existing functionality, usually without requiring user intervention +* `fix`: fixes a bug. +* `docs`: documentation improvement, like rewording an entire section or adding missing docs. +* `deprecation`: feature deprecation. +* `breaking`: a change which may break existing uses, such as feature removal or behaviour change. +* `trivial`: fixing a small typo or internal change that might be noteworthy. + +So for example: `123.feature.md`, `456.fix.md`. + +Since you need the merge request number for the filename, you must submit a MR first. +From this MR, you can get the MR number and then create the news file. +A single MR can also have multiple news items, +for example a given MR may add a feature as well as deprecate some existing functionality. + +If you are not sure what issue type to use, don't hesitate to ask in your MR. + +`towncrier` preserves multiple paragraphs and formatting +(code blocks, lists, and so on), +but for entries other than features it is usually better to stick to a single paragraph to keep it concise. +You may also use [mkdocs-autorefs cross-referencing](https://mkdocstrings.github.io/autorefs/) +within your news items to link to other documentation. + +You can also run `towncrier build --draft --version draft` +to see the draft changelog that will be appended to the CHANGELOG on the next release. diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md new file mode 100644 index 0000000..f4a7a47 --- /dev/null +++ b/docs/NAVIGATION.md @@ -0,0 +1,16 @@ + +- [Home](index.md) +- [Installation](installation.md) +- [How-to guides](how-to-guides/index.md) + - [Do a basic calculation](how-to-guides/basic-calculation.md) + - [Run code in a notebook](how-to-guides/run-code-in-a-notebook.py) +- [Tutorials](tutorials/index.md) +- [Further background](further-background/index.md) + - [Dependency pinning and testing](further-background/dependency-pinning-and-testing.md) +- [Development](development.md) +- [API reference](api/pymc_elicito/) +- [Changelog](changelog.md) diff --git a/docs/api/.gitkeep b/docs/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..b4db368 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,22 @@ +# Changelog + +Versions follow [Semantic Versioning](https://semver.org/) (`..`). + +Backward incompatible (breaking) changes will only be introduced in major versions +with advance notice in the **Deprecations** section of releases. + + + + diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..8b3735a --- /dev/null +++ b/docs/development.md @@ -0,0 +1,103 @@ +# Development + +Notes for developers. +If you want to get involved, please do! +We welcome all kinds of contributions, for example: + +- docs fixes/clarifications +- bug reports +- bug fixes +- feature requests +- pull requests +- tutorials + +## Workflows + + + +We don't mind whether you use a branching or forking workflow. +However, please only push to your own branches. +Pushing to other people's branches is best avoided because +it is a recipe for disaster +and is never required in our experience. + +Try and keep your merge requests as small as possible +(focus on one thing if you can). +This makes life much easier for reviewers +which allows contributions to be accepted at a faster rate. + +## Language + +We use British English for our development. +We do this for consistency with the broader work context of our lead developers. + +## Versioning + +This package follows the version format described in [PEP440](https://peps.python.org/pep-0440/) +and [Semantic Versioning](https://semver.org/) to describe how the version +should change depending on the updates to the code base. + +[](){releasing-reference} +## Releasing + +Releasing is semi-automated via a CI job. +The CI job requires the type of version bump +that will be performed to be manually specified. +See the `uv version` docs (specifically the `--bump` flag) for the +[list of available bump rules](https://docs.astral.sh/uv/reference/cli/#uv-version). + +### Standard process + +The steps required are the following: + +1. Bump the version: manually trigger the "bump" workflow from the main branch + (see here: [bump workflow](https://github.com/pymc-devs/pymc-elicito/actions/workflows/bump.yaml)). + A valid "bump_rule" (see [uv's docs](https://docs.astral.sh/uv/reference/cli/#uv-version)) + will need to be specified. + This will then trigger a draft release. + +1. Edit the draft release which has been created + (see here: + [project releases](https://github.com/pymc-devs/pymc-elicito/releases)). + Once you are happy with the release + (removed placeholders, added key announcements etc.) + then hit 'Publish release'. + This triggers a release to PyPI + (which you can then add to the release if you want). + +1. Go to your conda feedstock repository + (likely something like https://github.com/conda-forge/pymc-elicito-feedstock) + and make a new merge request that updates your `recipe/meta.yaml` file + to point to the newly released version on PyPI. + + - If you have updated any dependencies, copy these across to your `recipe/meta.yaml` file. + - If you are releasing a locked version on conda too, + you can generate the pins for your lock file with `scripts/print-conda-recipe-pins.py`. + +1. That's it, release done, make noise on social media of choice, do whatever + else + +1. Enjoy the newly available version +[TODO: add in conda release instructions] + +#### Further details + +We use [uv's build backend](https://docs.astral.sh/uv/concepts/build-backend) for building our project +and `scripts/add-locked-targets-to-pyproject-toml.py` +to provide locked extra groups for our package. +Including locked extra groups is why we run `scripts/add-locked-targets-to-pyproject-toml.py` +before any step related to building the package in the CI. + +## Read the Docs + +Our documentation is hosted by [Read the Docs (RtD)](https://www.readthedocs.org/), +a service for which we are very grateful. +The RtD configuration can be found in the `.readthedocs.yaml` file +in the root of this repository. +The docs are automatically deployed at +[pymc-elicito.readthedocs.io](https://pymc-elicito.readthedocs.io/en/latest/). diff --git a/docs/further-background/dependency-pinning-and-testing.md b/docs/further-background/dependency-pinning-and-testing.md new file mode 100644 index 0000000..5d85fb2 --- /dev/null +++ b/docs/further-background/dependency-pinning-and-testing.md @@ -0,0 +1,138 @@ +# Dependency pinning and associated testing strategy + + +Here we explain our dependency pinning and associated testing strategy. +This will help you, as a user, to know what to expect +and what your options are. +As a developer, these docs can also be helpful to understand +the overall philosophy and thinking. + +## Dependency pinning + +We use lower-bound pinning. +In other words, we pin the lowest supported version of the packages on which we depend. +As a user, this helps you get a working install +while giving you freedom to use newer versions, should you wish. + +We don't use upper-bound pins. +The reason is that we have had bad experiences with upper-bound pinning. +In the majority of cases, new releases do not cause issues +so pinning simply forces users to workaround overly strict pins[^1] +(which can be done, see +[working around incorrectly set pins][working-around-incorrectly-set-pins]). +The tradeoff with this approach is that you run the risk that, +if a dependency releases a breaking change, +the function provided by our package may break too. + +[^1]: + Yes, if the entire world followed semantic versioning perfectly, + we could use upper-bound pins for the next major version with more confidence + but that isn't the current state of the ecosystem. + Even if it were, we still think this would result in unnecessary pins + in many cases because many major releases are still compatible + because most packages don't use the entire API of their dependencies. + +### Working around incorrectly set pins + +Despite our best efforts, it is possible that we will set our pins incorrectly. +Part of this is because we simply cannot test all possible combinations of package installs +(see [testing strategy][testing-strategy]), +so we might miss valid/invalid combinations. + +If we set our pins incorrectly and you need to effectively overwrite them, +unfortunately there is currently no universal solution. +There has been quite some discussion, +see e.g. [this issue](https://github.com/pypa/pip/issues/8076), +but no universal resolution. + +However, for some environment managers, there is a solution. +This comes in the form of dependency overrides, +which allow you to override a package's stated dependencies +(essentially fixing them on the fly, +rather than having to fix them upstream). +Here are the docs for the package managers that we know support this: + +- [uv dependency overrides](https://docs.astral.sh/uv/concepts/resolution/#dependency-overrides). +- [pdm dependency overrides](https://pdm-project.org/latest/usage/dependency/#dependency-overrides). + +We do not know if this strategy can be used for packaging. +For example, you are building package A. +This depends on version 2 of package B and version 1 of package C. +However, version 1 of package C (incorrectly) says +that it is only compatible with version 1 of package B. +We are not sure if the dependency overrides +can be used to release a version of package A +that can be relased to and installed from PyPI. +If this is the situation you are in and you would like a resolution, +please comment on [this issue](https://gitlab.com/openscm/copier-core-python-repository/-/issues/4). + +## Testing strategy + +We test against multiple python versions in our CI. +These tests run with the latest compatible versions of our dependencies +and a 'full' installation, i.e. with all optional dependencies too. +This gives us the best possible coverage of our code base +against the latest compatible version of all our possible dependencies. + +In an attempt to anticipate changes to the API's of our key dependencies, +we also test against the latest unreleased version of our key dependencies once a week. +As a user, this probably won't matter too much, +except that it should reduce the chance +that a new release of one of our dependencies breaks our package +without us knowing in advance and being able to set a pin in anticipation. +As a developer, this is important to be aware of, +so we can anticipate changes as early as possible. + +We additionally test with the lowest/oldest compatible versions of our direct dependencies. +This includes Python, i.e. these tests are only run +with the lowest/oldest version of Python compatible with our project. +This is because Python is itself a dependency of our project +and newer versions of Python tend to not work +with the lowest/oldest versions of our direct dependencies. +These tests ensure that our minimum supported versions are actually supported +(if they are all installed simultaneously, +see the next paragraph for why this caveat matters). +As a note for developers, +the key trick to making this work is to use `uv pip compile` +rather than `uv run` (or similar) in the CI. +The reason is that `uv pip compile` +allows you to install dependencies for a very specific combination of things, +which is different to `uv`'s normal 'all-at-once' environment handling +(for more details, see [here](https://github.com/astral-sh/uv/issues/10774#issuecomment-2601925564)). + +We do not test the combinations in between lowest-supported and latest, +e.g. the oldest compatible version of package A +with the newest compatiable version of package B. +The reason for this is simply combinatorics, +it is generally not feasible +for us to test all possible combinations of our dependencies' versions. + +We also don't test with the oldest versions of our dependencies' dependencies. +We don't do this because, in practice, +all that such tests actually test is +whether our dependencies have set their minimum support dependencies correctly, +which isn't our problem to solve. + +Once a week, we also test what happens when a user installs from PyPI on the 'happy path'. +In other words, they do `pip install pymc-elicito`. +We check that such an install passes all the tests that don't require extras +(for developers, this is why we have `tests-min` and `tests-full` dev dependency groups, +they allow us to test a truly minimal testing environment, +separate from any extras we install to get full coverage). +Finally, we also check the installation of the locked versions of the package, +i.e. installation with `pip install 'pymc-elicito[locked]'`. +These tests give us the greatest coverage of Python versions and operating systems +and help alert us to places where users may face issues. +Having said that, these tests do require 30 separate jobs, +which is why we don't run them in CI. + +Through this combination of CI testing and installation testing, +we get a pretty good coverage of the different ways in which our package can be used. +It is not perfect, largely because the combinatorics don't allow for testing everything. +If we find a particular, key, use case failing often, +then we would happily discuss whether this should be included in the CI too, +to catch issues in advance of use. diff --git a/docs/further-background/index.md b/docs/further-background/index.md new file mode 100644 index 0000000..3a766ee --- /dev/null +++ b/docs/further-background/index.md @@ -0,0 +1,15 @@ +# Further background + +This part of the project documentation +will focus on an **understanding-oriented** approach. +Here, we will describe the background of the project, +as well as reasoning about how it was implemented. + +Points we will aim to cover: + +- Context and background on the library +- Why it was created +- Help the reader make connections + +We will aim to avoid writing instructions or technical descriptions here, +they belong elsewhere. diff --git a/docs/gen_doc_stubs.py b/docs/gen_doc_stubs.py new file mode 100644 index 0000000..04facac --- /dev/null +++ b/docs/gen_doc_stubs.py @@ -0,0 +1,103 @@ +""" +Generate virtual doc files for the mkdocs site. + +This script can also be run directly to actually write out those files, +as a preview. + +Credit to the creators of: +https://oprypin.github.io/mkdocs-gen-files/ +and the docs at: +https://mkdocstrings.github.io/crystal/quickstart/migrate.html +and the mainters of: +https://github.com/mkdocstrings/python +""" + +from __future__ import annotations + +import importlib +import pkgutil +from pathlib import Path + +import mkdocs_gen_files +from attrs import define + +ROOT_DIR = Path("api") +PACKAGE_NAME_ROOT = "pymc_elicito" +nav = mkdocs_gen_files.Nav() + + +@define +class PackageInfo: + """ + Package information used to help us auto-generate the docs + + Not stricly needed anymore now that mkdocstrings-python has a summary option, + but being kept in case we need something like this pattern again. + """ + + full_name: str + stem: str + summary: str + + +def write_subpackage_pages(subpackage: object) -> tuple[PackageInfo, ...]: + """ + Write pages for the sub-packages of a module + """ + sub_sub_packages = [] + for _, name, is_pkg in pkgutil.walk_packages(subpackage.__path__): + subpackage_full_name = subpackage.__name__ + "." + name + sub_package_info = write_package_page(subpackage_full_name) + sub_sub_packages.append(sub_package_info) + + return tuple(sub_sub_packages) + + +def get_write_file(package_full_name: str) -> Path: + """Get directory in which to write the doc file""" + write_dir = ROOT_DIR + for sub_dir in package_full_name.split(".")[:-1]: + write_dir = write_dir / sub_dir + + write_file = write_dir / package_full_name.split(".")[-1] / "index.md" + + return write_file + + +def write_package_page( + package_full_name: str, +) -> PackageInfo: + """ + Write the docs pages for a package (or sub-package) + """ + package = importlib.import_module(package_full_name) + + if hasattr(package, "__path__"): + write_subpackage_pages(package) + + package_name = package_full_name.split(".")[-1] + + write_file = get_write_file(package_full_name) + + nav[package_full_name.split(".")] = write_file.relative_to( + ROOT_DIR / PACKAGE_NAME_ROOT + ).as_posix() + + with mkdocs_gen_files.open(write_file, "w") as fh: + fh.write(f"# {package_full_name}\n") + + fh.write("\n") + fh.write(f"::: {package_full_name}") + + package_doc_split = package.__doc__.splitlines() + if not package_doc_split[0]: + summary = package_doc_split[1] + else: + summary = package_doc_split[0] + + return PackageInfo(package_full_name, package_name, summary) + + +write_package_page(PACKAGE_NAME_ROOT) +with mkdocs_gen_files.open(ROOT_DIR / PACKAGE_NAME_ROOT / "NAVIGATION.md", "w") as fh: + fh.writelines(nav.build_literate_nav()) diff --git a/docs/how-to-guides/index.md b/docs/how-to-guides/index.md new file mode 100644 index 0000000..d8aa6fa --- /dev/null +++ b/docs/how-to-guides/index.md @@ -0,0 +1,12 @@ +# How-to guides + +This part of the project documentation +focuses on a **problem-oriented** approach. +We'll go over how to solve common tasks. + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1fe13ca --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +---8<--- "README.md:description" diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..cc59271 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,17 @@ +# Installation + +There are a few different ways to install PyMC elicito. +We provide these by use case below. +As a short summary, if you: + +- just want to use PyMC elicito for validation, + and nothing else, go to [installation as an application][as-an-application] +- want to use PyMC elicito's functionality + as a library within another application, + go to [installation as a library][as-a-library] +- want to develop PyMC elicito, + go to [installation for developers][for-developers] + +## By use case + +---8<--- "README.md:installation" diff --git a/docs/javascripts/katex.js b/docs/javascripts/katex.js new file mode 100644 index 0000000..f7fd704 --- /dev/null +++ b/docs/javascripts/katex.js @@ -0,0 +1,10 @@ +document$.subscribe(({ body }) => { + renderMathInElement(body, { + delimiters: [ + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, + { left: "\\(", right: "\\)", display: false }, + { left: "\\[", right: "\\]", display: true } + ], + }) +}) diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 0000000..21c69bb --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,15 @@ +# Tutorials + +This part of the project documentation +will focus on a **learning-oriented** approach. +You'll learn how to get started with the code in this project. + +It is currently empty, but we will aim to: + +- Help newcomers with getting started +- Teach readers about the library by having them write code +- Inspire confidence through examples that work for everyone, repeatably +- Give readers an immediate sense of achievement +- Show concrete examples without abstractions +- Provide the minimum necessary explanation +- Avoid any distraction diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..106fd4c --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,169 @@ +site_name: PyMC elicito +site_description: Learning prior distributions for model parameters in a Bayesian model based on expert information. +site_url: https://pymc-elicito.readthedocs.io +edit_uri: blob/master/docs/ + +repo_name: pymc-devs/pymc-elicito +repo_url: https://github.com/pymc-devs/pymc-elicito + +theme: + name: "material" + features: + - content.code.copy + palette: + # Light mode (toggle to dark mode) + - scheme: default + # Other colour choices at https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#color-palette + primary: blue-grey + # More info on toggles at https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#color-palette-toggle + toggle: + icon: material/weather-night + name: Switch to dark mode + + # Dark mode (toggle back to light mode) + - scheme: slate + primary: pink + toggle: + icon: material/brightness-7 + name: Switch to light mode + +plugins: + # https://mkdocstrings.github.io/autorefs/ + - autorefs + # Required for auto-generating our documentation stubs + # https://oprypin.github.io/mkdocs-gen-files/ + - gen-files: + scripts: + - docs/gen_doc_stubs.py + # Make the navigation easier to handle/auto-generate if we wish + # https://oprypin.github.io/mkdocs-literate-nav/ + - literate-nav: + nav_file: NAVIGATION.md + # Notebook support. + # Working out how to make this play with nb-exec would be nice, + # then it wouldn't run every time. + # See also: https://github.com/danielfrg/mkdocs-jupyter/issues/161 + # We could also get the nb-exec-table mentioned here: + # https://myst-nb.readthedocs.io/en/v0.12.2/use/execute.html + # One for another day. + - mkdocs-jupyter: + # Use filenames for titles + ignore_h1_titles: True + include: ["*.py"] + execute: true + # Toggle off for faster builds + # execute: false + allow_errors: false + # theme: dark + include_source: True + ignore: ["*.ipynb", "*.md", "docs/gen_doc_stubs.py"] + remove_tag_config: + remove_input_tags: + - remove_input + # Docstring generation + - mkdocstrings: + # See https://analog-garage.github.io/mkdocstrings-python-xref/1.6.0/ + default_handler: python_xref + enable_inventory: true + handlers: + python_xref: + paths: [src] + inventories: + # Cross-ref helpers (lots included here, remove what you don't want) + - https://www.attrs.org/en/stable/objects.inv + - https://unidata.github.io/cftime/objects.inv + - https://ipython.readthedocs.io/en/stable/objects.inv + - https://loguru.readthedocs.io/en/latest/objects.inv + - https://matplotlib.org/stable/objects.inv + - https://ncdata.readthedocs.io/en/stable/objects.inv + - https://numpy.org/doc/stable/objects.inv + - https://openscm-units.readthedocs.io/en/stable/objects.inv + - https://pandas.pydata.org/docs/objects.inv + - https://pint.readthedocs.io/en/stable/objects.inv + - https://www.fatiando.org/pooch/latest/objects.inv + - https://docs.python.org/3/objects.inv + - https://docs.scipy.org/doc/scipy/objects.inv + - https://scitools-iris.readthedocs.io/en/stable/objects.inv + - https://scmdata.readthedocs.io/en/stable/objects.inv + # # Not available for tqdm + # # https://github.com/tqdm/tqdm/issues/705 + # - https://tqdm.github.io/objects.inv + - https://validators.readthedocs.io/en/stable/objects.inv + - http://xarray.pydata.org/en/stable/objects.inv + options: + # It turns out that xref does this check without considering config. + # As a result, every docstring is parsed as a google-style docstring. + # As docstrings are only parsed once, this results in rendering failures. + # Hence, turn off the checks here. + # mkdocs_autorefs catches anything which doesn't end up being a correct reference. + # The fix might be as simple as changing + # `self.collect(ref, PythonOptions())` + # to `self.collect(ref, self.get_options({}))` + # in the python-xref source, but it's hard to predict the side effects + # so we haven't bothered with that route. + check_crossrefs: false + docstring_style: numpy + relative_crossrefs: true + separate_signature: true + show_root_heading: false + show_signature_annotations: true + show_source: true + signature_crossrefs: true + summary: + attributes: true + classes: true + functions: true + modules: true + # https://squidfunk.github.io/mkdocs-material/plugins/search/ + - search + # Add clickable sections to the sidebar + # https://oprypin.github.io/mkdocs-section-index/ + - section-index + +markdown_extensions: + # https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/#attribute-lists + - attr_list + - footnotes + # https://squidfunk.github.io/mkdocs-material/reference/math/#katex-mkdocsyml + - pymdownx.arithmatex: + generic: true + # Allow admonitions, useful for deprecation warnings + # https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/admonition/ + - pymdownx.blocks.admonition + # Code highlighting handiness + # https://facelessuser.github.io/pymdown-extensions/extensions/highlight/ + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + # https://facelessuser.github.io/pymdown-extensions/extensions/inlinehilite/ + - pymdownx.inlinehilite + # Enable the use of snippets (e.g. taking snippets from README and putting them in docs) + # https://facelessuser.github.io/pymdown-extensions/extensions/snippets/ + - pymdownx.snippets + # Support more complicated indents etc. + # https://facelessuser.github.io/pymdown-extensions/extensions/superfences/ + - pymdownx.superfences + # Tabbed sections (e.g. for our installation options) + # https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ + - pymdownx.tabbed: + alternate_style: true + # Support tables (used in our API docs) + # https://squidfunk.github.io/mkdocs-material/reference/data-tables/ + - tables + # Ensure that there are links to table of contents items + - toc: + permalink: "#" + +extra_javascript: + - javascripts/katex.js + - https://unpkg.com/katex@0/dist/katex.min.js + - https://unpkg.com/katex@0/dist/contrib/auto-render.min.js + +extra_css: + - https://unpkg.com/katex@0/dist/katex.min.css + +watch: + - README.md + # Auto-generate if `src` changes (because this changes API docs) + - src diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..afb7437 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,429 @@ +[project] +name = "pymc-elicito" +version = "0.1.0a1" +description = "Learning prior distributions for model parameters in a Bayesian model based on expert information." +authors = [ + { name = "Florence Bockting", email = "florence.bockting@tu-dortmund.de" }, +] +license = { text = "placeholder" } +requires-python = ">=3.9" +dependencies = [ + "pandas>=2.3.3", +] +readme = "README.md" +classifiers = [ + # Full list: https://pypi.org/classifiers/ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + ## If you apply an OSI-approved licence, you should uncomment the below + ## Classifiers can be found here: https://pypi.org/classifiers/ + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "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", + "Typing :: Typed", +] + +[project.urls] +Homepage = "https://pymc-elicito.readthedocs.io" +Documentation = "https://pymc-elicito.readthedocs.io" +Changelog = "https://pymc-elicito.readthedocs.io/en/stable/changelog" +Repository = "https://github.com/pymc-devs/pymc-elicito" +Issues = "https://github.com/pymc-devs/pymc-elicito/issues" + + +[project.optional-dependencies] +plots = [ + "matplotlib>=3.7.1", +] +full = [ + "pymc-elicito[plots]", +] + +[dependency-groups] +# The development dependencies are pinned +# to give a consistent starting point when using this template. +# They should be removed/updated/changed/loosened as suits your project. +# (As soon as you have a lock file, you can remove all of the implied dependencies.) +# (This is a workaround for the fact +# that we can't easily include the lock file in the copier template +# because of how locking works and the order of operations). +dev = [ + # Key dependencies + # ---------------- + "liccheck==0.9.2", + "mypy==1.14.0", + # Required for liccheck, see https://github.com/dhatim/python-license-check/pull/113 + "pip==24.3.1", + "pre-commit==4.0.1", + # Required for liccheck, see https://github.com/dhatim/python-license-check/pull/113 + "setuptools==75.6.0", + "towncrier==24.8.0", + "tomli-w==1.2.0", + "tomli==2.2.1", + "typer==0.15.2", + # Implied by the key dependencies above + # ------------------------------------- + "cfgv==3.4.0", + "click==8.1.8", + "colorama==0.4.6 ; sys_platform == 'win32'", + "distlib==0.3.9", + "filelock==3.16.1", + "identify==2.6.5", + "jinja2==3.1.5", + "markupsafe==3.0.2", + "mypy-extensions==1.0.0", + "nodeenv==1.9.1", + "platformdirs==4.3.6", + "pyyaml==6.0.2", + "semantic-version==2.10.0", + "toml==0.10.2", + "typing-extensions==4.12.2", + "virtualenv==20.28.1", + "markdown-it-py==3.0.0", + "mdurl==0.1.2", + "pygments==2.19.1", + "rich==13.9.4", + "shellingham==1.5.4", +] +docs = [ + # Key dependencies + # ---------------- + "attrs==25.3.0", + "mkdocs-autorefs==1.4.2", + "mkdocs-gen-files==0.5.0", + "mkdocs-literate-nav==0.6.2", + "mkdocs-material==9.6.16", + "mkdocs-section-index==0.3.10", + "mkdocs==1.6.1", + "mkdocstrings-python-xref==1.16.3", + "mkdocstrings-python==1.16.12", + "pymdown-extensions==10.16.1", + "ruff==0.12.8", + # Implied by the key dependencies above + # ------------------------------------- + "babel==2.16.0", + "backrefs==5.9", + "certifi==2024.12.14", + "charset-normalizer==3.4.1", + "click==8.1.8", + "colorama==0.4.6", + "ghp-import==2.1.0", + "griffe==1.11.0", + "idna==3.10", + "jinja2==3.1.5", + "markdown==3.7", + "markupsafe==3.0.2", + "mergedeep==1.3.4", + "mkdocs-get-deps==0.2.0", + "mkdocs-material-extensions==1.3.1", + "mkdocstrings==0.30.0", + "packaging==24.2", + "paginate==0.5.7", + "pathspec==0.12.1", + "platformdirs==4.3.6", + "pygments==2.19.1", + "python-dateutil==2.9.0.post0", + "pyyaml-env-tag==0.1", + "pyyaml==6.0.2", + "requests==2.32.3", + "six==1.17.0", + "urllib3==2.3.0", + "watchdog==6.0.0", + # Key dependencies for notebook_based_docs + # ---------------------------------------- + "jupyterlab==4.4.5", + "jupytext==1.17.2", + "mkdocs-jupyter==0.25.1", + # Implied by the key dependencies above + # ------------------------------------- + "anyio==4.8.0", + "appnope==0.1.4 ; sys_platform == 'darwin'", + "argon2-cffi-bindings==21.2.0", + "argon2-cffi==23.1.0", + "arrow==1.3.0", + "asttokens==3.0.0", + "async-lru==2.0.4", + "beautifulsoup4==4.12.3", + "bleach==6.2.0", + "cffi==1.17.1", + "comm==0.2.2", + "debugpy==1.8.11", + "decorator==5.1.1", + "defusedxml==0.7.1", + "executing==2.1.0", + "fastjsonschema==2.21.1", + "fqdn==1.5.1", + "h11==0.14.0", + "httpcore==1.0.7", + "httpx==0.28.1", + "ipykernel==6.29.5", + "isoduration==20.11.0", + "jedi==0.19.2", + "json5==0.10.0", + "jsonpointer==3.0.0", + "jsonschema-specifications==2024.10.1", + "jsonschema==4.23.0", + "jupyter-client==8.6.3", + "jupyter-core==5.7.2", + "jupyter-events==0.11.0", + "jupyter-lsp==2.2.5", + "jupyter-server-terminals==0.5.3", + "jupyter-server==2.15.0", + "jupyterlab-pygments==0.3.0", + "jupyterlab-server==2.27.3", + "markdown-it-py==3.0.0", + "matplotlib-inline==0.1.7", + "mdit-py-plugins==0.4.2", + "mdurl==0.1.2", + "mistune==3.0.2", + "nbclient==0.10.2", + "nbconvert==7.16.4", + "nbformat==5.10.4", + "nest-asyncio==1.6.0", + "notebook-shim==0.2.4", + "overrides==7.7.0", + "pandocfilters==1.5.1", + "parso==0.8.4", + "prometheus-client==0.21.1", + "prompt-toolkit==3.0.48", + "psutil==6.1.1", + "pure-eval==0.2.3", + "pycparser==2.22", + "python-json-logger==3.2.1", + "pywin32==308 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32'", + "pywinpty==2.0.14 ; os_name == 'nt'", + "pyzmq==26.2.0", + "referencing==0.35.1", + "rfc3339-validator==0.1.4", + "rfc3986-validator==0.1.1", + "rpds-py==0.22.3", + "send2trash==1.8.3", + "setuptools==75.6.0", + "sniffio==1.3.1", + "soupsieve==2.6", + "stack-data==0.6.3", + "terminado==0.18.1", + "tinycss2==1.4.0", + "tornado==6.4.2", + "traitlets==5.14.3", + "types-python-dateutil==2.9.0.20241206", + "uri-template==1.3.0", + "wcwidth==0.2.13", + "webcolors==24.11.1", + "webencodings==0.5.1", + "websocket-client==1.8.0", +] +# For minimum test dependencies. +# These are used when running our minimum PyPI install tests. +tests-min = [ + # Key dependencies + # ---------------- + "pytest==8.3.4", + # Implied by the key dependencies above + # ------------------------------------- + "colorama==0.4.6 ; sys_platform == 'win32'", + "iniconfig==2.0.0", + "packaging==24.2", + "pluggy==1.5.0", +] +# Full test dependencies. +tests-full = [ + # Key dependencies + # ---------------- + "pytest-cov==6.0.0", + # Implied by the key dependencies above + # ------------------------------------- + "colorama==0.4.6 ; sys_platform == 'win32'", + "coverage==7.6.10", + "iniconfig==2.0.0", + "packaging==24.2", + "pluggy==1.5.0", + "pytest==8.3.4", +] +# Test dependencies +# (partly split because liccheck uses toml, +# which doesn't support inhomogeneous arrays). +tests = [ + {include-group = "tests-min"}, + {include-group = "tests-full"}, +] +all-dev = [ + {include-group = "dev"}, + {include-group = "docs"}, + {include-group = "tests"}, +] + +[build-system] +requires = ["uv_build>=0.8.7,<0.9.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +source-include = [ + "src/pymc_elicito", + "LICENCE", +] + +[tool.coverage.run] +source = [ + "src", +] +branch = true + +[tool.coverage.report] +fail_under = 0 # TODO; old value: 90 +skip_empty = true +show_missing = true +exclude_also = [ + "if TYPE_CHECKING", + # Type overloading lines + "@overload", + "\\.\\.\\.", + +] + +[tool.mypy] +strict = true +disallow_any_unimported = true +show_error_codes = true +show_error_context = true +warn_unreachable = true +follow_imports = "normal" + +[tool.jupytext] +formats = "ipynb,py:percent" + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] + +[tool.ruff] +src = [ + "src", +] +target-version = "py39" +line-length = 88 + +[tool.ruff.lint] +select = [ + "E", + "W", + "F", + "I", + "D", + "PL", + "TRY", + "NPY", + "RUF", + "UP", + "S", +] +unfixable = [ + "PD002", +] +ignore = [ + "D200", + "D400", + "UP007", +] + +[tool.ruff.lint.per-file-ignores] +"test*.py" = [ + "D", + "S101", + "PLR2004", +] +"docs/*" = [ + "D100", + "E402", + "S101", +] +"scripts/*" = [ + "S101", +] +"stubs/*" = [ + "PLR0913", +] + +[tool.ruff.lint.isort] +known-first-party = [ + "src", +] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.ruff.format] +docstring-code-format = true + +[tool.towncrier] +package = "pymc_elicito" +package_dir = "src" +filename = "docs/changelog.md" +directory = "changelog/" +title_format = "## PyMC elicito {version} ({project_date})" +underlines = [ + "", + "", + "", +] +issue_format = "[#{issue}](https://github.com/pymc-devs/pymc-elicito/pull/{issue})" +type = [ + { directory = "breaking", name = "⚠️ Breaking Changes", showcontent = true }, + { directory = "deprecation", name = "🗑️ Deprecations", showcontent = true }, + { directory = "feature", name = "🆕 Features", showcontent = true }, + { directory = "improvement", name = "🎉 Improvements", showcontent = true }, + { directory = "fix", name = "🐛 Bug Fixes", showcontent = true }, + { directory = "docs", name = "📚 Improved Documentation", showcontent = true }, + { directory = "trivial", name = "🔧 Trivial/Internal Changes", showcontent = false }, +] + +[tool.liccheck] +authorized_licenses = [ + "bsd", + "bsd license", + "BSD 3-Clause", + "BSD-3-Clause", + "CC0", + "apache", + "apache 2.0", + "apache software", + "apache software license", + "Apache License, Version 2.0", + "CMU License (MIT-CMU)", + "Historical Permission Notice and Disclaimer (HPND)", + "isc", + "isc license", + "isc license (iscl)", + "gnu lgpl", + "lgpl with exceptions or zpl", + "LGPLv2+", + "GNU Lesser General Public License v2 (LGPLv2)", + "GNU Lesser General Public License v2 or later (LGPLv2+)", + "mit", + "mit license", + "Mozilla Public License 2.0 (MPL 2.0)", + "psf-2.0", + "python software foundation", + "python software foundation license", + "The Unlicense (Unlicense)", + "zpl 2.1", +] +unauthorized_licenses = [ + "agpl", + "gnu agpl", + "gpl v3", + "gplv3", + "gpl v2", + "gplv2", + "gpl v1", + "gplv1", +] diff --git a/requirements-docs-locked.txt b/requirements-docs-locked.txt new file mode 100644 index 0000000..fc60762 --- /dev/null +++ b/requirements-docs-locked.txt @@ -0,0 +1,150 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-docs-locked.txt --no-hashes --no-dev --no-emit-project --all-extras --group docs +anyio==4.8.0 +appnope==0.1.4 ; sys_platform == 'darwin' +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +arrow==1.3.0 +asttokens==3.0.0 +async-lru==2.0.4 +attrs==25.3.0 +babel==2.16.0 +backrefs==5.9 +beautifulsoup4==4.12.3 +bleach==6.2.0 +certifi==2024.12.14 +cffi==1.17.1 +charset-normalizer==3.4.1 +click==8.1.8 +colorama==0.4.6 +comm==0.2.2 +contourpy==1.3.0 ; python_full_version < '3.10' +contourpy==1.3.2 ; python_full_version == '3.10.*' +contourpy==1.3.3 ; python_full_version >= '3.11' +cycler==0.12.1 +debugpy==1.8.11 +decorator==5.1.1 +defusedxml==0.7.1 +exceptiongroup==1.3.0 ; python_full_version < '3.11' +executing==2.1.0 +fastjsonschema==2.21.1 +fonttools==4.60.1 +fqdn==1.5.1 +ghp-import==2.1.0 +griffe==1.11.0 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.28.1 +idna==3.10 +importlib-metadata==8.7.0 ; python_full_version < '3.10' +importlib-resources==6.5.2 ; python_full_version < '3.10' +ipykernel==6.29.5 +ipython==8.18.1 ; python_full_version < '3.10' +ipython==8.37.0 ; python_full_version == '3.10.*' +ipython==9.7.0 ; python_full_version >= '3.11' +ipython-pygments-lexers==1.1.1 ; python_full_version >= '3.11' +isoduration==20.11.0 +jedi==0.19.2 +jinja2==3.1.5 +json5==0.10.0 +jsonpointer==3.0.0 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +jupyter-client==8.6.3 +jupyter-core==5.7.2 +jupyter-events==0.11.0 +jupyter-lsp==2.2.5 +jupyter-server==2.15.0 +jupyter-server-terminals==0.5.3 +jupyterlab==4.4.5 +jupyterlab-pygments==0.3.0 +jupyterlab-server==2.27.3 +jupytext==1.17.2 +kiwisolver==1.4.7 ; python_full_version < '3.10' +kiwisolver==1.4.9 ; python_full_version >= '3.10' +markdown==3.7 +markdown-it-py==3.0.0 +markupsafe==3.0.2 +matplotlib==3.9.4 ; python_full_version < '3.10' +matplotlib==3.10.7 ; python_full_version >= '3.10' +matplotlib-inline==0.1.7 +mdit-py-plugins==0.4.2 +mdurl==0.1.2 +mergedeep==1.3.4 +mistune==3.0.2 +mkdocs==1.6.1 +mkdocs-autorefs==1.4.2 +mkdocs-gen-files==0.5.0 +mkdocs-get-deps==0.2.0 +mkdocs-jupyter==0.25.1 +mkdocs-literate-nav==0.6.2 +mkdocs-material==9.6.16 +mkdocs-material-extensions==1.3.1 +mkdocs-section-index==0.3.10 +mkdocstrings==0.30.0 +mkdocstrings-python==1.16.12 +mkdocstrings-python-xref==1.16.3 +nbclient==0.10.2 +nbconvert==7.16.4 +nbformat==5.10.4 +nest-asyncio==1.6.0 +notebook-shim==0.2.4 +numpy==2.0.2 ; python_full_version < '3.10' +numpy==2.2.6 ; python_full_version == '3.10.*' +numpy==2.3.5 ; python_full_version >= '3.11' +overrides==7.7.0 +packaging==24.2 +paginate==0.5.7 +pandas==2.3.3 +pandocfilters==1.5.1 +parso==0.8.4 +pathspec==0.12.1 +pexpect==4.9.0 ; (python_full_version < '3.10' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32') +pillow==11.3.0 ; python_full_version < '3.10' +pillow==12.0.0 ; python_full_version >= '3.10' +platformdirs==4.3.6 +prometheus-client==0.21.1 +prompt-toolkit==3.0.48 +psutil==6.1.1 +ptyprocess==0.7.0 ; (python_full_version < '3.10' and sys_platform == 'emscripten') or os_name != 'nt' or (sys_platform != 'emscripten' and sys_platform != 'win32') +pure-eval==0.2.3 +pycparser==2.22 +pygments==2.19.1 +pymdown-extensions==10.16.1 +pyparsing==3.2.5 +python-dateutil==2.9.0.post0 +python-json-logger==3.2.1 +pytz==2025.2 +pywin32==308 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32' +pywinpty==2.0.14 ; os_name == 'nt' +pyyaml==6.0.2 +pyyaml-env-tag==0.1 +pyzmq==26.2.0 +referencing==0.35.1 +requests==2.32.3 +rfc3339-validator==0.1.4 +rfc3986-validator==0.1.1 +rpds-py==0.22.3 +ruff==0.12.8 +send2trash==1.8.3 +setuptools==75.6.0 +six==1.17.0 +sniffio==1.3.1 +soupsieve==2.6 +stack-data==0.6.3 +terminado==0.18.1 +tinycss2==1.4.0 +tomli==2.2.1 ; python_full_version < '3.11' +tornado==6.4.2 +traitlets==5.14.3 +types-python-dateutil==2.9.0.20241206 +typing-extensions==4.12.2 ; python_full_version < '3.13' +tzdata==2025.2 +uri-template==1.3.0 +urllib3==2.3.0 +watchdog==6.0.0 +wcwidth==0.2.13 +webcolors==24.11.1 +webencodings==0.5.1 +websocket-client==1.8.0 +zipp==3.23.0 ; python_full_version < '3.10' diff --git a/requirements-incl-optional-locked.txt b/requirements-incl-optional-locked.txt new file mode 100644 index 0000000..1d6bb03 --- /dev/null +++ b/requirements-incl-optional-locked.txt @@ -0,0 +1,25 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-incl-optional-locked.txt --no-hashes --no-dev --no-emit-project --all-extras +contourpy==1.3.0 ; python_full_version < '3.10' +contourpy==1.3.2 ; python_full_version == '3.10.*' +contourpy==1.3.3 ; python_full_version >= '3.11' +cycler==0.12.1 +fonttools==4.60.1 +importlib-resources==6.5.2 ; python_full_version < '3.10' +kiwisolver==1.4.7 ; python_full_version < '3.10' +kiwisolver==1.4.9 ; python_full_version >= '3.10' +matplotlib==3.9.4 ; python_full_version < '3.10' +matplotlib==3.10.7 ; python_full_version >= '3.10' +numpy==2.0.2 ; python_full_version < '3.10' +numpy==2.2.6 ; python_full_version == '3.10.*' +numpy==2.3.5 ; python_full_version >= '3.11' +packaging==24.2 +pandas==2.3.3 +pillow==11.3.0 ; python_full_version < '3.10' +pillow==12.0.0 ; python_full_version >= '3.10' +pyparsing==3.2.5 +python-dateutil==2.9.0.post0 +pytz==2025.2 +six==1.17.0 +tzdata==2025.2 +zipp==3.23.0 ; python_full_version < '3.10' diff --git a/requirements-locked.txt b/requirements-locked.txt new file mode 100644 index 0000000..cde6122 --- /dev/null +++ b/requirements-locked.txt @@ -0,0 +1,10 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-locked.txt --no-hashes --no-dev --no-emit-project +numpy==2.0.2 ; python_full_version < '3.10' +numpy==2.2.6 ; python_full_version == '3.10.*' +numpy==2.3.5 ; python_full_version >= '3.11' +pandas==2.3.3 +python-dateutil==2.9.0.post0 +pytz==2025.2 +six==1.17.0 +tzdata==2025.2 diff --git a/requirements-only-tests-locked.txt b/requirements-only-tests-locked.txt new file mode 100644 index 0000000..58f28a8 --- /dev/null +++ b/requirements-only-tests-locked.txt @@ -0,0 +1,12 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-only-tests-locked.txt --no-hashes --no-dev --no-emit-project --only-group tests +colorama==0.4.6 ; sys_platform == 'win32' +coverage==7.6.10 +exceptiongroup==1.3.0 ; python_full_version < '3.11' +iniconfig==2.0.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +pytest-cov==6.0.0 +tomli==2.2.1 ; python_full_version <= '3.11' +typing-extensions==4.12.2 ; python_full_version < '3.11' diff --git a/requirements-only-tests-min-locked.txt b/requirements-only-tests-min-locked.txt new file mode 100644 index 0000000..f0aaf57 --- /dev/null +++ b/requirements-only-tests-min-locked.txt @@ -0,0 +1,10 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-only-tests-min-locked.txt --no-hashes --no-dev --no-emit-project --only-group tests-min +colorama==0.4.6 ; sys_platform == 'win32' +exceptiongroup==1.3.0 ; python_full_version < '3.11' +iniconfig==2.0.0 +packaging==24.2 +pluggy==1.5.0 +pytest==8.3.4 +tomli==2.2.1 ; python_full_version < '3.11' +typing-extensions==4.12.2 ; python_full_version < '3.11' diff --git a/requirements-upstream-dev.txt b/requirements-upstream-dev.txt new file mode 100644 index 0000000..ea76147 --- /dev/null +++ b/requirements-upstream-dev.txt @@ -0,0 +1,5 @@ +# Upstream packages can be listed in here. +# The basic pattern is +# @git+ +# e.g. +# pandas@git+https://github.com/pandas-dev/pandas diff --git a/scripts/add-locked-targets-to-pyproject-toml.py b/scripts/add-locked-targets-to-pyproject-toml.py new file mode 100644 index 0000000..96514ab --- /dev/null +++ b/scripts/add-locked-targets-to-pyproject-toml.py @@ -0,0 +1,85 @@ +""" +Add locked targets to `pyproject.toml` + +This adds a "locked" target +plus a "