Skip to content
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
49 changes: 49 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes above make it a little bit easier to work with the CI. Instantly of failing fast one gets all the errors of the different checks at the end.

25 changes: 25 additions & 0 deletions billiard-stubs/allowlist.txt
Original file line number Diff line number Diff line change
@@ -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
30 changes: 26 additions & 4 deletions billiard-stubs/einfo.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
141 changes: 141 additions & 0 deletions celery-stubs/allowlist.txt
Original file line number Diff line number Diff line change
@@ -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__
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and some of the other stuff in the ignore list can be removed with python 3.12+ or 3.14+, and I would move to python 3.14 soon-ish. But one after the other...


6 changes: 3 additions & 3 deletions celery-stubs/contrib/sphinx.pyi
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions kombu-stubs/allowlist.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 0 additions & 2 deletions kombu-stubs/utils/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 16 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,30 @@ Repository = "https://github.com/sbdchd/celery-types"

[dependency-groups]
dev = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These below are just alphabetically sorted now...

"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",
]


Expand Down
Loading
Loading