Compare commits

...

29 Commits

Author SHA1 Message Date
Jakub Kuczys edce32364f Fix Red.send_interactive() permission check again (#6697) 2026-05-10 16:30:03 -04:00
Jakub Kuczys 7305f44f68 Make changelog easy to parse (#6711) 2026-05-10 16:11:36 -04:00
Jakub Kuczys cbd4643bd3 Fix deprecated use of shutdown_timeout kwarg in TCPSite (#6743) 2026-05-10 16:00:10 -04:00
Jakub Kuczys b02fa38423 Stop referencing V2 everywhere and move migration guide (#6737) 2026-05-10 15:51:36 -04:00
Jakub Kuczys 99babf9ad3 Update uvloop to 0.22.1 (#6705) 2026-05-10 15:46:57 -04:00
Jakub Kuczys 169d0eed49 Reuse name input logic and allow retry in data path input (#6696) 2026-04-19 20:04:05 +00:00
Jakub Kuczys 70faa8cd52 Make logging setup consistent between redbot and redbot-setup (#6695)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2026-04-19 19:41:50 +00:00
Jakub Kuczys 2ea4c766ad Add basic backcompat for people using Downloader internals (#6713) 2026-04-07 18:17:11 -04:00
Jakub Kuczys 6ceb45b35c Fix remaining issues with internal Downloader API (#6721) 2026-04-07 17:57:49 -04:00
Jakub Kuczys 4032648dcc Simplify bot class (Red) __init__ arguments, remove unused (#6714) 2026-04-05 15:52:43 -04:00
Jakub Kuczys f70c48ec30 Warn when venv/install doesn't match previously used one (#6715) 2026-04-05 15:44:17 -04:00
Jakub Kuczys fcb8bc0265 Prefer using repr() on the Dev cog results (excl. str instances) (#6726) 2026-04-03 02:05:54 +02:00
Jakub Kuczys ee1db01a2f Rip out Downloader's non-UI functionality into private core API (#6706) 2026-03-29 22:25:04 +02:00
EternalllZM e2acec0862 [Docs] Cleanup "About Virtual Environment" docs (#6701) 2026-03-29 13:54:58 -04:00
EternalllZM b83b882921 Update README to Explain Discord (#6650)
Co-authored-by: Michael Oliveira <34169552+Flame442@users.noreply.github.com>
2026-03-29 13:51:08 -04:00
Chris 99d7b0e3b7 Update Twitch API token setup link (#6703) 2026-03-23 21:13:59 -08:00
Jakub Kuczys 9270373c56 Update Spotify API instructions + use SetApiView (#6699) 2026-03-15 12:57:14 +01:00
Jakub Kuczys e8f0ea0510 Switch PAT use to app token (#6698) 2026-03-10 14:30:20 +01:00
github-actions[bot] b42bab4de9 Version bump to 3.5.25.dev1 (#6691)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-06 02:43:22 +01:00
github-actions[bot] e868872214 Version bump to 3.5.24 (#6689)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-06 02:21:33 +01:00
github-actions[bot] bee0ddbffc Automated Crowdin downstream (#6690)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-06 02:21:14 +01:00
Jakub Kuczys 2de3d03cc9 Red 3.5.24 - Changelog (#6686) 2026-03-06 02:15:52 +01:00
Jakub Kuczys 056f2de557 Bump Lavalink version to 3.7.13+red.5 (#6688) 2026-03-06 02:14:27 +01:00
Jakub Kuczys 34cbd15ba9 Stop waiting for update check on bot startup (#6687) 2026-03-06 01:51:22 +01:00
EternalllZM 9a458fdd83 [Docs] Misc fixes (#6685) 2026-03-05 23:36:42 +01:00
Jakub Kuczys 0e78051c5d Bump Lavalink version to 3.7.13+red.3 (#6683) 2026-03-05 20:52:23 +01:00
Jakub Kuczys 53766173d0 Update supported Java versions (#6681) 2026-03-05 20:52:15 +01:00
Jakub Kuczys 36a5f752a2 Add --no-debug flag for resetting the verbosity level (#6680) 2026-03-05 01:04:56 +01:00
github-actions[bot] b2007a718d Version bump to 3.5.24.dev1 (#6679)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-05 00:37:09 +01:00
97 changed files with 22946 additions and 21165 deletions
+7 -5
View File
@@ -50,10 +50,6 @@
- redbot/cogs/downloader/*
# Docs
- docs/cog_guides/downloader.rst
# Tests
- redbot/pytest/downloader.py
- redbot/pytest/downloader_testrepo.*
- tests/cogs/downloader/**/*
"Category: Cogs - Economy":
# Source
- redbot/cogs/economy/*
@@ -212,6 +208,13 @@
- redbot/core/_cli.py
- redbot/core/_debuginfo.py
- redbot/setup.py
"Category: Core - Downloader":
# Source
- redbot/core/_downloader/**/*
# Tests
- redbot/pytest/downloader.py
- redbot/pytest/downloader_testrepo.*
- tests/core/_downloader/**/*
"Category: Core - Help":
- redbot/core/commands/help.py
"Category: Core - i18n":
@@ -263,7 +266,6 @@
- docs/framework_events.rst
- docs/guide_cog_creation.rst
- docs/guide_cog_creators.rst
- docs/guide_migration.rst
- docs/guide_publish_cogs.rst
- docs/guide_slash_and_interactions.rst
"Category: Docs - Install Guides":
+37 -38
View File
@@ -7,18 +7,24 @@ on:
required: false
default: 'auto'
permissions:
contents: write
pull-requests: write
jobs:
crowdin_download_translations:
environment: Prepare Release
needs: pr_stable_bump
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ secrets.RED_RELEASER_CLIENT_ID }}
private-key: ${{ secrets.RED_RELEASER_PRIVATE_KEY }}
# Checkout repository and install Python
- uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.8'
- name: Install dependencies
@@ -43,7 +49,7 @@ jobs:
id: cpr_crowdin
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
commit-message: Automated Crowdin downstream
title: "Automated Crowdin downstream"
body: |
@@ -51,31 +57,32 @@ jobs:
Please ensure that there are no errors or invalid files are in the PR.
labels: "Automated PR, Changelog Entry: Skipped"
branch: "automated/i18n"
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
committer: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
author: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
milestone: ${{ needs.pr_stable_bump.outputs.milestone_number }}
- name: Close and reopen the PR with different token to trigger CI
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ steps.cpr_crowdin.outputs.pull-request-number }}
PR_OPERATION: ${{ steps.cpr_crowdin.outputs.pull-request-operation }}
with:
github-token: ${{ secrets.cogcreators_bot_repo_scoped }}
script: |
const script = require(
`${process.env.GITHUB_WORKSPACE}/.github/workflows/scripts/close_and_reopen_pr.js`
);
console.log(script({github, context}));
pr_stable_bump:
environment: Prepare Release
runs-on: ubuntu-latest
outputs:
milestone_number: ${{ steps.get_milestone_number.outputs.result }}
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ secrets.RED_RELEASER_CLIENT_ID }}
private-key: ${{ secrets.RED_RELEASER_PRIVATE_KEY }}
# Checkout repository and install Python
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
token: ${{ steps.app-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.8'
@@ -105,7 +112,7 @@ jobs:
id: cpr_bump_stable
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
commit-message: Version bump to ${{ steps.bump_version_stable.outputs.new_version }}
title: Version bump to ${{ steps.bump_version_stable.outputs.new_version }}
body: |
@@ -113,18 +120,10 @@ jobs:
Please ensure that there are no errors or invalid files are in the PR.
labels: "Automated PR, Changelog Entry: Skipped"
branch: "automated/pr_bumps/${{ steps.bump_version_stable.outputs.new_version }}"
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
committer: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
author: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
milestone: ${{ steps.get_milestone_number.outputs.result }}
- name: Close and reopen the PR with different token to trigger CI
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ steps.cpr_bump_stable.outputs.pull-request-number }}
PR_OPERATION: ${{ steps.cpr_bump_stable.outputs.pull-request-operation }}
with:
github-token: ${{ secrets.cogcreators_bot_repo_scoped }}
script: |
const script = require(
`${process.env.GITHUB_WORKSPACE}/.github/workflows/scripts/close_and_reopen_pr.js`
);
console.log(await script({github, context}));
+17 -20
View File
@@ -147,9 +147,7 @@ jobs:
print-hash: true
pr_dev_bump:
permissions:
contents: write
pull-requests: write
environment: Prepare Release
needs: release_to_pypi
name: Update Red version number to dev
runs-on: ubuntu-latest
@@ -160,11 +158,18 @@ jobs:
run: |
echo "BASE_BRANCH=${TAG_BASE_BRANCH#'refs/heads/'}" >> $GITHUB_ENV
- uses: actions/checkout@v4
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ secrets.RED_RELEASER_CLIENT_ID }}
private-key: ${{ secrets.RED_RELEASER_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
ref: ${{ env.BASE_BRANCH }}
token: ${{ steps.app-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.8'
@@ -194,7 +199,7 @@ jobs:
id: cpr_bump_dev
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
commit-message: Version bump to ${{ steps.bump_version_dev.outputs.new_version }}
title: Version bump to ${{ steps.bump_version_dev.outputs.new_version }}
body: |
@@ -202,19 +207,11 @@ jobs:
Please ensure that there are no errors or invalid files are in the PR.
labels: "Automated PR, Changelog Entry: Skipped"
branch: "automated/pr_bumps/${{ steps.bump_version_dev.outputs.new_version }}"
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
committer: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
author: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<263745220+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
milestone: ${{ steps.get_milestone_number.outputs.result }}
base: ${{ env.BASE_BRANCH }}
- name: Close and reopen the PR with different token to trigger CI
uses: actions/github-script@v6
env:
PR_NUMBER: ${{ steps.cpr_bump_dev.outputs.pull-request-number }}
PR_OPERATION: ${{ steps.cpr_bump_dev.outputs.pull-request-operation }}
with:
github-token: ${{ secrets.cogcreators_bot_repo_scoped }}
script: |
const script = require(
`${process.env.GITHUB_WORKSPACE}/.github/workflows/scripts/close_and_reopen_pr.js`
);
console.log(await script({github, context}));
+4
View File
@@ -7,6 +7,10 @@ build:
jobs:
install:
- pip install .[doc]
post_build:
- mkdir -p docs/_build/doctrees docs/_build/markdown "$READTHEDOCS_OUTPUT/html/_markdown"
- python -m sphinx -T -b markdown -d docs/_build/doctrees -D "language=$READTHEDOCS_LANGUAGE" docs docs/_build/markdown
- cp docs/_build/markdown/changelog.md "$READTHEDOCS_OUTPUT/html/_markdown/changelog.md"
sphinx:
configuration: docs/conf.py
+957 -130
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -64,6 +64,12 @@ liking, making it completely customizable. This is a *self-hosted bot* meani
to host and maintain your own instance. You can turn Red into an admin bot, music bot, trivia bot,
new best friend or all of these together!
Red is built for [Discord](https://discord.com/), a popular VOIP and instant messaging platform.
It's best suited for use in guilds (also known as servers), where it utilizes Discord's
well-documented API to communicate and deliver its many features. Discord offers its API to
encourage developers to explore their creativity by building programs, tools, and services that
enhance the Discord experience.
[Installation](#installation) is easy, and you do **NOT** need to know anything about coding! Aside
from installing and updating, every part of the bot can be controlled from within Discord.
+43
View File
@@ -0,0 +1,43 @@
from typing import Any, Dict, List
from docutils import nodes
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective
class ChangelogContributors(SphinxDirective):
has_content = True
def run(self) -> List[nodes.Node]:
contributors = [contributor for line in self.content for contributor in line.split()]
comment_value = " ".join(contributors)
line_nodes = []
for contributor in contributors:
if line_nodes:
line_nodes.append(nodes.Text(", "))
line_nodes.append(
nodes.reference(
contributor,
f"@{contributor}",
internal=False,
refuri=f"https://github.com/sponsors/{contributor}",
)
)
node = nodes.line_block(
"",
nodes.comment("", f"RED-CHANGELOG-CONTRIBUTORS: {comment_value}"),
nodes.line("", "Thanks to all these amazing people who contributed to this release:"),
nodes.line("", "", *line_nodes),
)
return [node]
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_directive("changelog-contributors", ChangelogContributors)
return {
"version": "1.0",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
+17 -17
View File
@@ -3,16 +3,14 @@
==========================
About Virtual Environments
==========================
Creating a virtual environment is really easy and usually prevents many common installation
problems.
Creating a virtual environment is simple and helps prevent installation problems.
**What Are Virtual Environments For?**
Virtual environments allow you to isolate Red's library dependencies, cog dependencies and python
binaries from the rest of your system. There is no performance overhead to using virtual environment
and it saves you from a lot of troubles during setup. It also makes sure Red and its dependencies
are installed to a predictable location which makes uninstalling Red as simple as removing a single folder,
without worrying about losing your data or other things on your system becoming broken.
Virtual environments allow you to isolate Red's library dependencies, cog dependencies, and Python
binaries from the rest of your system with no performance overhead, ensuring those dependencies
and Red are installed to a predictable location. This makes uninstalling Red as simple as removing
a single folder, preventing any data loss or breaking other things on your system.
--------------------------------------------
@@ -21,19 +19,21 @@ Virtual Environments with Multiple Instances
If you are running multiple instances of Red on the same machine, you have the option of either
using the same virtual environment for all of them, or creating separate ones.
.. note::
Using a *single* virtual environment for all of your instances means you:
This only applies for multiple instances of V3. If you are running a V2 instance as well,
you **must** use separate virtual environments.
- Only need to update Red once for all instances.
- Must shut down all instances prior to updating.
- Will save space on your hard drive.
- Want all instances to share the same version/dependencies.
The advantages of using a *single* virtual environment for all of your V3 instances are:
Using *multiple* virtual environments for each individual or select groups of instances means you:
- When updating Red, you will only need to update it once for all instances (however you will still need to restart all instances for the changes to take effect)
- It will save space on your hard drive
On the other hand, you may wish to update each of your instances individually.
- Need to update Red within each virtual environment separately.
- Can update Red without needing to update all instances.
- Only need to shut down the instance(s) being updated.
- Want different Red/dependency versions on different instances.
.. important::
Windows users with multiple instances should create *separate* virtual environments, as
updating multiple running instances at once is likely to cause errors.
Regardless of which option you choose, do not update while any instances within that virtual
environment are running. This is especially true for Windows, as files are locked by the system while in use.
+1 -1
View File
@@ -4,7 +4,7 @@
Backing Up and Restoring Red
============================
Red can be backed up and restored to any device as long as it is supported operating system. See page: :ref:`end-user-guarantees`.
Red can be backed up and restored to any device as long as it is a supported operating system. See page: :ref:`end-user-guarantees`.
Backup steps are to be done in order and carefully to avoid any issues.
+9 -9
View File
@@ -116,18 +116,18 @@ How can I use this playlist link with playlist commands in audio?**
:ref:`setting up Audio for multiple bots<multibots>`. Otherwise, another process is using the
port, so you need to figure out what is using port 2333 and terminate/disconnect it yourself.
**Q: My terminal is saying that I "must install Java 17 or 11 for Lavalink to run". How can I fix this?**
**Q: My terminal is saying that I "must install Java 21 or 17 for Lavalink to run". How can I fix this?**
You are getting this error because you have a different version of Java installed, or you don't have
Java installed at all. As the error states, Java 17 or 11 is required, and can be installed from
`here <https://adoptium.net/temurin/releases/?version=17>`__.
Java installed at all. As the error states, Java 21 or 17 is required, and can be installed from
`here <https://adoptium.net/temurin/releases/?version=21>`__.
If you have Java 17 or 11 installed, and are still getting this error, you will have to manually tell Audio where your Java install is located.
Use ``[p]llset java <path_to_java_17_or_11_executable>``, to make Audio launch Lavalink with a
If you have Java 21 or 17 installed, and are still getting this error, you will have to manually tell Audio where your Java install is located.
Use ``[p]llset java <path_to_java_21_or_17_executable>``, to make Audio launch Lavalink with a
specific Java binary. To do this, you will need to locate your ``java.exe``/``java`` file
in your **Java 17 or 11 install**.
in your **Java 21 or 17 install**.
Alternatively, update your PATH settings so that Java 17 or 11 is the one used by ``java``. However,
Alternatively, update your PATH settings so that Java 21 or 17 is the one used by ``java``. However,
you should confirm that nothing other than Red is running on the machine that requires Java.
.. _queue_commands:
@@ -550,7 +550,7 @@ uses OpenJDK 17 in the managed Lavalink configuration. It can be installed by ru
sudo apt install openjdk-17-jre-headless -y
Otherwise, Lavalink works well with most versions of Java 11, 13, 15, 16, 17, and 18. Azul
Otherwise, Lavalink works well with most versions of Java 17 and higher. Azul
Zulu builds are suggested, see `here <https://github.com/lavalink-devs/Lavalink/#requirements>`__ for more information.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -3651,7 +3651,7 @@ This command shouldn't need to be used most of the time,
and is only useful if the host machine has conflicting Java versions.
If changing this make sure that the Java executable you set is supported by Audio.
The current supported versions are Java 17 and 11.
The current supported versions are Java 21 or 17.
**Arguments**
+1 -1
View File
@@ -324,7 +324,7 @@ Explains how to set the Twitch token.
To set the Twitch API tokens, follow these steps:
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
1. Go to this page: https://dev.twitch.tv/console/apps.
2. Click Register Your Application.
+10
View File
@@ -44,7 +44,9 @@ extensions = [
"sphinx.ext.napoleon",
"sphinx.ext.doctest",
"sphinxcontrib_trio",
"sphinx_markdown_builder",
"sphinx-prompt",
"changelog_contributors",
"deprecated_removed",
"prompt_builder",
]
@@ -230,6 +232,14 @@ linkcheck_ignore = [r"https://java.com*", r"https://chocolatey.org*"]
linkcheck_retries = 3
# -- Options for markdown builder ----------------------------------------
markdown_http_base = os.environ.get(
"READTHEDOCS_CANONICAL_URL", "https://docs.discord.red/en/stable"
)
markdown_uri_doc_suffix = ".html"
# -- Options for extensions -----------------------------------------------
if dpy_version_info.releaselevel == "final":
-3
View File
@@ -7,9 +7,6 @@
Bank
====
Bank has now been separated from Economy for V3. New to bank is support for
having a global bank.
***********
Basic Usage
***********
+1 -1
View File
@@ -9,7 +9,7 @@ Bot
Red
^^^
.. autoclass:: Red
.. autoclass:: Red()
:members:
:exclude-members: get_context, get_embed_color
-43
View File
@@ -446,49 +446,6 @@ Of course, if we're less than responsible pet owners, there are consequences::
"how poorly it was taken care of."
)
*************
V2 Data Usage
*************
There has been much conversation on how to bring V2 data into V3 and, officially, we recommend that cog developers
make use of the public interface in Config (using the categories as described in these docs) rather than simply
copying and pasting your V2 data into V3. Using Config as recommended will result in a much better experience for
you in the long run and will simplify cog creation and maintenance.
However.
We realize that many of our cog creators have expressed disinterest in writing converters for V2 to V3 style data.
As a result we have opened up config to take standard V2 data and allow cog developers to manipulate it in V3 in
much the same way they would in V2. The following examples will demonstrate how to accomplish this.
.. warning::
By following this method to use V2 data in V3 you may be at risk of data corruption if your cog is used on a bot
with multiple shards. USE AT YOUR OWN RISK.
.. code-block:: python
from redbot.core import Config, commands
class ExampleCog(commands.Cog):
def __init__(self):
self.config = Config.get_conf(self, 1234567890)
self.config.init_custom("V2", 1)
self.data = {}
async def load_data(self):
self.data = await self.config.custom("V2", "V2").all()
async def save_data(self):
await self.config.custom("V2", "V2").set(self.data)
async def setup(bot):
cog = ExampleCog()
await cog.load_data()
await bot.add_cog(cog)
************************************
Best practices and performance notes
************************************
-2
View File
@@ -8,8 +8,6 @@
Mod log
=======
Mod log has now been separated from Mod for V3.
***********
Basic Usage
***********
+1 -1
View File
@@ -9,7 +9,7 @@ RPC
RPC support is included in Red on a `provisional <developer-guarantees-exclusions>` basis.
Backwards incompatible changes (up to and including removal of the RPC) may occur if deemed necessary.
V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
Red comes default with an internal RPC server that may be used to remotely control the bot in various ways.
Cogs must register functions to be exposed to RPC clients.
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.
+7 -9
View File
@@ -3,11 +3,11 @@
.. role:: python(code)
:language: python
========================
Creating cogs for Red V3
========================
=====================
Creating cogs for Red
=====================
This guide serves as a tutorial on creating cogs for Red V3.
This guide serves as a tutorial on creating cogs for Red.
It will cover the basics of setting up a package for your
cog and the basics of setting up the file structure. We will
also point you towards some further resources that may assist
@@ -111,8 +111,8 @@ Make sure that both files are saved.
Testing your cog
----------------
To test your cog, you will need a running instance of V3.
Assuming you installed V3 as outlined above, run :code:`redbot-setup`
To test your cog, you will need a running instance of Red.
Assuming you installed Red as outlined above, run :code:`redbot-setup`
and provide the requested information. Once that's done, run Red
by doing :code:`redbot <instance name> --dev` to start Red.
Complete the initial setup by providing a valid token and setting a
@@ -169,6 +169,4 @@ Becoming an Approved Cog Creator
Additional resources
--------------------
Be sure to check out the :doc:`/guide_migration` for some resources
on developing cogs for V3. This will also cover differences between V2 and V3 for
those who developed cogs for V2.
If you've developed cogs for V2, you might find `incompatible_changes/v2_migration` document helpful.
+2 -2
View File
@@ -1,7 +1,7 @@
.. Publishing cogs for V3
Publishing cogs for Red V3
==========================
Publishing cogs for Red
=======================
Users of Red install 3rd-party cogs using Downloader cog. To make your cog available
to install for others, you will have to create a git repository
+2 -2
View File
@@ -86,7 +86,7 @@ Average Providers
| `OVH <https://us.ovhcloud.com/vps/>`_ is a company focused on providing hosting
and cloud services with locations in Europe, North America and Asia Pacific.
| `Time4VPS <https://www.time4vps.eu/>`_ is a Lithuanian VPS provider mainly focused
| `Time4VPS <https://www.time4vps.com/>`_ is a Lithuanian VPS provider mainly focused
on lower cost.
| `GalaxyGate <https://galaxygate.net/>`_ is a VPS and dedicated server provider
@@ -113,7 +113,7 @@ Average Providers
| `LowEndBox <http://lowendbox.com/>`_ is a website where hosting providers are
discussed and curated, often with lower costs and less known providers.
| `AlphaVps <https://alphavps.com>`_ is a Bulgaria VPS and dedicated server provider
| `AlphaVps <https://alphavps.com>`_ is a Bulgarian VPS and dedicated server provider
with locations in Los Angeles, New York, England, Germany and Bulgaria.
--------------------
+1
View File
@@ -10,3 +10,4 @@ Backward incompatible changes
future
3.5
v2_migration
@@ -38,6 +38,49 @@ per-server/member/user/role/channel or global basis. Be sure to check
out :doc:`/framework_config` for the API docs for Config as well as a
tutorial on using Config.
*************
V2 Data Usage
*************
There has been much conversation on how to bring V2 data into V3 and, officially, we recommend that cog developers
make use of the public interface in Config (using the categories as described in these docs) rather than simply
copying and pasting your V2 data into V3. Using Config as recommended will result in a much better experience for
you in the long run and will simplify cog creation and maintenance.
However.
We realize that many of our cog creators have expressed disinterest in writing converters for V2 to V3 style data.
As a result we have opened up config to take standard V2 data and allow cog developers to manipulate it in V3 in
much the same way they would in V2. The following examples will demonstrate how to accomplish this.
.. warning::
By following this method to use V2 data in V3 you may be at risk of data corruption if your cog is used on a bot
with multiple shards. USE AT YOUR OWN RISK.
.. code-block:: python
from redbot.core import Config, commands
class ExampleCog(commands.Cog):
def __init__(self):
self.config = Config.get_conf(self, 1234567890)
self.config.init_custom("V2", 1)
self.data = {}
async def load_data(self):
self.data = await self.config.custom("V2", "V2").all()
async def save_data(self):
await self.config.custom("V2", "V2").set(self.data)
async def setup(bot):
cog = ExampleCog()
await cog.load_data()
await bot.add_cog(cog)
----
Bank
----
-1
View File
@@ -62,7 +62,6 @@ Welcome to Red - Discord Bot's documentation!
:maxdepth: 2
:caption: Red Development Framework Reference:
guide_migration
guide_cog_creation
guide_slash_and_interactions
guide_publish_cogs
+4 -4
View File
@@ -59,11 +59,11 @@ Alma Linux 8 x86-64, aarch64 2029-05-31 (`securi
Alma Linux 9 x86-64, aarch64 2032-05-31 (`security support <https://wiki.almalinux.org/release-notes/>`__)
Amazon Linux 2023 x86-64, aarch64 2028-03-15 (`end-of-life <https://docs.aws.amazon.com/linux/al2023/release-notes/support-info-by-support-statement.html#support-info-by-support-statement-eol>`__)
Arch Linux x86-64 forever (support is only provided for an up-to-date system)
CentOS Stream 9 x86-64, aarch64 2027-05-31 (`expected EOL <https://centos.org/stream9/#timeline>`__)
CentOS Stream 9 x86-64, aarch64 2027-05-31 (`Expected EOL <https://centos.org/stream9/#timeline>`__)
Debian 12 Bookworm x86-64, aarch64, armv7l 2026-06-10 (`End of life <https://wiki.debian.org/DebianReleases#Production_Releases>`__)
Fedora Linux 42 x86-64, aarch64 2026-05-13 (`End of Life <https://fedorapeople.org/groups/schedule/f-42/f-42-key-tasks.html>`__)
Fedora Linux 43 x86-64, aarch64 2026-12-09 (`End of Life <https://fedorapeople.org/groups/schedule/f-43/f-43-key-tasks.html>`__)
openSUSE Leap 15.6 x86-64, aarch64 2025-12-31 (`end of maintenance life cycle <https://en.opensuse.org/Lifetime#openSUSE_Leap>`__)
openSUSE Leap 15.6 x86-64, aarch64 2025-12-31 (`end of maintenance lifecycle <https://en.opensuse.org/Lifetime#openSUSE_Leap>`__)
openSUSE Tumbleweed x86-64, aarch64 forever (support is only provided for an up-to-date system)
Oracle Linux 8 x86-64, aarch64 2029-07-31 (`End of Premier Support <https://www.oracle.com/us/support/library/elsp-lifetime-069338.pdf>`__)
Oracle Linux 9 x86-64, aarch64 2032-06-31 (`End of Premier Support <https://www.oracle.com/us/support/library/elsp-lifetime-069338.pdf>`__)
@@ -73,8 +73,8 @@ RHEL 8.10 x86-64, aarch64 2029-05-31 (`End of
RHEL 9 (latest) x86-64, aarch64 2032-05-31 (`End of Maintenance Support <https://access.redhat.com/support/policy/updates/errata#Life_Cycle_Dates>`__)
RHEL 9.4 x86-64, aarch64 2026-04-30 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
RHEL 9.6 x86-64, aarch64 2027-05-31 (`End of Extended Update Support <https://access.redhat.com/support/policy/updates/errata#Extended_Update_Support>`__)
Rocky Linux 8 x86-64, aarch64 2029-05-31 (`(i) Planned EOL <https://rockylinux.org/download>`__)
Rocky Linux 9 x86-64, aarch64 2032-05-31 (`(i) Planned EOL <https://rockylinux.org/download>`__)
Rocky Linux 8 x86-64, aarch64 2029-05-31 (`End of Life <https://wiki.rockylinux.org/rocky/version/>`__)
Rocky Linux 9 x86-64, aarch64 2032-05-31 (`End of Life <https://wiki.rockylinux.org/rocky/version/>`__)
Ubuntu 22.04 LTS x86-64, aarch64 2027-06-30 (`End of Standard Support <https://wiki.ubuntu.com/Releases#Current>`__)
Ubuntu 24.04 LTS x86-64, aarch64 2029-06-30 (`End of Standard Support <https://wiki.ubuntu.com/Releases#Current>`__)
================================ ======================= ============================================================
+1 -15
View File
@@ -289,19 +289,6 @@ class VersionInfo:
return version("Red-DiscordBot")
def _update_event_loop_policy():
if _sys.implementation.name == "cpython":
# Let's not force this dependency, uvloop is much faster on cpython
try:
import uvloop
except ImportError:
pass
else:
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def _ensure_no_colorama():
# a hacky way to ensure that nothing initialises colorama
# if we're not running with legacy Windows command line mode
@@ -334,12 +321,11 @@ def _early_init():
# This function replaces logger so we preferably (though not necessarily) want that to happen
# before importing anything that calls `logging.getLogger()`, i.e. `asyncio`.
_update_logger_class()
_update_event_loop_policy()
_ensure_no_colorama()
# This is bumped automatically by release workflow (`.github/workflows/scripts/bump_version.py`)
_VERSION = "3.5.23"
_VERSION = "3.5.25.dev1"
__version__, version_info = VersionInfo._get_version()
+19 -37
View File
@@ -25,9 +25,9 @@ import rich
import redbot.logging
from redbot import __version__
from redbot.core.bot import Red, ExitCodes, _NoOwnerSet
from redbot.core._cli import interactive_config, confirm, parse_cli_flags
from redbot.core._cli import interactive_config, confirm, parse_cli_flags, new_event_loop
from redbot.setup import get_data_dir, get_name, save_config
from redbot.core import data_manager, _drivers
from redbot.core import data_manager, _drivers, _downloader
from redbot.core._debuginfo import DebugInfo
from redbot.core._sharedlibdeprecation import SharedLibImportWarner
@@ -182,32 +182,10 @@ async def _edit_owner(red, owner, no_prompt):
def _edit_instance_name(old_name, new_name, confirm_overwrite, no_prompt):
if new_name:
name = new_name
if name in _get_instance_names() and not confirm_overwrite:
name = old_name
print(
"An instance with this name already exists.\n"
"If you want to remove the existing instance and replace it with this one,"
" run this command with --overwrite-existing-instance flag."
)
name = get_name(new_name, confirm_overwrite=confirm_overwrite)
elif not no_prompt and confirm("Would you like to change the instance name?", default=False):
name = get_name("")
if name in _get_instance_names():
print(
"WARNING: An instance already exists with this name. "
"Continuing will overwrite the existing instance config."
)
if not confirm(
"Are you absolutely certain you want to continue with this instance name?",
default=False,
):
print("Instance name will remain unchanged.")
name = old_name
else:
print("Instance name updated.")
else:
print("Instance name updated.")
print()
name = get_name(confirm_overwrite=confirm_overwrite)
print("Instance name updated.\n")
else:
name = old_name
return name
@@ -272,7 +250,7 @@ def early_exit_runner(
"""
This one exists to not log all the things like it's a full run of the bot.
"""
loop = asyncio.new_event_loop()
loop = new_event_loop()
asyncio.set_event_loop(loop)
try:
if not cli_flags.instance_name:
@@ -281,7 +259,7 @@ def early_exit_runner(
return
data_manager.load_basic_configuration(cli_flags.instance_name)
red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None)
red = Red(cli_flags=cli_flags)
driver_cls = _drivers.get_driver_class()
loop.run_until_complete(driver_cls.initialize(**data_manager.storage_details()))
loop.run_until_complete(func(red, cli_flags))
@@ -317,19 +295,23 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
redbot.logging.init_logging(
level=cli_flags.logging_level,
location=data_manager.core_data_path() / "logs",
cli_flags=cli_flags,
rich_logging=cli_flags.rich_logging,
rich_tracebacks=cli_flags.rich_tracebacks,
rich_traceback_extra_lines=cli_flags.rich_traceback_extra_lines,
rich_traceback_show_locals=cli_flags.rich_traceback_show_locals,
)
log.debug("====Basic Config====")
log.debug("Data Path: %s", data_manager._base_data_path())
log.debug("Storage Type: %s", data_manager.storage_type())
await _downloader._init(red)
# lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061)
# We might want to change handling of requirements in Downloader at later date
LIB_PATH = data_manager.cog_data_path(raw_name="Downloader") / "lib"
LIB_PATH.mkdir(parents=True, exist_ok=True)
if str(LIB_PATH) not in sys.path:
sys.path.append(str(LIB_PATH))
lib_path = str(_downloader.LIB_PATH)
if lib_path not in sys.path:
sys.path.append(lib_path)
# "It's important to note that the global `working_set` object is initialized from
# `sys.path` when `pkg_resources` is first imported, but is only updated if you do
@@ -339,7 +321,7 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
# Source: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#workingset-objects
pkg_resources = sys.modules.get("pkg_resources")
if pkg_resources is not None:
pkg_resources.working_set.add_entry(str(LIB_PATH))
pkg_resources.working_set.add_entry(lib_path)
sys.meta_path.insert(0, SharedLibImportWarner())
if cli_flags.token:
@@ -478,7 +460,7 @@ def main():
early_exit_runner(cli_flags, edit_instance)
return
try:
loop = asyncio.new_event_loop()
loop = new_event_loop()
asyncio.set_event_loop(loop)
if cli_flags.no_instance:
@@ -493,7 +475,7 @@ def main():
data_manager.load_basic_configuration(cli_flags.instance_name)
red = Red(cli_flags=cli_flags, description="Red V3", dm_help=None)
red = Red(cli_flags=cli_flags)
if os.name != "nt":
# None of this works on windows.
+27 -14
View File
@@ -16,6 +16,7 @@ from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, humanize_number
from redbot.core.utils.menus import menu, start_adding_reactions
from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate
from redbot.core.utils.views import SetApiView
from ...audio_dataclasses import LocalPath
from ...converters import ScopeParser
@@ -1280,26 +1281,38 @@ class AudioSetCommands(MixinMeta, metaclass=CompositeMetaClass):
"6. Click on Create Credential at the top.\n"
'7. At the top click the link for "API key".\n'
"8. No application restrictions are needed. Click Create at the bottom.\n"
"9. You now have a key to add to `{prefix}set api youtube api_key <your_api_key_here>`"
).format(prefix=ctx.prefix)
await ctx.maybe_send_embed(message)
"9. Click the button below this message and set your API key"
" with the data shown in Google Developers Console."
)
await ctx.send(
message,
view=SetApiView(default_service="youtube", default_keys={"api_key": ""}),
)
@command_audioset.command(name="spotifyapi")
@commands.is_owner()
async def command_audioset_spotifyapi(self, ctx: commands.Context):
"""Instructions to set the Spotify API tokens."""
message = _(
"1. Go to Spotify developers and log in with your Spotify account.\n"
"(https://developer.spotify.com/dashboard/applications)\n"
'2. Click "Create An App".\n'
"3. Fill out the form provided with your app name, etc.\n"
'4. When asked if you\'re developing commercial integration select "No".\n'
"5. Accept the terms and conditions.\n"
"6. Copy your client ID and your client secret into:\n"
"`{prefix}set api spotify client_id <your_client_id_here> "
"client_secret <your_client_secret_here>`"
).format(prefix=ctx.prefix)
await ctx.maybe_send_embed(message)
"1. Go to Spotify for Developers and log in with your Spotify account."
" If this is your first time, you'll be asked to accept the terms and conditions.\n"
"(https://developer.spotify.com/dashboard)\n"
'2. Click "Create app".\n'
"3. Fill out the form provided with your app name and description."
" These can be anything you want. Website field can be left empty.\n"
"4. Add `https://localhost` to your Redirect URIs. This will not be used"
" but is required when filling out the form.\n"
'5. Select "Web API" when asked which API/SDKs you are planning to use.\n'
"6. Confirm that you agree to the terms and conditions and save the application.\n"
"7. Click the button below this message and set your client ID and your client secret"
" with the data shown in Spotify's dashboard."
)
await ctx.send(
message,
view=SetApiView(
default_service="spotify", default_keys={"client_id": "", "client_secret": ""}
),
)
@command_audioset.command(name="countrycode")
@commands.guild_only()
+74 -67
View File
@@ -53,7 +53,7 @@ msgstr "Não foi possível tocar a música"
#: redbot/cogs/audio/core/utilities/player.py:442
#: redbot/cogs/audio/core/utilities/player.py:524
msgid "Queue size limit reached."
msgstr ""
msgstr "Limite da fila atingindo."
#: redbot/cogs/audio/core/utilities/formatting.py:154
#: redbot/cogs/audio/core/utilities/player.py:599
@@ -63,41 +63,41 @@ msgstr "Faixa Enfileirada"
#: redbot/cogs/audio/core/utilities/formatting.py:168
#: redbot/cogs/audio/core/utilities/player.py:548
msgid "This track is not allowed in this server."
msgstr ""
msgstr "Esta faixa não é permitida neste servidor."
#: redbot/cogs/audio/core/utilities/formatting.py:185
#: redbot/cogs/audio/core/utilities/player.py:570
msgid "Track exceeds maximum length."
msgstr ""
msgstr "Faixa excede comprimento máximo."
#: redbot/cogs/audio/core/utilities/formatting.py:200
#: redbot/cogs/audio/core/utilities/player.py:602
msgid "{time} until track playback: #{position} in queue"
msgstr ""
msgstr "{time} até a reprodução da faixa: #{position} na fila"
#: redbot/cogs/audio/core/utilities/formatting.py:260
msgid "Tracks Found:"
msgstr ""
msgstr "Faixas Encontradas:"
#: redbot/cogs/audio/core/utilities/formatting.py:261
msgid "search results"
msgstr ""
msgstr "resultados da pesquisa"
#: redbot/cogs/audio/core/utilities/formatting.py:263
msgid "Folders Found:"
msgstr ""
msgstr "Pastas Encontradas:"
#: redbot/cogs/audio/core/utilities/formatting.py:264
msgid "local folders"
msgstr ""
msgstr "pastas locais"
#: redbot/cogs/audio/core/utilities/formatting.py:266
msgid "Files Found:"
msgstr ""
msgstr "Arquivos Encontrados:"
#: redbot/cogs/audio/core/utilities/formatting.py:267
msgid "local tracks"
msgstr ""
msgstr "faixas locais"
#: redbot/cogs/audio/core/utilities/formatting.py:379
#: redbot/cogs/audio/core/utilities/playlists.py:240
@@ -122,15 +122,15 @@ msgstr "Ambiente inválido"
#: redbot/cogs/audio/core/utilities/local_tracks.py:109
msgid "No localtracks folder."
msgstr ""
msgstr "Sem pasta localtracks."
#: redbot/cogs/audio/core/utilities/miscellaneous.py:50
msgid "Not enough {currency}"
msgstr ""
msgstr "Sem {currency} suficiente"
#: redbot/cogs/audio/core/utilities/miscellaneous.py:51
msgid "{required_credits} {currency} required, but you have {bal}."
msgstr ""
msgstr "{required_credits} {currency} necessário, mas você possui {bal}."
#: redbot/cogs/audio/core/utilities/player.py:78
msgid "music in {} servers"
@@ -140,54 +140,56 @@ msgstr "música em {} servidores"
#: redbot/cogs/audio/core/utilities/player.py:139
#: redbot/cogs/audio/core/utilities/player.py:144
msgid "There's nothing in the queue."
msgstr ""
msgstr "Não há nada na fila."
#: redbot/cogs/audio/core/utilities/player.py:141
msgid "Currently livestreaming {track}"
msgstr ""
msgstr "Transmitindo agora {track}"
#: redbot/cogs/audio/core/utilities/player.py:146
msgid "{time} left on {track}"
msgstr ""
msgstr "{time} restante de {track}"
#: redbot/cogs/audio/core/utilities/player.py:154
#: redbot/cogs/audio/core/utilities/player.py:189
msgid "Track Skipped"
msgstr ""
msgstr "Faixa Pulada"
#: redbot/cogs/audio/core/utilities/player.py:167
msgid "Track number must be equal to or greater than 1."
msgstr ""
msgstr "O número da faixa deve ser igual ou maior que 1."
#: redbot/cogs/audio/core/utilities/player.py:173
msgid "There are only {queuelen} songs currently queued."
msgstr ""
msgstr "Há apenas músicas {queuelen} na fila."
#: redbot/cogs/audio/core/utilities/player.py:179
msgid "{skip_to_track} Tracks Skipped"
msgstr ""
msgstr "{skip_to_track} Faixas Puladas"
#: redbot/cogs/audio/core/utilities/player.py:235
msgid "The owner needs to set the Spotify client ID and Spotify client secret, before Spotify URLs or codes can be used. \n"
"See `{prefix}audioset spotifyapi` for instructions."
msgstr ""
msgstr "O proprietário precisa definir o ID do cliente do Spotify e o Spotify Client Secret, antes que possam ser usadas URLs ou códigos do Spotify. \n"
"Veja `{prefix}audioset spotifyapi` para instruções."
#: redbot/cogs/audio/core/utilities/player.py:245
msgid "The owner needs to set the YouTube API key before Spotify URLs or codes can be used.\n"
"See `{prefix}audioset youtubeapi` for instructions."
msgstr ""
msgstr "O proprietário precisa definir a chave da API do YouTube antes que URLs ou códigos do Spotify possam ser usados.\n"
"Veja `{prefix}audioset youtubeapi` para instruções."
#: redbot/cogs/audio/core/utilities/player.py:254
#: redbot/cogs/audio/core/utilities/player.py:363
#: redbot/cogs/audio/core/utilities/playlists.py:594
msgid "Unable To Get Tracks"
msgstr ""
msgstr "Não foi possível obter as faixas"
#: redbot/cogs/audio/core/utilities/player.py:255
#: redbot/cogs/audio/core/utilities/player.py:364
#: redbot/cogs/audio/core/utilities/playlists.py:595
msgid "Wait until the playlist has finished loading."
msgstr ""
msgstr "Aguarde até que a playlist termine de carregar."
#: redbot/cogs/audio/core/utilities/player.py:266
#: redbot/cogs/audio/core/utilities/player.py:308
@@ -203,7 +205,7 @@ msgstr "Nada encontrado."
#: redbot/cogs/audio/core/utilities/playlists.py:607
#: redbot/cogs/audio/core/utilities/playlists.py:640
msgid "Track is not playable."
msgstr ""
msgstr "Faixa não é reproduzível."
#: redbot/cogs/audio/core/utilities/player.py:270
#: redbot/cogs/audio/core/utilities/player.py:311
@@ -211,7 +213,7 @@ msgstr ""
#: redbot/cogs/audio/core/utilities/playlists.py:608
#: redbot/cogs/audio/core/utilities/playlists.py:641
msgid "**{suffix}** is not a fully supported format and some tracks may not play."
msgstr ""
msgstr "**{suffix}** não é um formato totalmente suportado e algumas faixas podem não reproduzir."
#: redbot/cogs/audio/core/utilities/player.py:300
#: redbot/cogs/audio/core/utilities/player.py:393
@@ -235,7 +237,7 @@ msgstr "A chave de API do Spotify ou segredo do cliente não foram definidos cor
#: redbot/cogs/audio/core/utilities/player.py:351
msgid "Unable To Find Tracks"
msgstr ""
msgstr "Não foi possível encontrar as faixas"
#: redbot/cogs/audio/core/utilities/player.py:352
msgid "This doesn't seem to be a supported Spotify URL or code."
@@ -243,26 +245,27 @@ msgstr "Isto não parece ser uma URL ou código do Spotify válido."
#: redbot/cogs/audio/core/utilities/player.py:378
msgid "{query} is not an allowed query."
msgstr ""
msgstr "{query} não é uma solicitação permitida."
#: redbot/cogs/audio/core/utilities/player.py:394
#: redbot/cogs/audio/core/utilities/playlists.py:627
#: redbot/cogs/audio/core/utilities/playlists.py:656
msgid "I'm unable to get a track from Lavalink node at the moment, try again in a few minutes."
msgstr ""
msgstr "Não foi possível obter uma faixa do Lavalink Node no momento, tente novamente em alguns minutos."
#: redbot/cogs/audio/core/utilities/player.py:416
msgid "Local tracks will not work if the `Lavalink.jar` cannot see the track.\n"
"This may be due to permissions or because Lavalink.jar is being run in a different machine than the local tracks."
msgstr ""
msgstr "As faixas locais não funcionarão se o `Lavalink.jar` não conseguir ver a faixa.\n"
"Isto pode ser devido a permissões ou porque o Lavalink.jar está sendo executado em uma máquina diferente das faixas locais."
#: redbot/cogs/audio/core/utilities/player.py:486
msgid " {bad_tracks} tracks cannot be queued."
msgstr ""
msgstr " {bad_tracks} faixas não puderam ser adicionadas."
#: redbot/cogs/audio/core/utilities/player.py:492
msgid "No Title"
msgstr ""
msgstr "Sem Título"
#: redbot/cogs/audio/core/utilities/player.py:494
msgid "Playlist Enqueued"
@@ -270,7 +273,7 @@ msgstr "Lista de reprodução enfileirada"
#: redbot/cogs/audio/core/utilities/player.py:494
msgid "Album Enqueued"
msgstr ""
msgstr "Álbum Adicionado"
#: redbot/cogs/audio/core/utilities/player.py:502
msgid "Added {num} tracks to the queue.{maxlength_msg}"
@@ -286,25 +289,25 @@ msgstr "Nada foi encontrado"
#: redbot/cogs/audio/core/utilities/player.py:623
msgid "Please wait, finding tracks..."
msgstr ""
msgstr "Por favor, aguarde, encontrando faixas..."
#: redbot/cogs/audio/core/utilities/player.py:629
msgid "Getting track {num}/{total}..."
msgstr ""
msgstr "Obtendo faixa {num}/{total}..."
#: redbot/cogs/audio/core/utilities/player.py:630
msgid "Matching track {num}/{total}..."
msgstr ""
msgstr "Correspondendo faixa {num}/{total}..."
#: redbot/cogs/audio/core/utilities/player.py:631
#: redbot/cogs/audio/core/utilities/playlists.py:341
#: redbot/cogs/audio/core/utilities/playlists.py:414
msgid "Loading track {num}/{total}..."
msgstr ""
msgstr "Carregando faixa {num}/{total}..."
#: redbot/cogs/audio/core/utilities/player.py:632
msgid "Approximate time remaining: {seconds}"
msgstr ""
msgstr "Tempo restante aproximado: {seconds}"
#: redbot/cogs/audio/core/utilities/player.py:658
msgid "I'm unable to get a track from Lavalink at the moment, try again in a few minutes."
@@ -316,27 +319,27 @@ msgstr "A conexão foi redefinida durante o carregamento da lista de reproduçã
#: redbot/cogs/audio/core/utilities/playlists.py:83
msgid "You do not have the permissions to manage {name} (`{id}`) [**{scope}**]."
msgstr ""
msgstr "Você não tem as permissões para gerenciar {name} (`{id}`) [**{scope}**]."
#: redbot/cogs/audio/core/utilities/playlists.py:101
msgid "You do not have the permissions to manage that playlist in {guild}."
msgstr ""
msgstr "Você não tem permissão para gerenciar essa playlist no {guild}."
#: redbot/cogs/audio/core/utilities/playlists.py:108
msgid "You do not have the permissions to manage playlist owned by {user}."
msgstr ""
msgstr "Você não tem permissão para gerenciar a playlist de {user}."
#: redbot/cogs/audio/core/utilities/playlists.py:112
msgid "You do not have the permissions to manage playlists in {scope} scope."
msgstr ""
msgstr "Você não tem as permissões para gerenciar playlists no escopo {scope}."
#: redbot/cogs/audio/core/utilities/playlists.py:116
msgid "No access to playlist."
msgstr ""
msgstr "Sem acesso à playlist."
#: redbot/cogs/audio/core/utilities/playlists.py:224
msgid "{match_count} playlists match {original_input}: Please try to be more specific, or use the playlist ID."
msgstr ""
msgstr "{match_count} playlists correspondem {original_input}: Por favor, tente ser mais específico, ou use o ID da playlist."
#: redbot/cogs/audio/core/utilities/playlists.py:241
msgid "{number}. <{playlist.name}>\n"
@@ -344,24 +347,28 @@ msgid "{number}. <{playlist.name}>\n"
" - ID: < {playlist.id} >\n"
" - Tracks: < {tracks} >\n"
" - Author: < {author} >\n\n"
msgstr ""
msgstr "{number}. <{playlist.name}>\n"
" - Escopo: < {scope} >\n"
" - ID: < {playlist.id} >\n"
" - Faixas: < {tracks} >\n"
" - Autor: < {author} >\n\n"
#: redbot/cogs/audio/core/utilities/playlists.py:258
msgid "{playlists} playlists found, which one would you like?"
msgstr ""
msgstr "{playlists} playlists encontradas, de qual você gostaria?"
#: redbot/cogs/audio/core/utilities/playlists.py:277
#: redbot/cogs/audio/core/utilities/playlists.py:283
msgid "Too many matches found and you did not select which one you wanted."
msgstr ""
msgstr "Muitas opções foram encontradas e você não selecionou qual você queria."
#: redbot/cogs/audio/core/utilities/playlists.py:308
msgid "Playlists you can access in this server:"
msgstr ""
msgstr "Playlists que você pode acessar neste servidor:"
#: redbot/cogs/audio/core/utilities/playlists.py:314
msgid "Playlists for {scope}:"
msgstr ""
msgstr "Playlists para {scope}:"
#: redbot/cogs/audio/core/utilities/playlists.py:318
msgid "Page {page_num}/{total_pages} | {num} playlists."
@@ -370,46 +377,46 @@ msgstr ""
#: redbot/cogs/audio/core/utilities/playlists.py:334
#: redbot/cogs/audio/core/utilities/playlists.py:412
msgid "Please wait, adding tracks..."
msgstr ""
msgstr "Por favor, aguarde, adicionando faixas..."
#: redbot/cogs/audio/core/utilities/playlists.py:361
#: redbot/cogs/audio/core/utilities/playlists.py:464
msgid "Empty playlist {name} (`{id}`) [**{scope}**] created."
msgstr ""
msgstr "Playlist vazia {name} (`{id}`) [**{scope}**] criada."
#: redbot/cogs/audio/core/utilities/playlists.py:366
#: redbot/cogs/audio/core/utilities/playlists.py:469
msgid "Added {num} tracks from the {playlist_name} playlist. {num_bad} track(s) could not be loaded."
msgstr ""
msgstr "Adicionadas {num} músicas da lista {playlist_name} . Não foi possível carregar a(s) faixa(s) {num_bad}."
#: redbot/cogs/audio/core/utilities/playlists.py:371
#: redbot/cogs/audio/core/utilities/playlists.py:474
msgid "Added {num} tracks from the {playlist_name} playlist."
msgstr ""
msgstr "Adicionadas {num} músicas da lista {playlist_name}."
#: redbot/cogs/audio/core/utilities/playlists.py:375
#: redbot/cogs/audio/core/utilities/playlists.py:478
msgid "Playlist Saved"
msgstr ""
msgstr "Playlist salva"
#: redbot/cogs/audio/core/utilities/playlists.py:540
#: redbot/cogs/audio/core/utilities/playlists.py:553
#: redbot/cogs/audio/core/utilities/playlists.py:560
#: redbot/cogs/audio/core/utilities/playlists.py:571
msgid "Unable To Get Playlists"
msgstr ""
msgstr "Não foi possível obter as playlists"
#: redbot/cogs/audio/core/utilities/playlists.py:541
msgid "I don't have permission to connect and speak in your channel."
msgstr ""
msgstr "Não tenho permissão para conectar e falar em seu canal."
#: redbot/cogs/audio/core/utilities/playlists.py:572
msgid "You must be in the voice channel to use the playlist command."
msgstr ""
msgstr "Você deve estar no canal de voz para usar esse comando."
#: redbot/cogs/audio/core/utilities/playlists.py:680
msgid "the Global"
msgstr ""
msgstr "o Global"
#: redbot/cogs/audio/core/utilities/playlists.py:680
msgid "Global"
@@ -417,7 +424,7 @@ msgstr "Global"
#: redbot/cogs/audio/core/utilities/playlists.py:682
msgid "the Server"
msgstr ""
msgstr "o Servidor"
#: redbot/cogs/audio/core/utilities/playlists.py:682
msgid "Server"
@@ -425,7 +432,7 @@ msgstr "Servidor"
#: redbot/cogs/audio/core/utilities/playlists.py:684
msgid "the User"
msgstr ""
msgstr "o Usuário"
#: redbot/cogs/audio/core/utilities/playlists.py:684
msgid "User"
@@ -433,20 +440,20 @@ msgstr "Usuário"
#: redbot/cogs/audio/core/utilities/queue.py:40
msgid "__Too many songs in the queue, only showing the first 500__.\n\n"
msgstr ""
msgstr "__Muitas músicas na fila, mostrando apenas os primeiros 500__.\n\n"
#: redbot/cogs/audio/core/utilities/queue.py:57
msgid "**Currently livestreaming:**\n"
msgstr ""
msgstr "**Transmitindo agora:**\n"
#: redbot/cogs/audio/core/utilities/queue.py:59
#: redbot/cogs/audio/core/utilities/queue.py:64
msgid "Requested by: **{user}**"
msgstr ""
msgstr "Solicitado por: **{user}**"
#: redbot/cogs/audio/core/utilities/queue.py:62
msgid "Playing: "
msgstr ""
msgstr "Reproduzindo: "
#: redbot/cogs/audio/core/utilities/queue.py:76
msgid "requested by **{user}**\n"
@@ -454,11 +461,11 @@ msgstr ""
#: redbot/cogs/audio/core/utilities/queue.py:80
msgid "Queue for __{guild_name}__"
msgstr ""
msgstr "Fila para __{guild_name}__"
#: redbot/cogs/audio/core/utilities/queue.py:88
msgid "Page {page_num}/{total_pages} | {num_tracks} tracks, {num_remaining} remaining\n"
msgstr ""
msgstr "Página {page_num}/{total_pages} {num_tracks} faixas, {num_remaining} restantes\n"
#: redbot/cogs/audio/core/utilities/queue.py:97
msgid "Auto-Play"
@@ -474,7 +481,7 @@ msgstr "Repetir"
#: redbot/cogs/audio/core/utilities/queue.py:161
msgid "Matching Tracks:"
msgstr ""
msgstr "Faixas correspondentes:"
#: redbot/cogs/audio/core/utilities/queue.py:164
msgid "Page {page_num}/{total_pages} | {num_tracks} tracks"
@@ -11,9 +11,9 @@ __all__ = (
)
JAR_VERSION: Final[LavalinkVersion] = LavalinkVersion(3, 7, 13, red=2)
JAR_VERSION: Final[LavalinkVersion] = LavalinkVersion(3, 7, 13, red=5)
YT_PLUGIN_VERSION: Final[str] = "1.18.0"
# keep this sorted from oldest to latest
SUPPORTED_JAVA_VERSIONS: Final[Tuple[int, ...]] = (11, 17)
SUPPORTED_JAVA_VERSIONS: Final[Tuple[int, ...]] = (17, 21)
LATEST_SUPPORTED_JAVA_VERSION: Final = SUPPORTED_JAVA_VERSIONS[-1]
OLDER_SUPPORTED_JAVA_VERSIONS: Final[Tuple[int, ...]] = SUPPORTED_JAVA_VERSIONS[:-1]
-1
View File
@@ -6,4 +6,3 @@ from .downloader import Downloader
async def setup(bot: Red) -> None:
cog = Downloader(bot)
await bot.add_cog(cog)
cog.create_init_task()
+15 -7
View File
@@ -1,7 +1,8 @@
import discord
from redbot.core import commands
from redbot.core import _downloader, commands
from redbot.core.i18n import Translator
from .installable import InstalledModule
from redbot.core._downloader.installable import InstalledModule
from redbot.core._downloader.repo_manager import Repo as _Repo
_ = Translator("Koala", __file__)
@@ -9,14 +10,21 @@ _ = Translator("Koala", __file__)
class InstalledCog(InstalledModule):
@classmethod
async def convert(cls, ctx: commands.Context, arg: str) -> InstalledModule:
downloader = ctx.bot.get_cog("Downloader")
if downloader is None:
raise commands.CommandError(_("No Downloader cog found."))
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
cog = discord.utils.get(await _downloader.installed_cogs(), name=arg)
if cog is None:
raise commands.BadArgument(
_("Cog `{cog_name}` is not installed.").format(cog_name=arg)
)
return cog
class Repo(_Repo):
@classmethod
async def convert(cls, ctx: commands.Context, argument: str) -> _Repo:
poss_repo = _downloader._repo_manager.get_repo(argument)
if poss_repo is None:
raise commands.BadArgument(
_('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument)
)
return poss_repo
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -23,16 +23,16 @@ msgstr ""
#: redbot/cogs/downloader/checks.py:38
msgid "Your response has timed out, please try again."
msgstr ""
msgstr "Sua resposta expirou. Por favor, tente novamente."
#: redbot/cogs/downloader/converters.py:14
#: redbot/cogs/downloader/repo_manager.py:176
msgid "No Downloader cog found."
msgstr ""
msgstr "Nenhum cog Downloader foi encontrado."
#: redbot/cogs/downloader/converters.py:19
msgid "Cog `{cog_name}` is not installed."
msgstr ""
msgstr "O Cog `{cog_name}` não está instalado."
#: redbot/cogs/downloader/downloader.py:31
msgid "\n"
@@ -67,24 +67,24 @@ msgstr ""
#: redbot/cogs/downloader/downloader.py:508
msgid "Libraries installed."
msgstr ""
msgstr "Bibliotecas instaladas."
#: redbot/cogs/downloader/downloader.py:508
msgid "Library installed."
msgstr ""
msgstr "Biblioteca instalada."
#: redbot/cogs/downloader/downloader.py:511
msgid "Some libraries failed to install. Please check your logs for a complete list."
msgstr ""
msgstr "Não foi possível instalar algumas bibliotecas. Verifique os seus logs para ter uma lista completa."
#: redbot/cogs/downloader/downloader.py:516
msgid "The library failed to install. Please check your logs for a complete list."
msgstr ""
msgstr "A biblioteca não foi instalada. Por favor, verifique os seus logs para ter uma lista completa."
#: redbot/cogs/downloader/downloader.py:524
#, docstring
msgid "Base command for repository management."
msgstr ""
msgstr "Comando base para gerenciamento do repositório."
#: redbot/cogs/downloader/downloader.py:531
#, docstring
File diff suppressed because it is too large Load Diff
+41 -31
View File
@@ -18,87 +18,87 @@ msgstr ""
#: redbot/cogs/general/general.py:49
#, docstring
msgid "General commands."
msgstr ""
msgstr "Comandos gerais."
#: redbot/cogs/general/general.py:54
msgid "As I see it, yes"
msgstr ""
msgstr "Como eu vejo, sim"
#: redbot/cogs/general/general.py:55
msgid "It is certain"
msgstr ""
msgstr "Com certeza"
#: redbot/cogs/general/general.py:56
msgid "It is decidedly so"
msgstr ""
msgstr "É decididamente assim"
#: redbot/cogs/general/general.py:57
msgid "Most likely"
msgstr ""
msgstr "Muito provável"
#: redbot/cogs/general/general.py:58
msgid "Outlook good"
msgstr ""
msgstr "Perspectiva boa"
#: redbot/cogs/general/general.py:59
msgid "Signs point to yes"
msgstr ""
msgstr "Os sinais indicam que sim"
#: redbot/cogs/general/general.py:60
msgid "Without a doubt"
msgstr ""
msgstr "Sem dúvida"
#: redbot/cogs/general/general.py:61
msgid "Yes"
msgstr ""
msgstr "Sim"
#: redbot/cogs/general/general.py:62
msgid "Yes definitely"
msgstr ""
msgstr "Sim definitivamente"
#: redbot/cogs/general/general.py:63
msgid "You may rely on it"
msgstr ""
msgstr "Você pode contar com isso"
#: redbot/cogs/general/general.py:64
msgid "Reply hazy, try again"
msgstr ""
msgstr "Resposta confusa, tente novamente"
#: redbot/cogs/general/general.py:65
msgid "Ask again later"
msgstr ""
msgstr "Pergunte novamente mais tarde"
#: redbot/cogs/general/general.py:66
msgid "Better not tell you now"
msgstr ""
msgstr "Melhor não te contar agora"
#: redbot/cogs/general/general.py:67
msgid "Cannot predict now"
msgstr ""
msgstr "Não consigo prever agora"
#: redbot/cogs/general/general.py:68
msgid "Concentrate and ask again"
msgstr ""
msgstr "Concentre-se e pergunte de novo"
#: redbot/cogs/general/general.py:69
msgid "Don't count on it"
msgstr ""
msgstr "Não conte com isso"
#: redbot/cogs/general/general.py:70
msgid "My reply is no"
msgstr ""
msgstr "Minha resposta é não"
#: redbot/cogs/general/general.py:71
msgid "My sources say no"
msgstr ""
msgstr "Minhas fontes dizem que não"
#: redbot/cogs/general/general.py:72
msgid "Outlook not so good"
msgstr ""
msgstr "A previsão não é muito boa"
#: redbot/cogs/general/general.py:73
msgid "Very doubtful"
msgstr ""
msgstr "Muito duvidoso"
#: redbot/cogs/general/general.py:88
#, docstring
@@ -107,11 +107,15 @@ msgid "Choose between multiple options.\n\n"
" Options are separated by spaces.\n\n"
" To denote options which include whitespace, you should enclose the options in double quotes.\n"
" "
msgstr ""
msgstr "Escolha entre múltiplas opções.\n\n"
" Deve haver pelo menos 2 opções para escolher.\n"
" As opções são separadas por espaços.\n\n"
" Para denotar opções que incluem espaços em branco, você deve colocar as opções entre aspas duplas.\n"
" "
#: redbot/cogs/general/general.py:97
msgid "Not enough options to pick from."
msgstr ""
msgstr "Opções insuficientes para escolher."
#: redbot/cogs/general/general.py:103
#, docstring
@@ -119,39 +123,45 @@ msgid "Roll a random number.\n\n"
" The result will be between 1 and `<number>`.\n\n"
" `<number>` defaults to 100.\n"
" "
msgstr ""
msgstr "Role um número aleatório.\n\n"
" O resultado será entre 1 e `<number>`.\n\n"
" `<number>` o padrão é 100.\n"
" "
#: redbot/cogs/general/general.py:118
msgid "{author.mention} Maybe higher than 1? ;P"
msgstr ""
msgstr "{author.mention} Talvez maior que 1? ;P"
#: redbot/cogs/general/general.py:121
msgid "{author.mention} Max allowed number is {maxamount}."
msgstr ""
msgstr "{author.mention} O número máximo permitido é {maxamount}."
#: redbot/cogs/general/general.py:128
#, docstring
msgid "Flip a coin... or a user.\n\n"
" Defaults to a coin.\n"
" "
msgstr ""
msgstr "Jogue uma moeda... ou um usuário.\n\n"
" O padrão é uma moeda.\n"
" "
#: redbot/cogs/general/general.py:136
msgid "Nice try. You think this is funny?\n"
" How about *this* instead:\n\n"
msgstr ""
msgstr "Boa tentativa. Você pensa que isso é engraçado?\n"
" Que tal *isso* em vez disso:\n\n"
#: redbot/cogs/general/general.py:147
msgid "*flips a coin and... "
msgstr ""
msgstr "*vira uma moeda e... "
#: redbot/cogs/general/general.py:147
msgid "HEADS!*"
msgstr ""
msgstr "CARA!*"
#: redbot/cogs/general/general.py:147
msgid "TAILS!*"
msgstr ""
msgstr "COROA!*"
#: redbot/cogs/general/general.py:151
#, docstring
+1 -1
View File
@@ -572,7 +572,7 @@ msgstr ""
#: redbot/cogs/mod/settings.py:85 redbot/cogs/mod/settings.py:93
#: redbot/cogs/mod/settings.py:96 redbot/cogs/mod/settings.py:108
msgid "Yes"
msgstr ""
msgstr "Sim"
#: redbot/cogs/mod/settings.py:31 redbot/cogs/mod/settings.py:57
#: redbot/cogs/mod/settings.py:62 redbot/cogs/mod/settings.py:67
+1 -1
View File
@@ -532,7 +532,7 @@ msgstr ""
#: redbot/cogs/mutes/mutes.py:1794
msgid "this server"
msgstr ""
msgstr "este servidor"
#: redbot/cogs/mutes/voicemutes.py:42
msgid "That user is not in a voice channel."
+1 -1
View File
@@ -171,7 +171,7 @@ msgstr ""
#: redbot/cogs/trivia/trivia.py:44
msgid "Yes"
msgstr ""
msgstr "Sim"
#: redbot/cogs/trivia/trivia.py:46
msgid "No"
+41 -1
View File
@@ -3,13 +3,15 @@ import asyncio
import logging
import sys
from enum import IntEnum
from typing import Optional
from typing import Any, Coroutine, Optional, TypeVar
import discord
from discord import __version__ as discord_version
from redbot.core.utils._internal_utils import cli_level_to_log_level
_T = TypeVar("_T")
# This needs to be an int enum to be used
# with sys.exit
@@ -244,6 +246,14 @@ def parse_cli_flags(args):
dest="logging_level",
help="Increase the verbosity of the logs, each usage of this flag increases the verbosity level by 1.",
)
parser.add_argument(
"--no-verbose",
"--no-debug",
action="store_const",
const=0,
dest="logging_level",
help="Set the verbosity level to 0.",
)
parser.add_argument("--dev", action="store_true", help="Enables developer mode")
parser.add_argument(
"--mentionable",
@@ -360,3 +370,33 @@ def parse_cli_flags(args):
args.logging_level = cli_level_to_log_level(args.logging_level)
return args
def asyncio_run(coro: Coroutine[Any, Any, _T]) -> _T:
if sys.version_info >= (3, 11):
with asyncio.Runner(loop_factory=new_event_loop) as runner:
return runner.run(coro)
if sys.implementation.name == "cpython":
# Let's not force this dependency, uvloop is much faster on cpython
try:
import uvloop
except ImportError:
pass
else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
return asyncio.run(coro)
def new_event_loop() -> asyncio.AbstractEventLoop:
if sys.implementation.name == "cpython":
# Let's not force this dependency, uvloop is much faster on cpython
try:
import uvloop
except ImportError:
pass
else:
return uvloop.new_event_loop()
return asyncio.new_event_loop()
+866
View File
@@ -0,0 +1,866 @@
# TODO list:
# - design ergonomic APIs instead of whatever you want to call what we have now
# - try to be consistent about requiring Installable vs cog name
# between cog install and other functionality
# - use immutable objects more
# - change Installable's equality to include its commit
# (note: we currently heavily rely on this *not* being the case)
# - add asyncio.Lock appropriately for things that Downloader does
# - avoid doing some of the work on RepoManager initialization to speedup bot startup
from __future__ import annotations
import contextlib
import dataclasses
import os
import shutil
import sys
import time
from collections import defaultdict
from pathlib import Path
from typing import (
Dict,
FrozenSet,
Iterable,
List,
Literal,
Optional,
Sequence,
Set,
Tuple,
Union,
cast,
TYPE_CHECKING,
)
import discord
from redbot.core import commands, Config, version_info as red_version_info
from redbot.core._cog_manager import CogManager
from redbot.core.data_manager import cog_data_path
from . import errors
from .log import log
from .installable import InstallableType, Installable, InstalledModule
from .repo_manager import RepoManager, Repo
if TYPE_CHECKING:
from redbot.core.bot import Red
_SCHEMA_VERSION = 1
_config: Config
_bot_ref: Optional[Red]
_cog_mgr: CogManager
_repo_manager: RepoManager
LIB_PATH: Path
SHAREDLIB_PATH: Path
_SHAREDLIB_INIT: Path
async def _init(bot: Red) -> None:
global _bot_ref
_bot_ref = bot
await _init_without_bot(_bot_ref._cog_mgr)
async def _init_without_bot(cog_manager: CogManager) -> None:
global _cog_mgr
_cog_mgr = cog_manager
start = time.perf_counter()
global _config
_config = Config.get_conf(None, 998240343, cog_name="Downloader", force_registration=True)
_config.register_global(schema_version=0, installed_cogs={}, installed_libraries={})
await _migrate_config()
global LIB_PATH, SHAREDLIB_PATH, _SHAREDLIB_INIT
LIB_PATH = cog_data_path(raw_name="Downloader") / "lib"
SHAREDLIB_PATH = LIB_PATH / "cog_shared"
_SHAREDLIB_INIT = SHAREDLIB_PATH / "__init__.py"
_create_lib_folder()
global _repo_manager
_repo_manager = RepoManager()
await _repo_manager.initialize()
stop = time.perf_counter()
log.debug("Finished initialization in %.2fs", stop - start)
async def _migrate_config() -> None:
schema_version = await _config.schema_version()
if schema_version == _SCHEMA_VERSION:
return
if schema_version == 0:
await _schema_0_to_1()
schema_version += 1
await _config.schema_version.set(schema_version)
async def _schema_0_to_1():
"""
This contains migration to allow saving state
of both installed cogs and shared libraries.
"""
old_conf = await _config.get_raw("installed", default=[])
if not old_conf:
return
async with _config.installed_cogs() as new_cog_conf:
for cog_json in old_conf:
repo_name = cog_json["repo_name"]
module_name = cog_json["cog_name"]
if repo_name not in new_cog_conf:
new_cog_conf[repo_name] = {}
new_cog_conf[repo_name][module_name] = {
"repo_name": repo_name,
"module_name": module_name,
"commit": "",
"pinned": False,
}
await _config.clear_raw("installed")
# no reliable way to get installed libraries (i.a. missing repo name)
# but it only helps `[p]cog update` run faster so it's not an issue
def _create_lib_folder(*, remove_first: bool = False) -> None:
if remove_first:
shutil.rmtree(str(LIB_PATH))
SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
if not _SHAREDLIB_INIT.exists():
with _SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
pass
async def installed_cogs() -> Tuple[InstalledModule, ...]:
"""Get info on installed cogs.
Returns
-------
`tuple` of `InstalledModule`
All installed cogs.
"""
installed = await _config.installed_cogs()
# noinspection PyTypeChecker
return tuple(
InstalledModule.from_json(cog_json, _repo_manager)
for repo_json in installed.values()
for cog_json in repo_json.values()
)
async def installed_libraries() -> Tuple[InstalledModule, ...]:
"""Get info on installed shared libraries.
Returns
-------
`tuple` of `InstalledModule`
All installed shared libraries.
"""
installed = await _config.installed_libraries()
# noinspection PyTypeChecker
return tuple(
InstalledModule.from_json(lib_json, _repo_manager)
for repo_json in installed.values()
for lib_json in repo_json.values()
)
async def installed_modules() -> Tuple[InstalledModule, ...]:
"""Get info on installed cogs and shared libraries.
Returns
-------
`tuple` of `InstalledModule`
All installed cogs and shared libraries.
"""
return await installed_cogs() + await installed_libraries()
async def _save_to_installed(modules: Iterable[InstalledModule]) -> None:
"""Mark modules as installed or updates their json in Config.
Parameters
----------
modules : `list` of `InstalledModule`
The modules to check off.
"""
async with _config.all() as global_data:
installed_cogs = global_data["installed_cogs"]
installed_libraries = global_data["installed_libraries"]
for module in modules:
if module.type is InstallableType.COG:
installed = installed_cogs
elif module.type is InstallableType.SHARED_LIBRARY:
installed = installed_libraries
else:
continue
module_json = module.to_json()
repo_json = installed.setdefault(module.repo_name, {})
repo_json[module.name] = module_json
async def _remove_from_installed(modules: Iterable[InstalledModule]) -> None:
"""Remove modules from the saved list
of installed modules (corresponding to type of module).
Parameters
----------
modules : `list` of `InstalledModule`
The modules to remove.
"""
async with _config.all() as global_data:
installed_cogs = global_data["installed_cogs"]
installed_libraries = global_data["installed_libraries"]
for module in modules:
if module.type is InstallableType.COG:
installed = installed_cogs
elif module.type is InstallableType.SHARED_LIBRARY:
installed = installed_libraries
else:
continue
with contextlib.suppress(KeyError):
installed[module._json_repo_name].pop(module.name)
async def _shared_lib_load_check(cog_name: str) -> Optional[Repo]:
_is_installed, cog = await is_installed(cog_name)
if _is_installed and cog.repo is not None and cog.repo.available_libraries:
return cog.repo
return None
async def is_installed(
cog_name: str,
) -> Union[Tuple[Literal[True], InstalledModule], Tuple[Literal[False], None]]:
"""Check to see if a cog has been installed through Downloader.
Parameters
----------
cog_name : str
The name of the cog to check for.
Returns
-------
`tuple` of (`bool`, `InstalledModule`)
:code:`(True, InstalledModule)` if the cog is installed, else
:code:`(False, None)`.
"""
for installed_cog in await installed_cogs():
if installed_cog.name == cog_name:
return True, installed_cog
return False, None
async def _available_updates(
cogs: Iterable[InstalledModule],
) -> Tuple[Tuple[Installable, ...], Tuple[Installable, ...]]:
"""
Get cogs and libraries which can be updated.
Parameters
----------
cogs : `list` of `InstalledModule`
List of cogs, which should be checked against the updates.
Returns
-------
tuple
2-tuple of cogs and libraries which can be updated.
"""
repos = {cog.repo for cog in cogs if cog.repo is not None}
_installed_libraries = await installed_libraries()
modules: Set[InstalledModule] = set()
cogs_to_update: Set[Installable] = set()
libraries_to_update: Set[Installable] = set()
# split libraries and cogs into 2 categories:
# 1. `cogs_to_update`, `libraries_to_update` - module needs update, skip diffs
# 2. `modules` - module MAY need update, check diffs
for repo in repos:
for lib in repo.available_libraries:
try:
index = _installed_libraries.index(lib)
except ValueError:
libraries_to_update.add(lib)
else:
modules.add(_installed_libraries[index])
for cog in cogs:
if cog.repo is None:
# cog had its repo removed, can't check for updates
continue
if cog.commit:
modules.add(cog)
continue
# marking cog for update if there's no commit data saved (back-compat, see GH-2571)
last_cog_occurrence = await cog.repo.get_last_module_occurrence(cog.name)
if last_cog_occurrence is not None and not last_cog_occurrence.disabled:
cogs_to_update.add(last_cog_occurrence)
# Reduces diff requests to a single dict with no repeats
hashes: Dict[Tuple[Repo, str], Set[InstalledModule]] = defaultdict(set)
for module in modules:
module.repo = cast(Repo, module.repo)
if module.repo.commit != module.commit:
try:
should_add = await module.repo.is_ancestor(module.commit, module.repo.commit)
except errors.UnknownRevision:
# marking module for update if the saved commit data is invalid
last_module_occurrence = await module.repo.get_last_module_occurrence(module.name)
if last_module_occurrence is not None and not last_module_occurrence.disabled:
if last_module_occurrence.type is InstallableType.COG:
cogs_to_update.add(last_module_occurrence)
elif last_module_occurrence.type is InstallableType.SHARED_LIBRARY:
libraries_to_update.add(last_module_occurrence)
else:
if should_add:
hashes[(module.repo, module.commit)].add(module)
update_commits = []
for (repo, old_hash), modules_to_check in hashes.items():
modified = await repo.get_modified_modules(old_hash, repo.commit)
for module in modules_to_check:
try:
index = modified.index(module)
except ValueError:
# module wasn't modified - we just need to update its commit
module.commit = repo.commit
update_commits.append(module)
else:
modified_module = modified[index]
if modified_module.type is InstallableType.COG:
if not modified_module.disabled:
cogs_to_update.add(modified_module)
elif modified_module.type is InstallableType.SHARED_LIBRARY:
libraries_to_update.add(modified_module)
await _save_to_installed(update_commits)
return (tuple(cogs_to_update), tuple(libraries_to_update))
async def _install_cogs(
cogs: Iterable[Installable],
) -> Tuple[Tuple[InstalledModule, ...], Tuple[Installable, ...]]:
"""Installs a list of cogs.
Parameters
----------
cogs : `list` of `Installable`
Cogs to install. ``repo`` property of those objects can't be `None`
Returns
-------
tuple
2-tuple of installed and failed cogs.
"""
repos: Dict[str, Tuple[Repo, Dict[str, List[Installable]]]] = {}
for cog in cogs:
try:
repo_by_commit = repos[cog.repo_name]
except KeyError:
cog.repo = cast(Repo, cog.repo) # docstring specifies this already
repo_by_commit = repos[cog.repo_name] = (cog.repo, defaultdict(list))
cogs_by_commit = repo_by_commit[1]
cogs_by_commit[cog.commit].append(cog)
installed = []
failed = []
for repo, cogs_by_commit in repos.values():
exit_to_commit = repo.commit
for commit, cogs_to_install in cogs_by_commit.items():
await repo.checkout(commit)
for cog in cogs_to_install:
if await cog.copy_to(await _cog_mgr.install_path()):
installed.append(InstalledModule.from_installable(cog))
else:
failed.append(cog)
await repo.checkout(exit_to_commit)
# noinspection PyTypeChecker
return (tuple(installed), tuple(failed))
async def _reinstall_libraries(
libraries: Iterable[Installable],
) -> Tuple[Tuple[InstalledModule, ...], Tuple[Installable, ...]]:
"""Installs a list of shared libraries, used when updating.
Parameters
----------
libraries : `list` of `Installable`
Libraries to reinstall. ``repo`` property of those objects can't be `None`
Returns
-------
tuple
2-tuple of installed and failed libraries.
"""
repos: Dict[str, Tuple[Repo, Dict[str, Set[Installable]]]] = {}
for lib in libraries:
try:
repo_by_commit = repos[lib.repo_name]
except KeyError:
lib.repo = cast(Repo, lib.repo) # docstring specifies this already
repo_by_commit = repos[lib.repo_name] = (lib.repo, defaultdict(set))
libs_by_commit = repo_by_commit[1]
libs_by_commit[lib.commit].add(lib)
all_installed: List[InstalledModule] = []
all_failed: List[Installable] = []
for repo, libs_by_commit in repos.values():
exit_to_commit = repo.commit
for commit, libs in libs_by_commit.items():
await repo.checkout(commit)
installed, failed = await repo.install_libraries(
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH, libraries=libs
)
all_installed += installed
all_failed += failed
await repo.checkout(exit_to_commit)
# noinspection PyTypeChecker
return (tuple(all_installed), tuple(all_failed))
async def _install_requirements(cogs: Iterable[Installable]) -> Tuple[str, ...]:
"""
Installs requirements for given cogs.
Parameters
----------
cogs : `list` of `Installable`
Cogs whose requirements should be installed.
Returns
-------
tuple
Tuple of failed requirements.
"""
# Reduces requirements to a single list with no repeats
requirements = {requirement for cog in cogs for requirement in cog.requirements}
repos: List[Tuple[Repo, List[str]]] = [(repo, []) for repo in _repo_manager.repos]
# This for loop distributes the requirements across all repos
# which will allow us to concurrently install requirements
for i, req in enumerate(requirements):
repo_index = i % len(repos)
repos[repo_index][1].append(req)
has_reqs = list(filter(lambda item: len(item[1]) > 0, repos))
failed_reqs = []
for repo, reqs in has_reqs:
for req in reqs:
if not await repo.install_raw_requirements([req], LIB_PATH):
failed_reqs.append(req)
return tuple(failed_reqs)
async def _delete_cog(target: Path) -> None:
"""
Removes an (installed) cog.
:param target: Path pointing to an existing file or directory
:return:
"""
if not target.exists():
return
if target.is_dir():
shutil.rmtree(str(target))
elif target.is_file():
os.remove(str(target))
async def _get_cogs_to_check(
*,
repos: Optional[Iterable[Repo]] = None,
cogs: Optional[Iterable[InstalledModule]] = None,
update_repos: bool = True,
) -> Tuple[Set[InstalledModule], List[str]]:
failed: List[str] = []
if not (cogs or repos):
if update_repos:
__, failed = await _repo_manager.update_repos()
cogs_to_check = {
cog
for cog in await installed_cogs()
if cog.repo is not None and cog.repo.name not in failed
}
else:
# this is enough to be sure that `cogs` is not None (based on if above)
if not repos:
cogs = cast(Iterable[InstalledModule], cogs)
repos = {cog.repo for cog in cogs if cog.repo is not None}
if update_repos:
__, failed = await _repo_manager.update_repos(repos)
if failed:
# remove failed repos
repos = {repo for repo in repos if repo.name not in failed}
if cogs:
cogs_to_check = {cog for cog in cogs if cog.repo is not None and cog.repo in repos}
else:
cogs_to_check = {
cog for cog in await installed_cogs() if cog.repo is not None and cog.repo in repos
}
return (cogs_to_check, failed)
# functionality extracted from command implementations
# TODO: make them into nice APIs instead of what they are now...
async def pip_install(*deps: str) -> bool:
repo = Repo("", "", "", "", Path.cwd())
return await repo.install_raw_requirements(deps, LIB_PATH)
async def reinstall_requirements() -> Tuple[Tuple[str, ...], Tuple[Installable, ...]]:
_create_lib_folder(remove_first=True)
_installed_cogs = await installed_cogs()
cogs = []
repos = set()
for cog in _installed_cogs:
if cog.repo is None:
continue
repos.add(cog.repo)
cogs.append(cog)
failed_reqs = await _install_requirements(cogs)
all_installed_libs: List[InstalledModule] = []
all_failed_libs: List[Installable] = []
for repo in repos:
installed_libs, failed_libs = await repo.install_libraries(
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH
)
all_installed_libs += installed_libs
all_failed_libs += failed_libs
return failed_reqs, tuple(all_failed_libs)
async def install_cogs(
repo: Repo, rev: Optional[str], cog_names: Iterable[str]
) -> CogInstallResult:
commit = None
if rev is not None:
# raises errors.AmbiguousRevision and errors.UnknownRevision
commit = await repo.get_full_sha1(rev)
cog_names = set(cog_names)
_installed_cogs = await installed_cogs()
cogs: List[Installable] = []
unavailable_cogs: List[str] = []
already_installed: List[Installable] = []
name_already_used: List[Installable] = []
incompatible_python_version: List[Installable] = []
incompatible_bot_version: List[Installable] = []
result_installed_cogs: Tuple[InstalledModule, ...] = ()
result_failed_cogs: Tuple[Installable, ...] = ()
result_failed_reqs: Tuple[str, ...] = ()
result_installed_libs: Tuple[InstalledModule, ...] = ()
result_failed_libs: Tuple[Installable, ...] = ()
async with repo.checkout(commit, exit_to_rev=repo.branch):
for cog_name in cog_names:
cog: Optional[Installable] = discord.utils.get(repo.available_cogs, name=cog_name)
if cog is None:
unavailable_cogs.append(cog_name)
elif cog in _installed_cogs:
already_installed.append(cog)
elif discord.utils.get(_installed_cogs, name=cog.name):
name_already_used.append(cog)
elif cog.min_python_version > sys.version_info:
incompatible_python_version.append(cog)
elif cog.min_bot_version > red_version_info or (
# max version should be ignored when it's lower than min version
cog.min_bot_version <= cog.max_bot_version
and cog.max_bot_version < red_version_info
):
incompatible_bot_version.append(cog)
else:
cogs.append(cog)
if cogs:
result_failed_reqs = await _install_requirements(cogs)
if not result_failed_reqs:
result_installed_cogs, result_failed_cogs = await _install_cogs(cogs)
if cogs and not result_failed_reqs:
result_installed_libs, result_failed_libs = await repo.install_libraries(
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH
)
if rev is not None:
for cog in result_installed_cogs:
cog.pinned = True
await _save_to_installed(result_installed_cogs + result_installed_libs)
return CogInstallResult(
installed_cogs=result_installed_cogs,
installed_libs=result_installed_libs,
failed_cogs=result_failed_cogs,
failed_libs=result_failed_libs,
failed_reqs=result_failed_reqs,
unavailable_cogs=tuple(unavailable_cogs),
already_installed=tuple(already_installed),
name_already_used=tuple(name_already_used),
incompatible_python_version=tuple(incompatible_python_version),
incompatible_bot_version=tuple(incompatible_bot_version),
)
async def uninstall_cogs(*cogs: InstalledModule) -> tuple[list[str], list[str]]:
uninstalled_cogs = []
failed_cogs = []
for cog in set(cogs):
real_name = cog.name
poss_installed_path = (await _cog_mgr.install_path()) / real_name
if poss_installed_path.exists():
if _bot_ref is not None:
with contextlib.suppress(commands.ExtensionNotLoaded):
await _bot_ref.unload_extension(real_name)
await _bot_ref.remove_loaded_package(real_name)
await _delete_cog(poss_installed_path)
uninstalled_cogs.append(real_name)
else:
failed_cogs.append(real_name)
await _remove_from_installed(cogs)
return uninstalled_cogs, failed_cogs
async def check_cog_updates(
*,
repos: Optional[Iterable[Repo]] = None,
cogs: Optional[Iterable[InstalledModule]] = None,
update_repos: bool = True,
) -> CogUpdateCheckResult:
cogs_to_check, failed_repos = await _get_cogs_to_check(
repos=repos, cogs=cogs, update_repos=update_repos
)
outdated_cogs, outdated_libs = await _available_updates(cogs_to_check)
updatable_cogs: List[Installable] = []
incompatible_python_version: List[Installable] = []
incompatible_bot_version: List[Installable] = []
for cog in outdated_cogs:
if cog.min_python_version > sys.version_info:
incompatible_python_version.append(cog)
elif cog.min_bot_version > red_version_info or (
# max version should be ignored when it's lower than min version
cog.min_bot_version <= cog.max_bot_version
and cog.max_bot_version < red_version_info
):
incompatible_bot_version.append(cog)
else:
updatable_cogs.append(cog)
return CogUpdateCheckResult(
outdated_cogs=outdated_cogs,
outdated_libs=outdated_libs,
updatable_cogs=tuple(updatable_cogs),
failed_repos=tuple(failed_repos),
incompatible_python_version=tuple(incompatible_python_version),
incompatible_bot_version=tuple(incompatible_bot_version),
)
# update given cogs or all cogs
async def update_cogs(
*, cogs: Optional[List[InstalledModule]] = None, repos: Optional[List[Repo]] = None
) -> CogUpdateResult:
if cogs is not None and repos is not None:
raise ValueError("You can specify cogs or repos argument, not both")
cogs_to_check, failed_repos = await _get_cogs_to_check(repos=repos, cogs=cogs)
return await _update_cogs(cogs_to_check, failed_repos=failed_repos)
# update given cogs or all cogs from the specified repo
# using the specified revision (or latest if not specified)
async def update_repo_cogs(
repo: Repo, cogs: Optional[List[InstalledModule]] = None, *, rev: Optional[str] = None
) -> CogUpdateResult:
try:
await repo.update()
except errors.UpdateError:
return await _update_cogs(set(), failed_repos=(repo.name,))
# TODO: should this be set to `repo.branch` when `rev` is None?
commit = None
if rev is not None:
# raises errors.AmbiguousRevision and errors.UnknownRevision
commit = await repo.get_full_sha1(rev)
async with repo.checkout(commit, exit_to_rev=repo.branch):
cogs_to_check, __ = await _get_cogs_to_check(repos=[repo], cogs=cogs, update_repos=False)
return await _update_cogs(cogs_to_check, failed_repos=())
async def _update_cogs(
cogs_to_check: Set[InstalledModule], *, failed_repos: Sequence[str]
) -> CogUpdateResult:
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
cogs_to_check -= pinned_cogs
outdated_cogs: Tuple[Installable, ...] = ()
outdated_libs: Tuple[Installable, ...] = ()
updatable_cogs: List[Installable] = []
incompatible_python_version: List[Installable] = []
incompatible_bot_version: List[Installable] = []
updated_cogs: Tuple[InstalledModule, ...] = ()
failed_cogs: Tuple[Installable, ...] = ()
failed_reqs: Tuple[str, ...] = ()
updated_libs: Tuple[InstalledModule, ...] = ()
failed_libs: Tuple[Installable, ...] = ()
if cogs_to_check:
outdated_cogs, outdated_libs = await _available_updates(cogs_to_check)
for cog in outdated_cogs:
if cog.min_python_version > sys.version_info:
incompatible_python_version.append(cog)
elif cog.min_bot_version > red_version_info or (
# max version should be ignored when it's lower than min version
cog.min_bot_version <= cog.max_bot_version
and cog.max_bot_version < red_version_info
):
incompatible_bot_version.append(cog)
else:
updatable_cogs.append(cog)
if updatable_cogs or outdated_libs:
failed_reqs = await _install_requirements(updatable_cogs)
if not failed_reqs:
updated_cogs, failed_cogs = await _install_cogs(updatable_cogs)
updated_libs, failed_libs = await _reinstall_libraries(outdated_libs)
await _save_to_installed(updated_cogs + updated_libs)
return CogUpdateResult(
checked_cogs=frozenset(cogs_to_check),
pinned_cogs=frozenset(pinned_cogs),
updated_cogs=updated_cogs,
updated_libs=updated_libs,
failed_cogs=failed_cogs,
failed_libs=failed_libs,
failed_reqs=failed_reqs,
outdated_cogs=outdated_cogs,
outdated_libs=outdated_libs,
updatable_cogs=tuple(updatable_cogs),
failed_repos=tuple(failed_repos),
incompatible_python_version=tuple(incompatible_python_version),
incompatible_bot_version=tuple(incompatible_bot_version),
)
async def pin_cogs(
*cogs: InstalledModule,
) -> tuple[tuple[InstalledModule, ...], tuple[InstalledModule, ...]]:
already_pinned = []
pinned = []
for cog in set(cogs):
if cog.pinned:
already_pinned.append(cog)
continue
cog.pinned = True
pinned.append(cog)
if pinned:
await _save_to_installed(pinned)
return tuple(pinned), tuple(already_pinned)
async def unpin_cogs(
*cogs: InstalledModule,
) -> tuple[tuple[InstalledModule, ...], tuple[InstalledModule, ...]]:
not_pinned = []
unpinned = []
for cog in set(cogs):
if not cog.pinned:
not_pinned.append(cog)
continue
cog.pinned = False
unpinned.append(cog)
if unpinned:
await _save_to_installed(unpinned)
return tuple(unpinned), tuple(not_pinned)
# TODO: make kw_only
@dataclasses.dataclass
class CogInstallResult:
installed_cogs: Tuple[InstalledModule, ...]
installed_libs: Tuple[InstalledModule, ...]
failed_cogs: Tuple[Installable, ...]
failed_libs: Tuple[Installable, ...]
failed_reqs: Tuple[str, ...]
unavailable_cogs: Tuple[str, ...]
already_installed: Tuple[Installable, ...]
name_already_used: Tuple[Installable, ...]
incompatible_python_version: Tuple[Installable, ...]
incompatible_bot_version: Tuple[Installable, ...]
# TODO: make kw_only
@dataclasses.dataclass
class CogUpdateCheckResult:
outdated_cogs: Tuple[Installable, ...]
outdated_libs: Tuple[Installable, ...]
updatable_cogs: Tuple[Installable, ...]
failed_repos: Tuple[str, ...]
incompatible_python_version: Tuple[Installable, ...]
incompatible_bot_version: Tuple[Installable, ...]
@property
def updates_available(self) -> bool:
return bool(self.outdated_cogs or self.outdated_libs)
@property
def updates_installable(self) -> bool:
return bool(self.updatable_cogs or self.outdated_libs)
@property
def incompatible_cogs(self) -> Tuple[Installable, ...]:
return self.incompatible_python_version + self.incompatible_bot_version
# TODO: make kw_only
@dataclasses.dataclass
class CogUpdateResult(CogUpdateCheckResult):
# checked_cogs contains old modules, before update
checked_cogs: FrozenSet[InstalledModule]
pinned_cogs: FrozenSet[InstalledModule]
updated_cogs: Tuple[InstalledModule, ...]
updated_libs: Tuple[InstalledModule, ...]
failed_cogs: Tuple[Installable, ...]
failed_libs: Tuple[Installable, ...]
failed_reqs: Tuple[str, ...]
@property
def updated_modules(self) -> Tuple[InstalledModule, ...]:
return self.updated_cogs + self.updated_libs
class CogUnavailableError(Exception):
def __init__(self, repo_name: str, cog_name: str) -> None:
self.repo_name = repo_name
self.cog_name = cog_name
super().__init__(f"Couldn't find cog {cog_name!r} in {repo_name!r}")
@@ -2,7 +2,7 @@ from __future__ import annotations
import functools
import shutil
from enum import IntEnum
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Union, cast
@@ -16,8 +16,7 @@ if TYPE_CHECKING:
from .repo_manager import RepoManager, Repo
class InstallableType(IntEnum):
# using IntEnum, because hot-reload breaks its identity
class InstallableType(Enum):
UNKNOWN = 0
COG = 1
SHARED_LIBRARY = 2
@@ -139,7 +138,7 @@ class Installable(RepoJSONMixin):
super()._read_info_file()
update_mixin(self, INSTALLABLE_SCHEMA)
if self.type == InstallableType.SHARED_LIBRARY:
if self.type is InstallableType.SHARED_LIBRARY:
self.hidden = True
@@ -163,7 +162,7 @@ class InstalledModule(Installable):
json_repo_name: str = "",
):
super().__init__(location=location, repo=repo, commit=commit)
self.pinned: bool = pinned if self.type == InstallableType.COG else False
self.pinned: bool = pinned if self.type is InstallableType.COG else False
# this is here so that Downloader could use real repo name instead of "MISSING_REPO"
self._json_repo_name = json_repo_name
@@ -173,7 +172,7 @@ class InstalledModule(Installable):
"module_name": self.name,
"commit": self.commit,
}
if self.type == InstallableType.COG:
if self.type is InstallableType.COG:
module_json["pinned"] = self.pinned
return module_json
+3
View File
@@ -0,0 +1,3 @@
import logging
log = logging.getLogger("red.core.downloader")
File diff suppressed because it is too large Load Diff
+23 -11
View File
@@ -176,14 +176,15 @@ def init_events(bot, cli_flags):
if bot.intents.members: # Lets avoid 0 Unique Users
table_counts.add_row("Unique Users", str(users))
outdated_red_message = ""
rich_outdated_message = ""
pypi_version, py_version_req = await fetch_latest_red_version_info()
outdated = pypi_version and pypi_version > red_version_info
if outdated:
outdated_red_message, rich_outdated_message = get_outdated_red_messages(
pypi_version, py_version_req
)
fetch_version_task = asyncio.create_task(fetch_latest_red_version_info())
log.info("Fetching information about latest Red version...")
try:
await asyncio.wait_for(asyncio.shield(fetch_version_task), timeout=5)
except asyncio.TimeoutError:
log.info("Version information will continue to be fetched in the background...")
except Exception:
# these will be logged later
pass
rich_console = rich.get_console()
rich_console.print(INTRO, style="red", markup=False, highlight=False)
@@ -209,11 +210,22 @@ def init_events(bot, cli_flags):
rich_console.print(
f"Looking for a quick guide on setting up Red? Checkout {Text('https://start.discord.red', style='link https://start.discord.red}')}"
)
if rich_outdated_message:
rich_console.print(rich_outdated_message)
bot._red_ready.set()
if outdated_red_message:
try:
pypi_version, py_version_req = await fetch_version_task
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
log.error("Failed to fetch latest version information from PyPI.", exc_info=exc)
except (KeyError, ValueError) as exc:
log.error("Failed to parse version metadata received from PyPI.", exc_info=exc)
else:
outdated = pypi_version and pypi_version > red_version_info
if outdated:
outdated_red_message, rich_outdated_message = get_outdated_red_messages(
pypi_version, py_version_req
)
rich_console.print(rich_outdated_message)
await send_to_owners_with_prefix_replaced(bot, outdated_red_message)
@bot.event
-3
View File
@@ -97,9 +97,6 @@ class RPC:
self._runner,
host="127.0.0.1",
port=port,
shutdown_timeout=120
# Give the RPC server 2 minutes to finish up, else slap it!
# Seems like a reasonable time. See Red#6391
),
)
except Exception as exc:
+53 -8
View File
@@ -1,4 +1,5 @@
from __future__ import annotations
import argparse
import asyncio
import inspect
import logging
@@ -37,7 +38,18 @@ import discord
from discord.ext import commands as dpy_commands
from discord.ext.commands import when_mentioned_or
from . import Config, _i18n, i18n, app_commands, commands, errors, _drivers, modlog, bank
from . import (
Config,
_i18n,
i18n,
app_commands,
commands,
errors,
_drivers,
modlog,
bank,
_downloader,
)
from ._cli import ExitCodes
from ._cog_manager import CogManager, CogManagerUI
from .core_commands import Core
@@ -69,6 +81,8 @@ CUSTOM_GROUPS = "CUSTOM_GROUPS"
COMMAND_SCOPE = "COMMAND"
SHARED_API_TOKENS = "SHARED_API_TOKENS"
_DEFAULT_DESCRIPTION = "Red V3"
log = logging.getLogger("red")
__all__ = ("Red",)
@@ -101,7 +115,9 @@ class Red(
): # pylint: disable=no-member # barely spurious warning caused by shadowing
"""Our subclass of discord.ext.commands.AutoShardedBot"""
def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
def __init__(
self, *args: Any, cli_flags: argparse.Namespace, bot_dir: Path = Path.cwd(), **kwargs: Any
) -> None:
self._shutdown_mode = ExitCodes.CRITICAL
self._cli_flags = cli_flags
self._config = Config.get_core_conf(force_registration=False)
@@ -132,7 +148,7 @@ class Red(
help__tagline="",
help__use_tick=False,
help__react_timeout=30,
description="Red V3",
description=_DEFAULT_DESCRIPTION,
invite_public=False,
invite_perm=0,
invite_commands_scope=False,
@@ -141,6 +157,7 @@ class Red(
invoke_error_msg=None,
extra_owner_destinations=[],
owner_opt_out_list=[],
last_system_info__python_prefix=None,
last_system_info__python_version=[3, 7],
last_system_info__machine=None,
last_system_info__system=None,
@@ -238,7 +255,13 @@ class Red(
self._main_dir = bot_dir
self._cog_mgr = CogManager()
self._use_team_features = cli_flags.use_team_features
super().__init__(*args, help_command=None, tree_cls=RedTree, **kwargs)
super().__init__(
*args,
description=kwargs.pop("description", _DEFAULT_DESCRIPTION),
help_command=None,
tree_cls=RedTree,
**kwargs,
)
# Do not manually use the help formatter attribute here, see `send_help_for`,
# for a documented API. The internals of this object are still subject to change.
self._help_formatter = commands.help.RedHelpFormatter()
@@ -1198,14 +1221,35 @@ class Red(
last_system_info = await self._config.last_system_info()
last_python_prefix = last_system_info["python_prefix"]
if last_python_prefix is None:
await self._config.last_system_info.python_prefix.set(sys.prefix)
elif last_python_prefix != sys.prefix:
await self._config.last_system_info.python_prefix.set(sys.prefix)
try:
same_install = os.path.samefile(last_python_prefix, sys.prefix)
except OSError:
same_install = False
if not same_install:
if sys.prefix != sys.base_prefix:
install_info = "in the currently used virtual environment"
else:
install_info = "with the currently used Python installation"
log.warning(
"Red seems to have been started with a different Python installation"
" and/or virtual environment. This is not, in itself, an issue but is often"
" done unintentionally and may explain some, otherwise unexpected, behavior."
" This message will not be shown again, if you start Red %s again.",
install_info,
)
ver_info = list(sys.version_info[:2])
python_version_changed = False
LIB_PATH = cog_data_path(raw_name="Downloader") / "lib"
if ver_info != last_system_info["python_version"]:
await self._config.last_system_info.python_version.set(ver_info)
if any(LIB_PATH.iterdir()):
shutil.rmtree(str(LIB_PATH))
LIB_PATH.mkdir()
if any(_downloader.LIB_PATH.iterdir()):
shutil.rmtree(str(_downloader.LIB_PATH))
_downloader.LIB_PATH.mkdir()
asyncio.create_task(
send_to_owners_with_prefix_replaced(
self,
@@ -2502,6 +2546,7 @@ class Red(
n_remaining = len(messages) - idx
files_perm = (
isinstance(channel, discord.abc.User)
or channel.guild is None
or channel.permissions_for(channel.guild.me).attach_files
)
options = ("more", "file") if files_perm else ("more",)
+7 -6
View File
@@ -49,6 +49,7 @@ from . import (
i18n,
bank,
modlog,
_downloader,
)
from ._diagnoser import IssueDiagnoser
from .utils import AsyncIter, can_user_send_messages_in
@@ -215,12 +216,8 @@ class CoreLogic:
else:
await bot.add_loaded_package(name)
loaded_packages.append(name)
# remove in Red 3.4
downloader = bot.get_cog("Downloader")
if downloader is None:
continue
try:
maybe_repo = await downloader._shared_lib_load_check(name)
maybe_repo = await _downloader._shared_lib_load_check(name)
except Exception:
log.exception(
"Shared library check failed,"
@@ -424,7 +421,11 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
owner = app_info.owner
custom_info = await self.bot._config.custom_info()
pypi_version, py_version_req = await fetch_latest_red_version_info()
try:
pypi_version, __ = await fetch_latest_red_version_info()
except (aiohttp.ClientError, TimeoutError) as exc:
log.error("Failed to fetch latest version information from PyPI.", exc_info=exc)
pypi_version = None
outdated = pypi_version and pypi_version > red_version_info
if embed_links:
+1 -1
View File
@@ -158,7 +158,7 @@ class DevOutput:
output.append(self.formatted_exc)
elif self.always_include_result or self.result is not None:
try:
result = str(self.result)
result = str(self.result) if isinstance(self.result, str) else repr(self.result)
# ensure that the result can be encoded (GH-6485)
result.encode("utf-8")
except Exception as exc:
+604 -604
View File
File diff suppressed because it is too large Load Diff
+607 -607
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+607 -607
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+607 -607
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+607 -607
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+607 -607
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+1444 -897
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+607 -607
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+607 -607
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+604 -604
View File
File diff suppressed because it is too large Load Diff
+17 -6
View File
@@ -236,7 +236,7 @@ async def create_backup(dest: Path = Path.home()) -> Optional[Path]:
]
# Avoiding circular imports
from ...cogs.downloader.repo_manager import RepoManager
from redbot.core._downloader.repo_manager import RepoManager
repo_mgr = RepoManager()
await repo_mgr.initialize()
@@ -326,14 +326,25 @@ def expected_version(current: str, expected: str) -> bool:
return Requirement(f"x{expected}").specifier.contains(current, prereleases=True)
async def fetch_latest_red_version_info() -> Tuple[Optional[VersionInfo], Optional[str]]:
try:
async def fetch_latest_red_version_info() -> Tuple[VersionInfo, Optional[str]]:
"""
Fetch information about latest Red release on PyPI.
Raises
------
aiohttp.ClientError
An error occurred during request to PyPI.
TimeoutError
The request to PyPI timed out.
ValueError
An invalid version string was returned in PyPI metadata.
KeyError
The PyPI metadata is missing some of the required information.
"""
async with aiohttp.ClientSession() as session:
async with session.get("https://pypi.org/pypi/Red-DiscordBot/json") as r:
data = await r.json()
except (aiohttp.ClientError, asyncio.TimeoutError):
return None, None
else:
release = VersionInfo.from_str(data["info"]["version"])
required_python = data["info"]["requires_python"]
+33 -17
View File
@@ -282,7 +282,20 @@ class RedRichHandler(RichHandler):
self.console.print(traceback)
def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespace) -> None:
_FILE_FORMATTER = logging.Formatter(
"[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
)
def init_logging(
level: int,
*,
location: Optional[pathlib.Path] = None,
rich_logging: Optional[bool] = None,
rich_tracebacks: bool = False,
rich_traceback_extra_lines: int = 0,
rich_traceback_show_locals: bool = False,
) -> None:
root_logger = logging.getLogger()
root_logger.setLevel(level)
# DEBUG logging for discord.py is a bit too ridiculous :)
@@ -312,24 +325,21 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
enable_rich_logging = False
if isatty(0) and cli_flags.rich_logging is None:
if isatty(0) and rich_logging is None:
# Check if the bot thinks it has a active terminal.
enable_rich_logging = True
elif cli_flags.rich_logging is True:
elif rich_logging is True:
enable_rich_logging = True
file_formatter = logging.Formatter(
"[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
)
if enable_rich_logging is True:
rich_formatter = logging.Formatter("{message}", datefmt="[%X]", style="{")
stdout_handler = RedRichHandler(
rich_tracebacks=cli_flags.rich_tracebacks,
rich_tracebacks=rich_tracebacks,
show_path=False,
highlighter=NullHighlighter(),
tracebacks_extra_lines=cli_flags.rich_traceback_extra_lines,
tracebacks_show_locals=cli_flags.rich_traceback_show_locals,
tracebacks_extra_lines=rich_traceback_extra_lines,
tracebacks_show_locals=rich_traceback_show_locals,
tracebacks_theme=(
PygmentsSyntaxTheme(FixedMonokaiStyle)
if rich_console.color_system == "truecolor"
@@ -339,11 +349,22 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
stdout_handler.setFormatter(rich_formatter)
else:
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(file_formatter)
stdout_handler.setFormatter(_FILE_FORMATTER)
root_logger.addHandler(stdout_handler)
logging.captureWarnings(True)
if location is not None:
init_file_logging(location)
if not enable_rich_logging and rich_tracebacks:
log.warning(
"Rich tracebacks were requested but they will not be enabled"
" as Rich logging is not active."
)
def init_file_logging(location: pathlib.Path) -> None:
if not location.exists():
location.mkdir(parents=True, exist_ok=True)
# Rotate latest logs to previous logs
@@ -379,12 +400,7 @@ def init_logging(level: int, location: pathlib.Path, cli_flags: argparse.Namespa
encoding="utf-8",
)
root_logger = logging.getLogger()
for fhandler in (latest_fhandler, all_fhandler):
fhandler.setFormatter(file_formatter)
fhandler.setFormatter(_FILE_FORMATTER)
root_logger.addHandler(fhandler)
if not enable_rich_logging and cli_flags.rich_tracebacks:
log.warning(
"Rich tracebacks were requested but they will not be enabled"
" as Rich logging is not active."
)
+1 -3
View File
@@ -172,11 +172,9 @@ def red(config_fr):
cli_flags = parse_cli_flags(["ignore_me"])
description = "Red v3 - Alpha"
Config.get_core_conf = lambda *args, **kwargs: config_fr
red = Red(cli_flags=cli_flags, description=description, dm_help=None, owner_ids=set())
red = Red(cli_flags=cli_flags)
yield red
+2 -2
View File
@@ -6,8 +6,8 @@ import shutil
import pytest
from redbot.cogs.downloader.repo_manager import RepoManager, Repo, ProcessFormatter
from redbot.cogs.downloader.installable import Installable, InstalledModule
from redbot.core._downloader.repo_manager import RepoManager, Repo, ProcessFormatter
from redbot.core._downloader.installable import Installable, InstalledModule
__all__ = [
"GIT_VERSION",
+43 -45
View File
@@ -14,6 +14,7 @@ from typing import Dict, Any, Optional, Union
import click
import redbot.logging
from redbot.core._cli import confirm
from redbot.core.utils._internal_utils import (
safe_delete,
@@ -22,7 +23,7 @@ from redbot.core.utils._internal_utils import (
)
from redbot.core import config, data_manager
from redbot.core._config import migrate
from redbot.core._cli import ExitCodes
from redbot.core._cli import ExitCodes, asyncio_run
from redbot.core.data_manager import appdir, config_dir, config_file
from redbot.core._drivers import (
BackendType,
@@ -60,9 +61,9 @@ def save_config(name, data, remove=False):
def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive: bool) -> str:
if data_path is not None:
return str(data_path.resolve())
data_path = Path(appdir.user_data_dir) / "data" / instance_name
default_data_path = Path(appdir.user_data_dir) / "data" / instance_name
if not interactive:
return str(data_path.resolve())
return str(default_data_path.resolve())
print(
"We've attempted to figure out a sane default data location which is printed below."
@@ -70,12 +71,15 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
" otherwise input your desired data location."
)
print()
print("Default: {}".format(data_path))
print(f"Default: {default_data_path}")
while True:
data_path_input = input("> ")
if data_path_input != "":
data_path = Path(data_path_input)
else:
data_path = default_data_path
try:
exists = data_path.exists()
@@ -84,7 +88,7 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
"We were unable to check your chosen directory."
" Provided path may contain an invalid character."
)
sys.exit(ExitCodes.INVALID_CLI_USAGE)
continue
if not exists:
try:
@@ -97,10 +101,10 @@ def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive:
)
sys.exit(ExitCodes.INVALID_CLI_USAGE)
print("You have chosen {} to be your data directory.".format(data_path))
if not click.confirm("Please confirm", default=True):
print("Please start the process over.")
sys.exit(ExitCodes.CRITICAL)
print(f"You have chosen {str(data_path)!r} to be your data directory.")
if click.confirm("Please confirm", default=True):
break
return str(data_path.resolve())
@@ -131,8 +135,7 @@ def get_storage_type(backend: Optional[str], *, interactive: bool):
return storage_dict[storage]
def get_name(name: str) -> str:
INSTANCE_NAME_RE = re.compile(
INSTANCE_NAME_RE = re.compile(
r"""
[a-z0-9] # starts with letter or digit
(?:
@@ -142,7 +145,10 @@ def get_name(name: str) -> str:
)? # optional to allow strings of length 1
""",
re.VERBOSE | re.IGNORECASE,
)
)
def get_name(name: str = "", *, confirm_overwrite: bool = False) -> str:
if name:
if INSTANCE_NAME_RE.fullmatch(name) is None:
print(
@@ -151,9 +157,17 @@ def get_name(name: str) -> str:
" and non-consecutive underscores (_) and periods (.)."
)
sys.exit(ExitCodes.INVALID_CLI_USAGE)
if name in instance_data and not confirm_overwrite:
print(
"An instance with this name already exists.\n"
"If you want to remove the existing instance and replace it with this one,"
" run this command with --overwrite-existing-instance flag."
)
sys.exit(ExitCodes.INVALID_CLI_USAGE)
return name
while len(name) == 0:
name = ""
while not name:
print(
"Please enter a name for your instance,"
" it will be used to run your bot from here on out.\n"
@@ -176,6 +190,16 @@ def get_name(name: str) -> str:
default=False,
):
name = ""
elif name in instance_data and not confirm_overwrite:
print(
"WARNING: An instance already exists with this name."
" Continuing will overwrite the existing instance config."
)
if not click.confirm(
"Are you absolutely certain you want to continue with this instance name?",
default=False,
):
name = ""
print() # new line for aesthetics
return name
@@ -205,7 +229,7 @@ def basic_setup(
"Hello! Before we begin, we need to gather some initial information"
" for the new instance."
)
name = get_name(name)
name = get_name(name, confirm_overwrite=overwrite_existing_instance)
default_data_dir = get_data_dir(
instance_name=name, data_path=data_path, interactive=interactive
@@ -220,26 +244,6 @@ def basic_setup(
driver_cls = get_driver_class(storage_type)
default_dirs["STORAGE_DETAILS"] = driver_cls.get_config_details()
if name in instance_data:
if overwrite_existing_instance:
pass
elif interactive:
print(
"WARNING: An instance already exists with this name. "
"Continuing will overwrite the existing instance config."
)
if not click.confirm(
"Are you absolutely certain you want to continue?", default=False
):
print("Not continuing")
sys.exit(ExitCodes.SHUTDOWN)
else:
print(
"An instance with this name already exists.\n"
"If you want to remove the existing instance and replace it with this one,"
" run this command with --overwrite-existing-instance flag."
)
sys.exit(ExitCodes.INVALID_CLI_USAGE)
save_config(name, default_dirs)
if interactive:
@@ -436,15 +440,9 @@ def cli(
overwrite_existing_instance: bool,
) -> None:
"""Create a new instance."""
level = cli_level_to_log_level(debug)
base_logger = logging.getLogger("red")
base_logger.setLevel(level)
formatter = logging.Formatter(
"[{asctime}] [{levelname}] {name}: {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{"
)
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
base_logger.addHandler(stdout_handler)
redbot.logging.init_logging(level)
if ctx.invoked_subcommand is None:
basic_setup(
@@ -514,7 +512,7 @@ def delete(
remove_datapath: Optional[bool],
) -> None:
"""Removes an instance."""
asyncio.run(
asyncio_run(
remove_instance(
instance, interactive, delete_data, _create_backup, drop_db, remove_datapath
)
@@ -536,7 +534,7 @@ def convert(instance: str, backend: str) -> None:
if current_backend == BackendType.MONGOV1:
raise RuntimeError("Please see the 3.2 release notes for upgrading a bot using mongo.")
else:
new_storage_details = asyncio.run(do_migration(current_backend, target))
new_storage_details = asyncio_run(do_migration(current_backend, target))
if new_storage_details is not None:
default_dirs["STORAGE_TYPE"] = target.value
@@ -560,7 +558,7 @@ def convert(instance: str, backend: str) -> None:
)
def backup(instance: str, destination_folder: Path) -> None:
"""Backup instance's data."""
asyncio.run(create_backup(instance, destination_folder))
asyncio_run(create_backup(instance, destination_folder))
def run_cli():
+1 -1
View File
@@ -19,7 +19,7 @@ typing_extensions
yarl
distro; sys_platform == "linux"
# https://github.com/MagicStack/uvloop/issues/702
uvloop>=0.21.0,!=0.22.0,!=0.22.1; sys_platform != "win32" and platform_python_implementation == "CPython"
uvloop; sys_platform != "win32" and platform_python_implementation == "CPython"
# Used by discord.py[speedup]. See Pull request #6587 for more info.
Brotli
+1 -1
View File
@@ -88,7 +88,7 @@ importlib-metadata==8.5.0; python_version != "3.10" and python_version != "3.11"
# via markdown
pytz==2026.1.post1; python_version == "3.8"
# via babel
uvloop==0.21.0; (sys_platform != "win32" and platform_python_implementation == "CPython") and sys_platform != "win32"
uvloop==0.22.1; (sys_platform != "win32" and platform_python_implementation == "CPython") and sys_platform != "win32"
# via -r base.in
zipp==3.20.2; python_version != "3.10" and python_version != "3.11"
# via importlib-metadata
+1
View File
@@ -1,6 +1,7 @@
-c base.txt
Sphinx
sphinx-markdown-builder>=0.6.10
sphinx-prompt
sphinx_rtd_theme>1
sphinxcontrib-trio
+4
View File
@@ -34,6 +34,8 @@ sphinx==7.1.2
# sphinx-rtd-theme
# sphinxcontrib-jquery
# sphinxcontrib-trio
sphinx-markdown-builder==0.6.10
# via -r extra-doc.in
sphinx-prompt==1.7.0
# via -r extra-doc.in
sphinx-rtd-theme==3.1.0
@@ -54,6 +56,8 @@ sphinxcontrib-serializinghtml==1.1.5
# via sphinx
sphinxcontrib-trio==1.2.0
# via -r extra-doc.in
tabulate==0.9.0
# via sphinx-markdown-builder
urllib3==2.2.3
# via requests
zipp==3.20.2
+2 -4
View File
@@ -3,16 +3,14 @@ import os
import pytest
from redbot import _update_event_loop_policy
from redbot.core import _drivers, data_manager
_update_event_loop_policy()
from redbot.core._cli import new_event_loop
@pytest.fixture(scope="session")
def event_loop(request):
"""Create an instance of the default event loop for entire session."""
loop = asyncio.new_event_loop()
loop = new_event_loop()
asyncio.set_event_loop(loop)
yield loop
asyncio.set_event_loop(None)
@@ -9,9 +9,9 @@ from pytest_mock import MockFixture
from redbot.pytest.downloader import *
from redbot.cogs.downloader.repo_manager import Installable
from redbot.cogs.downloader.repo_manager import Candidate, ProcessFormatter, RepoManager, Repo
from redbot.cogs.downloader.errors import (
from redbot.core._downloader.repo_manager import Installable
from redbot.core._downloader.repo_manager import Candidate, ProcessFormatter, RepoManager, Repo
from redbot.core._downloader.errors import (
AmbiguousRevision,
ExistingGitRepo,
GitException,
@@ -322,9 +322,9 @@ async def test_update(mocker, repo):
async def test_add_repo(monkeypatch, repo_manager):
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
monkeypatch.setattr(
"redbot.cogs.downloader.repo_manager.Repo.current_commit", fake_current_commit
"redbot.core._downloader.repo_manager.Repo.current_commit", fake_current_commit
)
squid = await repo_manager.add_repo(
@@ -335,9 +335,9 @@ async def test_add_repo(monkeypatch, repo_manager):
async def test_lib_install_requirements(monkeypatch, library_installable, repo, tmpdir):
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
monkeypatch.setattr(
"redbot.cogs.downloader.repo_manager.Repo.available_libraries", (library_installable,)
"redbot.core._downloader.repo_manager.Repo.available_libraries", (library_installable,)
)
lib_path = Path(str(tmpdir)) / "cog_data_path" / "lib"
@@ -353,9 +353,9 @@ async def test_lib_install_requirements(monkeypatch, library_installable, repo,
async def test_remove_repo(monkeypatch, repo_manager):
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
monkeypatch.setattr(
"redbot.cogs.downloader.repo_manager.Repo.current_commit", fake_current_commit
"redbot.core._downloader.repo_manager.Repo.current_commit", fake_current_commit
)
await repo_manager.add_repo(
@@ -3,7 +3,7 @@ import subprocess as sp
import pytest
from redbot.cogs.downloader.repo_manager import ProcessFormatter, Repo
from redbot.core._downloader.repo_manager import ProcessFormatter, Repo
from redbot.pytest.downloader import (
GIT_VERSION,
cloned_git_repo,
@@ -4,14 +4,14 @@ from pathlib import Path
import pytest
from redbot.pytest.downloader import *
from redbot.cogs.downloader.installable import Installable, InstallableType
from redbot.core._downloader.installable import Installable, InstallableType
from redbot.core import VersionInfo
def test_process_info_file(installable):
for k, v in INFO_JSON.items():
if k == "type":
assert installable.type == InstallableType.COG
assert installable.type is InstallableType.COG
elif k in ("min_bot_version", "max_bot_version"):
assert getattr(installable, k) == VersionInfo.from_str(v)
else:
@@ -21,7 +21,7 @@ def test_process_info_file(installable):
def test_process_lib_info_file(library_installable):
for k, v in LIBRARY_INFO_JSON.items():
if k == "type":
assert library_installable.type == InstallableType.SHARED_LIBRARY
assert library_installable.type is InstallableType.SHARED_LIBRARY
elif k in ("min_bot_version", "max_bot_version"):
assert getattr(library_installable, k) == VersionInfo.from_str(v)
elif k == "hidden":
+1 -6
View File
@@ -965,12 +965,7 @@ def cli_contributors(version: str, *, show_not_merged: bool = False) -> None:
def get_contributors(version: str, *, show_not_merged: bool = False) -> None:
print(
", ".join(
f":ghuser:`{username}`"
for username in _get_contributors(version, show_not_merged=show_not_merged)
)
)
print(*_get_contributors(version, show_not_merged=show_not_merged))
def _get_contributors(version: str, *, show_not_merged: bool = False) -> List[str]:
+1
View File
@@ -59,6 +59,7 @@ extras = doc
commands =
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/html" -W --keep-going -bhtml
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/doctest" -W --keep-going -bdoctest
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out/markdown" docs/changelog.rst -W --keep-going -bmarkdown
[testenv:style]
description = Stylecheck the code with black to see if anything needs changes.