mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-05-11 02:35:58 -04:00
Compare commits
19 Commits
3.5.24
..
V3/develop
| Author | SHA1 | Date | |
|---|---|---|---|
| edce32364f | |||
| 7305f44f68 | |||
| cbd4643bd3 | |||
| b02fa38423 | |||
| 99babf9ad3 | |||
| 169d0eed49 | |||
| 70faa8cd52 | |||
| 2ea4c766ad | |||
| 6ceb45b35c | |||
| 4032648dcc | |||
| f70c48ec30 | |||
| fcb8bc0265 | |||
| ee1db01a2f | |||
| e2acec0862 | |||
| b83b882921 | |||
| 99d7b0e3b7 | |||
| 9270373c56 | |||
| e8f0ea0510 | |||
| b42bab4de9 |
+7
-5
@@ -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":
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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
|
||||
|
||||
+904
-132
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
***********
|
||||
|
||||
@@ -9,7 +9,7 @@ Bot
|
||||
Red
|
||||
^^^
|
||||
|
||||
.. autoclass:: Red
|
||||
.. autoclass:: Red()
|
||||
:members:
|
||||
:exclude-members: get_context, get_embed_color
|
||||
|
||||
|
||||
@@ -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
|
||||
************************************
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
Mod log
|
||||
=======
|
||||
|
||||
Mod log has now been separated from Mod for V3.
|
||||
|
||||
***********
|
||||
Basic Usage
|
||||
***********
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
----
|
||||
@@ -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
|
||||
|
||||
+1
-15
@@ -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.24"
|
||||
_VERSION = "3.5.25.dev1"
|
||||
|
||||
__version__, version_info = VersionInfo._get_version()
|
||||
|
||||
|
||||
+19
-37
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+33
-1
@@ -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
|
||||
@@ -368,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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("red.core.downloader")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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",)
|
||||
|
||||
@@ -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,"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
+33
-17
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
-c base.txt
|
||||
|
||||
Sphinx
|
||||
sphinx-markdown-builder>=0.6.10
|
||||
sphinx-prompt
|
||||
sphinx_rtd_theme>1
|
||||
sphinxcontrib-trio
|
||||
|
||||
@@ -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
@@ -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 +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,
|
||||
+3
-3
@@ -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":
|
||||
@@ -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]:
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user