From ece770ab4844f47a6b3650d89f5b6c3f55c5d6b8 Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Fri, 24 Jul 2026 21:03:04 +0200 Subject: [PATCH 01/12] Setting up a pipeline --- .gitea/workflows/ci.yml | 53 +++++++++++++++++++++++++++++++++++++++++ .gitignore | 10 ++++++++ pyproject.toml | 32 +++++++++++++++++++++++++ sage/toric_topdata.sage | 7 +++--- tests/test_smoke.py | 32 +++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 tests/test_smoke.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a65286f --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + lint: + runs-on: ubuntu-latest + container: + image: sagemath/sagemath:10.4 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install ruff + run: sage --pip install ruff + + - name: Preparse .sage files + # Turns each *.sage file into real Python (*.sage.py) so ruff can + # parse it, and prepends the `from sage.all import *` that `sage` + # normally injects at runtime, so Sage's globals (ZZ, var, matrix, ...) + # resolve instead of looking like undefined names. + run: | + set -e + for f in $(find . -name "*.sage" -not -path "./playground/*"); do + sage --preparse "$f" + printf 'from sage.all import * # noqa: F401,F403\n%s\n' "$(cat "${f}.py")" > "${f}.py" + done + + - name: ruff check + # --no-respect-gitignore: *.sage.py is gitignored (it's generated, see + # the previous step) but that's exactly what we need to lint here. + run: sage --python -m ruff check --no-respect-gitignore . + + test: + runs-on: ubuntu-latest + needs: lint + container: + image: sagemath/sagemath:10.4 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pytest + run: sage --pip install pytest + + - name: Run tests + run: sage --python -m pytest tests/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05fb66f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Sage preparser output (regenerated from .sage sources, not hand-maintained) +*.sage.py + +# Python / tooling caches +__pycache__/ +.mypy_cache/ +.ruff_cache/ +.pytest_cache/ + +.venv/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4659824 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] +ignore = [ + "E501", # symbolic-math expressions routinely exceed a "normal" line length +] + +[tool.ruff.lint.per-file-ignores] +# *.sage.py is generated by `sage --preparse` from *.sage sources. +# - F403/F405/F821/E741: Sage's runtime injects hundreds of globals (ZZ, var, +# matrix, LatticePolytope, ...) via `from sage.all import *` before +# executing these files, so plain static analysis can't see where names +# come from. +# - E402/E702/I001: Sage's preparser itself emits an import-then-semicolon- +# joined-constants preamble (e.g. `_sage_const_1 = Integer(1); ...`) ahead +# of the file's own imports; that's Sage's boilerplate, not this project's +# code style. +# - W291/W293: the preparser replaces every integer literal with +# `_sage_const_N ` (trailing space included) to preserve token boundaries, +# so trailing-whitespace warnings fire mechanically on almost every line +# with a number in it -- not something `ruff format` could fix anyway, +# since it wouldn't change the .sage source that generated it. +"*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293"] + +# tests/test_smoke.py does `from sage.all import *` and `load(...)` a .sage +# file to get at its classes -- the same dynamic-namespace situation as +# *.sage.py above, just in a hand-written file: ruff can't see that +# Polytope/ToricPolytope/etc. come from the loaded file. +"tests/test_smoke.py" = ["F403", "F405"] diff --git a/sage/toric_topdata.sage b/sage/toric_topdata.sage index c5fe0a3..c474a8e 100644 --- a/sage/toric_topdata.sage +++ b/sage/toric_topdata.sage @@ -2,7 +2,6 @@ import numpy as np import logging import os import json -from datetime import datetime, timezone import re # Logger @@ -233,7 +232,7 @@ class ToricPolytope(Polytope): def _setup_discriminant_symbols(self): mori_rays = self.Mori_cone.rays() - a_vars = [var("a_{}".format(u), latex_name="a_{{}}".format(u)) for u in (1..len(mori_rays))] + a_vars = [var("a_{}".format(u), latex_name="a_{{{}}}".format(u)) for u in (1..len(mori_rays))] z_vars = var('z', n=len(mori_rays)+1, latex_name='z') # z[0] is superfluous lambda_vars = var('l', n=len(mori_rays)+1, latex_name='l') # l[0] is superfluous a_row = matrix(a_vars) @@ -275,7 +274,7 @@ class ToricPolytope(Polytope): if polynomials[0] == 0: polynomials = maxima.eliminate(equation_system, lambda_symbols[:-1]).sage() reverse_solve = True - except: + except Exception: # In one-parameter cases there may be nothing to eliminate. polynomials = equation_system logger.debug("No elimination needed for the equation system: %s", equation_system) @@ -289,7 +288,7 @@ class ToricPolytope(Polytope): else: last_lambda = lambda_symbols[-1] polynomials = [poly / last_lambda ** (poly.degree(last_lambda)) for poly in polynomials] - except: + except Exception: pass return polynomials diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..72d7923 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,32 @@ +import os + +import pytest +from sage.all import * # noqa: F401,F403 + +load(os.path.join(os.path.dirname(__file__), "..", "sage", "toric_topdata.sage")) + + +def test_polytope_rejects_too_few_points(): + with pytest.raises(ValueError): + Polytope([[0, 0], [1, 0]]) # 2 points in dimension 2: not enough + + +def test_mirror_quintic_topdata(): + # Vertices e_1, ..., e_4, -e_1-...-e_4 -- the worked example from + # ToricPolytope's own docstring. + points = [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [-1, -1, -1, -1], + ] + + polytope = ToricPolytope(points, model_name="ci_smoke_test_quintic") + assert polytope.dimension == 4 + assert len(polytope.triangulations) >= 1 + + polytope.topdata() + assert polytope.cy_dimension == 3 + assert polytope.no_divs == 1 # P^4 has Picard rank 1 + assert polytope.intersection_numbers_CY == {(0, 0, 0): 5} # classical quintic self-intersection -- 2.54.0 From 1f930b01fc101105458310e665f5857296b2de3f Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Sun, 26 Jul 2026 12:54:40 +0200 Subject: [PATCH 02/12] Pipeline setup with Dockerfile --- .gitea/ci-image/Dockerfile | 21 +++++++++++++++++++++ .gitea/workflows/ci.yml | 13 +++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 .gitea/ci-image/Dockerfile diff --git a/.gitea/ci-image/Dockerfile b/.gitea/ci-image/Dockerfile new file mode 100644 index 0000000..22d9d6a --- /dev/null +++ b/.gitea/ci-image/Dockerfile @@ -0,0 +1,21 @@ +# Custom CI image for the arm64 (Raspberry Pi) Gitea Actions runner. +# +# The official sagemath/sagemath image is amd64-only; conda-forge's `sage` +# package is the one place SageMath is actually published for linux-aarch64, +# so this bakes it (plus the lint/test tools) into a single image, built and +# pushed once rather than reinstalled on every CI run. +# +# Build & push (run directly on the Pi, or any arm64 machine with Docker): +# docker build -t gitea.piribauer.ch/julian/sage-ci:latest -f .gitea/ci-image/Dockerfile . +# docker login gitea.piribauer.ch -u julian +# docker push gitea.piribauer.ch/julian/sage-ci:latest +# +# Rebuild and re-push whenever this Dockerfile changes (e.g. bumping the sage +# version) or the pinned tool versions need updating. + +FROM condaforge/miniforge3:latest + +RUN mamba install -y -c conda-forge sage ruff pytest \ + && mamba clean -afy + +WORKDIR /workspace diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a65286f..655193e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -9,16 +9,16 @@ on: jobs: lint: runs-on: ubuntu-latest + # Custom image (see .gitea/ci-image/Dockerfile): the official + # sagemath/sagemath image is amd64-only and this runner is arm64, and + # ruff/pytest are baked in here so jobs don't reinstall them every run. container: - image: sagemath/sagemath:10.4 + image: gitea.piribauer.ch/julian/sage-ci:latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Install ruff - run: sage --pip install ruff - - name: Preparse .sage files # Turns each *.sage file into real Python (*.sage.py) so ruff can # parse it, and prepends the `from sage.all import *` that `sage` @@ -40,14 +40,11 @@ jobs: runs-on: ubuntu-latest needs: lint container: - image: sagemath/sagemath:10.4 + image: gitea.piribauer.ch/julian/sage-ci:latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Install pytest - run: sage --pip install pytest - - name: Run tests run: sage --python -m pytest tests/ -- 2.54.0 From 3e6660a41491af581f69174cedc4c734e0e199bc Mon Sep 17 00:00:00 2001 From: julian Date: Sun, 26 Jul 2026 13:58:08 +0200 Subject: [PATCH 03/12] ci: add nodejs to sage-ci image for actions/checkout --- .gitea/ci-image/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/ci-image/Dockerfile b/.gitea/ci-image/Dockerfile index 22d9d6a..fa9b11c 100644 --- a/.gitea/ci-image/Dockerfile +++ b/.gitea/ci-image/Dockerfile @@ -18,4 +18,10 @@ FROM condaforge/miniforge3:latest RUN mamba install -y -c conda-forge sage ruff pytest \ && mamba clean -afy +# actions/checkout@v4 (and other JS-based actions) run via `node` inside this +# container -- act_runner doesn't inject a runtime of its own when a custom +# `container:` image is set, so one has to be present here. +RUN mamba install -y -c conda-forge nodejs \ + && mamba clean -afy + WORKDIR /workspace -- 2.54.0 From d649d5047f93d000d7562f940d3189f34f9e63ef Mon Sep 17 00:00:00 2001 From: julian Date: Sun, 26 Jul 2026 14:18:17 +0200 Subject: [PATCH 04/12] ci: fix sage CLI invocations for conda-forge's sage (no --preparse/--python flags) --- .gitea/workflows/ci.yml | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 655193e..2c189c3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -24,17 +24,31 @@ jobs: # parse it, and prepends the `from sage.all import *` that `sage` # normally injects at runtime, so Sage's globals (ZZ, var, matrix, ...) # resolve instead of looking like undefined names. + # + # The conda-forge `sage` CLI has no `--preparse` flag (unlike the + # classical sage script the image was originally written against), so + # this calls the same preparser Sage itself uses, directly in Python. run: | - set -e - for f in $(find . -name "*.sage" -not -path "./playground/*"); do - sage --preparse "$f" - printf 'from sage.all import * # noqa: F401,F403\n%s\n' "$(cat "${f}.py")" > "${f}.py" - done + python - <<'PYEOF' + import pathlib + from sage.repl.preparse import preparse_file + + for f in pathlib.Path(".").rglob("*.sage"): + if f.parts and f.parts[0] == "playground": + continue + out = preparse_file(f.read_text()) + f.with_suffix(f.suffix + ".py").write_text( + "from sage.all import * # noqa: F401,F403\n" + out + ) + PYEOF - name: ruff check # --no-respect-gitignore: *.sage.py is gitignored (it's generated, see # the previous step) but that's exactly what we need to lint here. - run: sage --python -m ruff check --no-respect-gitignore . + # `python` here is the conda env's own interpreter -- it already has + # sage/ruff/pytest importable, `sage --python` isn't a real flag on + # the conda-forge sage CLI. + run: python -m ruff check --no-respect-gitignore . test: runs-on: ubuntu-latest @@ -47,4 +61,4 @@ jobs: uses: actions/checkout@v4 - name: Run tests - run: sage --python -m pytest tests/ + run: python -m pytest tests/ -- 2.54.0 From 80072b291f8572798ae170aa1baaec58af9fa6d2 Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Sun, 26 Jul 2026 14:42:06 +0200 Subject: [PATCH 05/12] Ignore EOF blank line check --- .gitea/preparse.sh | 15 +++++++++++++++ .gitea/workflows/ci.yml | 7 +------ pyproject.toml | 5 ++++- 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100755 .gitea/preparse.sh diff --git a/.gitea/preparse.sh b/.gitea/preparse.sh new file mode 100755 index 0000000..13c706f --- /dev/null +++ b/.gitea/preparse.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Reproduces the CI "Preparse .sage files" step locally, so `ruff check` sees +# the same generated *.sage.py files that the pipeline lints. Run this before +# `sage --python -m ruff check --no-respect-gitignore .` to catch issues that +# only show up in the generated output (e.g. missing trailing newlines). +# +# The generated files are gitignored; clean them up afterwards with: +# git clean -x sage/ playground/ + +set -e + +for f in $(find . -name "*.sage" -not -path "./playground/*"); do + sage --preparse "$f" + printf 'from sage.all import * # noqa: F401,F403\n%s\n' "$(cat "${f}.py")" > "${f}.py" +done diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 655193e..4c89a0d 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -24,12 +24,7 @@ jobs: # parse it, and prepends the `from sage.all import *` that `sage` # normally injects at runtime, so Sage's globals (ZZ, var, matrix, ...) # resolve instead of looking like undefined names. - run: | - set -e - for f in $(find . -name "*.sage" -not -path "./playground/*"); do - sage --preparse "$f" - printf 'from sage.all import * # noqa: F401,F403\n%s\n' "$(cat "${f}.py")" > "${f}.py" - done + run: .gitea/preparse.sh - name: ruff check # --no-respect-gitignore: *.sage.py is gitignored (it's generated, see diff --git a/pyproject.toml b/pyproject.toml index 4659824..d9ee759 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,10 @@ ignore = [ # so trailing-whitespace warnings fire mechanically on almost every line # with a number in it -- not something `ruff format` could fix anyway, # since it wouldn't change the .sage source that generated it. -"*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293"] +# - W292: whether the reconstructed file ends in a real trailing newline +# depends on how many blank lines the preparser appends, which varies by +# Sage version -- not something worth pinning tool versions over. +"*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293", "W292"] # tests/test_smoke.py does `from sage.all import *` and `load(...)` a .sage # file to get at its classes -- the same dynamic-namespace situation as -- 2.54.0 From 3cc7bc0dda6bb50eda3f3cc7e2ea8664402a1fee Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Sun, 26 Jul 2026 14:52:53 +0200 Subject: [PATCH 06/12] Updating pre-parser for pipeline and local usage --- .gitea/preparse.sh | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.gitea/preparse.sh b/.gitea/preparse.sh index 13c706f..e87647d 100755 --- a/.gitea/preparse.sh +++ b/.gitea/preparse.sh @@ -1,15 +1,37 @@ #!/usr/bin/env bash # Reproduces the CI "Preparse .sage files" step locally, so `ruff check` sees # the same generated *.sage.py files that the pipeline lints. Run this before -# `sage --python -m ruff check --no-respect-gitignore .` to catch issues that -# only show up in the generated output (e.g. missing trailing newlines). +# `ruff check --no-respect-gitignore .` to catch issues that only show up in +# the generated output. +# +# Uses the sage.repl.preparse.preparse_file() Python API directly rather than +# the `sage --preparse` CLI flag: the conda-forge `sage` binary used in CI is +# a cut-down entry point that doesn't support `--preparse` (or `--python`) at +# all, unlike the official sagemath CLI. Whichever `python` on PATH can +# actually `import sage.repl` is used to run it -- in CI that's the conda +# env's own `python`; locally (official sage install) it's `sage --python`. # # The generated files are gitignored; clean them up afterwards with: # git clean -x sage/ playground/ set -e +if command -v python >/dev/null 2>&1 && python -c "import sage.repl" >/dev/null 2>&1; then + run_python() { python "$@"; } +else + run_python() { sage --python "$@"; } +fi + for f in $(find . -name "*.sage" -not -path "./playground/*"); do - sage --preparse "$f" - printf 'from sage.all import * # noqa: F401,F403\n%s\n' "$(cat "${f}.py")" > "${f}.py" + run_python -c " +import sys +from sage.repl.preparse import preparse_file + +path = sys.argv[1] +with open(path) as fh: + src = fh.read() +with open(path + '.py', 'w') as fh: + fh.write('from sage.all import * # noqa: F401,F403\n') + fh.write(preparse_file(src)) +" "$f" done -- 2.54.0 From d17bdc8949915f9fa6f25740025d9373b97b5ee0 Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Wed, 29 Jul 2026 19:16:48 +0200 Subject: [PATCH 07/12] Reducing comments --- .gitea/ci-image/Dockerfile | 16 +++------------- .gitea/preparse.sh | 17 +++-------------- .gitea/workflows/ci.yml | 13 +------------ pyproject.toml | 24 +++--------------------- 4 files changed, 10 insertions(+), 60 deletions(-) diff --git a/.gitea/ci-image/Dockerfile b/.gitea/ci-image/Dockerfile index fa9b11c..8ddd6d3 100644 --- a/.gitea/ci-image/Dockerfile +++ b/.gitea/ci-image/Dockerfile @@ -1,26 +1,16 @@ -# Custom CI image for the arm64 (Raspberry Pi) Gitea Actions runner. +# Custom CI image for the arm64 (Raspberry Pi) Gitea Actions runner, +# since the official sagemath/sagemath image is amd64-only. # -# The official sagemath/sagemath image is amd64-only; conda-forge's `sage` -# package is the one place SageMath is actually published for linux-aarch64, -# so this bakes it (plus the lint/test tools) into a single image, built and -# pushed once rather than reinstalled on every CI run. -# -# Build & push (run directly on the Pi, or any arm64 machine with Docker): +# Build & push commands: # docker build -t gitea.piribauer.ch/julian/sage-ci:latest -f .gitea/ci-image/Dockerfile . # docker login gitea.piribauer.ch -u julian # docker push gitea.piribauer.ch/julian/sage-ci:latest -# -# Rebuild and re-push whenever this Dockerfile changes (e.g. bumping the sage -# version) or the pinned tool versions need updating. FROM condaforge/miniforge3:latest RUN mamba install -y -c conda-forge sage ruff pytest \ && mamba clean -afy -# actions/checkout@v4 (and other JS-based actions) run via `node` inside this -# container -- act_runner doesn't inject a runtime of its own when a custom -# `container:` image is set, so one has to be present here. RUN mamba install -y -c conda-forge nodejs \ && mamba clean -afy diff --git a/.gitea/preparse.sh b/.gitea/preparse.sh index e87647d..17687c4 100755 --- a/.gitea/preparse.sh +++ b/.gitea/preparse.sh @@ -1,18 +1,7 @@ #!/usr/bin/env bash -# Reproduces the CI "Preparse .sage files" step locally, so `ruff check` sees -# the same generated *.sage.py files that the pipeline lints. Run this before -# `ruff check --no-respect-gitignore .` to catch issues that only show up in -# the generated output. -# -# Uses the sage.repl.preparse.preparse_file() Python API directly rather than -# the `sage --preparse` CLI flag: the conda-forge `sage` binary used in CI is -# a cut-down entry point that doesn't support `--preparse` (or `--python`) at -# all, unlike the official sagemath CLI. Whichever `python` on PATH can -# actually `import sage.repl` is used to run it -- in CI that's the conda -# env's own `python`; locally (official sage install) it's `sage --python`. -# -# The generated files are gitignored; clean them up afterwards with: -# git clean -x sage/ playground/ + +# This script is run in the CI container to preparse all .sage files into .sage.py files +# and can also be run locally for local linting/testing. set -e diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a37bf3d..1e280d9 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -9,9 +9,7 @@ on: jobs: lint: runs-on: ubuntu-latest - # Custom image (see .gitea/ci-image/Dockerfile): the official - # sagemath/sagemath image is amd64-only and this runner is arm64, and - # ruff/pytest are baked in here so jobs don't reinstall them every run. + # Custom image (see .gitea/ci-image/Dockerfile) container: image: gitea.piribauer.ch/julian/sage-ci:latest @@ -20,18 +18,9 @@ jobs: uses: actions/checkout@v4 - name: Preparse .sage files - # Turns each *.sage file into real Python (*.sage.py) so ruff can - # parse it, and prepends the `from sage.all import *` that `sage` - # normally injects at runtime, so Sage's globals (ZZ, var, matrix, ...) - # resolve instead of looking like undefined names. run: .gitea/preparse.sh - name: ruff check - # --no-respect-gitignore: *.sage.py is gitignored (it's generated, see - # the previous step) but that's exactly what we need to lint here. - # `python` here is the conda env's own interpreter -- it already has - # sage/ruff/pytest importable, `sage --python` isn't a real flag on - # the conda-forge sage CLI. run: python -m ruff check --no-respect-gitignore . test: diff --git a/pyproject.toml b/pyproject.toml index d9ee759..5475544 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,33 +3,15 @@ line-length = 120 target-version = "py311" [tool.ruff.lint] +# E: pycodestyle errors, F: pyflakes (undefined/unused names), I: isort (import order), W: pycodestyle warnings select = ["E", "F", "I", "W"] ignore = [ "E501", # symbolic-math expressions routinely exceed a "normal" line length ] [tool.ruff.lint.per-file-ignores] -# *.sage.py is generated by `sage --preparse` from *.sage sources. -# - F403/F405/F821/E741: Sage's runtime injects hundreds of globals (ZZ, var, -# matrix, LatticePolytope, ...) via `from sage.all import *` before -# executing these files, so plain static analysis can't see where names -# come from. -# - E402/E702/I001: Sage's preparser itself emits an import-then-semicolon- -# joined-constants preamble (e.g. `_sage_const_1 = Integer(1); ...`) ahead -# of the file's own imports; that's Sage's boilerplate, not this project's -# code style. -# - W291/W293: the preparser replaces every integer literal with -# `_sage_const_N ` (trailing space included) to preserve token boundaries, -# so trailing-whitespace warnings fire mechanically on almost every line -# with a number in it -- not something `ruff format` could fix anyway, -# since it wouldn't change the .sage source that generated it. -# - W292: whether the reconstructed file ends in a real trailing newline -# depends on how many blank lines the preparser appends, which varies by -# Sage version -- not something worth pinning tool versions over. +# *.sage.py is preparser output: star-import globals, semicolon preamble, and literal-substitution artifacts trip static analysis. "*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293", "W292"] -# tests/test_smoke.py does `from sage.all import *` and `load(...)` a .sage -# file to get at its classes -- the same dynamic-namespace situation as -# *.sage.py above, just in a hand-written file: ruff can't see that -# Polytope/ToricPolytope/etc. come from the loaded file. +# test_smoke.py star-imports sage.all and loads a .sage file, so ruff can't see where its names come from. "tests/test_smoke.py" = ["F403", "F405"] -- 2.54.0 From d7ed05771b615addb43506728529eb24a51df3a0 Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Wed, 29 Jul 2026 19:44:10 +0200 Subject: [PATCH 08/12] Updating tests --- tests/playground_models.sage | 104 +++++++++++++++++++++++++++++++++++ tests/test_smoke.py | 32 ----------- tests/topdata_tests.py | 34 ++++++++++++ 3 files changed, 138 insertions(+), 32 deletions(-) create mode 100644 tests/playground_models.sage delete mode 100644 tests/test_smoke.py create mode 100644 tests/topdata_tests.py diff --git a/tests/playground_models.sage b/tests/playground_models.sage new file mode 100644 index 0000000..d4c624b --- /dev/null +++ b/tests/playground_models.sage @@ -0,0 +1,104 @@ +# Entries are (id, factory, expected_cy_dimension, expected_no_divs, expected_disc, +# expected_intersection_numbers_CY), where expected_disc is a list of +# (str(discriminant_factor), codimension) pairs as returned by ToricPolytope.disc(). + +PLAYGROUND_MODEL_FACTORIES = [ + ( + "K3_two_parameter_family", + lambda: ToricPolytopeProjectiveSpace([1, 1, 2, 4], model_name="K3_two_parameter_family"), + 2, # cy_dimension + 2, # no_divs + [("z2 - 1/4", 2), ("4096*z1^2*(4*z2 - 1) + 128*z1 - 1", 0)], + {(0, 0): 4, (0, 1): 2, (1, 1): 0}, + ), + ( + "CY3_two_parameter_family", + lambda: ToricPolytopeProjectiveSpace([1, 1, 2, 2, 2], model_name="CY3_two_parameter_family"), + 3, + 2, + [("z2 - 1/4", 3), ("65536*z1^2*(4*z2 - 1) + 512*z1 - 1", 0)], + {(0, 0, 0): 8, (0, 0, 1): 4, (0, 1, 1): 0, (1, 1, 1): 0}, + ), + ( + "CY4_two_parameter_family", + lambda: ToricPolytopeProjectiveSpace([1, 1, 1, 1, 8, 12], model_name="CY4_two_parameter_family"), + 4, + 2, + [ + ("z2 - 1/256", 2), + ("34828517376*z1^4*(256*z2 - 1) + 322486272*z1^3 - 1119744*z1^2 + 1728*z1 - 1", 0), + ], + {(0, 0, 0, 0): 64, (0, 0, 0, 1): 16, (0, 0, 1, 1): 4, (0, 1, 1, 1): 1, (1, 1, 1, 1): 0}, + ), + ( + "CICY3_one_parameter", + lambda: ToricPolytopeCICY([[3, 3]], model_name="CICY3_one_parameter"), + 3, + 1, + [("z1 - 1/46656", 0)], + {(0, 0, 0): 9}, + ), + ( + "CICY3_two_parameter", + lambda: ToricPolytopeCICY([[3], [3]], model_name="CICY3_two_parameter"), + 3, + 2, + [ + ( + "-19683*z1^3 - 2187*(27*z1 + 1)*z2^2 - 19683*z2^3 - 2187*z1^2 " + "- 81*(729*z1^2 - 189*z1 + 1)*z2 - 81*z1 - 1", + 0, + ) + ], + {(0, 0, 0): 0, (0, 0, 1): 3, (0, 1, 1): 3, (1, 1, 1): 0}, + ), + ( + "CICY5_two_parameter", + lambda: ToricPolytopeCICY([[6, 1], [0, 2]], model_name="CICY5_two_parameter"), + 5, + 2, + [ + ( + "16384*z2^7 - 28672*z2^6 + 21504*z2^5 - 448*(1647086*z1 - 5)*z2^3 - 8960*z2^4 " + "- 112*(8235430*z1 + 3)*z2^2 - 678223072849*z1^2 - 28*(4941258*z1 - 1)*z2 - 1647086*z1 - 1", + 0, + ) + ], + { + (0, 0, 0, 0, 0): 6, + (0, 0, 0, 0, 1): 12, + (0, 0, 0, 1, 1): 0, + (0, 0, 1, 1, 1): 0, + (0, 1, 1, 1, 1): 0, + (1, 1, 1, 1, 1): 0, + }, + ), + ( + "CICY3_two_parameter_manual", + lambda: ToricPolytope( + [ + [1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [-1, -1, 0, 0, 0, 0], + [0, 0, -1, -1, -1, -1], + ], + nef_partition=[[0, 1, 2, 3], [4, 5, 6, 7]], + model_name="CICY3_two_parameter_manual", + ), + 4, + 2, + [ + ( + "-14348907*z1^5 - 2657205*z1^4 - 196830*z1^3 - 29296875*(270*z1 + 1)*z2^2 " + "- 30517578125*z2^3 - 7290*z1^2 + 9375*(98415*z1^3 - 32805*z1^2 + 810*z1 - 1)*z2 " + "- 135*z1 - 1", + 0, + ) + ], + {(0, 0, 0, 0): 0, (0, 0, 0, 1): 0, (0, 0, 1, 1): 6, (0, 1, 1, 1): 8, (1, 1, 1, 1): 2}, + ), +] diff --git a/tests/test_smoke.py b/tests/test_smoke.py deleted file mode 100644 index 72d7923..0000000 --- a/tests/test_smoke.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -import pytest -from sage.all import * # noqa: F401,F403 - -load(os.path.join(os.path.dirname(__file__), "..", "sage", "toric_topdata.sage")) - - -def test_polytope_rejects_too_few_points(): - with pytest.raises(ValueError): - Polytope([[0, 0], [1, 0]]) # 2 points in dimension 2: not enough - - -def test_mirror_quintic_topdata(): - # Vertices e_1, ..., e_4, -e_1-...-e_4 -- the worked example from - # ToricPolytope's own docstring. - points = [ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1], - [-1, -1, -1, -1], - ] - - polytope = ToricPolytope(points, model_name="ci_smoke_test_quintic") - assert polytope.dimension == 4 - assert len(polytope.triangulations) >= 1 - - polytope.topdata() - assert polytope.cy_dimension == 3 - assert polytope.no_divs == 1 # P^4 has Picard rank 1 - assert polytope.intersection_numbers_CY == {(0, 0, 0): 5} # classical quintic self-intersection diff --git a/tests/topdata_tests.py b/tests/topdata_tests.py new file mode 100644 index 0000000..94e950f --- /dev/null +++ b/tests/topdata_tests.py @@ -0,0 +1,34 @@ +import os + +import pytest +from sage.all import * # noqa: F401,F403 +from sage.geometry.lattice_polytope import set_palp_dimension + +load(os.path.join(os.path.dirname(__file__), "..", "sage", "toric_topdata.sage")) +load(os.path.join(os.path.dirname(__file__), "playground_models.sage")) + +set_palp_dimension(11) + +PLAYGROUND_MODELS = [pytest.param(*row[1:], id=row[0]) for row in PLAYGROUND_MODEL_FACTORIES] # noqa: F821 + + +@pytest.mark.parametrize( + "make_model, expected_cy_dimension, expected_no_divs, expected_disc, expected_intersection_numbers_CY", + PLAYGROUND_MODELS, +) +def test_playground_models_disc_and_topdata( + make_model, + expected_cy_dimension, + expected_no_divs, + expected_disc, + expected_intersection_numbers_CY, +): + polytope = make_model() + + discriminants = polytope.disc() + assert [(str(factor), codim) for factor, codim in discriminants] == expected_disc + + polytope.topdata() + assert polytope.cy_dimension == expected_cy_dimension + assert polytope.no_divs == expected_no_divs + assert polytope.intersection_numbers_CY == expected_intersection_numbers_CY -- 2.54.0 From 91360457a423f8e5b313a822c28d4de81eb5d28e Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Wed, 29 Jul 2026 19:50:10 +0200 Subject: [PATCH 09/12] Updating lint stuff --- pyproject.toml | 2 +- tests/{playground_models.sage => test_models.sage} | 2 +- tests/topdata_tests.py | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) rename tests/{playground_models.sage => test_models.sage} (99%) diff --git a/pyproject.toml b/pyproject.toml index 5475544..bd2f04e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,4 +14,4 @@ ignore = [ "*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293", "W292"] # test_smoke.py star-imports sage.all and loads a .sage file, so ruff can't see where its names come from. -"tests/test_smoke.py" = ["F403", "F405"] +"tests/topdata_tests.py" = ["F403", "F405"] diff --git a/tests/playground_models.sage b/tests/test_models.sage similarity index 99% rename from tests/playground_models.sage rename to tests/test_models.sage index d4c624b..d69e8c1 100644 --- a/tests/playground_models.sage +++ b/tests/test_models.sage @@ -2,7 +2,7 @@ # expected_intersection_numbers_CY), where expected_disc is a list of # (str(discriminant_factor), codimension) pairs as returned by ToricPolytope.disc(). -PLAYGROUND_MODEL_FACTORIES = [ +TEST_MODELS = [ ( "K3_two_parameter_family", lambda: ToricPolytopeProjectiveSpace([1, 1, 2, 4], model_name="K3_two_parameter_family"), diff --git a/tests/topdata_tests.py b/tests/topdata_tests.py index 94e950f..f85eb59 100644 --- a/tests/topdata_tests.py +++ b/tests/topdata_tests.py @@ -1,22 +1,22 @@ import os import pytest -from sage.all import * # noqa: F401,F403 +from sage.all import * # noqa: F401 from sage.geometry.lattice_polytope import set_palp_dimension load(os.path.join(os.path.dirname(__file__), "..", "sage", "toric_topdata.sage")) -load(os.path.join(os.path.dirname(__file__), "playground_models.sage")) +load(os.path.join(os.path.dirname(__file__), "test_models.sage")) set_palp_dimension(11) -PLAYGROUND_MODELS = [pytest.param(*row[1:], id=row[0]) for row in PLAYGROUND_MODEL_FACTORIES] # noqa: F821 +TEST_MODELS_PARAMETRISED = [pytest.param(*row[1:], id=row[0]) for row in TEST_MODELS] @pytest.mark.parametrize( "make_model, expected_cy_dimension, expected_no_divs, expected_disc, expected_intersection_numbers_CY", - PLAYGROUND_MODELS, + TEST_MODELS_PARAMETRISED, ) -def test_playground_models_disc_and_topdata( +def test_topdata_and_disc( make_model, expected_cy_dimension, expected_no_divs, -- 2.54.0 From 154a1e9a0b15f48bd4bcef47f0a481ebf40cbd0b Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Wed, 29 Jul 2026 19:52:46 +0200 Subject: [PATCH 10/12] File renaming for pytest --- tests/{topdata_tests.py => test_topdata_and_disc.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{topdata_tests.py => test_topdata_and_disc.py} (100%) diff --git a/tests/topdata_tests.py b/tests/test_topdata_and_disc.py similarity index 100% rename from tests/topdata_tests.py rename to tests/test_topdata_and_disc.py -- 2.54.0 From e0b377dd3e64901552c6ae5e47b0119ff7b535cd Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Wed, 29 Jul 2026 19:54:00 +0200 Subject: [PATCH 11/12] Adjusting ruff ignore file rename --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd2f04e..4b852c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,4 +14,4 @@ ignore = [ "*.sage.py" = ["F403", "F405", "F821", "E741", "E402", "E702", "I001", "W291", "W293", "W292"] # test_smoke.py star-imports sage.all and loads a .sage file, so ruff can't see where its names come from. -"tests/topdata_tests.py" = ["F403", "F405"] +"tests/test_topdata_and_disc.py" = ["F403", "F405"] -- 2.54.0 From 4c234190432ca121d4ff8acedb1aa34164105ae2 Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Wed, 29 Jul 2026 19:57:51 +0200 Subject: [PATCH 12/12] Triangulation log --- sage/toric_topdata.sage | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sage/toric_topdata.sage b/sage/toric_topdata.sage index 71c3b34..597a1bd 100644 --- a/sage/toric_topdata.sage +++ b/sage/toric_topdata.sage @@ -101,7 +101,8 @@ class ToricPolytope(Polytope): if no_triangulation < 0 or no_triangulation >= len(self.triangulations): raise IndexError("Invalid triangulation index {} (available: 0..{}).".format(no_triangulation, len(self.triangulations) - 1)) - logger.info("Using triangulation index %d of [0..%d].", no_triangulation, len(self.triangulations) - 1) + if len(self.triangulations) > 1: + logger.info("Using triangulation index %d of [0..%d].", no_triangulation, len(self.triangulations) - 1) self.triangulation = self.triangulations[no_triangulation] def set_nef_partition(self, nef_partition): -- 2.54.0