Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ The tool makes the process relatively painless. There are a few
workarounds (crude string subsitutions) we need to apply which are all
configured in [pyproject.toml](./pyproject.toml).

To update vendored dependecies, run:

```
./scripts/update.sh
Comment thread
rebkwok marked this conversation as resolved.
```

## opensafely run

Historically, this repo consumed [`job-runner`](https://github.com/opensafely-core/job-runner)
Expand Down
21 changes: 21 additions & 0 deletions opensafely/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import sys
import tempfile
import warnings
from datetime import datetime, timedelta
from pathlib import Path

Expand Down Expand Up @@ -32,6 +33,26 @@
VERSION_FILE = Path(tempfile.gettempdir()) / "opensafely-version-check"


_original_formatwarning = warnings.formatwarning


# format ProjectWarnings (warnings about project.yamls from the pipeline library)
# to print only the message on the console
def warning_message_only(
message,
category,
filename,
lineno,
line=None,
):
if str(message).startswith("ProjectWarning"):
return f"{message}\n"
return _original_formatwarning(message, category, filename, lineno, line)


warnings.formatwarning = warning_message_only


Comment thread
rebkwok marked this conversation as resolved.
def should_version_check():
if not VERSION_FILE.exists():
return True
Expand Down
2 changes: 2 additions & 0 deletions opensafely/_vendor/pipeline/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from .constants import RUN_ALL_COMMAND
from .exceptions import ProjectValidationError, YAMLError
from .main import load_pipeline
Expand Down
2 changes: 2 additions & 0 deletions opensafely/_vendor/pipeline/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"UNIQUE_OUTPUT_PATH": (2, None),
"EXPECTATIONS_POPULATION": (3, 3),
"REMOVE_SUPPORT_FOR_COHORT_EXTRACTOR": (4, None),
"REMOVE_SUPPORT_FOR_RUN_ALL_ACTION": (5, None),
"REMOVE_SUPPORT_FOR_LATEST_TAG": (5, None),
}

LATEST_VERSION = max([v[0] for v in FEATURE_FLAGS_BY_VERSION.values()])
Expand Down
24 changes: 20 additions & 4 deletions opensafely/_vendor/pipeline/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pathlib
import re
import shlex
import warnings
from collections import defaultdict
from dataclasses import dataclass
from typing import Any
Expand All @@ -17,6 +18,8 @@
validate_glob_pattern,
validate_no_kwargs,
validate_not_cohort_extractor_action,
validate_not_latest_tag,
validate_not_run_all_action,
validate_type,
)

