simcord 2.2.0__tar.gz → 2.2.1__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.2.1}/CHANGELOG.md +7 -0
- {simcord-2.2.0 → simcord-2.2.1}/PKG-INFO +1 -1
- {simcord-2.2.0 → simcord-2.2.1}/docs/concepts.md +4 -1
- {simcord-2.2.0 → simcord-2.2.1}/pyproject.toml +1 -1
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/_dpy_internals.py +17 -1
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_settlement_contract.py +47 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_dpy_internals.py +72 -0
- {simcord-2.2.0 → simcord-2.2.1}/uv.lock +1 -1
- {simcord-2.2.0 → simcord-2.2.1}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.github/dependabot.yml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.github/workflows/ci.yml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.github/workflows/dpy-master.yml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.github/workflows/release.yml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.github/workflows/verify.yml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.gitignore +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.pre-commit-config.yaml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.python-version +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/.readthedocs.yaml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/CODE_OF_CONDUCT.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/CONTRIBUTING.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/LICENSE +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/README.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/SECURITY.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/benchmarks/_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/benchmarks/test_dispatch_benchmarks.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/benchmarks/test_perf_guards.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/changes/README.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/api.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/architecture.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/assets/social-card.png +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/changelog.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/cookbook.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/faq.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/ai-coding-agents.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/components.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/diagnostics.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/fixtures.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/intents.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/interactions.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/messages.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/permissions.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/preview.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/test-without-token.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/testing-slash-commands.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/threads-reactions-dms.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/guides/time-control.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/index.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/installation.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/llms.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/parity-matrix.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/performance.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/quickstart.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/robots.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/docs/stability.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/examples/README.md +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/examples/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/examples/bot.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/examples/conftest.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/examples/preview_example.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/examples/test_bot.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/mkdocs.yml +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/overrides/main.html +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/scripts/capture_visual_reference.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/scripts/check_discoverability.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/scripts/check_package.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/scripts/compare_visual_reference.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/scripts/discord_reference_bot.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/actors.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/asserts.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/access.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/cdn.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/errors.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/automod.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/channel.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/expression.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/guild.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/interaction.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/invite.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/member.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/message.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/role.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/user.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/voice.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/base.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/channels.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/guilds.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/messages.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/polls.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/scheduled_events.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/serializers.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/backend/state.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/builders.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/components.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/enums.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/env.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/gateway.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/client.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/router.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/application.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/automod.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/channels.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/commands.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/guilds.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/invites.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/messages.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/intents.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/parity.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_actions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_assets.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_capture.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_markdown.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_media.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_pages.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_server.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/_snapshot.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/protocol.schema.json +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/app.js +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/components.js +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Arabic.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/OFL-1.1-Noto-CJK.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Devanagari.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Emoji.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Hebrew.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/OFL-1.1-Noto.txt +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/manifest.json +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-color-emoji-v2.051.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-sans-arabic-v2.012.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-sans-devanagari-v2.007.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-sans-hebrew-v3.001.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-sans-latin-italic-v2.015.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-sans-latin-v2.015.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-sans-mono-v2.014.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/fonts/noto-sans-sc-v2.004.ttf +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/index.html +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/preview/static/preview.css +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/py.typed +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/pytest_plugin.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/results.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/src/simcord/types.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/conformance/_world.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/conformance/test_serializers.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/conftest.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/preview/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/preview/catalog.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/preview/coverage.json +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/preview/measurements.json +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/preview/profiles.json +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/preview/states.json +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/preview_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_app_commands.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_application_emojis.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_asserts.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_audit_logs.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_automod.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_bot_users_and_webhooks.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_channels.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_command_permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_diagnostics.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_edge_cases.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_expressions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_forum.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_guilds.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_intents.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_interaction_followups.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_interactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_invites.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_lifecycle.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_members.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_message_routes.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_messages.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_misc_branches.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_moderation.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_polls.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_additional.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_capture.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_capture_dx.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_coverage.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_dx.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_edges.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_fixes.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_fonts.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_preview_security.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_reactions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_roles_and_ordering.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_setup_hook_suspension.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_sharding.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_stage_instances.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_threads.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_time.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_transport_edges.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_voice_state.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/integration/test_world_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_components_v2_validation.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_errors.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_intent_rules.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_lazy_import.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_parity.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_permission_validators.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_permissions.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_pytest_plugin.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_router.py +0 -0
- {simcord-2.2.0 → simcord-2.2.1}/tests/unit/test_version.py +0 -0
|
@@ -4,6 +4,13 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
|
|
|
4
4
|
|
|
5
5
|
<!-- towncrier release notes start -->
|
|
6
6
|
|
|
7
|
+
## 2.2.1 (2026-09-24)
|
|
8
|
+
|
|
9
|
+
### Bug fixes
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
## 2.2.0 (2026-09-23)
|
|
8
15
|
|
|
9
16
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simcord
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
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
|
|
|
@@ -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
|
|
@@ -1074,6 +1074,53 @@ async def test_view_button_click_before_expiry_is_joined():
|
|
|
1074
1074
|
assert timeouts == 0
|
|
1075
1075
|
|
|
1076
1076
|
|
|
1077
|
+
@pytest.mark.asyncio
|
|
1078
|
+
async def test_fully_dynamic_view_expiry_is_virtual_only():
|
|
1079
|
+
"""A LayoutView whose only dispatchable item is a DynamicItem lands solely
|
|
1080
|
+
in the store's pattern table — no ``_views`` entry — yet its expiry task is
|
|
1081
|
+
still a recognized wait: the verb returns promptly, the real clock never
|
|
1082
|
+
fires it, and advance_time() runs on_timeout exactly once."""
|
|
1083
|
+
bot = create_bot()
|
|
1084
|
+
timeouts = 0
|
|
1085
|
+
|
|
1086
|
+
class ApproveButton(discord.ui.DynamicItem[discord.ui.Button], template=r"approve:\d+"):
|
|
1087
|
+
def __init__(self) -> None:
|
|
1088
|
+
super().__init__(discord.ui.Button(label="Approve", custom_id="approve:1"))
|
|
1089
|
+
|
|
1090
|
+
async def callback(self, interaction: discord.Interaction) -> None:
|
|
1091
|
+
await interaction.response.send_message("approved")
|
|
1092
|
+
|
|
1093
|
+
class OfferView(discord.ui.LayoutView):
|
|
1094
|
+
def __init__(self) -> None:
|
|
1095
|
+
super().__init__(timeout=0.3)
|
|
1096
|
+
row = discord.ui.ActionRow()
|
|
1097
|
+
row.add_item(ApproveButton())
|
|
1098
|
+
self.add_item(row)
|
|
1099
|
+
|
|
1100
|
+
async def on_timeout(self) -> None:
|
|
1101
|
+
nonlocal timeouts
|
|
1102
|
+
timeouts += 1
|
|
1103
|
+
|
|
1104
|
+
@bot.tree.command(name="dynamic-offer")
|
|
1105
|
+
async def dynamic_offer(interaction: discord.Interaction) -> None:
|
|
1106
|
+
await interaction.response.send_message(view=OfferView())
|
|
1107
|
+
|
|
1108
|
+
async with simcord.run(bot, settle_timeout=1.0) as env:
|
|
1109
|
+
guild = env.create_guild()
|
|
1110
|
+
alice = guild.add_member(env.create_user("alice"))
|
|
1111
|
+
channel = guild.create_text_channel("general")
|
|
1112
|
+
started = time.monotonic()
|
|
1113
|
+
result = await alice.slash(channel, "dynamic-offer")
|
|
1114
|
+
assert time.monotonic() - started < 0.25
|
|
1115
|
+
assert result.response is not None
|
|
1116
|
+
await asyncio.sleep(0.4) # past the timeout on the real clock
|
|
1117
|
+
assert timeouts == 0
|
|
1118
|
+
await env.advance_time(0.3)
|
|
1119
|
+
assert timeouts == 1
|
|
1120
|
+
await env.advance_time(0.3)
|
|
1121
|
+
assert timeouts == 1
|
|
1122
|
+
|
|
1123
|
+
|
|
1077
1124
|
@pytest.mark.asyncio
|
|
1078
1125
|
async def test_external_wait_covers_timed_awaitable():
|
|
1079
1126
|
"""A declared external_wait around a timed awaitable is fully virtual:
|
|
@@ -143,6 +143,65 @@ async def test_view_timeout_task_identity_handles_missing_store() -> None:
|
|
|
143
143
|
assert not _dpy_internals._view_timeout_task_identity(client, _fake_task())
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
def _impl_coro_task(view: Any, waiter: Any = None) -> tuple[Any, Any]:
|
|
147
|
+
"""A fake task running the view's real ``__timeout_task_impl`` coroutine."""
|
|
148
|
+
task = _fake_task(waiter)
|
|
149
|
+
coro = view._BaseView__timeout_task_impl() # never awaited; caller closes it
|
|
150
|
+
task.get_coro = lambda: coro
|
|
151
|
+
return task, coro
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def test_view_timeout_task_identity_matches_impl_coroutine() -> None:
|
|
155
|
+
"""A fully-dynamic view lands in no store container; the coroutine itself
|
|
156
|
+
still identifies its expiry task."""
|
|
157
|
+
view = discord.ui.LayoutView(timeout=5)
|
|
158
|
+
task, coro = _impl_coro_task(view)
|
|
159
|
+
try:
|
|
160
|
+
assert _dpy_internals._view_timeout_task_identity(_store_client(), task)
|
|
161
|
+
finally:
|
|
162
|
+
coro.close()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def test_view_timeout_task_identity_rejects_stopped_impl_owner() -> None:
|
|
166
|
+
view = discord.ui.LayoutView(timeout=5)
|
|
167
|
+
task, coro = _impl_coro_task(view)
|
|
168
|
+
stopped: asyncio.Future[Any] = asyncio.Future()
|
|
169
|
+
stopped.set_result(None)
|
|
170
|
+
stopped_name = next(name for name in vars(view) if name.endswith("__stopped"))
|
|
171
|
+
vars(view)[stopped_name] = stopped
|
|
172
|
+
try:
|
|
173
|
+
assert not _dpy_internals._view_timeout_task_identity(_store_client(), task)
|
|
174
|
+
finally:
|
|
175
|
+
coro.close()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def test_view_timeout_task_identity_rejects_foreign_coroutine() -> None:
|
|
179
|
+
task = _fake_task()
|
|
180
|
+
task.get_coro = lambda: SimpleNamespace(cr_code=object(), cr_frame=None)
|
|
181
|
+
assert not _dpy_internals._view_timeout_task_identity(_store_client(), task)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def test_view_timeout_task_identity_ignores_closed_impl_coroutine() -> None:
|
|
185
|
+
"""A finished impl coroutine has no live frame, so no owner to match."""
|
|
186
|
+
view = discord.ui.LayoutView(timeout=5)
|
|
187
|
+
coro = view._BaseView__timeout_task_impl()
|
|
188
|
+
coro.close()
|
|
189
|
+
task = _fake_task()
|
|
190
|
+
task.get_coro = lambda: coro
|
|
191
|
+
assert not _dpy_internals._view_timeout_task_identity(_store_client(), task)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def test_is_intentional_wait_wakeup_impl_coroutine() -> None:
|
|
195
|
+
view = discord.ui.LayoutView(timeout=5)
|
|
196
|
+
task, coro = _impl_coro_task(view)
|
|
197
|
+
try:
|
|
198
|
+
assert _dpy_internals.is_intentional_wait_wakeup(
|
|
199
|
+
_store_client(), _set_result_unless_cancelled, (), task
|
|
200
|
+
)
|
|
201
|
+
finally:
|
|
202
|
+
coro.close()
|
|
203
|
+
|
|
204
|
+
|
|
146
205
|
async def test_view_expiry_task_requires_pending_waiter() -> None:
|
|
147
206
|
view = discord.ui.View(timeout=5)
|
|
148
207
|
task = _task_with_timeout(view)
|
|
@@ -454,6 +513,19 @@ async def test_park_reason_names_view_expiry_and_loop_interval(env: Any) -> None
|
|
|
454
513
|
assert env._park_reason(loop_task, 5.0) == "discord.ext.tasks loop interval"
|
|
455
514
|
|
|
456
515
|
|
|
516
|
+
async def test_park_reason_names_unregistered_view_expiry(env: Any) -> None:
|
|
517
|
+
"""A fully-dynamic view occupies no store container; its expiry task is
|
|
518
|
+
still a recognized wait, identified by the coroutine it runs."""
|
|
519
|
+
view = discord.ui.LayoutView(timeout=60)
|
|
520
|
+
waiter: asyncio.Future[Any] = asyncio.Future()
|
|
521
|
+
task, coro = _impl_coro_task(view, waiter)
|
|
522
|
+
try:
|
|
523
|
+
_record(env, _set_result_unless_cancelled, (waiter,), 10.0)
|
|
524
|
+
assert env._park_reason(task, 5.0) == "discord View/Modal expiry timer"
|
|
525
|
+
finally:
|
|
526
|
+
coro.close()
|
|
527
|
+
|
|
528
|
+
|
|
457
529
|
async def test_park_reason_composed_and_sleep_waits(env: Any) -> None:
|
|
458
530
|
listener: asyncio.Future[Any] = asyncio.Future()
|
|
459
531
|
env.bot._listeners.setdefault("on_message", []).append((listener, None))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|