Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2026 CERN <home.cern>
#
# SPDX-License-Identifier: LGPL-2.1-or-later

.git
.github
.gitlab-ci.yml
.venv
.pytest_cache
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
coverage-html/
coverage-report.xml
junit-report.xml
pts_reports/
.idea/
.vscode/
30 changes: 30 additions & 0 deletions .github/RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!--
SPDX-FileCopyrightText: 2026 CERN <home.cern>

SPDX-License-Identifier: CC-BY-SA-4.0
-->

# Release configuration

Before creating the first release tag, a GitHub organization owner must create
two repository Environments:

* `testpypi`, with no required reviewers;
* `pypi`, with required reviewers and deployment access restricted to protected
tags.

Register trusted publishers in the TestPyPI and PyPI project settings using the
following values:

| Setting | Value |
| --- | --- |
| Owner | `CERN` |
| Repository | `pts-framework` |
| Workflow | `release.yml` |
| Environment | `testpypi` or `pypi`, respectively |

The project is published as `pts-framework`. Protect the `master` branch and
limit creation of `vX.Y.Z` tags to maintainers. A release tag first publishes its
artifact to TestPyPI. Install and smoke-test that version using the command in
the README, then approve the pending `pypi` Environment deployment to publish
the identical artifact to PyPI.
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: 2026 CERN <home.cern>
#
# SPDX-License-Identifier: LGPL-2.1-or-later

name: CI

on:
pull_request:
push:
branches: [master]

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out source
uses: actions/checkout@v6

- name: Build the CI image
run: >-
docker build
--build-arg SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTS_FRAMEWORK=0.0+g${GITHUB_SHA::7}
--tag pts-framework-ci:${GITHUB_SHA}
.

- name: Run tests
run: >-
docker run --rm pts-framework-ci:${GITHUB_SHA}
python -m pytest tests -vv --durations=20
94 changes: 94 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# SPDX-FileCopyrightText: 2026 CERN <home.cern>
#
# SPDX-License-Identifier: LGPL-2.1-or-later

name: Release

on:
push:
tags: ["v*"]

permissions:
contents: read

jobs:
build:
name: Test and build distributions
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out source
uses: actions/checkout@v6

- name: Validate and expose the release version
id: version
shell: bash
run: |
if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Release tags must use the vX.Y.Z format." >&2
exit 1
fi
echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Build the disposable CI image
run: >-
docker build
--build-arg SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTS_FRAMEWORK=${{ steps.version.outputs.value }}
--tag pts-framework-release:${{ github.sha }}
.

- name: Run tests in the CI image
run: docker run --rm pts-framework-release:${{ github.sha }}

- name: Build wheel and source distribution
run: >-
docker run --rm
--volume "${{ github.workspace }}/dist:/dist"
pts-framework-release:${{ github.sha }}
python -m build --outdir /dist

- name: Store release distributions temporarily
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/
if-no-files-found: error
retention-days: 30

publish-testpypi:
name: Publish to TestPyPI
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
environment: testpypi
permissions:
id-token: write
steps:
- name: Download release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/

- name: Publish distributions
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/

publish-pypi:
name: Publish to PyPI
needs: publish-testpypi
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pypi
permissions:
id-token: write
steps:
- name: Download release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/

- name: Publish distributions
uses: pypa/gh-action-pypi-publish@release/v1
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SPDX-FileCopyrightText: 2026 CERN <home.cern>
#
# SPDX-License-Identifier: LGPL-2.1-or-later

ARG PYTHON_VERSION=3.13.14
FROM python:${PYTHON_VERSION}-slim-trixie

