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 # Packaging
*.egg-info *.egg-info
bin bin
build docs/build
dist dist
MANIFEST 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: 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.shards: Dict[int, Shard] = {}
self.http = HTTPClient(token) self.http = HTTPClient(token)
self._events = {} self._events = {}
@ -19,26 +45,26 @@ class Client:
self.guilds = [] self.guilds = []
self.user = None 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.intents = intents
self._token = token self._token = token
self._activity = kwargs.get("activity") self._activity = activity
self._status = kwargs.get("status") self._status = status
async def _get_gateway(self): async def _get_gateway(self):
"""Get Gateway information""" """Get Gateway information"""
return GatewayBotInfo.from_dict(await self.http.get("gateway/bot")) return GatewayBotInfo.from_dict(await self.http.get("gateway/bot"))
def listen(self, callback: Coro): def listen(self, callback: Coro):
"""Method to set the listener. """Method or Decorator to set the listener.
Parameters Parameters
---------- ----------
callback (:obj:`function`) callback : :class:`melisa.utils.types.Coro`
Coroutine Callback Function Coroutine Callback Function
""" """
if not asyncio.iscoroutinefunction(callback): if not asyncio.iscoroutinefunction(callback):
@ -53,8 +79,8 @@ class Client:
""" """
inited_shard = Shard(self, 0, 1) inited_shard = Shard(self, 0, 1)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop) asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
self.loop.run_forever() self._loop.run_forever()
def run_shards(self, num_shards: int, *, shard_ids: List[int] = None): def run_shards(self, num_shards: int, *, shard_ids: List[int] = None):
""" """
@ -64,9 +90,6 @@ class Client:
---------- ----------
num_shards : :class:`int` num_shards : :class:`int`
The endpoint to send the request to. The endpoint to send the request to.
Keyword Arguments:
shard_ids: Optional[:class:`List[int]`] shard_ids: Optional[:class:`List[int]`]
List of Ids of shards to start. List of Ids of shards to start.
""" """
@ -76,8 +99,8 @@ class Client:
for shard_id in shard_ids: for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards) inited_shard = Shard(self, shard_id, num_shards)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop) asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
self.loop.run_forever() self._loop.run_forever()
def run_autosharded(self): def run_autosharded(self):
""" """
@ -89,8 +112,8 @@ class Client:
for shard_id in shard_ids: for shard_id in shard_ids:
inited_shard = Shard(self, shard_id, num_shards) inited_shard = Shard(self, shard_id, num_shards)
asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self.loop) asyncio.ensure_future(inited_shard.launch(activity=self._activity, status=self._status), loop=self._loop)
self.loop.run_forever() self._loop.run_forever()
async def fetch_user(self, user_id: Union[Snowflake, str, int]): async def fetch_user(self, user_id: Union[Snowflake, str, int]):
""" """

View file

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

View file

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