docs, update docstrings, fix recursion

This commit is contained in:
grey-cat-1908 2022-03-17 21:32:43 +03:00
parent 345a728973
commit a5ebb9a972
15 changed files with 612 additions and 45 deletions

2
.gitignore vendored
View file

@ -24,7 +24,7 @@ _testing.py
# Packaging
*.egg-info
bin
build
docs/build
dist
MANIFEST

10
.readthedocs.yml Normal file
View file

@ -0,0 +1,10 @@
version: 2
sphinx:
configuration: docs/source/conf.py
python:
version: "3.8"
install:
- requirements: requirements.txt
- requirements: docs-requirements.txt

3
docs-requirements.txt Normal file
View file

@ -0,0 +1,3 @@
furo
sphinxcontrib_trio
sphinx_design

20
docs/Makefile Normal file
View file

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

37
docs/make.bat Normal file
View file

@ -0,0 +1,37 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
@pause

View file

@ -0,0 +1,46 @@
.py-attribute-table {
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 2em;
padding-top: 16px;
}
.py-attribute-table-column {
flex: 1 1 auto;
}
.py-attribute-table-column:not(:first-child) {
margin-top: 1em;
}
.py-attribute-table-column > span {
font-weight: bold;
}
main .py-attribute-table-column > ul {
list-style: none;
margin: 4px 0;
padding-left: 0;
font-size: 0.95em;
}
.py-attribute-table-entry {
margin: 0;
padding: 2px 0 2px 0.2em;
display: flex;
line-height: 1.2em;
}
.py-attribute-table-entry > a {
padding-left: 0.5em;
flex-grow: 1;
}
.py-attribute-table-badge {
flex-basis: 3em;
text-align: right;
font-size: 0.9em;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
}

16
docs/source/client.rst Normal file
View file

@ -0,0 +1,16 @@
.. currentmodule:: melisa
Melisa Module
=============
Client
-------
.. attributetable:: Client
.. autoclass:: Client
:exclude-members: listen
:inherited-members:
.. automethod:: Client.listen()
:decorator:

75
docs/source/conf.py Normal file
View file

