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
80 changes: 27 additions & 53 deletions .github/workflows/pyharp.yml → .github/workflows/harp.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: pyharp test suite
name: harp

on:
pull_request:
push:
branches:
- master
- main
release:
types: [published]
workflow_dispatch:
Expand Down Expand Up @@ -38,58 +38,42 @@ jobs:
- name: Run ruff check
run: uv run ruff check

- name: Run ty
run: uv run ty check
- name: Run pyright
run: uv run pyright

- name: Run pytest
run: uv run pytest --cov harp

- name: Build
run: uv build --all-packages

prepare-release:
# ---- Release build ----
build-release:
runs-on: ubuntu-latest
name: Set version from release tag
name: Build release distributions
needs: tests
if: github.event_name == 'release' && github.event.action == 'published'
outputs:
version: ${{ steps.get_version.outputs.version }}

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: master

- uses: astral-sh/setup-uv@v8.3.2
with:
enable-cache: true

- name: Extract version from tag
id: get_version
shell: bash
run: |
version=$(echo "${{ github.event.release.tag_name }}" | sed 's/^v//')
echo "version=$version" >> $GITHUB_OUTPUT
echo "Setting version to: $version"

- name: Validate version format
run: uv version ${{ steps.get_version.outputs.version }} --dry-run
- name: Build
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.release.tag_name }}
run: uv build --all-packages

- name: Set version across all workspace packages
- name: Verify setuptools-scm applied the release tag
shell: bash
run: |
version="${{ steps.get_version.outputs.version }}"
# Discover every workspace member (root + src/packages/*) by its declared
# package name so all versions move in lock-step with the release tag.
mapfile -t pkgs < <(grep -h '^name = ' pyproject.toml src/packages/*/pyproject.toml | sed -E 's/^name = "(.*)"/\1/')
for pkg in "${pkgs[@]}"; do
echo "Setting $pkg -> $version"
uv version --package "$pkg" "$version"
done

- name: Build
run: uv build --all-packages
ls -1 dist/
if ls dist/*-0.0.0.tar.gz >/dev/null 2>&1; then
echo "::error::Built version 0.0.0, so setuptools-scm did not apply the tag. Check that every pyproject.toml still declares a tool.setuptools_scm table."
exit 1
fi

- name: Remove internal-only packages from dist
shell: bash
Expand All @@ -101,27 +85,17 @@ jobs:
name: dist
path: dist/

# This step seems a bit complicated, but it allows for idempotent commits,
# so that if the release tag is moved to a new commit,
# the version will be updated and committed again.
- name: Commit version changes
shell: bash
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add .
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "Set version ${{ steps.get_version.outputs.version }} [skip ci]"
git push origin master
# Move the release tag to point to this new commit.
git tag -fa "${{ github.event.release.tag_name }}" -m "${{ github.event.release.tag_name }}"
git push origin "${{ github.event.release.tag_name }}" --force

# ---- Publish to PyPI ----
publish-to-pypi:
runs-on: ubuntu-latest
name: Publish to PyPI
needs: prepare-release
needs: build-release
environment: pypi
permissions:
# uv publish exchanges this token with PyPI trusted publishing.
id-token: write
# action-gh-release attaches the distributions to the release.
contents: write
steps:
- name: Download wheels artifact
uses: actions/download-artifact@v8
Expand All @@ -134,7 +108,7 @@ jobs:
enable-cache: true

- name: Publish to PyPI
run: uv publish --token ${{ secrets.PYPI_TOKEN }}
run: uv publish

- name: Upload wheels to GitHub release
uses: softprops/action-gh-release@v3
Expand All @@ -145,13 +119,13 @@ jobs:
build-docs:
name: Build and deploy documentation to GitHub Pages
runs-on: ubuntu-latest
needs: prepare-release
needs: build-release
if: github.event_name == 'release' && !github.event.release.prerelease
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: master
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v8.3.2
Expand Down
48 changes: 34 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "harp"
version = "0.0.0"
dynamic = ["version"]
description = "Library for data acquisition and control of devices implementing the Harp protocol."
authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }]
license = "MIT"
Expand Down Expand Up @@ -44,13 +44,14 @@ Documentation = "https://harp-tech.org/pyharp/"
Changelog = "https://github.com/harp-tech/pyharp/releases"

[build-system]
requires = ["uv_build>=0.9.5"]
build-backend = "uv_build"
requires = ["setuptools>=77", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[tool.uv.build-backend]
module-name = "harp"
module-root = "src"
namespace = true
[tool.setuptools_scm]

[tool.setuptools.packages.find]
where = ["src"]
include = ["harp*"]

[tool.uv.sources]
harp-protocol = { workspace = true }
Expand All @@ -77,10 +78,10 @@ docs = [

dev = [
"codespell>=2.3.0",
"pyright>=1.1.411",
"pytest>=8.3.5",
"pytest-cov>=6.1.1",
"ruff>=0.11.0",
"ty>=0.0.0",
"harp-protocol",
"harp-device",
"harp-serial",
Expand Down Expand Up @@ -109,17 +110,36 @@ python_files = [
line-length = 100
target-version = "py311"

[tool.ty.environment]
python-version = "3.11"
extra-paths = ["tests"]

[tool.ty.src]
[tool.pyright]
pythonVersion = "3.11"
typeCheckingMode = "standard"
include = [
"src/packages/harp-protocol/src",
"src/packages/harp-device/src",
"src/packages/harp-serial/src",
"src/packages/harp-data/src",
]
exclude = [
"**/node_modules",
"**/__pycache__",
"**/.*",
".venv",
"tests",
]
]
venvPath = "."
venv = ".venv"
reportImportCycles = "error"
reportUnusedImport = "error"
reportUnusedClass = "error"
reportUnusedFunction = "error"
reportUnusedVariable = "error"
reportDuplicateImport = "error"
reportWildcardImportFromLibrary = "error"
reportCallInDefaultInitializer = "error"
reportUnnecessaryIsInstance = "error"
reportUnnecessaryCast = "error"
reportUnnecessaryContains = "error"
reportAssertAlwaysTrue = "error"
reportSelfClsParameterName = "error"
reportUnusedExpression = "error"
reportMatchNotExhaustive = "error"
15 changes: 9 additions & 6 deletions src/packages/harp-benchmarks/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "harp-benchmarks"
version = "0.1.0"
dynamic = ["version"]
description = "Internal parsing-speed benchmarks for the Harp register/payload API."
requires-python = ">=3.11"
# INTERNAL ONLY — never published to PyPI. The "Private :: Do Not Upload" trove
Expand All @@ -19,9 +19,12 @@ harp-benchmark = "harp.benchmarks.benchmark:main"
harp-benchmark-generate = "harp.benchmarks.generate:main"

