From ece770ab4844f47a6b3650d89f5b6c3f55c5d6b8 Mon Sep 17 00:00:00 2001 From: Julian Piribauer Date: Fri, 24 Jul 2026 21:03:04 +0200 Subject: [PATCH] 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