Expand Down Expand Up @@ -270,12 +273,20 @@ def build(
raise ValidationError(
f"`version` must be a number between 1 and {LATEST_VERSION}"
)
else:
if version != LATEST_VERSION:
warnings.warn(
f"ProjectWarning: Your project file is using an old version ({version}); consider updating to version {LATEST_VERSION}",
stacklevel=2,
)

feat = get_feature_flags_for_version(version)

validate_type(actions, dict, "Project `actions` section")

_actions = dict()
for action_id, action_config in actions.items():
validate_not_run_all_action(action_id, feat)
validate_action_config(action_id, action_config)
_actions[action_id] = Action.build(action_id, **action_config)
actions = _actions
Expand All @@ -284,6 +295,10 @@ def build(
for config in actions.values():
validate_not_cohort_extractor_action(config)

if feat.REMOVE_SUPPORT_FOR_LATEST_TAG:
for config in actions.values():
validate_not_latest_tag(config)

seen: dict[Command, list[str]] = defaultdict(list)
for name, config in actions.items():
run = config.run
Expand Down Expand Up @@ -339,9 +354,10 @@ def all_actions(self) -> list[str]:
"""
Get all actions for this Pipeline instance

We ignore any manually defined run_all action (in later project
versions this will be an error). We use a list comprehension rather
than set operators as previously so we preserve the original order.
Versions < 5 ignore any manually defined run_all action and raise a
warning (later project versions the raise an error).
We use a list comprehension rather than set operators as previously so we preserve
the original order.
"""
return [action for action in self.actions.keys() if action != RUN_ALL_COMMAND]

Expand All @@ -355,7 +371,7 @@ def action_images(self) -> set[str]:
images = set()
for action in self.actions.values():
# for hysterical raisins, :latest is actually mapped to v1, not v2 or later.
# We hope to fix this at some point
# version 5 removes use of :latest
version = "v1" if action.run.version == "latest" else action.run.version
images.add(f"{action.run.name}:{version}")

Expand Down
24 changes: 24 additions & 0 deletions opensafely/_vendor/pipeline/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import fnmatch
import posixpath
import warnings
from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath
from types import SimpleNamespace
from typing import TYPE_CHECKING, Any

from .constants import LEVEL4_FILE_TYPES
Expand Down Expand Up @@ -97,6 +99,28 @@ def validate_not_cohort_extractor_action(action: Action) -> None:
)


def validate_not_run_all_action(action_id: str, feat: SimpleNamespace) -> None:
if action_id != "run_all":
return
if feat.REMOVE_SUPPORT_FOR_RUN_ALL_ACTION:
raise ValidationError(
"`run_all` is a reserved action name and is not allowed for user-defined actions."
)
else:
warnings.warn(
"ProjectWarning: `run_all` is a reserved action name; user-defined actions with this name "
"are ignored and will raise an error in later versions.",
stacklevel=3,
)


def validate_not_latest_tag(action: Action) -> None:
if action.run.parts[0].endswith(":latest"):
raise ValidationError(
f"Action {action.action_id} uses `{action.run.parts[0]}`, which is not supported. Provide a version e.g. `:v2` instead"
)


def validate_cohortextractor_outputs(action_id: str, action: Action) -> None:
"""
Check cohortextractor's output config is valid for this command
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,10 @@ target-version = "py38"
[tool.ruff.isort]
lines-after-imports = 2
known-third-party = ["opensafely._vendor"]

[tool.pytest.ini_options]
markers = ["functional: mark as a functional test."]
filterwarnings = [
"ignore:ProjectWarning:UserWarning:opensafely._vendor.pipeline.*:",
"ignore:ProjectWarning:UserWarning:tests.*:",
]
2 changes: 1 addition & 1 deletion requirements.prod.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
opensafely-pipeline@https://github.com/opensafely-core/pipeline/archive/refs/tags/v2026.01.26.163426.zip
opensafely-pipeline@https://github.com/opensafely-core/pipeline/archive/refs/tags/v2026.03.12.140752.zip
ruyaml
requests
opentelemetry-api==1.33.1
Expand Down
2 changes: 1 addition & 1 deletion requirements.prod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ idna==3.10
# via requests
importlib-metadata==8.5.0
# via opentelemetry-api
opensafely-pipeline @ https://github.com/opensafely-core/pipeline/archive/refs/tags/v2026.01.26.163426.zip
opensafely-pipeline @ https://github.com/opensafely-core/pipeline/archive/refs/tags/v2026.03.12.140752.zip
# via -r requirements.prod.in
opentelemetry-api==1.33.1
# via -r requirements.prod.in
Expand Down
29 changes: 29 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import warnings
from datetime import datetime, timedelta

import pytest

import opensafely
from tests.test_pull import expect_local_images

Expand Down Expand Up @@ -52,3 +55,29 @@ def test_warn_if_updates_needed_images_outdated(capsys, monkeypatch, tmp_path, r
" opensafely pull",
"",
]


def test_warnings_format():
Comment thread
rebkwok marked this conversation as resolved.
Outdated
with pytest.warns(UserWarning) as raised_warnings:
warnings.warn("Warning: foo", UserWarning)
warnings.warn("ProjectWarning: foo", UserWarning)

assert len(raised_warnings.list) == 2
standard_warning, project_warning = raised_warnings.list

# standard warning includes the warning category (and other standard warning formatting)
assert "UserWarning: Warning: foo" in warnings.formatwarning(
standard_warning.message,
standard_warning.category,
standard_warning.filename,
standard_warning.lineno,
standard_warning.line,
)
# project warning contains just the warning message
assert "ProjectWarning: foo\n" == warnings.formatwarning(
project_warning.message,
project_warning.category,
project_warning.filename,
project_warning.lineno,
project_warning.line,
)
Loading