# PySide6 wheels include Qt itself. These runtime libraries are required for Qt's
# headless platform plugin while the test suite is running in the container.
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
libdbus-1-3 \
libegl1 \
libfontconfig1 \
libglib2.0-0 \
libgl1 \
libxkbcommon0 \
&& rm -rf /var/lib/apt/lists/*

ARG SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTS_FRAMEWORK=0+unknown
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
QT_QPA_PLATFORM=offscreen \
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTS_FRAMEWORK=${SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTS_FRAMEWORK}

WORKDIR /app

COPY pyproject.toml README.md ./
COPY src ./src
COPY tests ./tests

RUN python -m pip install --no-cache-dir --upgrade pip \
&& python -m pip install --no-cache-dir ".[test]" build

CMD ["python", "-m", "pytest", "tests"]
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ For full documentation, please visit [PTS Framework Documentation](https://acc-p

=======

## Continuous integration and releases

The project is tested in a disposable Python 3.13 Docker image. To reproduce the
GitHub CI build locally, run:

```sh
docker build --build-arg SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTS_FRAMEWORK=0+local -t pts-framework-ci .
docker run --rm pts-framework-ci
```

Creating a protected `vX.Y.Z` tag runs the same tests, builds the wheel and source
distribution, and publishes them to TestPyPI. Verify the TestPyPI package in a clean
environment before approving the `pypi` GitHub Environment:

```sh
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ pts-framework==X.Y.Z
```

Approval publishes the same distributions to PyPI. GitHub retains the release
artifact for 30 days; PyPI is the permanent package source. See
[release configuration](.github/RELEASE.md) for the one-time setup.

## Licences

pypts is distributed under the **LGPL-2.1-or-later** licence.
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ authors = [
]
# Specify the minimum Python version required. It is not safe to use
# this to control the *upper* version of Python.
requires-python = ">=3.11"
requires-python = ">=3.13"

# The core dependencies of the project.
dependencies = [
Expand All @@ -48,6 +48,7 @@ test = [
"pytest",
"pytest-qt",
"pytest-cov",
"pytest-timeout",
]
doc = [
"Sphinx",
Expand Down Expand Up @@ -96,6 +97,8 @@ features = [
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "--strict-markers"
timeout = 90
faulthandler_timeout = 60
testpaths = ["tests"]
markers = [
"unit: unit tests",
Expand Down
6 changes: 5 additions & 1 deletion src/pypts/YamVIEW/recipe_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,18 @@ def __init__(self):
self.setup_status_and_layouts()
self.toggle_dark_mode_action.setChecked(self.dark_mode)
self.toggle_dark_mode(self.dark_mode, log_change=False)
install_system_theme_sync(QApplication.instance(), self._set_dark_mode)
self._disconnect_system_theme_sync = install_system_theme_sync(QApplication.instance(), self._set_dark_mode)

# Define the shortcut activation action
save_shortcut = QShortcut(QKeySequence("Ctrl+S"), self)
save_shortcut.activated.connect(self.on_save_clicked)

self.log("✅ Application started.")

def closeEvent(self, event):
self._disconnect_system_theme_sync()
super().closeEvent(event)

def setup_menu(self):
menubar = self.menuBar()
# File menu
Expand Down
25 changes: 18 additions & 7 deletions src/pypts/event_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,23 @@ def __init__(self, event_q: SimpleQueue):
self._test_package: str | None = None

def RecipeEventProxyRunner(self):
"""Fetch and process one event, stopping if the queue itself fails."""
try:
event = self.event_q.get()
if event is None: # Sentinel to stop
self._running = False
return

except Exception as e:
logger.error("Error retrieving RecipeEventProxy event: %s", e, exc_info=True)
self._running = False
return

if event is None: # Sentinel to stop
self._running = False
return

self._process_event(event)

def _process_event(self, event):
"""Transform one already-retrieved event and emit its Qt signal."""
try:
event_name, event_data = event
logger.debug(f"Event Proxy received: {event_name}")
logger.debug(f"Event data type: {type(event_data)}, length: {len(event_data) if hasattr(event_data, '__len__') else 'N/A'}")
Expand Down Expand Up @@ -183,9 +194,9 @@ def RecipeEventProxyRunner(self):
logger.warning(f"No dictionary created for event: {event_name}")

except Exception as e:
logger.error(f"Error in RecipeEventProxy loop: {e}", exc_info=True)
# Depending on desired behavior, you might want to break or continue
# For robustness, we'll continue here
# A malformed event must not kill the proxy: the next queue read
# blocks normally and can still deliver valid recipe events.
logger.error(f"Error processing RecipeEventProxy event: {e}", exc_info=True)

@Slot()
def run(self):
Expand Down
13 changes: 12 additions & 1 deletion src/pypts/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def __init__(self, return_content=True, binary=False):
class MainWindow(QMainWindow):
"""The main application window, displaying recipe progress and results."""

closing = Signal()

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand All @@ -123,13 +125,22 @@ def __init__(self, *args, **kwargs):
self._build_central()
self._build_statusbar()
self._apply_theme()
install_system_theme_sync(QApplication.instance(), self._set_dark_mode)
self._disconnect_system_theme_sync = install_system_theme_sync(QApplication.instance(), self._set_dark_mode)
self._close_notified = False
self._switch_screen(SCREEN_IDLE)

self.log_handler = TextEditLoggerHandler(self)
self.log_handler.setFormatter(logging.Formatter("%(levelname)s : %(name)s : %(message)s"))
self.log_handler.new_message.connect(self.log_text_box.append_line)

def closeEvent(self, event):
"""Release application-wide signal connections before Qt closes us."""
if not self._close_notified:
self._close_notified = True
self._disconnect_system_theme_sync()
self.closing.emit()
super().closeEvent(event)

def _build_menu(self):
menu_bar = QMenuBar(self)
self.setMenuBar(menu_bar)
Expand Down
Loading