diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3731ac9..94d6d1a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,13 +22,62 @@ jobs: run: uv sync - name: Format check + id: format + continue-on-error: true run: uv run ruff format --check - name: Lint + id: lint + continue-on-error: true run: uv run ruff check - name: Type check (basedpyright) + id: basedpyright + continue-on-error: true run: uv run basedpyright typings tests - name: Type check (mypy) + id: mypy + continue-on-error: true run: uv run mypy tests + + - name: Stub test (celery) + id: stubtest-celery + continue-on-error: true + run: uv run stubtest celery --allowlist celery-stubs/allowlist.txt + + - name: Stub test (kombu) + id: stubtest-kombu + continue-on-error: true + run: uv run stubtest kombu --allowlist kombu-stubs/allowlist.txt + + - name: Stub test (billiard) + id: stubtest-billiard + continue-on-error: true + run: uv run stubtest billiard --allowlist billiard-stubs/allowlist.txt + + - name: Stub test (amqp) + id: stubtest-amqp + continue-on-error: true + run: uv run stubtest amqp + + - name: Stub test (vine) + id: stubtest-vine + continue-on-error: true + run: uv run stubtest vine + + - name: Check results + if: always() + run: | + if [[ "${{ steps.format.outcome }}" == "failure" ]] || \ + [[ "${{ steps.lint.outcome }}" == "failure" ]] || \ + [[ "${{ steps.basedpyright.outcome }}" == "failure" ]] || \ + [[ "${{ steps.mypy.outcome }}" == "failure" ]] || \ + [[ "${{ steps.stubtest-celery.outcome }}" == "failure" ]] || \ + [[ "${{ steps.stubtest-kombu.outcome }}" == "failure" ]] || \ + [[ "${{ steps.stubtest-billiard.outcome }}" == "failure" ]] || \ + [[ "${{ steps.stubtest-amqp.outcome }}" == "failure" ]] || \ + [[ "${{ steps.stubtest-vine.outcome }}" == "failure" ]]; then + echo "One or more checks failed" + exit 1 + fi diff --git a/billiard-stubs/allowlist.txt b/billiard-stubs/allowlist.txt index 9e70d2a..172ebe2 100644 --- a/billiard-stubs/allowlist.txt +++ b/billiard-stubs/allowlist.txt @@ -1,2 +1,27 @@ +# Billiard stubtest allowlist +# =========================== + +# ----------------------------------------------------------------------------- +# Platform-specific modules +# ----------------------------------------------------------------------------- + # billiard.popen_spawn_win32 is Windows-only and cannot be tested on Linux billiard.popen_spawn_win32 + +# ----------------------------------------------------------------------------- +# Lock type mismatches (Python < 3.13) +# ----------------------------------------------------------------------------- +# threading.Lock was a factory function (allocate_lock) until Python 3.13, +# where it became a proper class. Since CI runs on Python 3.10, stubtest sees +# the function while the stub types it as a class (matching typeshed and user +# expectations for type annotations). + +billiard.dummy.Lock +billiard.pool.Lock + +# ----------------------------------------------------------------------------- +# Disjoint base classes +# ----------------------------------------------------------------------------- +# AuthenticationString uses __slots__ which makes it a disjoint base. + +billiard.process.AuthenticationString diff --git a/billiard-stubs/einfo.pyi b/billiard-stubs/einfo.pyi index 2d484b7..7f82f32 100644 --- a/billiard-stubs/einfo.pyi +++ b/billiard-stubs/einfo.pyi @@ -4,16 +4,38 @@ from typing import Any, TypeAlias __all__ = ["ExceptionInfo", "Traceback"] class _Code: + co_filename: str + co_name: str + co_argcount: int + co_cellvars: tuple[()] + co_firstlineno: int + co_flags: int + co_freevars: tuple[()] + co_code: bytes + co_lnotab: bytes + co_names: tuple[str, ...] + co_nlocals: int + co_stacksize: int + co_varnames: tuple[()] + def __init__(self, code: Any) -> None: ... - @property - def co_positions(self) -> Any: ... class _Frame: Code = _Code + f_builtins: dict[str, Any] + f_globals: dict[str, Any] + f_locals: dict[str, Any] + f_back: None + f_trace: None + f_exc_traceback: None + f_exc_type: None + f_exc_value: None + f_code: _Code + f_lineno: int + f_lasti: int + f_restricted: bool def __init__(self, frame: Any) -> None: ... - @property - def co_positions(self) -> Any: ... class Traceback: Frame = _Frame diff --git a/celery-stubs/allowlist.txt b/celery-stubs/allowlist.txt new file mode 100644 index 0000000..d3ede2f --- /dev/null +++ b/celery-stubs/allowlist.txt @@ -0,0 +1,141 @@ +# Celery stubtest allowlist +# ========================= +# This file contains stubtest errors that are intentional design choices. + +# ----------------------------------------------------------------------------- +# Class-level vs instance-level attributes +# ----------------------------------------------------------------------------- +# These attributes are None at class level but always set on instances. +# We type them as non-optional since users work with instances, not classes. +# This provides better IDE autocomplete and type checking for the common case. + +# Celery app instance attributes (always set in __init__) +celery.app.base.Celery.main +celery.app.base.Celery.steps +celery.app.base.Celery.user_options + +# Celery signal attributes (Signal instances, not None, on app instances) +celery.app.base.Celery.on_configure +celery.app.base.Celery.on_after_configure +celery.app.base.Celery.on_after_finalize +celery.app.base.Celery.on_after_fork + +# Task instance attributes (always set when task is bound) +celery.app.task.Task.app +celery.app.task.Task.name +celery.Task.acks_late +celery.Task.acks_on_failure_or_timeout +celery.Task.app +celery.Task.name + +# AsyncResult instance attributes (always resolved in __init__) +celery.result.AsyncResult.app +celery.result.AsyncResult.backend +celery.result.AsyncResult.id + +# Signal.receivers (always initialized as list in __init__) +celery.utils.dispatch.Signal.receivers +celery.utils.dispatch.signal.Signal.receivers + +# Proxy objects for current app/task (always return app/task, not None) +celery._state.current_app +celery._state.current_task +celery.current_app +celery.current_task +celery.app.default_app + +# ----------------------------------------------------------------------------- +# getitem_property descriptor issues +# ----------------------------------------------------------------------------- +# Celery uses a custom getitem_property descriptor that provides both +# dict-style access (sig['args']) and attribute access (sig.args). +# stubtest cannot reconcile @property stubs with this runtime descriptor. +# The @property typing is correct for users who access these as attributes. + +celery.canvas.Signature.args +celery.canvas.Signature.id +celery.canvas.Signature.immutable +celery.canvas.Signature.kwargs +celery.canvas.Signature.options +celery.canvas.Signature.parent_id +celery.canvas.Signature.root_id +celery.canvas.Signature.task +celery.canvas._chain.tasks +celery.canvas.group.tasks + +# Re-exports of Signature (same getitem_property issues) +celery.Signature.args +celery.Signature.id +celery.Signature.immutable +celery.Signature.kwargs +celery.Signature.options +celery.Signature.parent_id +celery.Signature.root_id +celery.Signature.task +celery.group.tasks + +# ----------------------------------------------------------------------------- +# SQLAlchemy model metaclass issues +# ----------------------------------------------------------------------------- +# SQLAlchemy models use a metaclass that creates class-level descriptors. +# stubtest cannot properly introspect these InstrumentedAttribute types. + +celery.backends.database.models.Task +celery.backends.database.models.TaskExtended +celery.backends.database.models.TaskSet + +# ----------------------------------------------------------------------------- +# Intentionally stricter signatures +# ----------------------------------------------------------------------------- + +# Control.__init__: runtime has app=None but immediately crashes if None is +# passed because it accesses app.conf. Our stub correctly requires app. +celery.app.control.Control.__init__ + +# add_periodic_task: runtime uses tuple() as falsy default for kwargs, which +# gets converted to {} internally. We type as dict | None for clarity. +celery.app.base.Celery.add_periodic_task +celery.app.Celery.add_periodic_task +celery.Celery.add_periodic_task + +# ----------------------------------------------------------------------------- +# Module __all__ differences +# ----------------------------------------------------------------------------- +# Runtime __all__ includes standard module attributes (__path__, __package__, +# __file__, __doc__) which are not meaningful in type stubs. + +celery.__all__ + +# ----------------------------------------------------------------------------- +# Re-exports (duplicate errors from base modules) +# ----------------------------------------------------------------------------- +# These are re-exported from submodules, same issues as originals above. + +celery.app.Celery.main +celery.app.Celery.steps +celery.app.Celery.user_options +celery.app.Celery.on_configure +celery.app.Celery.on_after_configure +celery.app.Celery.on_after_finalize +celery.app.Celery.on_after_fork +celery.Celery.main +celery.Celery.steps +celery.Celery.user_options +celery.Celery.on_configure +celery.Celery.on_after_configure +celery.Celery.on_after_finalize +celery.Celery.on_after_fork + +# ----------------------------------------------------------------------------- +# @functools.total_ordering generated methods (Python 3.10 only) +# ----------------------------------------------------------------------------- +# These comparison methods are generated by @total_ordering and have an +# internal NotImplemented=NotImplemented parameter in Python 3.10. + +celery.beat.ScheduleEntry.__ge__ +celery.beat.ScheduleEntry.__gt__ +celery.beat.ScheduleEntry.__le__ +celery.utils.timer2.Entry.__ge__ +celery.utils.timer2.Entry.__gt__ +celery.utils.timer2.Entry.__le__ + diff --git a/celery-stubs/contrib/sphinx.pyi b/celery-stubs/contrib/sphinx.pyi index 1ecbf33..6e8e31c 100644 --- a/celery-stubs/contrib/sphinx.pyi +++ b/celery-stubs/contrib/sphinx.pyi @@ -1,4 +1,4 @@ -from typing import Any, ClassVar, Literal +from typing import Any, Literal from celery.app.task import Task as BaseTask from docutils import nodes @@ -7,8 +7,8 @@ from sphinx.ext.autodoc import Documenter, FunctionDocumenter from typing_extensions import override class TaskDocumenter(FunctionDocumenter): - objtype: ClassVar[str] - member_order: ClassVar[int] + objtype: str # type: ignore[misc] # override parent's instance var with class var + member_order: int # type: ignore[misc] # override parent's instance var with class var @override @classmethod diff --git a/kombu-stubs/allowlist.txt b/kombu-stubs/allowlist.txt new file mode 100644 index 0000000..91b287d --- /dev/null +++ b/kombu-stubs/allowlist.txt @@ -0,0 +1,40 @@ +# Kombu stubtest allowlist +# ======================== + +# ----------------------------------------------------------------------------- +# Optional transports / extensions +# ----------------------------------------------------------------------------- + +# librabbitmq is a legacy optional C extension. +kombu.transport.librabbitmq + +# gcpubsub.Transport.channel_errors has a complex runtime type that's +# a dynamically constructed tuple of exception classes from google-cloud-pubsub. +# We simplify this to tuple[type[Exception], ...] in the stub. +kombu.transport.gcpubsub.Transport.channel_errors + +# ----------------------------------------------------------------------------- +# @functools.total_ordering generated methods (Python 3.10 only) +# ----------------------------------------------------------------------------- +# These comparison methods are generated by @total_ordering and have an +# internal NotImplemented=NotImplemented parameter in Python 3.10. + +kombu.asynchronous.timer.Entry.__ge__ +kombu.asynchronous.timer.Entry.__gt__ +kombu.asynchronous.timer.Entry.__le__ + +# ----------------------------------------------------------------------------- +# Lock type mismatches (Python < 3.13) +# ----------------------------------------------------------------------------- +# threading.Lock was a factory function (allocate_lock) until Python 3.13, +# where it became a proper class. The stub types Lock as a class to match +# typeshed and user expectations for type annotations. + +kombu.clocks.LamportClock.__init__ + +# ----------------------------------------------------------------------------- +# Not yet released in kombu +# ----------------------------------------------------------------------------- +# reprkwargs is fixed in kombu main but not released yet (kombu 5.6.2). +# Remove this entry once kombu releases a version with the fix. +kombu.utils.reprkwargs diff --git a/kombu-stubs/utils/__init__.pyi b/kombu-stubs/utils/__init__.pyi index e343bf6..c5be933 100644 --- a/kombu-stubs/utils/__init__.pyi +++ b/kombu-stubs/utils/__init__.pyi @@ -7,8 +7,6 @@ from kombu.utils.functional import reprkwargs as reprkwargs from kombu.utils.functional import retry_over_time as retry_over_time from kombu.utils.objects import cached_property as cached_property -# Note: runtime __all__ includes reprkwargs but it's not actually imported in kombu (kombu bug) -# We include it here to match runtime __all__, stubtest allowlist handles the missing definition __all__ = ( "EqualityDict", "cached_property", diff --git a/pyproject.toml b/pyproject.toml index 2f2e285..3e024d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,29 +36,30 @@ Repository = "https://github.com/sbdchd/celery-types" [dependency-groups] dev = [ + "azure-servicebus>=7.14.3", + "azure-storage-queue>=12.14.1", + "basedpyright>=1.36.1", + "boto3-stubs>=1.42.19", + "boto3>=1.42.19", + "botocore>=1.42.19", "celery>=5.0,<6", + "confluent-kafka>=2.12.2", "django-types>=0.3.1,<0.4", "django>=3.1,<4", - "ruff>=0.14.9", + "google-cloud-monitoring>=2.28.0", + "google-cloud-pubsub>=2.34.0", + "kombu>=5.6.2", "mypy>=1.19.0", - "basedpyright>=1.36.1", "prek>=0.2.23", - "sphinx>=8.1.3", + "pycurl>=7.45.7", + "pymongo>=4.15.5", "pytest>=9.0.2", - "types-docutils>=0.22.3.20251115", - "boto3>=1.42.19", - "botocore>=1.42.19", - "azure-servicebus>=7.14.3", - "azure-storage-queue>=12.14.1", - "google-cloud-pubsub>=2.34.0", "redis>=7.1.0", - "sqlalchemy>=2.0.45", - "pymongo>=4.15.5", - "boto3-stubs>=1.42.19", + "ruff>=0.14.9", "six>=1.16.0", - "pycurl>=7.45.7", - "confluent-kafka>=2.12.2", - "google-cloud-monitoring>=2.28.0", + "sphinx>=8.1.3", + "sqlalchemy>=2.0.45", + "types-docutils>=0.22.3.20251115", ] diff --git a/uv.lock b/uv.lock index 25d3cd5..d45ac7f 100644 --- a/uv.lock +++ b/uv.lock @@ -225,6 +225,7 @@ dev = [ { name = "django-types" }, { name = "google-cloud-monitoring" }, { name = "google-cloud-pubsub" }, + { name = "kombu" }, { name = "mypy" }, { name = "prek" }, { name = "pycurl" }, @@ -256,6 +257,7 @@ dev = [ { name = "django-types", specifier = ">=0.3.1,<0.4" }, { name = "google-cloud-monitoring", specifier = ">=2.28.0" }, { name = "google-cloud-pubsub", specifier = ">=2.34.0" }, + { name = "kombu", specifier = ">=5.6.2" }, { name = "mypy", specifier = ">=1.19.0" }, { name = "prek", specifier = ">=0.2.23" }, { name = "pycurl", specifier = ">=7.45.7" }, @@ -983,7 +985,7 @@ wheels = [ [[package]] name = "kombu" -version = "5.6.1" +version = "5.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "amqp" }, @@ -991,9 +993,9 @@ dependencies = [ { name = "tzdata" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/05/749ada8e51718445d915af13f1d18bc4333848e8faa0cb234028a3328ec8/kombu-5.6.1.tar.gz", hash = "sha256:90f1febb57ad4f53ca327a87598191b2520e0c793c75ea3b88d98e3b111282e4", size = 471548, upload-time = "2025-11-25T11:07:33.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409", size = 214141, upload-time = "2025-11-25T11:07:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, ] [[package]]