simcord 0.1.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.
Files changed (90) hide show
  1. simcord-0.1.0/.github/ISSUE_TEMPLATE/bug.md +17 -0
  2. simcord-0.1.0/.github/ISSUE_TEMPLATE/feature.md +12 -0
  3. simcord-0.1.0/.github/ISSUE_TEMPLATE/parity-gap.md +11 -0
  4. simcord-0.1.0/.github/dependabot.yml +10 -0
  5. simcord-0.1.0/.github/workflows/ci.yml +43 -0
  6. simcord-0.1.0/.github/workflows/dpy-master.yml +22 -0
  7. simcord-0.1.0/.github/workflows/release.yml +45 -0
  8. simcord-0.1.0/.gitignore +26 -0
  9. simcord-0.1.0/.python-version +1 -0
  10. simcord-0.1.0/.readthedocs.yaml +16 -0
  11. simcord-0.1.0/CHANGELOG.md +34 -0
  12. simcord-0.1.0/CODE_OF_CONDUCT.md +8 -0
  13. simcord-0.1.0/CONTRIBUTING.md +52 -0
  14. simcord-0.1.0/LICENSE +21 -0
  15. simcord-0.1.0/PKG-INFO +210 -0
  16. simcord-0.1.0/README.md +170 -0
  17. simcord-0.1.0/SECURITY.md +9 -0
  18. simcord-0.1.0/changes/README.md +1 -0
  19. simcord-0.1.0/docs/architecture.md +52 -0
  20. simcord-0.1.0/docs/guides/interactions.md +80 -0
  21. simcord-0.1.0/docs/guides/messages.md +64 -0
  22. simcord-0.1.0/docs/guides/permissions.md +39 -0
  23. simcord-0.1.0/docs/index.md +40 -0
  24. simcord-0.1.0/docs/migrating-from-dpytest.md +29 -0
  25. simcord-0.1.0/docs/parity-matrix.md +104 -0
  26. simcord-0.1.0/docs/quickstart.md +70 -0
  27. simcord-0.1.0/examples/__init__.py +0 -0
  28. simcord-0.1.0/examples/bot.py +37 -0
  29. simcord-0.1.0/examples/conftest.py +9 -0
  30. simcord-0.1.0/examples/test_bot.py +30 -0
  31. simcord-0.1.0/mkdocs.yml +30 -0
  32. simcord-0.1.0/pyproject.toml +114 -0
  33. simcord-0.1.0/src/simcord/__init__.py +50 -0
  34. simcord-0.1.0/src/simcord/_dpy_internals.py +86 -0
  35. simcord-0.1.0/src/simcord/actors.py +393 -0
  36. simcord-0.1.0/src/simcord/backend/__init__.py +11 -0
  37. simcord-0.1.0/src/simcord/backend/cdn.py +30 -0
  38. simcord-0.1.0/src/simcord/backend/errors.py +84 -0
  39. simcord-0.1.0/src/simcord/backend/models/__init__.py +31 -0
  40. simcord-0.1.0/src/simcord/backend/models/channel.py +53 -0
  41. simcord-0.1.0/src/simcord/backend/models/guild.py +28 -0
  42. simcord-0.1.0/src/simcord/backend/models/interaction.py +97 -0
  43. simcord-0.1.0/src/simcord/backend/models/member.py +15 -0
  44. simcord-0.1.0/src/simcord/backend/models/message.py +61 -0
  45. simcord-0.1.0/src/simcord/backend/models/role.py +15 -0
  46. simcord-0.1.0/src/simcord/backend/models/user.py +10 -0
  47. simcord-0.1.0/src/simcord/backend/models/webhook.py +14 -0
  48. simcord-0.1.0/src/simcord/backend/permissions.py +70 -0
  49. simcord-0.1.0/src/simcord/backend/serializers.py +291 -0
  50. simcord-0.1.0/src/simcord/backend/state.py +666 -0
  51. simcord-0.1.0/src/simcord/builders.py +230 -0
  52. simcord-0.1.0/src/simcord/enums.py +58 -0
  53. simcord-0.1.0/src/simcord/env.py +381 -0
  54. simcord-0.1.0/src/simcord/gateway.py +31 -0
  55. simcord-0.1.0/src/simcord/http/__init__.py +11 -0
  56. simcord-0.1.0/src/simcord/http/_helpers.py +76 -0
  57. simcord-0.1.0/src/simcord/http/client.py +158 -0
  58. simcord-0.1.0/src/simcord/http/router.py +141 -0
  59. simcord-0.1.0/src/simcord/http/routes/__init__.py +13 -0
  60. simcord-0.1.0/src/simcord/http/routes/application.py +48 -0
  61. simcord-0.1.0/src/simcord/http/routes/channels.py +130 -0
  62. simcord-0.1.0/src/simcord/http/routes/commands.py +27 -0
  63. simcord-0.1.0/src/simcord/http/routes/guilds.py +202 -0
  64. simcord-0.1.0/src/simcord/http/routes/interactions.py +140 -0
  65. simcord-0.1.0/src/simcord/http/routes/messages.py +112 -0
  66. simcord-0.1.0/src/simcord/http/routes/reactions.py +45 -0
  67. simcord-0.1.0/src/simcord/interactions.py +177 -0
  68. simcord-0.1.0/src/simcord/parity.py +63 -0
  69. simcord-0.1.0/src/simcord/py.typed +0 -0
  70. simcord-0.1.0/src/simcord/pytest_plugin.py +59 -0
  71. simcord-0.1.0/src/simcord/results.py +118 -0
  72. simcord-0.1.0/tests/conftest.py +30 -0
  73. simcord-0.1.0/tests/fixtures/__init__.py +0 -0
  74. simcord-0.1.0/tests/fixtures/sample_bot/__init__.py +36 -0
  75. simcord-0.1.0/tests/fixtures/sample_bot/events.py +23 -0
  76. simcord-0.1.0/tests/fixtures/sample_bot/general.py +42 -0
  77. simcord-0.1.0/tests/fixtures/sample_bot/interactions.py +111 -0
  78. simcord-0.1.0/tests/fixtures/sample_bot/moderation.py +29 -0
  79. simcord-0.1.0/tests/integration/test_app_commands.py +49 -0
  80. simcord-0.1.0/tests/integration/test_channels.py +82 -0
  81. simcord-0.1.0/tests/integration/test_diagnostics.py +93 -0
  82. simcord-0.1.0/tests/integration/test_interactions.py +139 -0
  83. simcord-0.1.0/tests/integration/test_lifecycle.py +37 -0
  84. simcord-0.1.0/tests/integration/test_members.py +78 -0
  85. simcord-0.1.0/tests/integration/test_messages.py +112 -0
  86. simcord-0.1.0/tests/integration/test_time.py +38 -0
  87. simcord-0.1.0/tests/unit/test_parity.py +14 -0
  88. simcord-0.1.0/tests/unit/test_permissions.py +79 -0
  89. simcord-0.1.0/tests/unit/test_router.py +39 -0
  90. simcord-0.1.0/uv.lock +1587 -0
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something behaves differently than real Discord, or crashes
4
+ labels: bug
5
+ ---
6
+
7
+ **What happened?**
8
+
9
+ **Minimal failing test** (the perfect bug report is a test using `simcord`):
10
+
11
+ ```python
12
+ ```
13
+
14
+ **Versions**: simcord, discord.py, Python
15
+
16
+ **Checked the [parity matrix](https://simcord.readthedocs.io/parity-matrix/)?**
17
+ If the feature is listed as unimplemented, please use the parity gap template instead.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: Feature request
3
+ about: New testing API, assertion helper, or framework capability
4
+ labels: enhancement
5
+ ---
6
+
7
+ **The problem you're trying to solve**:
8
+
9
+ **Proposed API** (sketch the test you'd like to be able to write):
10
+
11
+ ```python
12
+ ```
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: Parity gap
3
+ about: My bot needs an API route or Discord behavior the virtual backend doesn't implement
4
+ labels: parity
5
+ ---
6
+
7
+ **The route or feature** (paste the `RouteNotImplemented` message if you have one):
8
+
9
+ **What your bot does with it**:
10
+
11
+ **How you'd want to drive/assert it from a test**:
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
7
+ - package-ecosystem: pip
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
@@ -0,0 +1,43 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - run: uv sync --extra dev
18
+ - run: uv run ruff check src tests examples
19
+ - run: uv run ruff format --check src tests examples
20
+ - run: uv run pyright src
21
+
22
+ test:
23
+ runs-on: ubuntu-latest
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ python-version: ["3.11", "3.12", "3.13"]
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v5
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ - run: uv sync --extra dev
34
+ - run: uv run coverage run -m pytest tests examples -q
35
+ - run: uv run coverage report
36
+
37
+ docs:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: astral-sh/setup-uv@v5
42
+ - run: uv sync --extra docs
43
+ - run: uv run mkdocs build --strict
@@ -0,0 +1,22 @@
1
+ # Early warning for discord.py internal changes: run the suite against the
2
+ # upstream development branch every week (and on demand).
3
+ name: discord.py master
4
+
5
+ on:
6
+ schedule:
7
+ - cron: "0 6 * * 1"
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+ - run: |
20
+ uv sync --extra dev
21
+ uv pip install -U "discord.py @ git+https://github.com/Rapptz/discord.py"
22
+ - run: uv run pytest tests examples -q
@@ -0,0 +1,45 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv sync --extra dev
17
+ - run: uv run pytest tests examples -q
18
+
19
+ build:
20
+ needs: test
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: astral-sh/setup-uv@v5
25
+ - run: uv build
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write # PyPI trusted publishing (no token secrets)
37
+ steps:
38
+ - uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+ # Pinned to a commit SHA (v1.14.0) rather than the mutable release/v1
43
+ # branch: this job holds id-token: write, so a moved tag/branch could be
44
+ # used to mint a trusted-publishing token.
45
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
@@ -0,0 +1,26 @@
1
+ # Virtual environments
2
+ .venv/
3
+ venv/
4
+
5
+ # Python artifacts
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+
12
+ # Tooling caches
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .hypothesis/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Editors
20
+ .idea/
21
+ .vscode/
22
+
23
+ # Local agent/planning files (not part of the project)
24
+ PLAN.md
25
+ .claude/
26
+ site/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,16 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: "3.12"
7
+
8
+ mkdocs:
9
+ configuration: mkdocs.yml
10
+
11
+ python:
12
+ install:
13
+ - method: pip
14
+ path: .
15
+ extra_requirements:
16
+ - docs
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
4
+
5
+ <!-- towncrier release notes start -->
6
+
7
+ ## 0.1.0 (2026-06-12)
8
+
9
+ ### Features
10
+
11
+ - Added `env.advance_time(seconds)`: fast-forward the virtual clock so view timeouts fire, cooldowns reset, and `asyncio.sleep` chains complete — instantly, with no real waiting. The event-loop clock and message timestamps advance together.
12
+ - Added `env.raise_errors()`, which re-raises everything the bot raised during the test (command handlers, app-command callbacks, event listeners) as an `ExceptionGroup` — a one-call way to assert the bot ran cleanly. Does nothing when no errors were captured.
13
+ - Failing tests now automatically include a transcript of everything that crossed the two seams — gateway events injected and REST calls the bot made, in order — attached by the pytest plugin. Also available programmatically as `env.transcript()`.
14
+ - Uninspected bot errors now fail the test: `dpt.run` re-raises captured errors as an `ExceptionGroup` at teardown unless the test read `env.errors` or called `env.raise_errors()`. Opt out with `dpt.run(bot, check_errors=False)`.
15
+
16
+ ### Bug fixes
17
+
18
+ - A deferred component interaction (callback type 6) followed by `edit_original_response` now edits the clicked message in place, matching real Discord, instead of creating a new message. `@original` for a type-6 defer also resolves to the component's source message.
19
+ - An interaction is now marked acknowledged only after its callback is handled successfully, so a callback that 400s (e.g. an oversized embed) no longer consumes the interaction — a retried response gets through instead of a spurious `40060`.
20
+ - DMing a bot now opens the channel successfully and fails on send with `403 50007` (caught by `except discord.Forbidden`), matching Discord. Ephemeral messages no longer leak into the bot-facing channel history and pins endpoints, and webhook-authored messages now carry `webhook_id`.
21
+ - Serialized timestamps now come from the same virtual clock as snowflakes, so a message's `created_at` (derived from its id) and its `timestamp` agree instead of differing by months, and timestamps are deterministic across runs.
22
+ - Tightened server-side permission parity: the bot can no longer edit another user's message (`50005`), assign/edit roles at or above its own top role or grant permissions it lacks (`50013`), or delete the `@everyone` role. `mention_everyone` is only set when the author actually has the permission.
23
+ - Unimplemented routes now raise `RouteNotImplemented` directly rather than as a `discord.HTTPException`, so a bot's broad `except discord.HTTPException` can no longer silently swallow the "not implemented" signal. `history(around=...)` is now supported.
24
+ - `Env.settle()` now waits out `asyncio.sleep`-style pauses in handlers (cooldowns, backoff) instead of returning early, and only abandons tasks genuinely parked on a future. Assertions after an actor verb no longer race against a handler that paused before replying.
25
+
26
+ ### Miscellaneous
27
+
28
+ - CI hardening: pinned the PyPI publish action to a commit SHA, added a test gate before release publishing, and set default `contents: read` permissions on the CI workflows.
29
+ - Centralised the Discord wire-protocol magic numbers (interaction, callback, option and component types) into `IntEnum`s in `simcord.enums`, replacing scattered bare integer constants in the actors, payload builders and interaction route.
30
+ - Dropped Python 3.10 support; the minimum is now Python 3.11.
31
+ - Every state mutation now lives on `Backend` paired with its own gateway emit (channel/overwrite edits, member field/role edits, role edits), and route handlers parse, permission-check via the new `ctx.require_channel_permissions`/`ctx.require_guild_permissions` helpers, then call a single backend method. This removes the copy-pasted permission preamble and the "mutate then remember to announce" pattern, so a write can't be announced inconsistently or forgotten as route coverage grows.
32
+ - Setup and not-implemented errors now attach supporting detail (available options, the parity-matrix pointer) as exception notes, keeping the primary message tight while still surfacing the context in tracebacks.
33
+ - The interaction response lifecycle is now a typed `Interaction` dataclass with a `ResponseKind` enum, replacing the untyped dict that was mutated across the route, actor, and result layers. `InteractionResult` no longer exposes the raw `.record` dict; use its typed properties (`acknowledged`, `deferred`, `ephemeral`, `modal`, `response`, `followups`) instead.
34
+ - The parity matrix's route inventory is now generated from the route table (`python -m simcord.parity`) and guarded by a sync test, so it is exact by construction. Also: `Backend` is no longer in `__all__` (still importable, documented as internal/unstable), and CI now enforces a coverage floor.
@@ -0,0 +1,8 @@
1
+ # Code of Conduct
2
+
3
+ This project follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
4
+
5
+ In short: be respectful, be constructive, assume good faith. Harassment and personal
6
+ attacks are not tolerated.
7
+
8
+ Report unacceptable behavior to the maintainers via GitHub.
@@ -0,0 +1,52 @@
1
+ # Contributing
2
+
3
+ Thanks for helping make Discord bot testing better!
4
+
5
+ ## Development setup
6
+
7
+ The project uses [uv](https://docs.astral.sh/uv/):
8
+
9
+ ```bash
10
+ git clone https://github.com/SilentHacks/simcord
11
+ cd simcord
12
+ uv sync --extra dev
13
+ uv run pytest tests examples
14
+ uv run ruff check src tests examples && uv run ruff format --check src tests examples
15
+ uv run pyright src
16
+ ```
17
+
18
+ ## Project layout
19
+
20
+ - `src/simcord/backend/` — the virtual Discord: dataclass models, wire-format
21
+ serializers (annotated against `discord.types`), the permissions engine, error catalog,
22
+ in-memory CDN, and the central `Backend` store.
23
+ - `src/simcord/http/` — the route table and per-resource REST handlers, plus the
24
+ fake `HTTPClient`/webhook adapter.
25
+ - `src/simcord/gateway.py` — feeds payloads into discord.py's real parsers.
26
+ - `src/simcord/{env,builders,actors,results,interactions}.py` — the public API.
27
+ - `src/simcord/_dpy_internals.py` — **every** touch of a private discord.py API,
28
+ behind an import-time self-check. New private-API usage goes here, nowhere else.
29
+
30
+ ## Guidelines
31
+
32
+ - **Fidelity first.** The backend must behave like real Discord: real error codes, real
33
+ validation limits, real payload shapes. When in doubt, check the
34
+ [Discord API docs](https://discord.com/developers/docs) and discord.py's
35
+ `discord.types` definitions.
36
+ - **Never fake success silently.** Unimplemented routes must raise `RouteNotImplemented`.
37
+ - Every new route or feature needs an integration test in `tests/integration/` driving
38
+ the sample bot (`tests/fixtures/sample_bot/`) through it, plus a parity-matrix update:
39
+ run `uv run python -m simcord.parity docs/parity-matrix.md` to regenerate the
40
+ route inventory (a unit test enforces it), and update the curated feature table by hand.
41
+ Pure backend logic (permissions, routing) gets unit tests in `tests/unit/`.
42
+ - New state mutations live on `Backend`, paired with their gateway emit; route handlers
43
+ parse, permission-check (`ctx.require_*_permissions`), call one backend method, and
44
+ serialize.
45
+ - Public API lives on `Env` and the handle objects — no module-global state.
46
+ - Add a towncrier news fragment in `changes/` for user-visible changes
47
+ (e.g. `changes/42.feature.md`).
48
+
49
+ ## Reporting bugs
50
+
51
+ A failing test using `simcord` is the perfect bug report. If your bot hits an
52
+ unimplemented route, the error message names it — include that in the issue.
simcord-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SilentHacks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
simcord-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: simcord
3
+ Version: 0.1.0
4
+ Summary: SimCord — discord.py testing framework. Simulate Discord, test your bot offline: no network, no token, no ToS.
5
+ Project-URL: Homepage, https://github.com/SilentHacks/simcord
6
+ Project-URL: Documentation, https://simcord.readthedocs.io/
7
+ Project-URL: Issues, https://github.com/SilentHacks/simcord/issues
8
+ Project-URL: Changelog, https://github.com/SilentHacks/simcord/blob/master/CHANGELOG.md
9
+ Author: SilentHacks
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: bot testing,discord,discord.py,integration testing,mock,mock discord,pytest,simulator,testing,unit testing
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Pytest
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Classifier: Topic :: Software Development :: Testing :: Mocking
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: discord-py<3,>=2.7
25
+ Provides-Extra: dev
26
+ Requires-Dist: coverage[toml]>=7; extra == 'dev'
27
+ Requires-Dist: hypothesis>=6; extra == 'dev'
28
+ Requires-Dist: pyright>=1.1; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
30
+ Requires-Dist: pytest>=8; extra == 'dev'
31
+ Requires-Dist: ruff>=0.8; extra == 'dev'
32
+ Requires-Dist: towncrier>=24; extra == 'dev'
33
+ Provides-Extra: docs
34
+ Requires-Dist: mkdocs-material>=9; extra == 'docs'
35
+ Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
36
+ Provides-Extra: pytest
37
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'pytest'
38
+ Requires-Dist: pytest>=8; extra == 'pytest'
39
+ Description-Content-Type: text/markdown
40
+
41
+ <div align="center">
42
+
43
+ # SimCord
44
+
45
+ **The discord.py testing framework — simulate Discord, test your bot offline.**
46
+
47
+ Test your discord.py bot with a full virtual Discord environment: no network, no token,
48
+ no test server, no Terms of Service concerns. SimCord is the missing testing library
49
+ for discord.py bots.
50
+
51
+ [![CI](https://github.com/SilentHacks/simcord/actions/workflows/ci.yml/badge.svg)](https://github.com/SilentHacks/simcord/actions/workflows/ci.yml)
52
+ [![Docs](https://app.readthedocs.org/projects/simcord/badge/?version=latest)](https://simcord.readthedocs.io/)
53
+ [![PyPI](https://img.shields.io/pypi/v/simcord)](https://pypi.org/project/simcord/)
54
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/simcord/)
55
+ [![discord.py](https://img.shields.io/badge/discord.py-2.7%2B-5865F2)](https://github.com/Rapptz/discord.py)
56
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
57
+
58
+ [Quickstart](#quickstart) · [Documentation](https://simcord.readthedocs.io/) · [Parity matrix](https://simcord.readthedocs.io/parity-matrix/) · [Contributing](CONTRIBUTING.md)
59
+
60
+ </div>
61
+
62
+ ---
63
+
64
+ SimCord is a **discord.py testing framework** that gives your bot a fake but faithful
65
+ Discord to run against. Simulate users sending messages, invoking slash commands, clicking
66
+ buttons and submitting modals — then assert on exactly what your bot did:
67
+
68
+ ```python
69
+ async def test_ping(simcord_env):
70
+ channel = simcord_env.create_guild().create_text_channel("general")
71
+ alice = simcord_env.guild.add_member(simcord_env.create_user("alice"))
72
+
73
+ await alice.send(channel, "!ping") # full gateway round trip
74
+
75
+ assert channel.last_message.content == "Pong!"
76
+ ```
77
+
78
+ > ⚠️ **Alpha.** The core surface (messages, prefix commands, slash commands, components,
79
+ > modals, permissions, reactions, threads, DMs, time control) works; see the
80
+ > [parity matrix](https://simcord.readthedocs.io/parity-matrix/) for the
81
+ > long tail. Unimplemented routes always fail loudly — SimCord never silently fakes
82
+ > success.
83
+
84
+ ## Why SimCord?
85
+
86
+ Unit tests cover your business logic, but the bugs that bite Discord bots live in the
87
+ glue: converters, checks, permissions, forgotten `tree.sync()` calls, double-acknowledged
88
+ interactions, oversized embeds. Until now the only way to test that layer was manually,
89
+ in a real server. SimCord runs all of discord.py's real machinery — its parsers, cache,
90
+ command frameworks and views — against a faithful mock of Discord's REST API and gateway,
91
+ entirely in-process.
92
+
93
+ | | |
94
+ | --- | --- |
95
+ | 🎯 **Real discord.py semantics** | Server-side permission checks with authentic error codes (`50013 Missing Permissions`…), interaction lifecycle rules (`40060` on double-ack), role hierarchy, timeouts, ephemeral visibility, validation limits. |
96
+ | 🐛 **Real bugs caught** | Invoking a never-synced slash command fails your test, just like production. Clicking a disabled button is impossible, just like the client. Unhandled bot errors fail the test by default. |
97
+ | ⚡ **Fast & deterministic** | No sleeps, no network, reproducible IDs and timestamps. The framework tracks the bot's tasks and settles after every action. |
98
+ | ⏩ **Time control** | `env.advance_time(180)` fires view timeouts and resets cooldowns instantly — no real waiting. |
99
+ | 🔍 **Debuggable failures** | Failing tests automatically include a transcript of every gateway event and REST call — exactly what your bot did, in order. |
100
+ | 📢 **Loud gaps** | Anything not implemented raises `RouteNotImplemented` naming the route. Never silent fake success. |
101
+
102
+ ## Install
103
+
104
+ ```bash
105
+ pip install simcord[pytest]
106
+ ```
107
+
108
+ Requires Python 3.11+ and discord.py 2.7+. Zero dependencies beyond discord.py itself.
109
+
110
+ ## Quickstart
111
+
112
+ Tell the bundled pytest plugin how to build your bot:
113
+
114
+ ```python
115
+ # conftest.py
116
+ import pytest
117
+ from mybot import create_bot # however your project builds its commands.Bot
118
+
119
+ @pytest.fixture
120
+ def simcord_bot():
121
+ return create_bot()
122
+ ```
123
+
124
+ Then write tests against the `simcord_env` fixture:
125
+
126
+ ```python
127
+ import discord
128
+
129
+ async def test_ban_slash_command(simcord_env):
130
+ guild = simcord_env.create_guild()
131
+ channel = guild.create_text_channel("mod")
132
+ mods = guild.create_role("Mods", permissions=discord.Permissions(ban_members=True))
133
+ mod = guild.add_member(simcord_env.create_user("mod"), roles=[mods])
134
+ target = guild.add_member(simcord_env.create_user("spammer"))
135
+
136
+ result = await mod.slash(channel, "ban", user=target, reason="spam")
137
+
138
+ assert result.ephemeral
139
+ assert result.response.content == f"Banned {target.mention}: spam"
140
+ assert guild.get_ban(target) is not None
141
+
142
+ async def test_offer_expires(simcord_env):
143
+ channel = simcord_env.create_guild().create_text_channel("general")
144
+ alice = simcord_env.guild.add_member(simcord_env.create_user("alice"))
145
+
146
+ result = await alice.slash(channel, "offer") # bot replies with a View(timeout=180)
147
+ await simcord_env.advance_time(180) # instant — the view times out
148
+
149
+ assert "expired" in channel.last_message.content
150
+ ```
151
+
152
+ Buttons, selects, modals, context menus, autocomplete, reactions, threads, DMs, fault
153
+ injection and more: see the [documentation](https://simcord.readthedocs.io/).
154
+ Prefer explicit control? `async with simcord.run(bot) as env:` works in any async
155
+ test framework.
156
+
157
+ ## How it works
158
+
159
+ discord.py has two narrow seams: every REST call funnels through `HTTPClient.request`,
160
+ and every gateway event enters through `ConnectionState.parsers`. SimCord replaces the
161
+ first with a fake routed to an in-memory backend (a single source of truth for guilds,
162
+ channels, members, messages, commands and interactions) and injects Discord-shaped payloads
163
+ through the second. Everything between those seams — which is everything your bot touches
164
+ — is real discord.py code running unmodified.
165
+
166
+ ```
167
+ test ──► builders/actors ──► virtual backend (single source of truth)
168
+ │ │
169
+ gateway payloads ▼ ▼ REST responses
170
+ ConnectionState.parsers FakeHTTPClient route table
171
+ │ ▲
172
+ ▼ │
173
+ your real, unmodified bot
174
+ ```
175
+
176
+ Details in the [architecture docs](https://simcord.readthedocs.io/architecture/).
177
+
178
+ ## discord.py testing — common use cases
179
+
180
+ SimCord covers the full range of discord.py bot testing scenarios:
181
+
182
+ - **discord.py unit testing** — test individual commands in isolation
183
+ - **discord.py integration testing** — test full command flows with permissions, roles and channels
184
+ - **discord.py mock events** — fire any gateway event (member join, reaction add, voice state…) without a real server
185
+ - **discord.py mock Discord** — a full in-memory Discord server your bot can't tell from the real thing
186
+ - **discord.py command testing** — prefix commands, slash commands, context menus, autocomplete
187
+ - **discord.py interaction testing** — buttons, selects, modals, ephemeral responses, deferred replies
188
+ - **discord.py bot testing without a token** — no `.env`, no test guild, no rate limits
189
+
190
+ ## Comparison
191
+
192
+ | | SimCord | dpytest | Manual test server |
193
+ |---|---|---|---|
194
+ | No network / no token | ✅ | ✅ | ❌ |
195
+ | Real discord.py internals | ✅ | Partial | ✅ |
196
+ | Slash commands & components | ✅ | ❌ | ✅ |
197
+ | Authentic error codes | ✅ | ❌ | ✅ |
198
+ | Time control | ✅ | ❌ | ❌ |
199
+ | Failure transcripts | ✅ | ❌ | ❌ |
200
+ | Maintained for discord.py 2.x | ✅ | ❌ | — |
201
+
202
+ ## Contributing
203
+
204
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports with a failing test are gold; if your
205
+ bot hits an unimplemented route, the error names it — please open a
206
+ [parity gap issue](https://github.com/SilentHacks/simcord/issues/new?template=parity-gap.md).
207
+
208
+ ## License
209
+
210
+ [MIT](LICENSE). Unofficial — not affiliated with Discord Inc. or the discord.py project.