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
43 changes: 43 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: "Publish"

on:
push:
tags:
# Publish on any tag starting with a `v`, e.g., v0.1.0
- v*

jobs:
run:
runs-on: ubuntu-latest
environment:
name: pypi
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
- name: Install Python 3.12
run: uv python install 3.12
- name: Build
run: uv build
- name: Verify tag matches version
run: |
VERSION=$(python3 -c "import ast; d = {}; content = open('temoa/__about__.py').read(); exec(compile(ast.parse(content), 'temoa/__about__.py', 'exec'), d); print(d['__version__'])")
TAG=${GITHUB_REF#refs/tags/v}
if [ "$VERSION" != "$TAG" ]; then
echo "Error: Tag v$TAG does not match version $VERSION in temoa/__about__.py"
exit 1
fi
echo "Tag v$TAG matches version $VERSION"
# Check that basic features work and we didn't miss to include crucial files
- name: Smoke test (wheel)
timeout-minutes: 10
run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
- name: Smoke test (source distribution)
timeout-minutes: 10
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Publish
run: uv publish
Comment thread
coderabbitai[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
!output_files/
!.github/
!stubs/
!scripts/

# unignore files
!.gitignore
Expand Down
150 changes: 150 additions & 0 deletions scripts/bump_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from typing import Any


def parse_version(version_str: str) -> dict[str, Any]:
"""
Parses a PEP 440 version string into its components.
Basic supported format: major.minor.patch[a|b|rcN]
"""
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:([abc]|rc)(\d+))?$'
match = re.match(pattern, version_str)
if not match:
raise ValueError(f"Version '{version_str}' does not match expected format X.Y.Z[a|b|rcN]")

major, minor, patch, pre_type, pre_num = match.groups()
return {
'major': int(major),
'minor': int(minor),
'patch': int(patch),
'pre_type': pre_type,
'pre_num': int(pre_num) if pre_num else None,
}


def stringify_version(components: dict[str, Any]) -> str:
base = f'{components["major"]}.{components["minor"]}.{components["patch"]}'
if components['pre_type']:
return f'{base}{components["pre_type"]}{components["pre_num"] or 1}'
return base


def bump_version(components: dict[str, Any], part: str) -> dict[str, Any]:
new = components.copy()

if part == 'major':
new['major'] += 1
new['minor'] = 0
new['patch'] = 0
new['pre_type'] = None
new['pre_num'] = None
elif part == 'minor':
new['minor'] += 1
new['patch'] = 0
new['pre_type'] = None
new['pre_num'] = None
elif part == 'patch':
new['patch'] += 1
new['pre_type'] = None
new['pre_num'] = None
elif part in ['a', 'b', 'rc', 'alpha', 'beta']:
pre_map = {'alpha': 'a', 'beta': 'b', 'rc': 'rc', 'a': 'a', 'b': 'b'}
target_pre = pre_map[part]
pre_ordinal = {'a': 0, 'b': 1, 'rc': 2}

# Check for precedence to prevent downgrading within the same patch version
if new['pre_type'] and pre_ordinal[target_pre] < pre_ordinal[new['pre_type']]:
new['patch'] += 1
new['pre_type'] = target_pre
new['pre_num'] = 1
elif new['pre_type'] == target_pre:
new['pre_num'] = (new['pre_num'] or 0) + 1
else:
# If moving to higher precedence (e.g., a -> b) or starting from final
if not new['pre_type']:
new['patch'] += 1
new['pre_type'] = target_pre
new['pre_num'] = 1
elif part == 'final':
Comment thread
coderabbitai[bot] marked this conversation as resolved.
new['pre_type'] = None
new['pre_num'] = None
else:
raise ValueError(f'Unknown part to bump: {part}')

return new


def main() -> None:
parser = argparse.ArgumentParser(description='Bump Temoa version in temoa/__about__.py')
parser.add_argument(
'part',
choices=['major', 'minor', 'patch', 'alpha', 'beta', 'rc', 'final', 'a', 'b'],
help='The part of the version to bump',
)
parser.add_argument('--dry-run', action='store_true', help="Don't write to file")

args = parser.parse_args()

about_path = Path('temoa/__about__.py')
if not about_path.exists():
print(f'Error: {about_path} not found.')
sys.exit(1)

content = about_path.read_text()
version_match = re.search(r"__version__ = ['\"]([^'\"]+)['\"]", content)
if not version_match:
print('Error: Could not find __version__ in temoa/__about__.py')
sys.exit(1)

old_version_str = version_match.group(1)
try:
components = parse_version(old_version_str)
except ValueError as e:
print(f'Error: {e}')
sys.exit(1)

new_components = bump_version(components, args.part)
new_version_str = stringify_version(new_components)

if old_version_str == new_version_str:
print(f'Version is already {new_version_str}. No change made.')
return

print(f'Bumping version: {old_version_str} -> {new_version_str}')

if not args.dry_run:
new_content = content.replace(
f"__version__ = '{old_version_str}'", f"__version__ = '{new_version_str}'"
)
new_content = new_content.replace(
f'__version__ = "{old_version_str}"', f'__version__ = "{new_version_str}"'
)

# Also update TEMOA_MAJOR and TEMOA_MINOR if they changed
if new_components['major'] != components['major']:
new_content = re.sub(
r'TEMOA_MAJOR = \d+', f'TEMOA_MAJOR = {new_components["major"]}', new_content
)
if new_components['minor'] != components['minor']:
new_content = re.sub(
r'TEMOA_MINOR = \d+', f'TEMOA_MINOR = {new_components["minor"]}', new_content
)

about_path.write_text(new_content)
print(f'Successfully updated {about_path}')

print('\nNext steps:')
print(f' git add {about_path}')
print(f' git commit -m "chore: bump version to {new_version_str}"')
print(f' git tag -a v{new_version_str} -m "Release v{new_version_str}"')
print(f' git push origin v{new_version_str}')
else:
print('Dry run: file not modified.')


if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion temoa/__about__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re

__version__ = '4.0.0a1.dev20251201'
__version__ = '4.0.0a2'

# Parse the version string to get major and minor versions
# We use a regex to be robust against versions like "4.1a1" or "4.0.0.dev1"
Expand Down
45 changes: 45 additions & 0 deletions tests/smoke_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import shutil
import subprocess

import temoa


def _find_temoa_path() -> str:
path = shutil.which('temoa')
if not path:
raise RuntimeError('temoa executable not found in PATH')
return path


def test_import() -> None:
print(f'Importing temoa version: {temoa.__version__}')
assert temoa.__version__ is not None
Comment thread
ParticularlyPythonicBS marked this conversation as resolved.


def test_cli() -> None:
print('Running temoa --version CLI command...')
temoa_path = _find_temoa_path()

result = subprocess.run(
[temoa_path, '--version'], capture_output=True, text=True, timeout=10, check=True
)
print(f'CLI output: {result.stdout.strip()}')
assert 'Temoa Version:' in result.stdout
assert temoa.__version__ in result.stdout
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def test_help() -> None:
print('Running temoa --help CLI command...')
temoa_path = _find_temoa_path()

result = subprocess.run(
[temoa_path, '--help'], capture_output=True, text=True, timeout=10, check=True
)
assert 'The Temoa Project' in result.stdout


if __name__ == '__main__':
test_import()
test_cli()
test_help()
print('\n✅ Smoke test passed!')
Loading