Skip to content

chore: Update supported Python versions through copier template update #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: 2.5.0
_commit: 3.0.0
_src_path: gh:DiamondLightSource/python-copier-template
author_email: [email protected]
author_name: Tom Cobb
Expand Down
8 changes: 4 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@
}
},
"features": {
// Some default things like git config
"ghcr.io/devcontainers/features/common-utils:2": {
"upgradePackages": false
}
// add in eternal history and other bash features
"ghcr.io/diamondlightsource/devcontainer-features/bash-config:1": {}
},
// Create the config folder for the bash-config feature
"initializeCommand": "mkdir -p ${localEnv:HOME}/.config/bash-config",
"runArgs": [
// Allow the container to access the host X11 display and EPICS CA
"--net=host",
Expand Down
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ It is recommended that developers use a [vscode devcontainer](https://code.visua

This project was created using the [Diamond Light Source Copier Template](https://github.com/DiamondLightSource/python-copier-template) for Python projects.

For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/2.5.0/how-to.html).
For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/3.0.0/how-to.html).
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ assignees: ''

---

Describe the bug, including a clear and concise description of the expected behavior, the actual behavior and the context in which you encountered it (ideally include details of your environment).
Describe the bug, including a clear and concise description of the expected behaviour, the actual behavior and the context in which you encountered it (ideally include details of your environment).

## Steps To Reproduce
Steps to reproduce the behavior:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Create GitHub Release
# We pin to the SHA, not the tag, for security reasons.
# https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
with:
prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }}
files: "*"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
run: tox -e tests

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
name: ${{ inputs.python-version }}/${{ inputs.runs-on }}
files: cov.xml
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
runs-on: ["ubuntu-latest", "windows-latest", "macos-latest"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.11", "3.12", "3.13"]
include:
# Include one that runs in the dev environment
- runs-on: "ubuntu-latest"
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ repos:
name: Run black
stages: [pre-commit]
language: system
entry: black --check --diff
entry: ruff check --force-exclude --fix
types: [python]

- id: ruff
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
Unreleased_
-----------

Nothing yet
Fixed:

- `Bump supported Python versions and update copier template <../../pull/59>`

1.8.1_ - 2024-11-01
-----------
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Check your version of python

You will need python 3.10 or later. You can check your version of python by
You will need python 3.11 or later. You can check your version of python by
typing into a terminal:

```
Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ name = "aioca"
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
description = "Asynchronous Channel Access client for asyncio and Python using libca via ctypes"
dependencies = [
Expand All @@ -20,7 +19,7 @@ dependencies = [
dynamic = ["version"]
license.file = "LICENSE"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.11"

[project.optional-dependencies]
dev = [
Expand Down
92 changes: 38 additions & 54 deletions src/aioca/_catools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import asyncio
import atexit
import collections
import ctypes
import functools
import inspect
Expand All @@ -9,20 +8,12 @@
import time
import traceback
import warnings
from collections import deque
from collections.abc import Awaitable, Callable, Sequence
from typing import (
Any,
Awaitable,
Callable,
Deque,
Dict,
Generic,
List,
Optional,
Sequence,
Set,
Tuple,
TypeVar,
Union,
overload,
)

Expand All @@ -31,7 +22,7 @@
from .types import AugmentedValue, Count, Datatype, Dbe, Format, Timeout

T = TypeVar("T")
PVs = Union[List[str], Tuple[str, ...]]
PVs = list[str] | tuple[str, ...]

DEFAULT_TIMEOUT = 5.0

Expand All @@ -48,10 +39,10 @@

class ValueEvent(Generic[T]):
def __init__(self) -> None:
self.value: Union[T, Exception] = RuntimeError("No value set")
self.value: T | Exception = RuntimeError("No value set")
self._event = asyncio.Event()

def set(self, value: Union[T, Exception]):
def set(self, value: T | Exception):
self._event.set()
self.value = value

Expand Down Expand Up @@ -85,7 +76,7 @@ def __init__(self, name: str, errorcode=cadef.ECA_NORMAL) -> None:
self.errorcode: int = errorcode

def __repr__(self):
return "CANothing(%r, %d)" % (self.name, self.errorcode)
return f"CANothing('{self.name}', {self.errorcode})"

def __str__(self):
return f"{self.name}: {cadef.ca_message(self.errorcode)}"
Expand Down Expand Up @@ -153,14 +144,14 @@ async def ca_timeout(awaitable: Awaitable[T], name: str, timeout: Timeout = None
timeout = timeout[0] - time.time()
try:
result = await asyncio.wait_for(awaitable, timeout)
except asyncio.TimeoutError as e:
except TimeoutError as e:
raise CANothing(name, cadef.ECA_TIMEOUT) from e
else:
result = await awaitable
return result


def parallel_timeout(kwargs: Dict[str, Any]) -> Dict[str, Any]:
def parallel_timeout(kwargs: dict[str, Any]) -> dict[str, Any]:
"""Return kwargs with a suitable timeout for running in parallel"""
if kwargs.get("throw", True):
# told to throw, so remove the timeout as it will be done at the top level
Expand All @@ -169,8 +160,8 @@ def parallel_timeout(kwargs: Dict[str, Any]) -> Dict[str, Any]:


async def in_parallel(
awaitables: Sequence[Awaitable[T]], kwargs: Dict[str, Any]
) -> List[T]:
awaitables: Sequence[Awaitable[T]], kwargs: dict[str, Any]
) -> list[T]:
if kwargs.get("throw", True):
# timeout at this level, awaitables will not timeout themselves
timeout = kwargs.get("timeout", DEFAULT_TIMEOUT)
Expand Down Expand Up @@ -224,7 +215,7 @@ def on_ca_connect_(self, op):
def __init__(self, name: str, loop: asyncio.AbstractEventLoop):
"""Creates a channel access channel with the given name."""
self.name = name
self.__subscriptions: Set[Subscription] = set()
self.__subscriptions: set[Subscription] = set()
self.__connect_event = ValueEvent[None]()
self.__event_loop = loop

Expand Down Expand Up @@ -275,10 +266,10 @@ class ChannelCache:
ensure a clean shutdown."""

def __init__(self, loop: asyncio.AbstractEventLoop):
self.__channels: Dict[str, Channel] = {}
self.__channels: dict[str, Channel] = {}
self.__loop = loop
self.__waiting = True
self.__callbacks: Deque[Tuple[Callable, Tuple]] = collections.deque()
self.__callbacks: deque[tuple[Callable, tuple]] = deque()

def get_channel(self, name: str) -> Channel:
try:
Expand All @@ -290,7 +281,7 @@ def get_channel(self, name: str) -> Channel:
self.__channels[name] = channel
return channel

def get_channels(self) -> List[Channel]:
def get_channels(self) -> list[Channel]:
return list(self.__channels.values())

def purge(self):
Expand Down Expand Up @@ -355,7 +346,7 @@ class Subscription:
def __init__(
self,
name: str,
callback: Callable[[Any], Union[None, Awaitable]],
callback: Callable[[Any], Awaitable | None],
events: Dbe,
datatype: Datatype,
format: Format,
Expand All @@ -372,12 +363,12 @@ def __init__(
#: while another callback was in progress
self.dropped_callbacks: int = 0
self.__event_loop = asyncio.get_event_loop()
self.pending_values: Deque[AugmentedValue] = collections.deque(
self.pending_values: deque[AugmentedValue] = deque(
maxlen=None if all_updates else 1
)
self.__future: Optional[asyncio.Future] = None
self.__future: asyncio.Future | None = None
self.__lock = threading.Lock() # Used for update merging.
self.__is_async: Optional[bool] = None
self.__is_async: bool | None = None

# If events not specified then compute appropriate default corresponding
# to the requested format.
Expand Down Expand Up @@ -407,9 +398,9 @@ def __on_event(args) -> None: # pragma: no cover
self: Subscription = args.usr

try:
assert (
args.status == cadef.ECA_NORMAL
), f"Subscription {self.name} got bad status {args.status}"
assert args.status == cadef.ECA_NORMAL, (
f"Subscription {self.name} got bad status {args.status}"
)
# Good data: extract value from the dbr. Note that this can fail
value = self.dbr_to_value(args.raw_dbr, args.type, args.count)
self.__queue_value(value)
Expand Down Expand Up @@ -565,7 +556,7 @@ async def __create_subscription(
@overload
def camonitor(
pv: str,
callback: Callable[[Any], Union[None, Awaitable]],
callback: Callable[[Any], Awaitable | None],
events: Dbe = ...,
datatype: Datatype = ...,
format: Format = ...,
Expand All @@ -579,15 +570,15 @@ def camonitor(
@overload
def camonitor(
pv: PVs,
callback: Callable[[Any, int], Union[None, Awaitable]],
callback: Callable[[Any, int], Awaitable | None],
events: Dbe = ...,
datatype: Datatype = ...,
format: Format = ...,
count: Count = ...,
all_updates: bool = ...,
notify_disconnect: bool = ...,
connect_timeout: Timeout = ...,
) -> List[Subscription]: ... # pragma: no cover
) -> list[Subscription]: ... # pragma: no cover


def camonitor(
Expand Down Expand Up @@ -684,7 +675,7 @@ async def caget(
count: Count = ...,
timeout: Timeout = ...,
throw: bool = ...,
) -> List[AugmentedValue]: ... # pragma: no cover
) -> list[AugmentedValue]: ... # pragma: no cover


@maybe_throw
Expand Down Expand Up @@ -788,7 +779,7 @@ async def caput(
wait: bool = ...,
timeout: Timeout = ...,
throw: bool = ...,
) -> List[CANothing]: ... # pragma: no cover
) -> list[CANothing]: ... # pragma: no cover


@maybe_throw
Expand Down Expand Up @@ -864,7 +855,8 @@ async def caput_array(pvs: PVs, values, repeat_value=False, **kwargs):
values = [values] * len(pvs)
assert len(pvs) == len(values), "PV and value lists must match in length"
coros = [
caput(pv, value, **parallel_timeout(kwargs)) for pv, value in zip(pvs, values)
caput(pv, value, **parallel_timeout(kwargs))
for pv, value in zip(pvs, values, strict=False)
]
results = await in_parallel(coros, kwargs)
return results
Expand Down Expand Up @@ -917,20 +909,12 @@ def __init__(self, pv: str, channel: Channel):
self.datatype = 7 # DBF_NO_ACCESS

def __str__(self):
return """%s:
State: %s
Host: %s
Access: %s, %s
Data type: %s
Count: %d""" % (
self.name,
self.state_strings[self.state],
self.host,
self.read,
self.write,
self.datatype_strings[self.datatype],
self.count,
)
return f"""{self.name}:
State: {self.state_strings[self.state]}
Host: {self.host}
Access: {self.read}, {self.write}
Data type: {self.datatype_strings[self.datatype]}
Count: {self.count}"""


@overload
Expand All @@ -942,7 +926,7 @@ async def connect(
@overload
async def connect(
pv: PVs, wait: bool = ..., timeout: Timeout = ..., throw: bool = ...
) -> List[CANothing]: ... # pragma: no cover
) -> list[CANothing]: ... # pragma: no cover


@maybe_throw
Expand Down Expand Up @@ -988,7 +972,7 @@ async def cainfo(
@overload
async def cainfo(
pv: PVs, wait: bool = ..., timeout: Timeout = ..., throw: bool = ...
) -> List[CAInfo]: ... # pragma: no cover
) -> list[CAInfo]: ... # pragma: no cover


@maybe_throw
Expand Down Expand Up @@ -1018,7 +1002,7 @@ async def cainfo_array(pvs: PVs, wait=True, **kwargs):
class _Context:
_ca_context = None
_should_destroy = False
_channel_caches: Dict[asyncio.AbstractEventLoop, ChannelCache] = {}
_channel_caches: dict[asyncio.AbstractEventLoop, ChannelCache] = {}

@classmethod
def purge_channel_caches(cls):
Expand Down Expand Up @@ -1122,7 +1106,7 @@ def __init__(self, name, connected, subscriber_count):
self.subscriber_count = subscriber_count


def get_channel_infos() -> List[ChannelInfo]:
def get_channel_infos() -> list[ChannelInfo]:
"""Return information about all Channels"""
return [
ChannelInfo(channel.name, channel.connected(), channel.count_subscriptions())
Expand Down
Loading
Loading