simcord 2.0.0__tar.gz → 2.0.2__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.0.0 → simcord-2.0.2}/CHANGELOG.md +16 -0
- {simcord-2.0.0 → simcord-2.0.2}/PKG-INFO +1 -1
- {simcord-2.0.0 → simcord-2.0.2}/pyproject.toml +1 -1
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/guilds.py +17 -4
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/serializers.py +12 -1
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/env.py +7 -1
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/guilds.py +2 -2
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_edge_cases.py +6 -1
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_roles_and_ordering.py +75 -0
- simcord-2.0.2/tests/integration/test_setup_hook_suspension.py +107 -0
- {simcord-2.0.0 → simcord-2.0.2}/uv.lock +1 -1
- {simcord-2.0.0 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.github/dependabot.yml +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.github/workflows/ci.yml +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.github/workflows/dpy-master.yml +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.github/workflows/release.yml +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.gitignore +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.pre-commit-config.yaml +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.python-version +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/.readthedocs.yaml +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/CODE_OF_CONDUCT.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/CONTRIBUTING.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/LICENSE +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/README.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/SECURITY.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/benchmarks/_helpers.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/benchmarks/test_dispatch_benchmarks.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/benchmarks/test_perf_guards.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/changes/README.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/api.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/architecture.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/assets/social-card.png +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/changelog.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/concepts.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/cookbook.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/faq.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/ai-coding-agents.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/components.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/diagnostics.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/fixtures.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/intents.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/interactions.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/messages.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/permissions.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/test-without-token.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/testing-slash-commands.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/threads-reactions-dms.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/guides/time-control.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/index.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/installation.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/llms.txt +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/parity-matrix.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/performance.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/quickstart.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/robots.txt +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/docs/stability.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/examples/README.md +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/examples/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/examples/bot.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/examples/conftest.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/examples/test_bot.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/mkdocs.yml +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/overrides/main.html +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/scripts/check_discoverability.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/_dpy_internals.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/actors.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/asserts.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/cdn.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/errors.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/automod.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/channel.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/expression.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/guild.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/interaction.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/invite.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/member.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/message.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/role.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/user.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/voice.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/base.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/channels.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/messages.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/polls.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/scheduled_events.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/permissions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/backend/state.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/builders.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/components.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/enums.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/gateway.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/_helpers.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/client.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/router.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/application.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/automod.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/channels.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/commands.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/interactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/invites.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/messages.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/intents.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/interactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/parity.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/py.typed +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/pytest_plugin.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/src/simcord/results.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/conformance/_world.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/conformance/test_serializers.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/conftest.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/fixtures/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_app_commands.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_application_emojis.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_asserts.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_audit_logs.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_automod.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_bot_users_and_webhooks.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_channels.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_command_permissions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_diagnostics.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_expressions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_forum.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_guilds.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_intents.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_interaction_followups.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_interactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_invites.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_lifecycle.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_members.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_message_routes.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_messages.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_misc_branches.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_moderation.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_polls.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_reactions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_settlement_contract.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_sharding.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_stage_instances.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_threads.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_time.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_transport_edges.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_voice_state.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/integration/test_world_helpers.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_components_v2_validation.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_errors.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_intent_rules.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_parity.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_permission_validators.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_permissions.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_pytest_plugin.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_router.py +0 -0
- {simcord-2.0.0 → simcord-2.0.2}/tests/unit/test_version.py +0 -0
|
@@ -4,6 +4,22 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
|
|
|
4
4
|
|
|
5
5
|
<!-- towncrier release notes start -->
|
|
6
6
|
|
|
7
|
+
## 2.0.2 (2026-09-18)
|
|
8
|
+
|
|
9
|
+
### Bug fixes
|
|
10
|
+
|
|
11
|
+
- Fixed ``create_role`` leaving stale ``Role.position`` values in the bot's cache: inserting a role bumps every existing role's position, and the backend now announces each shift with ``GUILD_ROLE_UPDATE`` after ``GUILD_ROLE_CREATE``, as real Discord does. ``Member.top_role``, ``Role`` comparisons, and hierarchy checks now see the true ordering after role creation.
|
|
12
|
+
- Fixed ``role.colour`` always reading ``0`` in the bot's cache: role payloads now carry the ``colors`` object (``primary_color``/``secondary_color``/``tertiary_color``) that discord.py reads, alongside the deprecated flat ``color`` field.
|
|
13
|
+
- Fixed guild role listings to match real Discord's ordering: ``GET``/``PATCH /guilds/{id}/roles`` responses and the ``roles`` array inside guild payloads (``GUILD_CREATE``/``GUILD_UPDATE`` events and guild REST responses) are now sorted by role id ascending instead of insertion order (observable when a guild is created with an explicit id, which can make ``@everyone``'s snowflake the largest).
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## 2.0.1 (2026-09-10)
|
|
17
|
+
|
|
18
|
+
### Bug fixes
|
|
19
|
+
|
|
20
|
+
- Fixed a 2.0.0 crash where ``Env.start()`` raised ``ValueError: ... was created in a different Context`` whenever ``setup_hook`` suspended on a timer or socket — for example opening a database pool with ``asyncpg.create_pool``, the documented discord.py startup pattern. Task resumption callbacks are now scheduled in the task's own ``Context`` instead of a copy, so ``ContextVar`` tokens created before a suspension remain valid after it. ([#24](https://github.com/SilentHacks/simcord/issues/24))
|
|
21
|
+
|
|
22
|
+
|
|
7
23
|
## 2.0.0 (2026-09-08)
|
|
8
24
|
|
|
9
25
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simcord
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
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/
|
|
@@ -180,9 +180,23 @@ class GuildMixin(BackendBase):
|
|
|
180
180
|
|
|
181
181
|
# ----------------------------------------------------------------- roles
|
|
182
182
|
|
|
183
|
+
def announce_role_updates(self, guild_id: int, roles: Iterable[Role]) -> None:
|
|
184
|
+
"""Announce each role's current state with GUILD_ROLE_UPDATE.
|
|
185
|
+
|
|
186
|
+
Real Discord emits an update for every role whose position changed as a
|
|
187
|
+
side effect of another operation; without them a client's cache keeps
|
|
188
|
+
stale positions, so position-driven mutations must announce their shifts.
|
|
189
|
+
"""
|
|
190
|
+
for role in list(roles):
|
|
191
|
+
self.emit(
|
|
192
|
+
"GUILD_ROLE_UPDATE",
|
|
193
|
+
{"guild_id": str(guild_id), "role": serializers.role_payload(role)},
|
|
194
|
+
)
|
|
195
|
+
|
|
183
196
|
def create_role(self, guild_id: int, name: str, *, permissions: int = 0, **fields: Any) -> Role:
|
|
184
197
|
guild = self.get_guild(guild_id)
|
|
185
198
|
position = fields.pop("position", None)
|
|
199
|
+
shifted: list[Role] = []
|
|
186
200
|
if position is None:
|
|
187
201
|
# New roles insert just above @everyone, pushing existing roles up
|
|
188
202
|
# (so the bot's integration role stays on top), as on real Discord.
|
|
@@ -190,6 +204,7 @@ class GuildMixin(BackendBase):
|
|
|
190
204
|
for existing in guild.roles.values():
|
|
191
205
|
if existing.position >= 1:
|
|
192
206
|
existing.position += 1
|
|
207
|
+
shifted.append(existing)
|
|
193
208
|
role = Role(
|
|
194
209
|
id=self.snowflake(),
|
|
195
210
|
name=name,
|
|
@@ -202,6 +217,7 @@ class GuildMixin(BackendBase):
|
|
|
202
217
|
"GUILD_ROLE_CREATE",
|
|
203
218
|
{"guild_id": str(guild_id), "role": serializers.role_payload(role)},
|
|
204
219
|
)
|
|
220
|
+
self.announce_role_updates(guild_id, shifted)
|
|
205
221
|
return role
|
|
206
222
|
|
|
207
223
|
def edit_role(self, guild_id: int, role_id: int, changes: Mapping[str, Any]) -> Role:
|
|
@@ -229,10 +245,7 @@ class GuildMixin(BackendBase):
|
|
|
229
245
|
if role.position != new_position:
|
|
230
246
|
role.position = new_position
|
|
231
247
|
changed.append(role)
|
|
232
|
-
|
|
233
|
-
self.emit(
|
|
234
|
-
"GUILD_ROLE_UPDATE", {"guild_id": str(guild_id), "role": serializers.role_payload(role)}
|
|
235
|
-
)
|
|
248
|
+
self.announce_role_updates(guild_id, changed)
|
|
236
249
|
return guild
|
|
237
250
|
|
|
238
251
|
def delete_role(self, guild_id: int, role_id: int) -> None:
|
|
@@ -88,6 +88,11 @@ def role_payload(role: Role) -> role_types.Role:
|
|
|
88
88
|
"permissions": str(role.permissions),
|
|
89
89
|
"position": role.position,
|
|
90
90
|
"color": role.color,
|
|
91
|
+
"colors": {
|
|
92
|
+
"primary_color": role.color,
|
|
93
|
+
"secondary_color": None,
|
|
94
|
+
"tertiary_color": None,
|
|
95
|
+
},
|
|
91
96
|
"hoist": role.hoist,
|
|
92
97
|
"managed": role.managed,
|
|
93
98
|
"mentionable": role.mentionable,
|
|
@@ -98,6 +103,12 @@ def role_payload(role: Role) -> role_types.Role:
|
|
|
98
103
|
)
|
|
99
104
|
|
|
100
105
|
|
|
106
|
+
def roles_payload(guild: Guild) -> list[role_types.Role]:
|
|
107
|
+
# Discord serves a guild's role list sorted by role id ascending (the
|
|
108
|
+
# @everyone role shares the guild id, so it can sort anywhere).
|
|
109
|
+
return [role_payload(r) for r in sorted(guild.roles.values(), key=lambda r: r.id)]
|
|
110
|
+
|
|
111
|
+
|
|
101
112
|
def member_payload(
|
|
102
113
|
backend: BackendBase, guild: Guild, member: Member, *, with_user: bool = True
|
|
103
114
|
) -> member_types.MemberWithUser:
|
|
@@ -355,7 +366,7 @@ def guild_create_payload(backend: BackendBase, guild: Guild) -> guild_types.Guil
|
|
|
355
366
|
"features": [],
|
|
356
367
|
"emojis": [guild_emoji_payload(backend, e) for e in guild.emojis.values()],
|
|
357
368
|
"stickers": [sticker_payload(backend, s) for s in guild.stickers.values()],
|
|
358
|
-
"roles":
|
|
369
|
+
"roles": roles_payload(guild),
|
|
359
370
|
"member_count": len(guild.members),
|
|
360
371
|
"large": False,
|
|
361
372
|
"unavailable": False,
|
|
@@ -272,6 +272,11 @@ class Env:
|
|
|
272
272
|
) -> asyncio.Handle:
|
|
273
273
|
bound_task = getattr(callback, "__self__", None)
|
|
274
274
|
if isinstance(bound_task, asyncio.Task):
|
|
275
|
+
# A task resumption callback (__step/__wakeup) must run in the exact
|
|
276
|
+
# Context the task was scheduled with — scheduling a copy resumes the
|
|
277
|
+
# coroutine in a different Context object, where ContextVar.reset of
|
|
278
|
+
# a pre-suspension token raises "created in a different Context".
|
|
279
|
+
# Ownership is already carried by the task's own context.
|
|
275
280
|
owner_record = self._task_records.get(bound_task)
|
|
276
281
|
scope = (
|
|
277
282
|
(self, owner_record.generation)
|
|
@@ -280,9 +285,10 @@ class Env:
|
|
|
280
285
|
if context is not None
|
|
281
286
|
else None
|
|
282
287
|
)
|
|
288
|
+
schedule_context = context
|
|
283
289
|
else:
|
|
284
290
|
scope = _BOT_SCOPE.get()
|
|
285
|
-
|
|
291
|
+
schedule_context = self._context_for_scope(context, scope) if context is not None else context
|
|
286
292
|
if scope is None or scope[0] is not self:
|
|
287
293
|
return original(*schedule_args, callback, *args, context=schedule_context)
|
|
288
294
|
label = getattr(callback, "__qualname__", None) or getattr(
|
|
@@ -640,7 +640,7 @@ def delete_role(ctx: RequestContext) -> Any:
|
|
|
640
640
|
@route("GET", "/guilds/{guild_id}/roles")
|
|
641
641
|
def get_roles(ctx: RequestContext) -> Any:
|
|
642
642
|
guild = ctx.backend.get_guild(ctx.int_arg("guild_id"))
|
|
643
|
-
return [dict(
|
|
643
|
+
return [dict(r) for r in serializers.roles_payload(guild)]
|
|
644
644
|
|
|
645
645
|
|
|
646
646
|
@route("GET", "/guilds/{guild_id}/roles/{role_id}")
|
|
@@ -668,4 +668,4 @@ def move_role_positions(ctx: RequestContext) -> Any:
|
|
|
668
668
|
backend.require_role_assignable(guild_id, bot_id, int(item["id"]))
|
|
669
669
|
backend.require_position_assignable(guild_id, bot_id, int(item["position"]))
|
|
670
670
|
guild = backend.reorder_roles(guild_id, positions)
|
|
671
|
-
return [dict(
|
|
671
|
+
return [dict(r) for r in serializers.roles_payload(guild)]
|
|
@@ -69,7 +69,12 @@ async def test_role_reorder_changes_positions(env):
|
|
|
69
69
|
await env.settle()
|
|
70
70
|
|
|
71
71
|
assert env.backend.get_role(env.guild.id, low.id).position == high_pos
|
|
72
|
-
|
|
72
|
+
# The reorder itself announced the moved role (create_role updates alone
|
|
73
|
+
# would leave this assertion passing otherwise).
|
|
74
|
+
assert any(
|
|
75
|
+
name == "GUILD_ROLE_UPDATE" and payload["role"]["id"] == str(low.id)
|
|
76
|
+
for _, name, payload in env.backend.transcript
|
|
77
|
+
)
|
|
73
78
|
|
|
74
79
|
|
|
75
80
|
async def test_second_stage_instance_rejected(env):
|
|
@@ -54,6 +54,81 @@ async def test_edit_role_positions(env):
|
|
|
54
54
|
assert guild.get_role(low.id).position == high_pos
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
async def test_create_role_announces_position_shifts(env):
|
|
58
|
+
guild = env.bot.get_guild(env.guild.id)
|
|
59
|
+
await guild.create_role(name="admin")
|
|
60
|
+
await guild.create_role(name="mod")
|
|
61
|
+
await env.settle()
|
|
62
|
+
|
|
63
|
+
# Each create inserts at position 1 and bumps every existing role up; the
|
|
64
|
+
# backend announces each shift with GUILD_ROLE_UPDATE (after the create, as
|
|
65
|
+
# real Discord does), so every cached position matches the backend's.
|
|
66
|
+
backend_guild = env.backend.get_guild(env.guild.id)
|
|
67
|
+
for backend_role in backend_guild.roles.values():
|
|
68
|
+
cached = guild.get_role(backend_role.id)
|
|
69
|
+
assert cached is not None
|
|
70
|
+
assert cached.position == backend_role.position
|
|
71
|
+
assert backend_guild.roles[guild.id].position == 0
|
|
72
|
+
|
|
73
|
+
# Each create lands first, then the shift updates it caused.
|
|
74
|
+
role_events = [
|
|
75
|
+
(name, payload["role"]["name"])
|
|
76
|
+
for _, name, payload in env.backend.transcript
|
|
77
|
+
if name in ("GUILD_ROLE_CREATE", "GUILD_ROLE_UPDATE")
|
|
78
|
+
]
|
|
79
|
+
assert role_events[:2] == [("GUILD_ROLE_CREATE", "admin"), ("GUILD_ROLE_UPDATE", "TestBot")]
|
|
80
|
+
assert role_events[2] == ("GUILD_ROLE_CREATE", "mod")
|
|
81
|
+
assert all(name == "GUILD_ROLE_UPDATE" for name, _ in role_events[3:])
|
|
82
|
+
|
|
83
|
+
# The bot's managed role rides on top of the hierarchy, so hierarchy checks
|
|
84
|
+
# like "bot must outrank target" see a true ordering.
|
|
85
|
+
assert guild.me.top_role.position == max(r.position for r in backend_guild.roles.values())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def test_delete_role_leaves_position_gap(env):
|
|
89
|
+
guild = env.bot.get_guild(env.guild.id)
|
|
90
|
+
low = await guild.create_role(name="low")
|
|
91
|
+
mid = await guild.create_role(name="mid")
|
|
92
|
+
await env.settle()
|
|
93
|
+
|
|
94
|
+
await mid.delete()
|
|
95
|
+
await env.settle()
|
|
96
|
+
|
|
97
|
+
# Discord does not renumber positions on delete: the hole at 1 stays a hole.
|
|
98
|
+
assert env.backend.get_role(guild.id, low.id).position == 2
|
|
99
|
+
assert guild.get_role(low.id).position == 2
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def test_role_colour_reaches_the_cache(env):
|
|
103
|
+
guild = env.bot.get_guild(env.guild.id)
|
|
104
|
+
role = await guild.create_role(name="Tinted", colour=discord.Colour(0x112233))
|
|
105
|
+
await env.settle()
|
|
106
|
+
|
|
107
|
+
# discord.py reads the role "colors" object (primary/secondary/tertiary);
|
|
108
|
+
# the deprecated flat "color" field alone leaves every cached colour at 0.
|
|
109
|
+
assert role.colour == discord.Colour(0x112233)
|
|
110
|
+
cached = guild.get_role(role.id)
|
|
111
|
+
assert cached.colour == discord.Colour(0x112233)
|
|
112
|
+
assert cached.secondary_colour is None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def test_guild_roles_list_sorted_by_id(env):
|
|
116
|
+
odd = env.create_guild("Odd", id=8_000_000_000_000_000_000)
|
|
117
|
+
await env.settle()
|
|
118
|
+
guild = env.bot.get_guild(odd.id)
|
|
119
|
+
await guild.create_role(name="one")
|
|
120
|
+
await guild.create_role(name="two")
|
|
121
|
+
await env.settle()
|
|
122
|
+
|
|
123
|
+
# Real Discord returns a guild's roles sorted by id ascending — with an
|
|
124
|
+
# explicit guild id the @everyone role's snowflake is the largest, so
|
|
125
|
+
# insertion order alone would wrongly put it first.
|
|
126
|
+
fetched = await guild.fetch_roles()
|
|
127
|
+
ids = [r.id for r in fetched]
|
|
128
|
+
assert ids == sorted(ids)
|
|
129
|
+
assert ids[-1] == odd.id
|
|
130
|
+
|
|
131
|
+
|
|
57
132
|
async def test_reorder_cannot_lift_role_above_bot(env):
|
|
58
133
|
guild = env.bot.get_guild(env.guild.id)
|
|
59
134
|
role = await guild.create_role(name="Climber")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""setup_hook may suspend on timers and sockets — e.g. opening a DB pool.
|
|
2
|
+
|
|
3
|
+
Regression tests for the 2.0.0 crash where ``Env.start()`` raised
|
|
4
|
+
``ValueError: ... was created in a different Context``: task resumption
|
|
5
|
+
callbacks were scheduled under a copied ``Context``, so a suspended
|
|
6
|
+
``setup_hook`` resumed in a different ``Context`` object and the
|
|
7
|
+
``_bot_scope`` ``ContextVar.reset`` became invalid.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextvars
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from discord.ext import commands
|
|
15
|
+
|
|
16
|
+
import simcord
|
|
17
|
+
from fixtures.sample_bot import create_bot
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _suspending_bot(suspend) -> commands.Bot:
|
|
21
|
+
"""A sample bot whose setup_hook suspends via ``suspend`` first."""
|
|
22
|
+
bot = create_bot()
|
|
23
|
+
original_setup = bot.setup_hook
|
|
24
|
+
|
|
25
|
+
async def setup_hook() -> None:
|
|
26
|
+
await suspend()
|
|
27
|
+
await original_setup()
|
|
28
|
+
|
|
29
|
+
bot.setup_hook = setup_hook
|
|
30
|
+
return bot
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
@pytest.mark.parametrize("delay", [0, 0.01])
|
|
35
|
+
async def test_setup_hook_may_suspend_on_timers(delay: float):
|
|
36
|
+
bot = _suspending_bot(lambda: asyncio.sleep(delay))
|
|
37
|
+
async with simcord.run(bot) as env:
|
|
38
|
+
guild = env.create_guild()
|
|
39
|
+
alice = guild.add_member(env.create_user("alice"))
|
|
40
|
+
channel = guild.create_text_channel("general")
|
|
41
|
+
await alice.send(channel, "!ping")
|
|
42
|
+
assert channel.last_message is not None
|
|
43
|
+
assert channel.last_message.content == "Pong!"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.asyncio
|
|
47
|
+
async def test_setup_hook_may_suspend_on_a_socket():
|
|
48
|
+
received: list[bytes] = []
|
|
49
|
+
|
|
50
|
+
async def echo(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
51
|
+
received.append(await reader.read(4))
|
|
52
|
+
writer.write(b"pong")
|
|
53
|
+
await writer.drain()
|
|
54
|
+
writer.close()
|
|
55
|
+
|
|
56
|
+
server = await asyncio.start_server(echo, "127.0.0.1", 0)
|
|
57
|
+
try:
|
|
58
|
+
port = server.sockets[0].getsockname()[1]
|
|
59
|
+
|
|
60
|
+
async def open_pool_like() -> None:
|
|
61
|
+
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
|
62
|
+
writer.write(b"ping")
|
|
63
|
+
await writer.drain()
|
|
64
|
+
assert await reader.read(4) == b"pong"
|
|
65
|
+
writer.close()
|
|
66
|
+
await writer.wait_closed()
|
|
67
|
+
|
|
68
|
+
bot = _suspending_bot(open_pool_like)
|
|
69
|
+
async with simcord.run(bot) as env:
|
|
70
|
+
env.create_guild()
|
|
71
|
+
finally:
|
|
72
|
+
server.close()
|
|
73
|
+
await server.wait_closed()
|
|
74
|
+
assert received == [b"ping"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_user_contextvar_survives_setup_hook_suspension():
|
|
79
|
+
"""A user ContextVar behaves as if setup_hook never left its own Context:
|
|
80
|
+
a value set before an await is visible after it, and a value set after an
|
|
81
|
+
await survives the next suspension (a copied resumption context would lose it)."""
|
|
82
|
+
var: contextvars.ContextVar[str] = contextvars.ContextVar("simcord_test_var")
|
|
83
|
+
observations: list[str] = []
|
|
84
|
+
bot = create_bot()
|
|
85
|
+
original_setup = bot.setup_hook
|
|
86
|
+
|
|
87
|
+
async def setup_hook() -> None:
|
|
88
|
+
var.set("before-await")
|
|
89
|
+
await asyncio.sleep(0.01)
|
|
90
|
+
observations.append(var.get())
|
|
91
|
+
var.set("after-await")
|
|
92
|
+
await asyncio.sleep(0.01)
|
|
93
|
+
observations.append(var.get())
|
|
94
|
+
await original_setup()
|
|
95
|
+
|
|
96
|
+
bot.setup_hook = setup_hook
|
|
97
|
+
async with simcord.run(bot):
|
|
98
|
+
pass
|
|
99
|
+
assert observations == ["before-await", "after-await"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.asyncio
|
|
103
|
+
async def test_restart_bot_with_suspending_setup_hook(env, channel, alice):
|
|
104
|
+
await env.restart_bot(_suspending_bot(lambda: asyncio.sleep(0.01)))
|
|
105
|
+
await alice.send(channel, "!ping")
|
|
106
|
+
assert channel.last_message is not None
|
|
107
|
+
assert channel.last_message.content == "Pong!"
|
|
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
|
|
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
|