@ -0,0 +1,75 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
import sys
import os
sys.path.append(os.path.abspath("../.."))
sys.path.append(os.path.abspath('extensions'))
project = 'Melisa'
copyright = '2022, MelisaDev'
author = 'MelisaDev'
# The full version, including alpha/beta/rc tags
release = '0.0.1'
# -- General configuration ---------------------------------------------------
extensions = [
'sphinx_design',
"sphinx.ext.napoleon",
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.autosectionlabel",
"sphinx.ext.extlinks",
"sphinxcontrib_trio",
"attributable"
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
exclude_patterns = []
intersphinx_mapping = {
"py": ("https://docs.python.org/3", None),
"aiohttp": ("https://docs.aiohttp.org/en/stable/", None),
}
# -- Options for HTML output -------------------------------------------------
html_theme = 'furo'
html_theme_options = {
"sidebar_hide_name": True,
}
pygments_style = 'monokai'
default_dark_mode = True
html_static_path = ['_static']
html_css_files = ["custom.css"]
rst_prolog = """
.. |coro| replace:: This function is a |coroutine_link|_.
.. |maybecoro| replace:: This function *could be a* |coroutine_link|_.
.. |coroutine_link| replace:: *coroutine*
.. _coroutine_link: https://docs.python.org/3/library/asyncio-task.html#coroutine
"""

View file

@ -0,0 +1,286 @@
"""
I am so sorry, but i found this extension somewhere in the internet.
IT LOOKS REALLY BEAUTIFUL!
"""
from sphinx.util.docutils import SphinxDirective
from sphinx.locale import _
from docutils import nodes
from sphinx import addnodes
from collections import OrderedDict, namedtuple
import importlib
import inspect
import re
class attributetable(nodes.General, nodes.Element):
pass
class attributetablecolumn(nodes.General, nodes.Element):
pass
class attributetabletitle(nodes.TextElement):
pass
class attributetableplaceholder(nodes.General, nodes.Element):
pass
class attributetablebadge(nodes.TextElement):
pass
class attributetable_item(nodes.Part, nodes.Element):
pass
def visit_attributetable_node(self, node):
class_ = node["python-class"]
self.body.append(
f'<div class="py-attribute-table" data-move-to-id="{class_}">'
)
def visit_attributetablecolumn_node(self, node):
self.body.append(self.starttag(
node, 'div', CLASS='py-attribute-table-column')
)
def visit_attributetabletitle_node(self, node):
self.body.append(self.starttag(node, 'span'))
def visit_attributetablebadge_node(self, node):
attributes = {
'class': 'py-attribute-table-badge',
'title': node['badge-type'],
}
self.body.append(self.starttag(node, 'span', **attributes))
def visit_attributetable_item_node(self, node):
self.body.append(self.starttag(
node, 'li', CLASS='py-attribute-table-entry')
)
def depart_attributetable_node(self, node):
self.body.append('</div>')
def depart_attributetablecolumn_node(self, node):
self.body.append('</div>')
def depart_attributetabletitle_node(self, node):
self.body.append('</span>')
def depart_attributetablebadge_node(self, node):
self.body.append('</span>')
def depart_attributetable_item_node(self, node):
self.body.append('</li>')
_name_parser_regex = re.compile(r'(?P<module>[\w.]+\.)?(?P<name>\w+)')
class PyAttributeTable(SphinxDirective):
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {}
def parse_name(self, content):
path, name = _name_parser_regex.match(content).groups()
if path:
modulename = path.rstrip('.')
else:
modulename = self.env.temp_data.get('autodoc:module')
if not modulename:
modulename = self.env.ref_context.get('py:module')
if modulename is None:
raise RuntimeError('modulename somehow None for %s in %s.' % (
content, self.env.docname))
return modulename, name
def run(self):
"""If you're curious on the HTML this is meant to generate:
<div class="py-attribute-table">
<div class="py-attribute-table-column">
<span>_('Attributes')</span>
<ul>
<li>
<a href="...">
</li>
</ul>
</div>
<div class="py-attribute-table-column">
<span>_('Methods')</span>
<ul>
<li>
<a href="..."></a>
<span class="py-attribute-badge" title="decorator">D</span>
</li>
</ul>
</div>
</div>
However, since this requires the tree to be complete
and parsed, it'll need to be done at a different stage and then
replaced.
"""
content = self.arguments[0].strip()
node = attributetableplaceholder('')
modulename, name = self.parse_name(content)
node['python-doc'] = self.env.docname
node['python-module'] = modulename
node['python-class'] = name
node['python-full-name'] = f'{modulename}.{name}'
return [node]
def build_lookup_table(env):
# Given an environment, load up a lookup table of
# full-class-name: objects
result = {}
domain = env.domains['py']
ignored = {
'data', 'exception', 'module', 'class',
}
for (fullname, _, objtype, docname, _, _) in domain.get_objects():
if objtype in ignored:
continue
classname, _, child = fullname.rpartition('.')
try:
result[classname].append(child)
except KeyError:
result[classname] = [child]
return result
TableElement = namedtuple('TableElement', 'fullname label badge')
def process_attributetable(app, doctree, fromdocname):
env = app.builder.env
lookup = build_lookup_table(env)
for node in doctree.traverse(attributetableplaceholder):
modulename, classname, fullname = node['python-module'], node['python-class'], node['python-full-name']
groups = get_class_results(lookup, modulename, classname, fullname)
table = attributetable('')
for label, subitems in groups.items():
if not subitems:
continue
table.append(class_results_to_node(
label, sorted(subitems, key=lambda c: c.label)))
table['python-class'] = fullname
node.replace_self([table] if table else [])
def get_class_results(lookup, modulename, name, fullname):
module = importlib.import_module(modulename)
cls = getattr(module, name)
groups = OrderedDict([
(_('Attributes'), []),
(_('Methods'), []),
])
try:
members = lookup[fullname]
except KeyError:
return groups
for attr in members:
attrlookup = f'{fullname}.{attr}'
key = _('Attributes')
badge = None
label = attr
value = None
for base in cls.__mro__:
value = base.__dict__.get(attr)
if value is not None:
break
if value is not None:
doc = value.__doc__ or ''
if inspect.iscoroutinefunction(value) or doc.startswith('|coro|'):
key = _('Methods')
badge = attributetablebadge('async', 'async')
badge['badge-type'] = _('coroutine')
elif isinstance(value, classmethod):
key = _('Methods')
label = f'{name}.{attr}'
badge = attributetablebadge('cls', 'cls')
badge['badge-type'] = _('classmethod')
elif (
inspect.isfunction(value)
or isinstance(value, staticmethod)
):
if (
doc.startswith(('A decorator', 'A shortcut decorator'))
or label in ("event", "loop")
):
# finicky but surprisingly consistent
badge = attributetablebadge('@', '@')
badge['badge-type'] = _('decorator')
key = _('Methods')
else:
key = _('Methods')
badge = attributetablebadge('def', 'def')
badge['badge-type'] = _('method')
groups[key].append(TableElement(
fullname=attrlookup, label=label, badge=badge))
return groups
def class_results_to_node(key, elements):
title = attributetabletitle(key, key)
ul = nodes.bullet_list('')
for element in elements:
ref = nodes.reference('', '', internal=True,
refuri='#' + element.fullname,
anchorname='',
*[nodes.Text(element.label)])
para = addnodes.compact_paragraph('', '', ref)
if element.badge is not None:
ul.append(attributetable_item('', element.badge, para))
else:
ul.append(attributetable_item('', para))
return attributetablecolumn('', title, ul)
def setup(app):
app.add_directive('attributetable', PyAttributeTable)
app.add_node(attributetable, html=(
visit_attributetable_node, depart_attributetable_node))
app.add_node(attributetablecolumn, html=(
visit_attributetablecolumn_node, depart_attributetablecolumn_node))
app.add_node(attributetabletitle, html=(
visit_attributetabletitle_node, depart_attributetabletitle_node))
app.add_node(attributetablebadge, html=(
visit_attributetablebadge_node, depart_attributetablebadge_node))
app.add_node(attributetable_item, html=(
visit_attributetable_item_node, depart_attributetable_item_node))
app.add_node(attributetableplaceholder)
app.connect('doctree-resolved', process_attributetable)

15
docs/source/index.rst Normal file
View file

@ -0,0 +1,15 @@
Welcome to Melisa's documentation!
==================================
.. toctree::
:maxdepth: 2
:caption: Documentation:
client
models/index
Indices and tables
==================
* :ref:`search`

View file

@ -0,0 +1,7 @@
Melisa Models
=====================
.. toctree::
:maxdepth: 1
user

View file

@ -0,0 +1,30 @@
.. currentmodule:: melisa.models
Melisa Models User
===========================
User
----
PremiumTypes
~~~~~~~~~~~~~
.. autoclass:: PremiumTypes()
UserFlags
~~~~~~~~~~
.. autoclass:: UserFlags()
VisibilityTypes
~~~~~~~~~~~~~~~~
.. autoclass:: VisibilityTypes()
User
~~~~~
.. attributetable:: User
.. autoclass:: User()

View file

@ -11,7 +11,33 @@ from typing import Dict, List, Union
class Client:
def __init__(self, token, intents, **kwargs):
"""
This is the main instance which is between the programmer and the Discord API.
This Client represents your bot.
Parameters
----------
token : :class:`str`
The token to login (you can found it in the developer portal)
intents : :class:`~objects.app.intents.Intents`
The Discord Intents values.
activity : :class:`~models.user.presence.BotActivity`
The Activity to set (on connecting)
status : :class:`str`
The Status to set (on connecting). Can be generated using :class:`~models.user.presence.StatusType`
Attributes
----------
user: :class:`~models.user.user.User`
The user object of the client
http: :class:`~core.http.HTTPClient`
HTTP client for the http-requests to the Discord API
shards: :class:`Dict[int, Shard]`
Bot's shards.
"""
def __init__(self, token, intents, *, activity=None, status: str = None):
self.shards: Dict[int, Shard] = {}
self.http = HTTPClient(token)
self._events = {}
@ -19,26 +45,26 @@ class Client:
self.guilds = []
self.user = None
self.loop = asyncio.get_event_loop()
self._loop = asyncio.get_event_loop()
self._gateway_info = self.loop.run_until_complete(self._get_gateway())
self._gateway_info = self._loop.run_until_complete(self._get_gateway())
self.intents = intents
self._token = token
self._activity = kwargs.get("activity")
self._status = kwargs.get("status")
self._activity = activity
self._status = status
async def _get_gateway(self):
"""Get Gateway information"""
return GatewayBotInfo.from_dict(await self.http.get("gateway/bot"))
def listen(self, callback: Coro):
"""Method to set the listener.
"""Method or Decorator to set the listener.
Parameters
----------
callback (:obj:`function`)
callback : :class:`melisa.utils.types.Coro`
Coroutine Callback Function
"""
if not asyncio.iscoroutinefunction(callback):
@ -53,8 +79,8 @@ class Client:
"""
inited_shard = Shard(self, 0, 1)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop)
self.loop.run_forever()
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
self._loop.run_forever()
def run_shards(self, num_shards: int, *, shard_ids: List[int] = None):
"""
@ -64,9 +90,6 @@ class Client:
----------
num_shards : :class:`int`
The endpoint to send the request to.
Keyword Arguments:
shard_ids: Optional[:class:`List[int]`]
List of Ids of shards to start.
"""
@ -76,8 +99,8 @@ class Client:
for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop)
self.loop.run_forever()
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
self._loop.run_forever()
def run_autosharded(self):
"""
@ -89,8 +112,8 @@ class Client:
for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop)
self.loop.run_forever()
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
self._loop.run_forever()
async def fetch_user(self, user_id: Union[Snowflake, str, int]):
"""

