From 62cb074e41ef3b885d85a01bfbed48bb70e42797 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Mon, 9 Nov 2026 15:30:18 +0000 Subject: [PATCH 1/4] Upgrade to Python 3.12 base layer and align workflows with survey - Base image baseevalutionfunctionlayer:main-3.8 -> main-3.12 (AL2023: dnf git+findutils, PYTHONPATH, pytest for healthcheck, module-style CMD) - Rename evaluation_test.py/preview_test.py -> *_tests.py so the base layer healthcheck can import them - Workflows copied from survey: Python 3.12, deploy.yml@main, PR-triggered test-lint, database tests on production deploy; staging/production target master (default branch) - Add pytest.ini Co-Authored-By: Claude Opus 5.5 --- .github/workflows/production-deploy.yml | 6 ++--- .github/workflows/staging-deploy.yml | 6 ++--- app/Dockerfile | 23 ++++++++++++------- ...evaluation_test.py => evaluation_tests.py} | 0 app/{preview_test.py => preview_tests.py} | 2 +- pytest.ini | 4 ++++ readme.md | 2 +- 7 files changed, 27 insertions(+), 16 deletions(-) rename app/{evaluation_test.py => evaluation_tests.py} (100%) rename app/{preview_test.py => preview_tests.py} (99%) create mode 100644 pytest.ini diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml index fe7fb79..fd910ab 100644 --- a/.github/workflows/production-deploy.yml +++ b/.github/workflows/production-deploy.yml @@ -16,7 +16,7 @@ on: description: 'Branch to release from' required: true type: string - default: 'main' + default: 'master' seed: description: 'Random seed for reproducible sampling (float in [-1.0, 1.0]). Leave blank to auto-generate.' required: false @@ -34,7 +34,7 @@ on: default: '5' jobs: deploy: - uses: lambda-feedback/evaluation-function-workflows/.github/workflows/deploy.yml@deploy-request + uses: lambda-feedback/evaluation-function-workflows/.github/workflows/deploy.yml@main with: template-repository-name: 'lambda-feedback/evaluation-function-boilerplate-python' build-file: "app/Dockerfile" @@ -42,7 +42,7 @@ jobs: environment: "production" version-bump: ${{ inputs.version-bump }} branch: ${{ inputs.branch }} - run-database-tests: false + run-database-tests: true seed: ${{ inputs.seed }} request_delay: ${{ inputs.request_delay }} max_concurrency: ${{ inputs.max_concurrency }} diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index 9c354f2..caf18f5 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -3,7 +3,7 @@ name: Deploy to Staging on: push: branches: - - main + - master workflow_dispatch: jobs: @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8"] + python-version: ["3.12"] steps: - name: Checkout uses: actions/checkout@v4 @@ -59,7 +59,7 @@ jobs: if-no-files-found: warn deploy: needs: test - uses: lambda-feedback/evaluation-function-workflows/.github/workflows/deploy.yml@deploy-request + uses: lambda-feedback/evaluation-function-workflows/.github/workflows/deploy.yml@main with: template-repository-name: "lambda-feedback/evaluation-function-boilerplate-python" build-file: "app/Dockerfile" diff --git a/app/Dockerfile b/app/Dockerfile index 53ecbea..5bc415e 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,22 +1,28 @@ -# Base image that bundles AWS Lambda Python 3.8 image with some middleware functions -# FROM base-eval-tmp -FROM ghcr.io/lambda-feedback/baseevalutionfunctionlayer:main-3.8 +# Base image that bundles the AWS Lambda Python image with the lambda-feedback middleware (app.handler) +FROM ghcr.io/lambda-feedback/baseevalutionfunctionlayer:main-3.12 + +# The 3.12 image is Amazon Linux 2023, which doesn't ship `find` (used below). +# git is needed to pip install latex2sympy from GitHub. +RUN dnf install -y git findutils && dnf clean all WORKDIR /app -RUN yum install -y git +ENV PYTHONPATH=/app # Copy and install any packages/modules needed for your evaluation script. COPY requirements.txt . RUN pip3 install -r requirements.txt +# The healthcheck command runs the pytest suite +RUN pip3 install pytest + # Copy the evaluation and testing scripts COPY evaluation.py ./app/ -COPY evaluation_test.py ./app/ +COPY evaluation_tests.py ./app/ # Copy the preview and testing scripts COPY preview.py ./app/ -COPY preview_test.py ./app/ +COPY preview_tests.py ./app/ # Copy additional files COPY expression_utilities.py ./app/ @@ -34,5 +40,6 @@ COPY docs/user.md ./app/docs/user.md RUN chmod 644 $(find . -type f) RUN chmod 755 $(find . -type d) -# The entrypoint for AWS is to invoke the handler function within the app package -CMD [ "/app/app.handler" ] +# The entrypoint for AWS is to invoke the handler function within the app package. +# Python 3.12's Lambda runtime imports this as a module name, so it can't be a path. +CMD [ "app.handler" ] diff --git a/app/evaluation_test.py b/app/evaluation_tests.py similarity index 100% rename from app/evaluation_test.py rename to app/evaluation_tests.py diff --git a/app/preview_test.py b/app/preview_tests.py similarity index 99% rename from app/preview_test.py rename to app/preview_tests.py index 5cf650f..1122bea 100755 --- a/app/preview_test.py +++ b/app/preview_tests.py @@ -2,7 +2,7 @@ import pytest from .preview import Params, extract_latex, preview_function -from .evaluation_test import elementary_function_test_cases +from .evaluation_tests import elementary_function_test_cases class TestPreviewFunction(): diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..589497f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +# evaluation_tests.py is the name the base layer's healthcheck imports, so +# include it alongside pytest's default patterns. +python_files = test_*.py *_test.py *_tests.py diff --git a/readme.md b/readme.md index 7240643..a0eb1a4 100644 --- a/readme.md +++ b/readme.md @@ -52,7 +52,7 @@ Once the code passes all these tests, it will then be uploaded to AWS and will b ## Pre-requisites Although all programming can be done through the GitHub interface, it is recommended you do this locally on your machine. To do this, you must have installed: -- Python 3.8 or higher. +- Python 3.12 or higher. - GitHub Desktop or the `git` CLI. From 66b7a4fafec84e6ae4baa9828fb0aca51b5ca3de Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Mon, 9 Nov 2026 15:36:29 +0000 Subject: [PATCH 2/4] Bump sympy 1.10.1 -> 1.12.1 for Python 3.12 tokenizer compatibility sympy 1.10.1's parse_expr mishandles '!' (factorial) and some unicode tokens under Python 3.12's new tokenize module (PEP 701), failing test_exclamation_mark_for_factorial and test_error_inappropriate_symbol[-inf]. Co-Authored-By: Claude Opus 5.5 --- app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/requirements.txt b/app/requirements.txt index 1f9031a..670bf6a 100755 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,5 +1,5 @@ typing_extensions mpmath==1.2.1 -sympy==1.10.1 +sympy==1.12.1 antlr4-python3-runtime==4.7.2 git+https://github.com/lambda-feedback/latex2sympy.git@master#egg=latex2sympy2 \ No newline at end of file From 258396da8491885175728b1d59fd1e652cf3d409 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Mon, 9 Nov 2026 17:14:14 +0000 Subject: [PATCH 3/4] Keep evaluation_test.py/preview_test.py names These are pytest-style test classes; under the *_tests.py names the base layer's unittest-based healthcheck imports and instantiates them and errors with 'TestEvaluationFunction() takes no arguments'. CI still collects them via pytest.ini. Co-Authored-By: Claude Opus 5.5 --- app/Dockerfile | 4 ++-- app/{evaluation_tests.py => evaluation_test.py} | 0 app/{preview_tests.py => preview_test.py} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename app/{evaluation_tests.py => evaluation_test.py} (100%) rename app/{preview_tests.py => preview_test.py} (99%) diff --git a/app/Dockerfile b/app/Dockerfile index 5bc415e..fa33537 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -18,11 +18,11 @@ RUN pip3 install pytest # Copy the evaluation and testing scripts COPY evaluation.py ./app/ -COPY evaluation_tests.py ./app/ +COPY evaluation_test.py ./app/ # Copy the preview and testing scripts COPY preview.py ./app/ -COPY preview_tests.py ./app/ +COPY preview_test.py ./app/ # Copy additional files COPY expression_utilities.py ./app/ diff --git a/app/evaluation_tests.py b/app/evaluation_test.py similarity index 100% rename from app/evaluation_tests.py rename to app/evaluation_test.py diff --git a/app/preview_tests.py b/app/preview_test.py similarity index 99% rename from app/preview_tests.py rename to app/preview_test.py index 1122bea..5cf650f 100755 --- a/app/preview_tests.py +++ b/app/preview_test.py @@ -2,7 +2,7 @@ import pytest from .preview import Params, extract_latex, preview_function -from .evaluation_tests import elementary_function_test_cases +from .evaluation_test import elementary_function_test_cases class TestPreviewFunction(): From 6d1b148a2f6553d051dddb0ff5aef70a1a53c9e3 Mon Sep 17 00:00:00 2001 From: Marcus Messer <12846590+m-messer@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:28:29 +0000 Subject: [PATCH 4/4] Merge pull request #81 from lambda-feedback/fix-py312-invalid-identifier Reject non-identifier symbols on Python 3.12 (stacked on #80) --- app/expression_utilities.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/expression_utilities.py b/app/expression_utilities.py index 17c313d..0253435 100644 --- a/app/expression_utilities.py +++ b/app/expression_utilities.py @@ -13,7 +13,9 @@ from sympy.parsing.sympy_parser import T as parser_transformations from sympy.printing.latex import LatexPrinter from sympy import Basic, Symbol +import io import re +import tokenize from typing import Dict, List, TypedDict class ModifiedLatexPrinter(LatexPrinter): @@ -573,6 +575,24 @@ def create_sympy_parsing_params(params, unsplittable_symbols=tuple(), symbol_ass return parsing_params +def reject_invalid_identifiers(expr): + ''' + Raise ValueError if expr contains a name token that is not a valid Python + identifier (e.g. `∞`). + + Python <= 3.11 tokenized such characters as ERRORTOKEN, which made + parse_expr fail. From Python 3.12 (PEP 701) they are emitted as NAME + tokens, and SymPy's auto_symbol silently turns them into symbols. + ''' + try: + tokens = list(tokenize.generate_tokens(io.StringIO(expr).readline)) + except (tokenize.TokenError, SyntaxError): + return # Leave genuine tokenizer errors for parse_expr to report + for token in tokens: + if token.type == tokenize.NAME and not token.string.isidentifier(): + raise ValueError(f"Invalid symbol `{token.string}` in expression `{expr}`") + + def parse_expression(expr, parsing_params): ''' Input: @@ -610,6 +630,7 @@ def parse_expression(expr, parsing_params): transformations = parser_transformations[0:5, 6]+extra_transformations+(split_symbols_custom(can_split),)+parser_transformations[8] if parsing_params.get("rationalise", False): transformations += parser_transformations[11] + reject_invalid_identifiers(expr) if parsing_params.get("simplify", False): parsed_expr = parse_expr(expr, transformations=transformations, local_dict=symbol_dict) parsed_expr = parsed_expr.simplify()