Skip to content

Commit fee96e0

Browse files
chrisburrclaude
andcommitted
feat: extract stateless utilities to DIRACCommon package
Extract ReturnValues and DErrno to separate DIRACCommon package to solve circular dependencies where DiracX needs DIRAC utilities without global state initialization. - Create DIRACCommon package with stateless utilities - Maintain backward compatibility in original DIRAC locations - Add independent testing and CI workflows - Configure automated deployment for both packages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e741794 commit fee96e0

File tree

25 files changed

+1092
-570
lines changed

25 files changed

+1092
-570
lines changed

.github/workflows/basic.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ jobs:
8484
- name: Run tests
8585
run: |
8686
# FIXME: The unit tests currently only work with editable installs
87+
# Install DIRACCommon first to ensure dependencies are resolved correctly
88+
pip install -e ./dirac-common
8789
pip install -e .[server,testing]
8890
${{ matrix.command }}
8991
env:

.github/workflows/deployment.yml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,19 @@ jobs:
106106
fi
107107
fi
108108
fi
109-
- name: Build distributions
109+
- name: Build DIRACCommon distribution
110+
if: steps.check-tag.outputs.create-release == 'true'
110111
run: |
112+
cd dirac-common
113+
python -m build
114+
cd ..
115+
- name: Pin DIRACCommon version and build DIRAC distribution
116+
run: |
117+
# If we're making a release, pin DIRACCommon to exact version
118+
if [[ "${{ steps.check-tag.outputs.create-release }}" == "true" ]]; then
119+
DIRACCOMMON_VERSION=$(cd dirac-common && python -m setuptools_scm | sed 's@Guessed Version @@g' | sed -E 's@(\.dev|\+g).+@@g')
120+
python .github/workflows/pin_diraccommon_version.py "$DIRACCOMMON_VERSION"
121+
fi
111122
python -m build
112123
- name: Make release on GitHub
113124
if: steps.check-tag.outputs.create-release == 'true'
@@ -123,7 +134,14 @@ jobs:
123134
--version="${NEW_VERSION}" \
124135
--rev="$(git rev-parse HEAD)" \
125136
--release-notes-fn="release.notes.new"
126-
- name: Publish package on PyPI
137+
- name: Publish DIRACCommon to PyPI
138+
if: steps.check-tag.outputs.create-release == 'true'
139+
uses: pypa/gh-action-pypi-publish@release/v1
140+
with:
141+
user: __token__
142+
password: ${{ secrets.PYPI_API_TOKEN }}
143+
packages-dir: dirac-common/dist/
144+
- name: Publish DIRAC to PyPI
127145
if: steps.check-tag.outputs.create-release == 'true'
128146
uses: pypa/gh-action-pypi-publish@release/v1
129147
with:

.github/workflows/dirac-common.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: DIRACCommon Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- integration
7+
- rel-*
8+
paths:
9+
- 'dirac-common/**'
10+
- '.github/workflows/dirac-common.yml'
11+
pull_request:
12+
branches:
13+
- integration
14+
- rel-*
15+
paths:
16+
- 'dirac-common/**'
17+
- '.github/workflows/dirac-common.yml'
18+
19+
jobs:
20+
test-dirac-common:
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0 # Need full history for setuptools_scm
27+
28+
- uses: prefix-dev/setup-pixi@v0.9.0
29+
with:
30+
run-install: false
31+
post-cleanup: false
32+
33+
- name: Apply workarounds
34+
run: |
35+
# Workaround for https://github.com/prefix-dev/pixi/issues/3762
36+
sed -i.bak 's@editable = true@editable = false@g' dirac-common/pyproject.toml
37+
rm dirac-common/pyproject.toml.bak
38+
# Show any changes
39+
git diff
40+
41+
- uses: prefix-dev/setup-pixi@v0.9.0
42+
with:
43+
cache: false
44+
environments: testing
45+
manifest-path: dirac-common/pyproject.toml
46+
47+
- name: Run tests with pixi
48+
run: |
49+
cd dirac-common
50+
pixi add --feature testing pytest-github-actions-annotate-failures
51+
pixi run pytest
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Pin DIRACCommon version in setup.cfg during deployment.
4+
5+
This script is used during the deployment process to ensure DIRAC
6+
depends on the exact version of DIRACCommon being released.
7+
"""
8+
9+
import re
10+
import sys
11+
from pathlib import Path
12+
import subprocess
13+
14+
15+
def get_diraccommon_version():
16+
"""Get the current version of DIRACCommon from setuptools_scm."""
17+
result = subprocess.run(
18+
["python", "-m", "setuptools_scm"], cwd="dirac-common", capture_output=True, text=True, check=True
19+
)
20+
# Extract version from output like "Guessed Version 9.0.0a65.dev7+g995f95504"
21+
version_match = re.search(r"Guessed Version (\S+)", result.stdout)
22+
if not version_match:
23+
# Try direct output format
24+
version = result.stdout.strip()
25+
else:
26+
version = version_match.group(1)
27+
28+
# Clean up the version for release (remove dev and git hash parts)
29+
version = re.sub(r"(\.dev|\+g).+", "", version)
30+
return version
31+
32+
33+
def pin_diraccommon_version(version):
34+
"""Pin DIRACCommon to exact version in setup.cfg."""
35+
setup_cfg = Path("setup.cfg")
36+
content = setup_cfg.read_text()
37+
38+
# Replace the DIRACCommon line with exact version pin
39+
updated_content = re.sub(r"^(\s*)DIRACCommon\s*$", f"\\1DIRACCommon=={version}", content, flags=re.MULTILINE)
40+
41+
if content == updated_content:
42+
print(f"Warning: DIRACCommon line not found or already pinned in setup.cfg")
43+
return False
44+
45+
setup_cfg.write_text(updated_content)
46+
print(f"Pinned DIRACCommon to version {version} in setup.cfg")
47+
return True
48+
49+
50+
def main():
51+
if len(sys.argv) > 1:
52+
version = sys.argv[1]
53+
else:
54+
version = get_diraccommon_version()
55+
56+
if pin_diraccommon_version(version):
57+
print(f"Successfully pinned DIRACCommon to {version}")
58+
sys.exit(0)
59+
else:
60+
print("Failed to pin DIRACCommon version")
61+
sys.exit(1)
62+
63+
64+
if __name__ == "__main__":
65+
main()

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,8 @@ docs/source/AdministratorGuide/CommandReference
8686
docs/source/UserGuide/CommandReference
8787
docs/_build
8888
docs/source/_build
89+
90+
# pixi environments
91+
.pixi
92+
*.egg-info
93+
pixi.lock

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ There are three available options for installation:
9393
mamba create --name my-dirac-env -c conda-forge dirac-grid
9494
conda activate my-dirac-env
9595
96-
3. **Pip:** Provided suitable dependencies are available DIRAC can be installed with ``pip install DIRAC``. Support for installing the dependencies should be sought from the upstream projects.
96+
3. **Pip:** Provided suitable dependencies are available DIRAC can be installed with ``pip install DIRACCommon DIRAC``. Note that DIRACCommon must be installed first as it provides core utilities. Support for installing the dependencies should be sought from the upstream projects.
9797

9898
Development
9999
===========

dirac-common/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# DIRACCommon
2+
3+
Stateless utilities extracted from DIRAC for use by DiracX and other projects without triggering DIRAC's global state initialization.
4+
5+
## Purpose
6+
7+
This package solves the circular dependency issue where DiracX needs DIRAC utilities but importing DIRAC triggers global state initialization. DIRACCommon contains only stateless utilities that can be safely imported without side effects.
8+
9+
## Contents
10+
11+
- `DIRACCommon.Utils.ReturnValues`: DIRAC's S_OK/S_ERROR return value system
12+
- `DIRACCommon.Utils.DErrno`: DIRAC error codes and utilities
13+
14+
## Installation
15+
16+
```bash
17+
pip install DIRACCommon
18+
```
19+
20+
## Usage
21+
22+
```python
23+
from DIRACCommon.Utils.ReturnValues import S_OK, S_ERROR
24+
25+
def my_function():
26+
if success:
27+
return S_OK("Operation successful")
28+
else:
29+
return S_ERROR("Operation failed")
30+
```
31+
32+
## Development
33+
34+
This package is part of the DIRAC project and shares its version number. When DIRAC is released, DIRACCommon is also released with the same version.
35+
36+
```bash
37+
pixi install
38+
pixi run pytest
39+
```
40+
41+
## Guidelines for Adding Code
42+
43+
Code added to DIRACCommon must:
44+
- Be completely stateless
45+
- Not import or use any of DIRAC's global objects (`gConfig`, `gLogger`, `gMonitor`, `Operations`)
46+
- Not establish database connections
47+
- Not have side effects on import

dirac-common/pyproject.toml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
[build-system]
2+
requires = ["hatchling", "hatch-vcs"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "DIRACCommon"
7+
description = "Stateless utilities extracted from DIRAC for use by DiracX and other projects"
8+
readme = "README.md"
9+
requires-python = ">=3.11"
10+
license = {text = "GPL-3.0-only"}
11+
authors = [
12+
{name = "DIRAC Collaboration", email = "dirac-dev@cern.ch"},
13+
]
14+
classifiers = [
15+
"Development Status :: 5 - Production/Stable",
16+
"Intended Audience :: Science/Research",
17+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
18+
"Programming Language :: Python :: 3",
19+
"Topic :: Scientific/Engineering",
20+
"Topic :: System :: Distributed Computing",
21+
]
22+
dependencies = [
23+
"typing-extensions>=4.0.0",
24+
]
25+
dynamic = ["version"]
26+
27+
[project.optional-dependencies]
28+
testing = [
29+
"pytest>=7.0.0",
30+
"pytest-cov>=4.0.0",
31+
]
32+
33+
[project.urls]
34+
Homepage = "https://github.com/DIRACGrid/DIRAC"
35+
Documentation = "https://dirac.readthedocs.io/"
36+
"Source Code" = "https://github.com/DIRACGrid/DIRAC"
37+
38+
[tool.hatch.version]
39+
source = "vcs"
40+
41+
[tool.hatch.version.raw-options]
42+
root = ".."
43+
44+
[tool.hatch.build.targets.sdist]
45+
include = [
46+
"/src",
47+
"/tests",
48+
]
49+
50+
[tool.hatch.build.targets.wheel]
51+
packages = ["src/DIRACCommon"]
52+
53+
[tool.pytest.ini_options]
54+
testpaths = ["tests"]
55+
python_files = "test_*.py"
56+
python_classes = "Test*"
57+
python_functions = "test_*"
58+
addopts = ["-v", "--cov=DIRACCommon", "--cov-report=term-missing"]
59+
60+
[tool.coverage.run]
61+
source = ["src/DIRACCommon"]
62+
omit = ["*/tests/*"]
63+
64+
[tool.coverage.report]
65+
exclude_lines = [
66+
"pragma: no cover",
67+
"def __repr__",
68+
"raise AssertionError",
69+
"raise NotImplementedError",
70+
"if __name__ == .__main__.:",
71+
"if TYPE_CHECKING:",
72+
]
73+
74+
[tool.mypy]
75+
python_version = "3.11"
76+
files = ["src/DIRACCommon"]
77+
strict = true
78+
warn_return_any = true
79+
warn_unused_configs = true
80+
disallow_untyped_defs = true
81+
disallow_incomplete_defs = true
82+
check_untyped_defs = true
83+
no_implicit_optional = true
84+
warn_redundant_casts = true
85+
warn_unused_ignores = true
86+
warn_no_return = true
87+
warn_unreachable = true
88+
strict_equality = true
89+
90+
[tool.ruff]
91+
line-length = 120
92+
target-version = "py311"
93+
select = [
94+
"E", # pycodestyle errors
95+
"F", # pyflakes
96+
"B", # flake8-bugbear
97+
"I", # isort
98+
"PLE", # pylint errors
99+
"UP", # pyupgrade
100+
]
101+
ignore = [
102+
"B905", # zip without explicit strict parameter
103+
"B008", # do not perform function calls in argument defaults
104+
"B006", # do not use mutable data structures for argument defaults
105+
]
106+
107+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
108+
# This ensures DIRACCommon never imports from DIRAC
109+
"DIRAC" = {msg = "DIRACCommon must not import from DIRAC to avoid global state initialization"}
110+
111+
[tool.black]
112+
line-length = 120
113+
target-version = ['py311']
114+
115+
[tool.isort]
116+
profile = "black"
117+
line_length = 120
118+
119+
[tool.pixi.workspace]
120+
channels = ["conda-forge"]
121+
platforms = ["linux-64", "linux-aarch64", "osx-arm64"]
122+
123+
[tool.pixi.pypi-dependencies]
124+
DIRACCommon = { path = ".", editable = true }
125+
126+
[tool.pixi.feature.testing.tasks.pytest]
127+
cmd = "pytest"
128+
129+
[tool.pixi.environments]
130+
default = { solve-group = "default" }
131+
testing = { features = ["testing"], solve-group = "default" }
132+
133+
[tool.pixi.tasks]

0 commit comments

Comments
 (0)