simcord 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simcord-2.2.0 → simcord-2.3.0}/CHANGELOG.md +24 -0
- {simcord-2.2.0 → simcord-2.3.0}/CONTRIBUTING.md +10 -0
- {simcord-2.2.0 → simcord-2.3.0}/PKG-INFO +1 -1
- {simcord-2.2.0 → simcord-2.3.0}/docs/concepts.md +4 -1
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/diagnostics.md +28 -9
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/fixtures.md +14 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/interactions.md +20 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/messages.md +29 -3
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/preview.md +9 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/stability.md +33 -4
- {simcord-2.2.0 → simcord-2.3.0}/pyproject.toml +1 -1
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/_dpy_internals.py +19 -3
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/actors.py +2 -15
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/asserts.py +2 -2
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/channels.py +6 -1
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/messages.py +17 -9
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/components.py +46 -2
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/env.py +35 -8
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/_helpers.py +109 -53
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/channels.py +13 -4
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/interactions.py +8 -7
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/protocol.schema.json +58 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/results.py +38 -4
- simcord-2.3.0/tests/integration/test_forum.py +201 -0
- simcord-2.3.0/tests/integration/test_message_routes.py +249 -0
- simcord-2.3.0/tests/integration/test_migration_bridge.py +339 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_dx.py +44 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_settlement_contract.py +47 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_components_v2_validation.py +38 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_dpy_internals.py +72 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_pytest_plugin.py +6 -8
- {simcord-2.2.0 → simcord-2.3.0}/uv.lock +1 -1
- simcord-2.2.0/tests/integration/test_forum.py +0 -79
- simcord-2.2.0/tests/integration/test_message_routes.py +0 -94
- simcord-2.2.0/tests/unit/test_version.py +0 -13
- {simcord-2.2.0 → simcord-2.3.0}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.github/dependabot.yml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.github/workflows/ci.yml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.github/workflows/dpy-master.yml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.github/workflows/release.yml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.github/workflows/verify.yml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.gitignore +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.pre-commit-config.yaml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.python-version +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/.readthedocs.yaml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/LICENSE +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/README.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/SECURITY.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/benchmarks/_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/benchmarks/test_dispatch_benchmarks.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/benchmarks/test_perf_guards.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/changes/README.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/api.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/architecture.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/assets/social-card.png +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/changelog.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/cookbook.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/faq.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/ai-coding-agents.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/components.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/intents.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/permissions.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/test-without-token.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/testing-slash-commands.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/threads-reactions-dms.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/guides/time-control.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/index.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/installation.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/llms.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/parity-matrix.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/performance.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/quickstart.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/docs/robots.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/examples/README.md +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/examples/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/examples/bot.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/examples/conftest.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/examples/preview_example.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/examples/test_bot.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/mkdocs.yml +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/overrides/main.html +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/scripts/capture_visual_reference.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/scripts/check_discoverability.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/scripts/check_package.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/scripts/compare_visual_reference.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/scripts/discord_reference_bot.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/access.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/cdn.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/errors.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/automod.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/channel.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/expression.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/guild.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/interaction.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/invite.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/member.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/message.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/role.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/user.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/voice.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/base.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/guilds.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/polls.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/scheduled_events.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/serializers.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/backend/state.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/builders.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/enums.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/gateway.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/client.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/router.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/application.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/automod.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/commands.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/guilds.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/invites.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/messages.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/intents.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/parity.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_actions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_assets.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_capture.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_markdown.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_media.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_pages.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_server.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/_snapshot.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/app.js +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/components.js +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Arabic.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-CJK.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Devanagari.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Emoji.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Hebrew.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/OFL-1.1-Noto.txt +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/manifest.json +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-color-emoji-v2.051.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-sans-arabic-v2.012.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-sans-devanagari-v2.007.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-sans-hebrew-v3.001.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-sans-latin-italic-v2.015.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-sans-latin-v2.015.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-sans-mono-v2.014.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/fonts/noto-sans-sc-v2.004.ttf +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/index.html +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/preview/static/preview.css +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/py.typed +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/pytest_plugin.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/src/simcord/types.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/conformance/_world.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/conformance/test_serializers.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/conftest.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/preview/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/preview/catalog.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/preview/coverage.json +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/preview/measurements.json +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/preview/profiles.json +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/preview/states.json +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/preview_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_app_commands.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_application_emojis.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_asserts.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_audit_logs.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_automod.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_bot_users_and_webhooks.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_channels.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_command_permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_diagnostics.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_edge_cases.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_expressions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_guilds.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_intents.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_interaction_followups.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_invites.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_lifecycle.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_members.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_messages.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_misc_branches.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_moderation.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_polls.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_additional.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_capture.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_capture_dx.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_coverage.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_edges.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_fixes.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_fonts.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_preview_security.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_reactions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_roles_and_ordering.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_setup_hook_suspension.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_sharding.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_stage_instances.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_threads.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_time.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_transport_edges.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_voice_state.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/integration/test_world_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_errors.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_intent_rules.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_lazy_import.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_parity.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_permission_validators.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.3.0}/tests/unit/test_router.py +0 -0
|
@@ -4,6 +4,30 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
|
|
|
4
4
|
|
|
5
5
|
<!-- towncrier release notes start -->
|
|
6
6
|
|
|
7
|
+
## 2.3.0 (2026-10-05)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add the temporary `future_behavior=False` migration bridge in SimCord 2.3. Opt in with `simcord.run(bot, future_behavior=True)` or the pytest marker to acknowledge only the current error prefix and detach nested mutable result observations. Legacy live error lists, lifetime acknowledgement and payload aliasing remain the defaults, with targeted deprecation warnings. SimCord 3.0 removes the flag and makes the opt-in semantics the defaults. Startup/restart READY and restart guild-replay settlement retain their independent five-second budgets.
|
|
12
|
+
|
|
13
|
+
### Bug fixes
|
|
14
|
+
|
|
15
|
+
- Describe all seven existing protocol-2 preview actions in the packaged schema, including required targets/controls and envelope fields. Preserve focus defaults, unknown metadata, multipart modal uploads, and runtime authorization/replay checks. Reuse the existing builder operation guard for actors without changing ownership or permissions.
|
|
16
|
+
- Share message validation across HTTP, actors and backend creation/editing: enforce the 10-embed count, combined 6000-character limit and individual embed text/field limits, excluding surrounding whitespace without changing stored text. Reject empty new user/bot messages while retaining attachment-, embed-, component- and poll-only sends, nullable/partial edits and empty interaction deferrals. Previously accepted invalid messages now fail with code 50035, and invalid interaction responses do not consume acknowledgement.
|
|
17
|
+
Interaction update callbacks without a source message now fail before acknowledgement instead of silently succeeding.
|
|
18
|
+
|
|
19
|
+
Prepare forum starter fields, components and every upload read before creating thread/message/CDN state, then publish the existing ordered events only after thread, starter and parent state are complete. Validation or upload-read failures no longer leave orphan posts or gateway events. Preparation consumes upload streams; an earlier successfully read stream must be rewound or replaced if a later read fails. Subscriber exceptions after publication begins are outside this atomicity boundary.
|
|
20
|
+
Starter reference identifiers are also validated before publication, preventing malformed references from corrupting the forum's previous last-post pointer.
|
|
21
|
+
- Recognize shielded Discord waits on Python 3.14.8 without treating the caller captured by asyncio's cancellation cleanup as an additional dependency.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## 2.2.1 (2026-09-24)
|
|
25
|
+
|
|
26
|
+
### Bug fixes
|
|
27
|
+
|
|
28
|
+
- Fixed View/Modal expiry recognition for views that never reach the store's dispatch tables — a view whose dispatchable items are all `DynamicItem`s (e.g. a `LayoutView` built only from patterned custom IDs) registered a live expiry task that settlement never recognized, so its `timeout=` blocked actor operations and fired on the wall clock. Expiry tasks are now identified by the coroutine they run (`BaseView.__timeout_task_impl`), which covers every registered view regardless of how the store indexes it.
|
|
29
|
+
|
|
30
|
+
|
|
7
31
|
## 2.2.0 (2026-09-23)
|
|
8
32
|
|
|
9
33
|
### Features
|
|
@@ -69,6 +69,16 @@ Maintainers update `pyproject.toml`, then consume fragments with
|
|
|
69
69
|
version commit are tagged `vX.Y.Z`; the release workflow verifies the tag,
|
|
70
70
|
quality gates, documentation, and built metadata before publishing.
|
|
71
71
|
|
|
72
|
+
For the 2.3 → 3.0 migration, keep the two release lines separate and verify both
|
|
73
|
+
legacy defaults and `future_behavior=True` before tagging 2.3.
|
|
74
|
+
|
|
75
|
+
The expanded 3.0 preview work additionally requires authorized Discord comparisons
|
|
76
|
+
with complete provenance and dated human accessibility checks. Its reference ledger
|
|
77
|
+
is `tests/fixtures/preview/coverage.json`; blocked or non-comparable rows are not
|
|
78
|
+
passing evidence. These preview-specific requirements do not block the compatible
|
|
79
|
+
2.3 bridge, which retains the already-released protocol-2 preview without new
|
|
80
|
+
visual fidelity claims.
|
|
81
|
+
|
|
72
82
|
## Reporting bugs
|
|
73
83
|
|
|
74
84
|
A failing test using `simcord` is the perfect bug report. If your bot hits an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simcord
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Offline discord.py bot testing with pytest. No network, token, or test server.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SilentHacks/simcord
|
|
6
6
|
Project-URL: Documentation, https://simcord.readthedocs.io/
|
|
@@ -95,13 +95,16 @@ assert channel.last_message.content == "Pong!" # the reply is already here
|
|
|
95
95
|
|
|
96
96
|
Bot-owned work stays bot-owned across parking, timeout, cancellation, later actions, startup,
|
|
97
97
|
and restart. Recognized waits — `Client.wait_for` (including its `timeout=`), View/Modal
|
|
98
|
-
completion,
|
|
98
|
+
completion, View/Modal expiry timers, `discord.ext.tasks` loop intervals,
|
|
99
99
|
composed `gather`/`shield`/`wait`/`TaskGroup`, explicit `env.external_wait(...)`, and verified
|
|
100
100
|
far-future sleeps — may remain. A timer whose only job is resuming a recognized wait — a
|
|
101
101
|
`wait_for` deadline on a listener, a View/Modal expiry, a loop interval, a wake-up timer
|
|
102
102
|
scheduled inside `external_wait` — is virtual: it fires only via `env.advance_time()` or the
|
|
103
103
|
awaited input, never the wall clock. Verified far-future sleeps keep their real fallback, and
|
|
104
104
|
a View sent with no dispatchable items never registers (its expiry keeps real-time behavior).
|
|
105
|
+
Views whose dispatchable items are all `DynamicItem`s are covered too — expiry is identified
|
|
106
|
+
from the task's coroutine, not the store's dispatch table; `view.wait()` on such a view is not
|
|
107
|
+
recognized, so `await env.external_wait(view.wait(), reason="...")` if a bot awaits one.
|
|
105
108
|
Unknown waits remain active and time out. Use a reasoned, scoped declaration for external
|
|
106
109
|
input:
|
|
107
110
|
|
|
@@ -27,10 +27,17 @@ excluded — a non-command message isn't a bug.)
|
|
|
27
27
|
### Errors fail tests by default
|
|
28
28
|
|
|
29
29
|
!!! warning "Uninspected errors are re-raised at teardown"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
With the 2.x default, reading `env.errors` acknowledges errors for the environment's
|
|
31
|
+
entire lifetime, including errors that arrive later. A saved list stays live.
|
|
32
|
+
SimCord 2.3 warns on this legacy read. Use `simcord.run(bot, future_behavior=True)`
|
|
33
|
+
to adopt the intended 3.0 behavior now: each read returns a shallow list snapshot and
|
|
34
|
+
acknowledges only the errors captured so far. A later error still fails teardown,
|
|
35
|
+
including one captured during shutdown; previously acknowledged errors are not
|
|
36
|
+
re-raised alongside it. Exception objects and their tracebacks are preserved.
|
|
37
|
+
|
|
38
|
+
The `future_behavior` flag defaults to `False` in 2.3. SimCord 3.0 removes the flag and
|
|
39
|
+
makes snapshot/prefix acknowledgement the default. Saving an empty opt-in snapshot does
|
|
40
|
+
not acknowledge errors that arrive later, and mutating a snapshot cannot change the history.
|
|
34
41
|
|
|
35
42
|
You have three ways to handle this:
|
|
36
43
|
|
|
@@ -46,9 +53,14 @@ async with simcord.run(bot, check_errors=False) as env:
|
|
|
46
53
|
...
|
|
47
54
|
```
|
|
48
55
|
|
|
49
|
-
`env.raise_errors()`
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
`env.raise_errors()` always raises an `ExceptionGroup` of the **entire captured history**
|
|
57
|
+
(even errors acknowledged earlier), or does nothing if there were none. It acknowledges
|
|
58
|
+
only the current prefix in opt-in mode, including an empty prefix; later errors still fail
|
|
59
|
+
teardown. `check_errors=False` disables automatic checking in either mode. Teardown never
|
|
60
|
+
masks an exception already propagating from the test body.
|
|
61
|
+
|
|
62
|
+
`env.error_cursor` and `env.errors_since(cursor)` are non-consuming observations used by
|
|
63
|
+
Preview diagnostics. They do not acknowledge errors or disable teardown checking.
|
|
52
64
|
|
|
53
65
|
## Assertions
|
|
54
66
|
|
|
@@ -89,8 +101,8 @@ assert_error(env, discord.Forbidden, code=50013) # matches the wrapped origina
|
|
|
89
101
|
assert_no_errors(env) # the symmetric "ran cleanly" check
|
|
90
102
|
```
|
|
91
103
|
|
|
92
|
-
`assert_error` reads `env.errors`,
|
|
93
|
-
|
|
104
|
+
`assert_error` reads `env.errors`, so it acknowledges the same errors as a direct read:
|
|
105
|
+
the whole lifetime by default, or only the current prefix with `future_behavior=True`.
|
|
94
106
|
|
|
95
107
|
## The transcript
|
|
96
108
|
|
|
@@ -162,8 +174,15 @@ simcord_env.inject_error("PUT", "/guilds/*/bans/*", status=403, code=50013, time
|
|
|
162
174
|
|
|
163
175
|
Because failures surface as genuine `discord.Forbidden` / `discord.HTTPException` with real
|
|
164
176
|
codes, your `except discord.HTTPException:` branches are exercised exactly as in production.
|
|
177
|
+
|
|
165
178
|
## Settlement timeouts and recovery
|
|
166
179
|
|
|
180
|
+
`settle_timeout` defaults to five seconds for actor operations, `env.settle()`, and the
|
|
181
|
+
pre-restart drain; `env.settle(timeout=...)` overrides that individual settle. Startup
|
|
182
|
+
READY settlement has its own fixed five-second budget, independent of `settle_timeout`.
|
|
183
|
+
Restart uses a fresh fixed five-second READY budget and then a separate fixed five-second
|
|
184
|
+
guild-replay budget. These are settlement budgets, not a timeout on `setup_hook` or login.
|
|
185
|
+
|
|
167
186
|
`TimeoutError` reports the operation, effective timeout, bot-owned task or callback, wait
|
|
168
187
|
reason, and any recognized waits left parked. A timeout or cancellation does not release
|
|
169
188
|
ownership or pretend work completed; state may already have changed. Release the real
|
|
@@ -96,9 +96,17 @@ Both `simcord.run(bot, **options)` and the underlying `Env` accept these keyword
|
|
|
96
96
|
| --- | --- | --- |
|
|
97
97
|
| `strict_sync` | `True` | Unsynced app commands can't be invoked — invoking one fails the test, catching forgotten `tree.sync()` calls. Set `False` to auto-register unsynced commands for isolated unit tests. |
|
|
98
98
|
| `check_errors` | `True` | At teardown, errors the bot raised but the test never inspected are re-raised as an `ExceptionGroup`, so bot bugs can't pass silently. Set `False` to opt out. |
|
|
99
|
+
| `future_behavior` | `False` | Temporary 2.3 bridge: shallow error snapshots with prefix acknowledgement, and detached mutable result payload snapshots. Must be a bool. In 3.0 the flag is removed and these semantics become the defaults. |
|
|
99
100
|
| `approved_intents` | all | Simulates developer-portal privileged-intent toggles. |
|
|
100
101
|
| `shard_count` | client setting | Supplies the Get Gateway Bot recommendation when an `AutoShardedClient` does not configure `shard_count` itself. |
|
|
101
102
|
| `settle_timeout` | `5.0` seconds | Default maximum time for joining runnable bot work; a direct settle `timeout=` overrides it. |
|
|
103
|
+
|
|
104
|
+
`settle_timeout` does not change startup's fixed five-second READY settlement budget.
|
|
105
|
+
Restart drains existing work using `settle_timeout`, then gets a fresh fixed five-second
|
|
106
|
+
READY budget and a separate fixed five-second guild-replay budget. These budgets do not
|
|
107
|
+
bound login or `setup_hook`. `strict_sync=False` remains an independent opt-out; the
|
|
108
|
+
migration bridge does not change command registration.
|
|
109
|
+
|
|
102
110
|
External waits must be scoped with `await env.external_wait(awaitable, reason="...")`; unknown waits time out. Overlapping actor, builder, lifecycle, and time-control operations reject before mutation. Teardown cancels bot-owned work but leaves caller-owned tasks alive.
|
|
103
111
|
|
|
104
112
|
```python
|
|
@@ -161,6 +169,12 @@ async def test_command_logic_in_isolation(simcord_env):
|
|
|
161
169
|
...
|
|
162
170
|
```
|
|
163
171
|
|
|
172
|
+
To opt into the 3.0 observation behavior for a plugin fixture, use
|
|
173
|
+
`@pytest.mark.simcord(future_behavior=True)`. The same flag works in a wrapper fixture
|
|
174
|
+
with `simcord.run(bot, future_behavior=True)`. Legacy `env.errors` and mutable result
|
|
175
|
+
payload reads emit `DeprecationWarning` with this migration guidance; startup and scalar
|
|
176
|
+
result reads do not.
|
|
177
|
+
|
|
164
178
|
For an override shared by many tests, drive `simcord.run` yourself in a small wrapper fixture
|
|
165
179
|
instead:
|
|
166
180
|
|
|
@@ -63,6 +63,26 @@ assert result.ephemeral
|
|
|
63
63
|
assert result.response.content == f"Banned {target.mention}: spam"
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
### The 2.3 migration bridge
|
|
67
|
+
|
|
68
|
+
Use `simcord.run(bot, future_behavior=True)` (or
|
|
69
|
+
`@pytest.mark.simcord(future_behavior=True)`) for the intended 3.0 observation semantics.
|
|
70
|
+
Results remain **live handles**: reading them again reflects later original-response edits
|
|
71
|
+
and followups. Each mutable payload read returns a detached nested snapshot, including
|
|
72
|
+
components, embeds, full message payloads, modal payloads and autocomplete choices.
|
|
73
|
+
Editing a returned snapshot does not edit Discord state, and subsequent backend changes
|
|
74
|
+
do not rewrite a previously returned snapshot. Lists and dictionaries keep their shapes;
|
|
75
|
+
absent modal/autocomplete payloads remain `None`, and empty choices remain `[]`.
|
|
76
|
+
|
|
77
|
+
Modal submission always uses the backend's canonical modal, not edits to a detached
|
|
78
|
+
`result.modal` snapshot. Make real changes through bot response/edit/followup APIs.
|
|
79
|
+
|
|
80
|
+
The flag defaults to `False` in 2.3: existing nested payload aliasing is preserved, with a
|
|
81
|
+
`DeprecationWarning` on mutable payload reads. Scalar reads such as `content`,
|
|
82
|
+
`acknowledged` and `ephemeral` do not warn. In 3.0 the flag is removed and detached
|
|
83
|
+
observations become the default. See [Errors & diagnostics](diagnostics.md) for the
|
|
84
|
+
matching error-acknowledgement transition.
|
|
85
|
+
|
|
66
86
|
## The interaction lifecycle is real
|
|
67
87
|
|
|
68
88
|
The full lifecycle behaves as on Discord:
|
|
@@ -121,9 +121,16 @@ Full treatment in [Errors & diagnostics](diagnostics.md).
|
|
|
121
121
|
|
|
122
122
|
## Validation limits
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
Message creation and edits share validation across bot HTTP requests, actor helpers and
|
|
125
|
+
the backend. Content is limited to 2000 characters. Messages may contain at most 10
|
|
126
|
+
embeds, with at most 6000 text characters **across all embeds combined**. Individual
|
|
127
|
+
embed limits are title 256, description 4096, 25 fields, field name 256, field value
|
|
128
|
+
1024, footer text 2048 and author name 256. Leading and trailing whitespace in each
|
|
129
|
+
embed text field is excluded from these counts; stored text is unchanged. Counts use
|
|
130
|
+
Python's Unicode `len`, as in earlier 2.x releases.
|
|
131
|
+
|
|
132
|
+
Invalid bot requests raise `discord.HTTPException` with code `50035`; actor/backend
|
|
133
|
+
calls raise `simcord.BackendError` with the same code:
|
|
127
134
|
|
|
128
135
|
```python
|
|
129
136
|
async def test_rejects_oversized(simcord_env):
|
|
@@ -133,6 +140,25 @@ async def test_rejects_oversized(simcord_env):
|
|
|
133
140
|
assert exc.value.code == 50035
|
|
134
141
|
```
|
|
135
142
|
|
|
143
|
+
Some invalid messages previously accepted by SimCord now fail, including embeds whose
|
|
144
|
+
combined text exceeds 6000 characters and oversized individual embed fields. New user
|
|
145
|
+
and bot messages must contain content, an attachment, an embed, components or a poll;
|
|
146
|
+
content-free messages with one of those payloads remain supported. Partial edits and
|
|
147
|
+
explicit `None` values can clear fields, including leaving an existing message empty.
|
|
148
|
+
System messages and empty interaction deferrals are not treated as empty message sends.
|
|
149
|
+
Stickers remain explicitly unsupported offline.
|
|
150
|
+
Interaction message responses validate before acknowledgement; a rejected response
|
|
151
|
+
can be retried. Updating an interaction's source message requires an actual source
|
|
152
|
+
message (a component interaction or a modal opened from one).
|
|
153
|
+
|
|
154
|
+
Forum posts prepare the entire starter message and read all uploaded files before
|
|
155
|
+
creating a thread or storing CDN objects. Validation or upload-read failures leave no
|
|
156
|
+
thread, starter message or gateway event behind. Subscribers to `THREAD_CREATE` and
|
|
157
|
+
`MESSAGE_CREATE` see the complete thread, starter and parent state. Upload preparation
|
|
158
|
+
does consume file streams: if a later upload read fails, earlier streams have already
|
|
159
|
+
been read and must be rewound or replaced before retrying. This boundary does not
|
|
160
|
+
include arbitrary subscriber exceptions after gateway publication begins.
|
|
161
|
+
|
|
136
162
|
## Next
|
|
137
163
|
|
|
138
164
|
- [Slash commands](interactions.md) — app commands, context menus and the interaction
|
|
@@ -270,6 +270,15 @@ identifies the focused projection. `modal`, `candidates`, `assets`, `entities`,
|
|
|
270
270
|
details may evolve under `protocolVersion`; assert on documented keys rather than exact payload
|
|
271
271
|
layout.
|
|
272
272
|
|
|
273
|
+
The packaged `protocol.schema.json` also describes protocol-2 action envelopes at
|
|
274
|
+
`#/$defs/action`: `click`, `select`, `modal_submit`, `viewer`, `focus`, `refresh`,
|
|
275
|
+
and `close`. Validate against that definition rather than the snapshot root.
|
|
276
|
+
Unknown metadata remains accepted, and an omitted/null focus target retains its
|
|
277
|
+
initial-target default. Modal values are broadly typed because controls determine
|
|
278
|
+
their types; file bytes are delivered through multipart transport, not JSON.
|
|
279
|
+
Schema validity does not establish authorization, freshness, control availability,
|
|
280
|
+
or sequence admission: runtime checks remain authoritative.
|
|
281
|
+
|
|
273
282
|
## Screenshot profiles, modes, and reports
|
|
274
283
|
|
|
275
284
|
`preview.screenshot(path, ...)` settles and pins the requested viewer/target before releasing the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Stability & versioning"
|
|
3
|
-
description: "
|
|
3
|
+
description: "SimCord's public API, semantic versioning, and the compatible 2.3 migration bridge for 3.0."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Stability & versioning
|
|
@@ -31,9 +31,8 @@ own supervisor.
|
|
|
31
31
|
|
|
32
32
|
SimCord follows [semantic versioning](https://semver.org/). The
|
|
33
33
|
**public API** below is covered by that promise: no breaking change to it without
|
|
34
|
-
a major version bump.
|
|
35
|
-
|
|
36
|
-
coverage, and offline-performance checks.
|
|
34
|
+
a major version bump. Historical 2.0 settlement changes are described above.
|
|
35
|
+
CI continues to enforce honest parity, coverage, and offline-performance checks.
|
|
37
36
|
|
|
38
37
|
The fuzzer proves that across every route whose body is a field set — message
|
|
39
38
|
send and edits, webhook execute, bulk delete, and the rest — an unrecognised
|
|
@@ -117,6 +116,36 @@ verified in CI, so they cannot quietly drift as discord.py evolves.
|
|
|
117
116
|
These exception types are part of the public API precisely so your tests can
|
|
118
117
|
assert on them.
|
|
119
118
|
|
|
119
|
+
## Preparing for 3.0 with 2.3
|
|
120
|
+
|
|
121
|
+
Use `simcord.run(bot, future_behavior=True)` (or decorate a fixture-using test with
|
|
122
|
+
`@pytest.mark.simcord(future_behavior=True)`) to exercise both
|
|
123
|
+
upcoming observation contracts without changing the default behavior of 2.x:
|
|
124
|
+
|
|
125
|
+
- Reading `env.errors` acknowledges only the currently captured prefix and returns
|
|
126
|
+
a list snapshot. Later errors, including shutdown errors, still fail teardown.
|
|
127
|
+
Exception objects and tracebacks are preserved. Explicit `raise_errors()` checks
|
|
128
|
+
the entire history; preview diagnostics never acknowledge errors.
|
|
129
|
+
- Result handles remain live, but mutable payloads are detached on each read.
|
|
130
|
+
Saved payloads no longer follow edits, and mutating them cannot change the world.
|
|
131
|
+
Read the handle again after a genuine edit or followup.
|
|
132
|
+
|
|
133
|
+
Legacy error inspection and mutable result reads emit `DeprecationWarning` with
|
|
134
|
+
opt-in guidance in 2.3. Enable these warnings while migrating, for example with
|
|
135
|
+
`pytest -W default::DeprecationWarning`. The temporary `future_behavior` option
|
|
136
|
+
will be removed in 3.0 when these semantics become unconditional. See the
|
|
137
|
+
[diagnostics guide](guides/diagnostics.md) and
|
|
138
|
+
[interaction guide](guides/interactions.md) for details.
|
|
139
|
+
|
|
140
|
+
The compatible correctness fixes reject previously accepted invalid messages and
|
|
141
|
+
prevent rejected forum requests from publishing partial state. Tests depending on
|
|
142
|
+
invalid Discord payloads must change; nullable edits remain supported.
|
|
143
|
+
Independent startup budgets and `strict_sync=False` remain supported.
|
|
144
|
+
|
|
145
|
+
This release retains preview protocol 2. The protocol-3 work and its additional
|
|
146
|
+
action kinds remain on the separate 3.0 development line. No Discord visual
|
|
147
|
+
calibration or human accessibility sign-off is implied by local automated checks.
|
|
148
|
+
|
|
120
149
|
## Deprecation policy
|
|
121
150
|
|
|
122
151
|
From 1.0 onward, a public-API symbol is never removed or changed incompatibly
|
|
@@ -77,6 +77,13 @@ def verify() -> None:
|
|
|
77
77
|
for suffix in ("__stopped", "__timeout_task"):
|
|
78
78
|
if not any(name.endswith(suffix) for name in vars(view_probe)): # pragma: no cover
|
|
79
79
|
problems.append(f"BaseView.{suffix}")
|
|
80
|
+
impl_probe = getattr(_view.BaseView, "_BaseView__timeout_task_impl", None)
|
|
81
|
+
if (
|
|
82
|
+
impl_probe is None
|
|
83
|
+
or not asyncio.iscoroutinefunction(impl_probe)
|
|
84
|
+
or "self" not in impl_probe.__code__.co_varnames
|
|
85
|
+
): # pragma: no cover
|
|
86
|
+
problems.append("BaseView._BaseView__timeout_task_impl coroutine")
|
|
80
87
|
if not hasattr(asyncio.timeouts.Timeout(0.0), "_task"): # pragma: no cover
|
|
81
88
|
problems.append("Timeout._task")
|
|
82
89
|
|
|
@@ -197,7 +204,16 @@ def is_wakeup_callback(callback: Any, args: tuple[Any, ...], waiter: Any, task:
|
|
|
197
204
|
|
|
198
205
|
|
|
199
206
|
def _view_timeout_task_identity(client: discord.Client, task: asyncio.Task[Any]) -> bool:
|
|
200
|
-
"""True when task is
|
|
207
|
+
"""True when task is a registered View/Modal's expiry task."""
|
|
208
|
+
coro = task.get_coro()
|
|
209
|
+
impl = getattr(_view.BaseView, "_BaseView__timeout_task_impl", None)
|
|
210
|
+
if impl is not None and getattr(coro, "cr_code", None) is getattr(impl, "__code__", None):
|
|
211
|
+
owner = getattr(getattr(coro, "cr_frame", None), "f_locals", {}).get("self")
|
|
212
|
+
if isinstance(owner, _view.BaseView):
|
|
213
|
+
for name, value in vars(owner).items():
|
|
214
|
+
if name.endswith("__stopped") and isinstance(value, asyncio.Future) and value.done():
|
|
215
|
+
return False
|
|
216
|
+
return True
|
|
201
217
|
store = getattr(get_state(client), "_view_store", None)
|
|
202
218
|
if store is None:
|
|
203
219
|
return False
|
|
@@ -301,9 +317,9 @@ def composed_tasks(task: asyncio.Task[Any], waiter: Any) -> list[Any]:
|
|
|
301
317
|
if getattr(callback, "__module__", "") == "asyncio.tasks" and getattr(
|
|
302
318
|
callback, "__qualname__", ""
|
|
303
319
|
).endswith("shield.<locals>._outer_done_callback"):
|
|
304
|
-
for cell in callback.__closure__ or ():
|
|
320
|
+
for name, cell in zip(callback.__code__.co_freevars, callback.__closure__ or (), strict=True):
|
|
305
321
|
value = cell.cell_contents
|
|
306
|
-
if isinstance(value, asyncio.Future):
|
|
322
|
+
if name == "inner" and isinstance(value, asyncio.Future):
|
|
307
323
|
found.append(value)
|
|
308
324
|
|
|
309
325
|
wait_codes = {
|
|
@@ -9,7 +9,6 @@ channels), then delivered as authentic gateway events.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from collections.abc import Sequence
|
|
12
|
-
from functools import wraps
|
|
13
12
|
from typing import TYPE_CHECKING, Any
|
|
14
13
|
|
|
15
14
|
import discord
|
|
@@ -18,7 +17,7 @@ from . import interactions as _interactions
|
|
|
18
17
|
from .backend import serializers
|
|
19
18
|
from .backend.access import can_access_channel, can_access_message
|
|
20
19
|
from .backend.errors import SetupError
|
|
21
|
-
from .builders import ChannelHandle, GuildHandle, RoleHandle, UserHandle
|
|
20
|
+
from .builders import ChannelHandle, GuildHandle, RoleHandle, UserHandle, _guard_builder_operation
|
|
22
21
|
from .components import validate_modal, walk_components
|
|
23
22
|
from .enums import SELECT_TYPES, AppCommandType, ComponentType, InteractionType
|
|
24
23
|
from .results import InteractionResult, ResponseMessage, to_discord_message
|
|
@@ -861,18 +860,6 @@ async def _dispatch_actor_interaction(
|
|
|
861
860
|
return InteractionResult(actor._env, record)
|
|
862
861
|
|
|
863
862
|
|
|
864
|
-
def _guard_actor_operation(method: Any) -> Any:
|
|
865
|
-
@wraps(method)
|
|
866
|
-
async def guarded(self: MemberActor, *args: Any, **kwargs: Any) -> Any:
|
|
867
|
-
token = self._env._begin_operation(method.__name__)
|
|
868
|
-
try:
|
|
869
|
-
return await method(self, *args, **kwargs)
|
|
870
|
-
finally:
|
|
871
|
-
self._env._end_operation(token)
|
|
872
|
-
|
|
873
|
-
return guarded
|
|
874
|
-
|
|
875
|
-
|
|
876
863
|
for _operation_name in (
|
|
877
864
|
"send",
|
|
878
865
|
"edit",
|
|
@@ -895,4 +882,4 @@ for _operation_name in (
|
|
|
895
882
|
"subscribe_event",
|
|
896
883
|
"unsubscribe_event",
|
|
897
884
|
):
|
|
898
|
-
setattr(MemberActor, _operation_name,
|
|
885
|
+
setattr(MemberActor, _operation_name, _guard_builder_operation(getattr(MemberActor, _operation_name)))
|
|
@@ -185,8 +185,8 @@ def assert_error(
|
|
|
185
185
|
|
|
186
186
|
discord.py wraps callback failures (e.g. ``CommandInvokeError.original``), so
|
|
187
187
|
the type and code are matched against the error *and* its ``.original``.
|
|
188
|
-
Returns the matched error. Reading ``env.errors``
|
|
189
|
-
|
|
188
|
+
Returns the matched error. Reading ``env.errors`` acknowledges the captured
|
|
189
|
+
prefix with ``future_behavior=True`` (the entire lifetime in legacy mode).
|
|
190
190
|
"""
|
|
191
191
|
captured = env.errors
|
|
192
192
|
|
|
@@ -194,6 +194,7 @@ class ChannelMixin(BackendBase):
|
|
|
194
194
|
auto_archive_duration: int = 1440,
|
|
195
195
|
message_id: int | None = None,
|
|
196
196
|
applied_tags: Iterable[int] = (),
|
|
197
|
+
broadcast: bool = True,
|
|
197
198
|
) -> Channel:
|
|
198
199
|
parent = self.get_channel(parent_id)
|
|
199
200
|
guild = self.get_guild(parent.guild_id) # type: ignore[arg-type]
|
|
@@ -214,10 +215,14 @@ class ChannelMixin(BackendBase):
|
|
|
214
215
|
self.channels[thread.id] = thread
|
|
215
216
|
self.messages.setdefault(thread.id, {})
|
|
216
217
|
guild.thread_ids.append(thread.id)
|
|
218
|
+
if broadcast:
|
|
219
|
+
self.announce_thread_create(thread)
|
|
220
|
+
return thread
|
|
221
|
+
|
|
222
|
+
def announce_thread_create(self, thread: Channel) -> None:
|
|
217
223
|
payload = dict(serializers.thread_payload(self, thread))
|
|
218
224
|
payload["newly_created"] = True
|
|
219
225
|
self.emit("THREAD_CREATE", payload)
|
|
220
|
-
return thread
|
|
221
226
|
|
|
222
227
|
# ------------------------------------------------------- stage instances
|
|
223
228
|
|
|
@@ -51,6 +51,8 @@ class MessageMixin(BackendBase):
|
|
|
51
51
|
content=content,
|
|
52
52
|
embeds=embed_value,
|
|
53
53
|
poll=poll,
|
|
54
|
+
attachments=attachments,
|
|
55
|
+
require_nonempty=True,
|
|
54
56
|
)
|
|
55
57
|
except (ComponentValidationError, TypeError, ValueError) as exc:
|
|
56
58
|
raise _component_error(
|
|
@@ -89,16 +91,20 @@ class MessageMixin(BackendBase):
|
|
|
89
91
|
if channel.is_thread:
|
|
90
92
|
channel.message_count += 1
|
|
91
93
|
if broadcast:
|
|
92
|
-
|
|
93
|
-
if channel.guild_id is not None:
|
|
94
|
-
guild = self.guilds[channel.guild_id]
|
|
95
|
-
if author_id in guild.members:
|
|
96
|
-
payload["member"] = serializers.member_payload(
|
|
97
|
-
self, guild, guild.members[author_id], with_user=False
|
|
98
|
-
)
|
|
99
|
-
self.emit("MESSAGE_CREATE", payload)
|
|
94
|
+
self.announce_message_create(message)
|
|
100
95
|
return message
|
|
101
96
|
|
|
97
|
+
def announce_message_create(self, message: Message) -> None:
|
|
98
|
+
channel = self.get_channel(message.channel_id)
|
|
99
|
+
payload = dict(serializers.message_payload(self, message))
|
|
100
|
+
if channel.guild_id is not None:
|
|
101
|
+
guild = self.guilds[channel.guild_id]
|
|
102
|
+
if message.author_id in guild.members:
|
|
103
|
+
payload["member"] = serializers.member_payload(
|
|
104
|
+
self, guild, guild.members[message.author_id], with_user=False
|
|
105
|
+
)
|
|
106
|
+
self.emit("MESSAGE_CREATE", payload)
|
|
107
|
+
|
|
102
108
|
def _mentions_everyone(self, channel: Channel, author_id: int, content: str) -> bool:
|
|
103
109
|
"""An @everyone only actually pings if the author may mention everyone."""
|
|
104
110
|
if "@everyone" not in content and "@here" not in content:
|
|
@@ -115,8 +121,10 @@ class MessageMixin(BackendBase):
|
|
|
115
121
|
embeds = fields["embeds"] if "embeds" in fields else message.embeds
|
|
116
122
|
components = fields["components"] if "components" in fields else message.components
|
|
117
123
|
attachments = fields["attachments"] if "attachments" in fields else message.attachments
|
|
118
|
-
flags = int(fields["flags"]) if "flags" in fields and fields["flags"] is not None else message.flags
|
|
119
124
|
try:
|
|
125
|
+
flags = (
|
|
126
|
+
int(fields["flags"]) if "flags" in fields and fields["flags"] is not None else message.flags
|
|
127
|
+
)
|
|
120
128
|
normalized_components = validate_message_state(
|
|
121
129
|
[] if components is None else components,
|
|
122
130
|
flags=flags,
|
|
@@ -551,6 +551,45 @@ def validate_components(components: Any, *, flags: int = 0) -> list[dict[str, An
|
|
|
551
551
|
return normalized
|
|
552
552
|
|
|
553
553
|
|
|
554
|
+
def validate_embeds(embeds: Any) -> None:
|
|
555
|
+
"""Check Discord's per-field and combined embed limits without trimming storage."""
|
|
556
|
+
if not isinstance(embeds, list):
|
|
557
|
+
raise ComponentValidationError("embeds must be an array")
|
|
558
|
+
if len(embeds) > 10:
|
|
559
|
+
raise ComponentValidationError("embeds: Must be 10 or fewer in length")
|
|
560
|
+
total = 0
|
|
561
|
+
for index, embed in enumerate(embeds):
|
|
562
|
+
if not isinstance(embed, Mapping):
|
|
563
|
+
raise ComponentValidationError(f"embeds[{index}] must be an object")
|
|
564
|
+
fields = embed.get("fields") if embed.get("fields") is not None else []
|
|
565
|
+
if not isinstance(fields, list) or len(fields) > 25:
|
|
566
|
+
raise ComponentValidationError(f"embeds[{index}].fields: Must be 25 or fewer in length")
|
|
567
|
+
texts = [("title", embed.get("title"), 256), ("description", embed.get("description"), 4096)]
|
|
568
|
+
for field_index, field in enumerate(fields):
|
|
569
|
+
if not isinstance(field, Mapping):
|
|
570
|
+
raise ComponentValidationError(f"embeds[{index}].fields[{field_index}] must be an object")
|
|
571
|
+
texts.extend(
|
|
572
|
+
(f"fields[{field_index}].{key}", field.get(key), limit)
|
|
573
|
+
for key, limit in (("name", 256), ("value", 1024))
|
|
574
|
+
)
|
|
575
|
+
for key, text_key, limit in (("footer", "text", 2048), ("author", "name", 256)):
|
|
576
|
+
section = embed.get(key) if embed.get(key) is not None else {}
|
|
577
|
+
if not isinstance(section, Mapping):
|
|
578
|
+
raise ComponentValidationError(f"embeds[{index}].{key} must be an object")
|
|
579
|
+
texts.append((f"{key}.{text_key}", section.get(text_key), limit))
|
|
580
|
+
for path, text, limit in texts:
|
|
581
|
+
if text is None:
|
|
582
|
+
continue
|
|
583
|
+
if not isinstance(text, str):
|
|
584
|
+
raise ComponentValidationError(f"embeds[{index}].{path} must be a string")
|
|
585
|
+
length = len(text.strip())
|
|
586
|
+
if length > limit:
|
|
587
|
+
raise ComponentValidationError(f"embeds[{index}].{path}: Must be {limit} or fewer characters")
|
|
588
|
+
total += length
|
|
589
|
+
if total > 6000:
|
|
590
|
+
raise ComponentValidationError("embeds: total size of embeds exceeds 6000 characters")
|
|
591
|
+
|
|
592
|
+
|
|
554
593
|
def validate_message_state(
|
|
555
594
|
components: Any,
|
|
556
595
|
*,
|
|
@@ -560,11 +599,12 @@ def validate_message_state(
|
|
|
560
599
|
poll: Any = None,
|
|
561
600
|
stickers: Any = None,
|
|
562
601
|
previous_flags: int = 0,
|
|
602
|
+
attachments: Any = None,
|
|
603
|
+
require_nonempty: bool = False,
|
|
563
604
|
) -> list[dict[str, Any]]:
|
|
564
605
|
if content is not None and (not isinstance(content, str) or len(content) > 2000):
|
|
565
606
|
raise _fail("", "content must be a string of 2000 or fewer characters")
|
|
566
|
-
|
|
567
|
-
raise _fail("", "embeds must be an array")
|
|
607
|
+
validate_embeds(embeds)
|
|
568
608
|
v2 = bool(int(flags) & COMPONENTS_V2_FLAG)
|
|
569
609
|
if int(previous_flags) & COMPONENTS_V2_FLAG and not v2:
|
|
570
610
|
raise _fail("", "the components_v2 flag cannot be removed")
|
|
@@ -572,6 +612,10 @@ def validate_message_state(
|
|
|
572
612
|
raise _fail("", "components_v2 messages require at least one component")
|
|
573
613
|
if v2 and ((content or "") or embeds or poll is not None or stickers):
|
|
574
614
|
raise _fail("", "components_v2 messages cannot contain content, embeds, polls, or stickers")
|
|
615
|
+
if require_nonempty and not (
|
|
616
|
+
(content or "").strip() or embeds or components or attachments or poll is not None
|
|
617
|
+
):
|
|
618
|
+
raise ComponentValidationError("Cannot send an empty message")
|
|
575
619
|
return validate_components(components, flags=int(flags))
|
|
576
620
|
|
|
577
621
|
|