View file

@ -95,13 +95,12 @@ class Gateway:
await self.resume()
async def check_heartbeating(self):
await asyncio.sleep(20)
while True:
await asyncio.sleep(20)
if self._last_send + 60.0 < time.perf_counter():
await self.ws.close(code=4000)
await self.handle_close(4000)
await self.check_heartbeating()
if self._last_send + 60.0 < time.perf_counter():
await self.ws.close(code=4000)
await self.handle_close(4000)
async def send(self, payload: str) -> None:
await self.ws.send_str(payload)

View file

@ -111,36 +111,40 @@ class User(APIObjectBase):
Attributes
----------
id:
id: :class:`~melisa.utils.types.Snowflake`
the user's id
username:
username: :class:`str`
the user's username, not unique across the platform
discriminator:
discriminator: :class:`int`
the user's 4-digit discord-tag
avatar:
avatar: :class:`str`
the user's avatar hash
bot:
bot: :class:`bool`
whether the user belongs to an OAuth2 application
system:
system: :class:`bool`
whether the user is an Official Discord System user (part of the urgent message system)
mfa_enabled:
mfa_enabled: :class:`bool`
whether the user has two factor enabled on their account
banner:
banner: :class:`str`
the user's banner hash
accent_color:
accent_color: :class:`int`
the user's banner color encoded as an integer representation of hexadecimal color code
locale:
locale: :class:`str`
the user's chosen language option
verified:
verified: :class:`bool`
whether the email on this account has been verified
email:
email: :class:`str`
the user's email
flags:
flags: :class:`~models.user.user.UserFlags`
the flags on a user's account
premium_type:
premium_type: :class:`int`
the type of Nitro subscription on a user's account
public_flags:
public_flags: :class:`int`
the public flags on a user's account
premium: :class:`PremiumTypes`
The user their premium type in a usable enum.
mention: :class:`str`
The user's mention string. (<@id>)
"""
id: APINullable[Snowflake] = None
@ -187,10 +191,6 @@ class User(APIObjectBase):
""":class:`str`: The user's mention string. (<@id>)"""
return "<@{}>".format(self.id)
@property
def avatar_url(self):
"""Avatar url (from the discord cdn server)"""
return (
"https://cdn.discordapp.com/avatars/{}/{}.png".format(self.id, self.avatar),
"?size=1024"
)
def avatar_url(self) -> str:
"""Avatar url (from the Discord CDN server)"""
return "https://cdn.discordapp.com/avatars/{}/{}.png?size=1024".format(self.id, self.avatar)