[build-system]
requires = ["uv_build>=0.9.5"]
build-backend = "uv_build"
requires = ["setuptools>=77", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[tool.uv.build-backend]
module-name = "harp.benchmarks"
module-root = "src"
[tool.setuptools_scm]
root = "../../.."

[tool.setuptools.packages.find]
where = ["src"]
include = ["harp*"]
15 changes: 9 additions & 6 deletions src/packages/harp-data/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "harp-data"
version = "0.1.0"
dynamic = ["version"]
description = "Load Harp device data into pandas DataFrames"
requires-python = ">=3.11"
dependencies = [
Expand All @@ -10,9 +10,12 @@ dependencies = [
]

[build-system]
requires = ["uv_build>=0.9.5"]
build-backend = "uv_build"
requires = ["setuptools>=77", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[tool.uv.build-backend]
module-name = "harp.data"
module-root = "src"
[tool.setuptools_scm]
root = "../../.."

[tool.setuptools.packages.find]
where = ["src"]
include = ["harp*"]
15 changes: 9 additions & 6 deletions src/packages/harp-device/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
[project]
name = "harp-device"
version = "0.1.0"
dynamic = ["version"]
description = "Transport-agnostic Harp device protocol layer"
requires-python = ">=3.11"
dependencies = [
"harp-protocol",
]

[build-system]
requires = ["uv_build>=0.9.5"]
build-backend = "uv_build"
requires = ["setuptools>=77", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[tool.uv.build-backend]
module-name = "harp.device"
module-root = "src"
[tool.setuptools_scm]
root = "../../.."

[tool.setuptools.packages.find]
where = ["src"]
include = ["harp*"]
15 changes: 9 additions & 6 deletions src/packages/harp-protocol/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "harp-protocol"
version = "0.4.0"
dynamic = ["version"]
description = "Library with the base types for Harp protocol usage."
authors = [{ name = "harp-tech", email = "contact@harp-tech.org" }]
license = "MIT"
Expand All @@ -12,9 +12,12 @@ dependencies = [
]

[build-system]
requires = ["uv_build>=0.9.5"]
build-backend = "uv_build"
requires = ["setuptools>=77", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

[tool.uv.build-backend]
module-name = "harp.protocol"
module-root = "src"
[tool.setuptools_scm]
root = "../../.."

[tool.setuptools.packages.find]
where = ["src"]
include = ["harp*"]
10 changes: 5 additions & 5 deletions src/packages/harp-protocol/src/harp/protocol/_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def __get__(self, obj: "PayloadBase | None", owner: object = None) -> Any:
if self._mask is not None:
raw = (obj._arr[self._slot] & self._mask) >> self._shift
return self._converter.decode_scalar(self._converter.dtype.type(raw))
return self._converter.decode_scalar(obj._arr[self._slot]) # ty: ignore[invalid-argument-type]
return self._converter.decode_scalar(obj._arr[self._slot]) # pyright: ignore[reportArgumentType]

def _to_batch(self) -> "_FieldBatch[T]":
"""Returns the metadata for the corresponding Batch type"""
Expand Down Expand Up @@ -599,7 +599,7 @@ def _build_struct_dtype(
else:
itemsize = max(slot.byte_offset + slot.dtype.itemsize for slot in slots.values())
_validate_no_overlap(cls, slots, itemsize)
return np.dtype( # ty: ignore[no-matching-overload]
return np.dtype(
{
"names": list(slots),
"formats": [slot.dtype for slot in slots.values()],
Expand Down Expand Up @@ -671,7 +671,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
for attr_name, value in kwargs.items():
desc = cls._mro_descriptor(attr_name)
if isinstance(desc, Field) and desc._mask is None: # whole-element Field
desc._converter.encode_into(arr[desc._slot], value) # ty: ignore[invalid-argument-type]
desc._converter.encode_into(arr[desc._slot], value)
elif isinstance(desc, (GroupMask, BitMask, Field)):
# masked sub-field -> encode, shift, merge into the shared slot
mask = desc._mask
Expand Down Expand Up @@ -951,7 +951,7 @@ def __init_subclass__(
f"multi-field payloads."
)
cls._root = True
super().__init_subclass__(**kwargs) # ty: ignore[invalid-argument-type]
super().__init_subclass__(**kwargs) # pyright: ignore[reportArgumentType]
return
# Raw scalar slot required, unless a Batch twin / array concrete supplies dtype.
if scalar_dtype is None and "_batch_of" not in kwargs and "dtype" not in cls.__dict__:
Expand All @@ -963,7 +963,7 @@ def __init_subclass__(
if scalar_dtype is not None:
cls.dtype = np.dtype(scalar_dtype)
cls._repr_fields = ()
super().__init_subclass__(**kwargs) # ty: ignore[invalid-argument-type]
super().__init_subclass__(**kwargs) # pyright: ignore[reportArgumentType]

def __init__(self, value: object = _MISSING_INIT, /, **kwargs: object) -> None: # type: ignore[override]
if type(self)._root:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def decode_batch(self, view: NDArray[np.generic]) -> Any:
[bytes(row).rstrip(b"\x00").decode(self._encoding) for row in view],
dtype=object,
)
return view.reshape(-1, self._length).view(f"S{self._length}").reshape(-1).astype(str) # ty: ignore[no-matching-overload]
return view.reshape(-1, self._length).view(f"S{self._length}").reshape(-1).astype(str)

def encode_into(self, view: NDArray[np.generic], value: str) -> None:
encoded = value.encode(self._encoding)[: self._length]
Expand Down
Loading