diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index d87b07b0..73a619f0 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,7 +17,7 @@ jobs: name: Benchmark runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-python@v6 @@ -40,7 +40,7 @@ jobs: - name: Restore previous results if: github.event_name != 'pull_request' - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .asv key: asv-${{ runner.os }} diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 3e41d8e5..c4ebcc3c 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -11,7 +11,7 @@ jobs: name: 🐍 sdist and universal wheel runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v2 @@ -24,14 +24,14 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-13, macos-latest] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: astral-sh/setup-uv@v7 - name: Build wheels via cibuildwheel - uses: pypa/cibuildwheel@v3.2 + uses: pypa/cibuildwheel@v3.3 - name: Upload wheels artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: cibw-wheels-${{ runner.os }}-${{ runner.arch }} path: ./wheelhouse/*.whl @@ -47,12 +47,12 @@ jobs: steps: - name: Get sdist - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: name: Packages path: dist - name: Get wheels - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v7 with: pattern: cibw-wheels-* path: dist @@ -61,7 +61,7 @@ jobs: - name: 🚢 Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c31b3ed..fcb194cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: check-manifest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - run: pipx run check-manifest test: @@ -55,7 +55,7 @@ jobs: compile: "1" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 with: enable-cache: true @@ -82,7 +82,7 @@ jobs: run: uv run coverage run -p -m pytest -v - name: Upload coverage - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: covreport-${{ matrix.os }}-py${{ matrix.python-version }}-mypyc${{ matrix.compile }}-${{ matrix.qt }} path: ./.coverage* @@ -105,7 +105,7 @@ jobs: typing: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 with: enable-cache: true @@ -116,7 +116,7 @@ jobs: benchmarks: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.13" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53792103..8acbced1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,29 +12,29 @@ repos: - id: validate-pyproject - repo: https://github.com/rhysd/actionlint - rev: v1.7.8 + rev: v1.7.9 hooks: - id: actionlint files: "^\\.github/workflows/.*\\.ya?ml$" - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.38.1 + rev: v1.40.0 hooks: - id: typos args: [--force-exclude] # omitting --write-changes - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.14.7 hooks: - id: ruff-check args: [--fix, --unsafe-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.0 hooks: - id: mypy - exclude: tests|_throttler.pyi + exclude: tests|_throttler.pyi|_group.pyi additional_dependencies: - types-attrs - pydantic diff --git a/src/psygnal/_group.py b/src/psygnal/_group.py index 54da3cf1..f8fa7932 100644 --- a/src/psygnal/_group.py +++ b/src/psygnal/_group.py @@ -539,16 +539,6 @@ def __getitem__(self, item: str) -> SignalInstance: """Get a signal instance by name.""" return self._psygnal_instances[item] - # this is just here for type checking, particularly on cases - # where the SignalGroup comes from the SignalGroupDescriptor - # (such as in evented dataclasses). In those cases, it's hard to indicate - # to mypy that all remaining attributes are SignalInstances. - def __getattr__(self, __name: str) -> SignalInstance: - """Get a signal instance by name.""" - raise AttributeError( # pragma: no cover - f"{type(self).__name__!r} object has no attribute {__name!r}" - ) - def __iter__(self) -> Iterator[str]: """Yield the names of all signals in the group.""" return iter(self._psygnal_instances) diff --git a/src/psygnal/_group.pyi b/src/psygnal/_group.pyi new file mode 100644 index 00000000..cefe578a --- /dev/null +++ b/src/psygnal/_group.pyi @@ -0,0 +1,39 @@ +# This stub provides __getattr__ for type checking, which cannot be in the +# main file because mypyc doesn't support __getattr__ in classes with +# allow_interpreted_subclasses=True. +from collections.abc import Iterator, Mapping +from typing import Any, ClassVar + +from psygnal._signal import Signal, SignalInstance + +class SignalRelay(SignalInstance): + instance: Any + def __init__( + self, signals: Mapping[str, SignalInstance], instance: Any = None + ) -> None: ... + +class SignalGroup: + _psygnal_signals: ClassVar[Mapping[str, Signal]] + _psygnal_uniform: ClassVar[bool] + _psygnal_name_conflicts: ClassVar[set[str]] + _psygnal_aliases: ClassVar[dict[str, str | None]] + _psygnal_instances: dict[str, SignalInstance] + + def __init__(self, instance: Any = None) -> None: ... + def __init_subclass__( + cls, + strict: bool = False, + signal_aliases: Mapping[str, str | None] = ..., + ) -> None: ... + @property + def instance(self) -> Any: ... + @property + def all(self) -> SignalRelay: ... + @property + def signals(self) -> Mapping[str, SignalInstance]: ... + def __len__(self) -> int: ... + def __getitem__(self, item: str) -> SignalInstance: ... + def __getattr__(self, name: str) -> SignalInstance: ... + def __iter__(self) -> Iterator[str]: ... + def __contains__(self, item: str) -> bool: ... + def __repr__(self) -> str: ...