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.
- simcord-0.1.0/.github/ISSUE_TEMPLATE/bug.md +17 -0
- simcord-0.1.0/.github/ISSUE_TEMPLATE/feature.md +12 -0
- simcord-0.1.0/.github/ISSUE_TEMPLATE/parity-gap.md +11 -0
- simcord-0.1.0/.github/dependabot.yml +10 -0
- simcord-0.1.0/.github/workflows/ci.yml +43 -0
- simcord-0.1.0/.github/workflows/dpy-master.yml +22 -0
- simcord-0.1.0/.github/workflows/release.yml +45 -0
- simcord-0.1.0/.gitignore +26 -0
- simcord-0.1.0/.python-version +1 -0
- simcord-0.1.0/.readthedocs.yaml +16 -0
- simcord-0.1.0/CHANGELOG.md +34 -0
- simcord-0.1.0/CODE_OF_CONDUCT.md +8 -0
- simcord-0.1.0/CONTRIBUTING.md +52 -0
- simcord-0.1.0/LICENSE +21 -0
- simcord-0.1.0/PKG-INFO +210 -0
- simcord-0.1.0/README.md +170 -0
- simcord-0.1.0/SECURITY.md +9 -0
- simcord-0.1.0/changes/README.md +1 -0
- simcord-0.1.0/docs/architecture.md +52 -0
- simcord-0.1.0/docs/guides/interactions.md +80 -0
- simcord-0.1.0/docs/guides/messages.md +64 -0
- simcord-0.1.0/docs/guides/permissions.md +39 -0
- simcord-0.1.0/docs/index.md +40 -0
- simcord-0.1.0/docs/migrating-from-dpytest.md +29 -0
- simcord-0.1.0/docs/parity-matrix.md +104 -0
- simcord-0.1.0/docs/quickstart.md +70 -0
- simcord-0.1.0/examples/__init__.py +0 -0
- simcord-0.1.0/examples/bot.py +37 -0
- simcord-0.1.0/examples/conftest.py +9 -0
- simcord-0.1.0/examples/test_bot.py +30 -0
- simcord-0.1.0/mkdocs.yml +30 -0
- simcord-0.1.0/pyproject.toml +114 -0
- simcord-0.1.0/src/simcord/__init__.py +50 -0
- simcord-0.1.0/src/simcord/_dpy_internals.py +86 -0
- simcord-0.1.0/src/simcord/actors.py +393 -0
- simcord-0.1.0/src/simcord/backend/__init__.py +11 -0
- simcord-0.1.0/src/simcord/backend/cdn.py +30 -0
- simcord-0.1.0/src/simcord/backend/errors.py +84 -0
- simcord-0.1.0/src/simcord/backend/models/__init__.py +31 -0
- simcord-0.1.0/src/simcord/backend/models/channel.py +53 -0
- simcord-0.1.0/src/simcord/backend/models/guild.py +28 -0
- simcord-0.1.0/src/simcord/backend/models/interaction.py +97 -0
- simcord-0.1.0/src/simcord/backend/models/member.py +15 -0
- simcord-0.1.0/src/simcord/backend/models/message.py +61 -0
- simcord-0.1.0/src/simcord/backend/models/role.py +15 -0
- simcord-0.1.0/src/simcord/backend/models/user.py +10 -0
- simcord-0.1.0/src/simcord/backend/models/webhook.py +14 -0
- simcord-0.1.0/src/simcord/backend/permissions.py +70 -0
- simcord-0.1.0/src/simcord/backend/serializers.py +291 -0
- simcord-0.1.0/src/simcord/backend/state.py +666 -0
- simcord-0.1.0/src/simcord/builders.py +230 -0
- simcord-0.1.0/src/simcord/enums.py +58 -0
- simcord-0.1.0/src/simcord/env.py +381 -0
- simcord-0.1.0/src/simcord/gateway.py +31 -0
- simcord-0.1.0/src/simcord/http/__init__.py +11 -0
- simcord-0.1.0/src/simcord/http/_helpers.py +76 -0
- simcord-0.1.0/src/simcord/http/client.py +158 -0
- simcord-0.1.0/src/simcord/http/router.py +141 -0
- simcord-0.1.0/src/simcord/http/routes/__init__.py +13 -0
- simcord-0.1.0/src/simcord/http/routes/application.py +48 -0
- simcord-0.1.0/src/simcord/http/routes/channels.py +130 -0
- simcord-0.1.0/src/simcord/http/routes/commands.py +27 -0
- simcord-0.1.0/src/simcord/http/routes/guilds.py +202 -0
- simcord-0.1.0/src/simcord/http/routes/interactions.py +140 -0
- simcord-0.1.0/src/simcord/http/routes/messages.py +112 -0
- simcord-0.1.0/src/simcord/http/routes/reactions.py +45 -0
- simcord-0.1.0/src/simcord/interactions.py +177 -0
- simcord-0.1.0/src/simcord/parity.py +63 -0
- simcord-0.1.0/src/simcord/py.typed +0 -0
- simcord-0.1.0/src/simcord/pytest_plugin.py +59 -0
- simcord-0.1.0/src/simcord/results.py +118 -0
- simcord-0.1.0/tests/conftest.py +30 -0
- simcord-0.1.0/tests/fixtures/__init__.py +0 -0
- simcord-0.1.0/tests/fixtures/sample_bot/__init__.py +36 -0
- simcord-0.1.0/tests/fixtures/sample_bot/events.py +23 -0
- simcord-0.1.0/tests/fixtures/sample_bot/general.py +42 -0
- simcord-0.1.0/tests/fixtures/sample_bot/interactions.py +111 -0
- simcord-0.1.0/tests/fixtures/sample_bot/moderation.py +29 -0
- simcord-0.1.0/tests/integration/test_app_commands.py +49 -0
- simcord-0.1.0/tests/integration/test_channels.py +82 -0
- simcord-0.1.0/tests/integration/test_diagnostics.py +93 -0
- simcord-0.1.0/tests/integration/test_interactions.py +139 -0
- simcord-0.1.0/tests/integration/test_lifecycle.py +37 -0
- simcord-0.1.0/tests/integration/test_members.py +78 -0
- simcord-0.1.0/tests/integration/test_messages.py +112 -0
- simcord-0.1.0/tests/integration/test_time.py +38 -0
- simcord-0.1.0/tests/unit/test_parity.py +14 -0
- simcord-0.1.0/tests/unit/test_permissions.py +79 -0
- simcord-0.1.0/tests/unit/test_router.py +39 -0
- 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,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
|
simcord-0.1.0/.gitignore
ADDED
|
@@ -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,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
|
+
[](https://github.com/SilentHacks/simcord/actions/workflows/ci.yml)
|
|
52
|
+
[](https://simcord.readthedocs.io/)
|
|
53
|
+
[](https://pypi.org/project/simcord/)
|
|
54
|
+
[](https://pypi.org/project/simcord/)
|
|
55
|
+
[](https://github.com/Rapptz/discord.py)
|
|
56
|
+
[](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.
|