Move docs generator into the telethon_generator package

This commit is contained in:
Lonami Exo
2018-04-14 20:35:05 +02:00
parent 3b7c4fe278
commit daebf5b7e8
8 changed files with 0 additions and 1 deletions

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html><head>
<title>Oopsie! | Telethon</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
body {
background-color: #f0f4f8;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 560px;
margin: 5em auto;
padding: 50px;
background-color: #fff;
border-radius: 1em;
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
body {
background-color: #fff;
}
div {
width: auto;
margin: 0 auto;
border-radius: 0;
padding: 1em;
}
}
</style>
</head>
<body>
<div>
<h1>You seem a bit lost…</h1>
<p>You seem to be lost! Don't worry, that's just Telegram's API being
itself. Shall we go back to the <a href="index.html">Main Page</a>?</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Telethon API</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="css/docs.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro" rel="stylesheet">
<style>
body {
overflow: scroll;
}
</style>
</head>
<body>
<div id="main_div">
<noscript>Please enable JavaScript if you would like to use search.</noscript>
<h1>Telethon API</h1>
<p>This documentation was generated straight from the <code>scheme.tl</code>
provided by Telegram. However, there is no official documentation per se
on what the methods, constructors and types mean. Nevertheless, this
page aims to provide easy access to all the available methods, their
definition and parameters.</p>
<p>Please note that when you see this:</p>
<pre>---functions---
users.getUsers#0d91a548 id:Vector&lt;InputUser&gt; = Vector&lt;User&gt;</pre>
<p>This is <b>not</b> Python code. It's the "TL definition". It's
an easy-to-read line that gives a quick overview on the parameters
and its result. You don't need to worry about this. See
<a href="http://telethon.readthedocs.io/en/latest/extra/developing/understanding-the-type-language.html">here</a>
for more details on it.</p>
<h3>Index</h3>
<ul>
<li>
<a href="#methods">Methods</a>
(<a href="methods/index.html">full list</a>)
</li>
<li>
<a href="#types">Types</a>
(<a href="types/index.html">full list</a>)
</li>
<li>
<a href="#constructors">Constructors</a>
(<a href="constructors/index.html">full list</a>)
</li>
<li><a href="#core">Core types</a></li>
<li><a href="#example">Full example</a></li>
</ul>
<h3 id="methods">Methods</h3>
<p>Currently there are <b>{method_count} methods</b> available for the layer
{layer}. The complete list can be seen <a href="methods/index.html">here</a>.
<br /><br />
Methods, also known as <i>requests</i>, are used to interact with the
Telegram API itself and are invoked through <code>client(Request(...))</code>.
<b>Only these</b> can be used like that! You cannot invoke types or
constructors, only requests. After this, Telegram will return a
<code>result</code>, which may be, for instance, a bunch of messages,
some dialogs, users, etc.</p>
<h3 id="types">Types</h3>
<p>Currently there are <b>{type_count} types</b>. You can see the full
list <a href="types/index.html">here</a>.</p>
<p>The Telegram types are the <i>abstract</i> results that you receive
after invoking a request. They are "abstract" because they can have
multiple constructors. For instance, the abstract type <code>User</code>
can be either <code>UserEmpty</code> or <code>User</code>. You should,
most of the time, make sure you received the desired type by using
the <code>isinstance(result, Constructor)</code> Python function.
When a request needs a Telegram type as argument, you should create
an instance of it by using one of its, possibly multiple, constructors.</p>
<h3 id="constructors">Constructors</h3>
<p>Currently there are <b>{constructor_count} constructors</b>. You can see
the full list <a href="constructors/index.html">here</a>.</p>
<p>Constructors are the way you can create instances of the abstract types
described above, and also the instances which are actually returned from
the functions although they all share a common abstract type.</p>
<h3 id="core">Core types</h3>
<p>Core types are types from which the rest of Telegram types build upon:</p>
<ul>
<li id="int"><b>int</b>:
The value should be an integer type, like <span class="sh1">42</span>.
It should have 32 bits or less. You can check the bit length by
calling <code>a.bit_length()</code>, where <code>a</code> is an
integer variable.
</li>
<li id="long"><b>long</b>:
Different name for an integer type. The numbers given should have
64 bits or less.
</li>
<li id="int128"><b>int128</b>:
Another integer type, should have 128 bits or less.
</li>
<li id="int256"><b>int256</b>:
The largest integer type, allowing 256 bits or less.
</li>
<li id="double"><b>double</b>:
The value should be a floating point value, such as
<span class="sh1">123.456</span>.
</li>
<li id="vector"><b>Vector&lt;T&gt;</b>:
If a type <code>T</code> is wrapped around <code>Vector&lt;T&gt;</code>,
then it means that the argument should be a <i>list</i> of it.
For instance, a valid value for <code>Vector&lt;int&gt;</code>
would be <code>[1, 2, 3]</code>.
</li>
<li id="string"><b>string</b>:
A valid UTF-8 string should be supplied. This is right how
Python strings work, no further encoding is required.
</li>
<li id="bool"><b>Bool</b>:
Either <code>True</code> or <code>False</code>.
</li>
<li id="true"><b>true</b>:
These arguments aren't actually sent but rather encoded as flags.
Any truthy value (<code>True</code>, <code>7</code>) will enable
this flag, although it's recommended to use <code>True</code> or
<code>None</code> to symbolize that it's not present.
</li>
<li id="bytes"><b>bytes</b>:
A sequence of bytes, like <code>b'hello'</code>, should be supplied.
</li>
<li id="date"><b>date</b>:
Although this type is internally used as an <code>int</code>,
you can pass a <code>datetime</code> or <code>date</code> object
instead to work with date parameters.<br />
Note that the library uses the date in <b>UTC+0</b>, since timezone
conversion is not responsibility of the library. Furthermore, this
eases converting into any other timezone without the need for a middle
step.
</li>
</ul>
<h3 id="example">Full example</h3>
<p>Documentation for this is now
<a href="http://telethon.readthedocs.io/en/latest/extra/advanced-usage/accessing-the-full-api.html">here</a>.
</p>
</div>
<script src="js/search.js"></script>
</body>
</html>

View File

@@ -0,0 +1,195 @@
body {
font-family: 'Nunito', sans-serif;
color: #333;
background-color:#fff;
font-size: 16px;
}
a {
color: #42aaed;
text-decoration: none;
}
pre {
font-family: 'Source Code Pro', monospace;
padding: 8px;
color: #567;
background: #f0f4f8;
border-radius: 0;
overflow-x: auto;
}
a:hover {
color: #64bbdd;
text-decoration: underline;
}
table {
width: 100%;
max-width: 100%;
}
table td {
border-top: 1px solid #eee;
padding: 8px;
}
.horizontal {
margin-bottom: 16px;
list-style: none;
background: #f0f4f8;
border-radius: 4px;
padding: 8px 16px;
}
.horizontal li {
display: inline-block;
margin: 0 8px 0 0;
}
.horizontal img {
display: inline-block;
margin: 0 8px -2px 0;
}
h1, summary.title {
font-size: 24px;
}
h3 {
font-size: 20px;
}
#main_div {
padding: 20px 0;
max-width: 800px;
margin: 0 auto;
}
pre::-webkit-scrollbar {
visibility: visible;
display: block;
height: 12px;
}
pre::-webkit-scrollbar-track:horizontal {
background: #def;
border-radius: 0;
height: 12px;
}
pre::-webkit-scrollbar-thumb:horizontal {
background: #bdd;
border-radius: 0;
height: 12px;
}
:target {
border: 2px solid #f8f800;
background: #f8f8f8;
padding: 4px;
}
/* 'sh' stands for Syntax Highlight */
span.sh1 {
color: #f70;
}
span.sh2 {
color: #0c7;
}
span.sh3 {
color: #aaa;
font-style: italic;
}
span.sh4 {
color: #06c;
}
span.tooltip {
border-bottom: 1px dashed #444;
}
#searchBox {
width: 100%;
border: none;
height: 20px;
padding: 8px;
font-size: 16px;
border-radius: 2px;
border: 2px solid #ddd;
}
#searchBox:placeholder-shown {
font-style: italic;
}
button {
border-radius: 2px;
font-size: 16px;
padding: 8px;
color: #000;
background-color: #fff;
border: 2px solid #42aaed;
transition-duration: 300ms;
}
button:hover {
background-color: #42aaed;
color: #fff;
}
/* https://www.w3schools.com/css/css_navbar.asp */
ul.together {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
ul.together li {
float: left;
}
ul.together li a {
display: block;
border-radius: 8px;
background: #f0f4f8;
padding: 4px 8px;
margin: 8px;
}
/* https://stackoverflow.com/a/30810322 */
.invisible {
left: 0;
top: -99px;
padding: 0;
width: 2em;
height: 2em;
border: none;
outline: none;
position: fixed;
box-shadow: none;
color: transparent;
background: transparent;
}
@media (max-width: 640px) {
h1, summary.title {
font-size: 18px;
}
h3 {
font-size: 16px;
}
#dev_page_content_wrap {
padding-top: 12px;
}
#dev_page_title {
margin-top: 10px;
margin-bottom: 20px;
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="svg2"
viewBox="0 0 9 15"
height="15"
width="9">
<defs
id="defs4" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-1037.3622)"
id="layer1">
<path
id="path4163"
d="m 0.1049,1039.6602 5.34527,5.0641 -5.43588,5.4358 1.81196,1.812 7.15946,-7.1594 -7.0291,-7.0604 z"
style="fill:#42aaed;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,208 @@
root = document.getElementById("main_div");
root.innerHTML = `
<!-- You can append '?q=query' to the URL to default to a search -->
<input id="searchBox" type="text" onkeyup="updateSearch()"
placeholder="Search for requests and types…" />
<div id="searchDiv">
<div id="exactMatch" style="display:none;">
<b>Exact match:</b>
<ul id="exactList" class="together">
</ul>
</div>
<details open><summary class="title">Methods (<span id="methodsCount">0</span>)</summary>
<ul id="methodsList" class="together">
</ul>
</details>
<details open><summary class="title">Types (<span id="typesCount">0</span>)</summary>
<ul id="typesList" class="together">
</ul>
</details>
<details><summary class="title">Constructors (<span id="constructorsCount">0</span>)</summary>
<ul id="constructorsList" class="together">
</ul>
</details>
</div>
<div id="contentDiv">
` + root.innerHTML + "</div>";
// HTML modified, now load documents
contentDiv = document.getElementById("contentDiv");
searchDiv = document.getElementById("searchDiv");
searchBox = document.getElementById("searchBox");
// Search lists
methodsList = document.getElementById("methodsList");
methodsCount = document.getElementById("methodsCount");
typesList = document.getElementById("typesList");
typesCount = document.getElementById("typesCount");
constructorsList = document.getElementById("constructorsList");
constructorsCount = document.getElementById("constructorsCount");
// Exact match
exactMatch = document.getElementById("exactMatch");
exactList = document.getElementById("exactList");
try {
requests = [{request_names}];
types = [{type_names}];
constructors = [{constructor_names}];
requestsu = [{request_urls}];
typesu = [{type_urls}];
constructorsu = [{constructor_urls}];
} catch (e) {
requests = [];
types = [];
constructors = [];
requestsu = [];
typesu = [];
constructorsu = [];
}
if (typeof prependPath !== 'undefined') {
for (var i = 0; i != requestsu.length; ++i) {
requestsu[i] = prependPath + requestsu[i];
}
for (var i = 0; i != typesu.length; ++i) {
typesu[i] = prependPath + typesu[i];
}
for (var i = 0; i != constructorsu.length; ++i) {
constructorsu[i] = prependPath + constructorsu[i];
}
}
// Assumes haystack has no whitespace and both are lowercase.
function find(haystack, needle) {
if (needle.length == 0) {
return true;
}
var hi = 0;
var ni = 0;
while (true) {
while (needle[ni] < 'a' || needle[ni] > 'z') {
++ni;
if (ni == needle.length) {
return true;
}
}
while (haystack[hi] != needle[ni]) {
++hi;
if (hi == haystack.length) {
return false;
}
}
++hi;
++ni;
if (ni == needle.length) {
return true;
}
if (hi == haystack.length) {
return false;
}
}
}
// Given two input arrays "original" and "original urls" and a query,
// return a pair of arrays with matching "query" elements from "original".
//
// TODO Perhaps return an array of pairs instead a pair of arrays (for cache).
function getSearchArray(original, originalu, query) {
var destination = [];
var destinationu = [];
for (var i = 0; i < original.length; ++i) {
if (find(original[i].toLowerCase(), query)) {
destination.push(original[i]);
destinationu.push(originalu[i]);
}
}
return [destination, destinationu];
}
// Modify "countSpan" and "resultList" accordingly based on the elements
// given as [[elements], [element urls]] (both with the same length)
function buildList(countSpan, resultList, foundElements) {
var result = "";
for (var i = 0; i < foundElements[0].length; ++i) {
result += '<li>';
result += '<a href="' + foundElements[1][i] + '">';
result += foundElements[0][i];
result += '</a></li>';
}
if (countSpan) {
countSpan.innerHTML = "" + foundElements[0].length;
}
resultList.innerHTML = result;
}
function updateSearch() {
if (searchBox.value) {
contentDiv.style.display = "none";
searchDiv.style.display = "";
var query = searchBox.value.toLowerCase();
var foundRequests = getSearchArray(requests, requestsu, query);
var foundTypes = getSearchArray(types, typesu, query);
var foundConstructors = getSearchArray(
constructors, constructorsu, query
);
buildList(methodsCount, methodsList, foundRequests);
buildList(typesCount, typesList, foundTypes);
buildList(constructorsCount, constructorsList, foundConstructors);
// Now look for exact matches
var original = requests.concat(constructors);
var originalu = requestsu.concat(constructorsu);
var destination = [];
var destinationu = [];
for (var i = 0; i < original.length; ++i) {
if (original[i].toLowerCase().replace("request", "") == query) {
destination.push(original[i]);
destinationu.push(originalu[i]);
}
}
if (destination.length == 0) {
exactMatch.style.display = "none";
} else {
exactMatch.style.display = "";
buildList(null, exactList, [destination, destinationu]);
return destinationu[0];
}
} else {
contentDiv.style.display = "";
searchDiv.style.display = "none";
}
}
function getQuery(name) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i != vars.length; ++i) {
var pair = vars[i].split("=");
if (pair[0] == name)
return pair[1];
}
}
var query = getQuery('q');
if (query) {
searchBox.value = query;
}
var exactUrl = updateSearch();
var redirect = getQuery('redirect');
if (exactUrl && redirect != 'no') {
window.location = exactUrl;
}

View File

@@ -0,0 +1,298 @@
import os
import re
class DocsWriter:
"""Utility class used to write the HTML files used on the documentation"""
def __init__(self, filename, type_to_path_function):
"""Initializes the writer to the specified output file,
creating the parent directories when used if required.
'type_to_path_function' should be a function which, given a type
name and a named argument relative_to, returns the file path for
the specified type, relative to the given filename
"""
self.filename = filename
self.handle = None
# Should be set before calling adding items to the menu
self.menu_separator_tag = None
# Utility functions TODO There must be a better way
self.type_to_path = lambda t: type_to_path_function(
t, relative_to=self.filename
)
# Control signals
self.menu_began = False
self.table_columns = 0
self.table_columns_left = None
self.write_copy_script = False
self._script = ''
# High level writing
def write_head(self, title, relative_css_path):
"""Writes the head part for the generated document,
with the given title and CSS
"""
self.write('''<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>''')
self.write(title)
self.write('''</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="''')
self.write(relative_css_path)
self.write('''" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro" rel="stylesheet">
</head>
<body>
<div id="main_div">''')
def set_menu_separator(self, relative_image_path):
"""Sets the menu separator.
Must be called before adding entries to the menu
"""
if relative_image_path:
self.menu_separator_tag = \
'<img src="{}" alt="/" />'.format(relative_image_path)
else:
self.menu_separator_tag = None
def add_menu(self, name, link=None):
"""Adds a menu entry, will create it if it doesn't exist yet"""
if self.menu_began:
if self.menu_separator_tag:
self.write(self.menu_separator_tag)
else:
# First time, create the menu tag
self.write('<ul class="horizontal">')
self.menu_began = True
self.write('<li>')
if link:
self.write('<a href="')
self.write(link)
self.write('">')
# Write the real menu entry text
self.write(name)
if link:
self.write('</a>')
self.write('</li>')
def end_menu(self):
"""Ends an opened menu"""
if not self.menu_began:
raise RuntimeError('No menu had been started in the first place.')
self.write('</ul>')
def write_title(self, title, level=1):
"""Writes a title header in the document body,
with an optional depth level
"""
self.write('<h%d>' % level)
self.write(title)
self.write('</h%d>' % level)
def write_code(self, tlobject):
"""Writes the code for the given 'tlobject' properly
formatted with hyperlinks
"""
self.write('<pre>---')
self.write('functions' if tlobject.is_function else 'types')
self.write('---\n')
# Write the function or type and its ID
if tlobject.namespace:
self.write(tlobject.namespace)
self.write('.')
self.write(tlobject.name)
self.write('#')
self.write(hex(tlobject.id)[2:].rjust(8, '0'))
# Write all the arguments (or do nothing if there's none)
for arg in tlobject.args:
self.write(' ')
add_link = not arg.generic_definition and not arg.is_generic
# "Opening" modifiers
if arg.generic_definition:
self.write('{')
# Argument name
self.write(arg.name)
self.write(':')
# "Opening" modifiers
if arg.is_flag:
self.write('flags.%d?' % arg.flag_index)
if arg.is_generic:
self.write('!')
if arg.is_vector:
self.write(
'<a href="%s">Vector</a>&lt;' % self.type_to_path('vector')
)
# Argument type
if arg.type:
if add_link:
self.write('<a href="%s">' % self.type_to_path(arg.type))
self.write(arg.type)
if add_link:
self.write('</a>')
else:
self.write('#')
# "Closing" modifiers
if arg.is_vector:
self.write('&gt;')
if arg.generic_definition:
self.write('}')
# Now write the resulting type (result from a function/type)
self.write(' = ')
generic_name = next((arg.name for arg in tlobject.args
if arg.generic_definition), None)
if tlobject.result == generic_name:
# Generic results cannot have any link
self.write(tlobject.result)
else:
if re.search('^vector<', tlobject.result, re.IGNORECASE):
# Notice that we don't simply make up the "Vector" part,
# because some requests (as of now, only FutureSalts),
# use a lower type name for it (see #81)
vector, inner = tlobject.result.split('<')
inner = inner.strip('>')
self.write('<a href="')
self.write(self.type_to_path(vector))
self.write('">%s</a>&lt;' % vector)
self.write('<a href="')
self.write(self.type_to_path(inner))
self.write('">%s</a>' % inner)
self.write('&gt;')
else:
self.write('<a href="')
self.write(self.type_to_path(tlobject.result))
self.write('">%s</a>' % tlobject.result)
self.write('</pre>')
def begin_table(self, column_count):
"""Begins a table with the given 'column_count', required to automatically
create the right amount of columns when adding items to the rows"""
self.table_columns = column_count
self.table_columns_left = 0
self.write('<table>')
def add_row(self, text, link=None, bold=False, align=None):
"""This will create a new row, or add text to the next column
of the previously created, incomplete row, closing it if complete"""
if not self.table_columns_left:
# Starting a new row
self.write('<tr>')
self.table_columns_left = self.table_columns
self.write('<td')
if align:
self.write(' style="text-align:')
self.write(align)
self.write('"')
self.write('>')
if bold:
self.write('<b>')
if link:
self.write('<a href="')
self.write(link)
self.write('">')
# Finally write the real table data, the given text
self.write(text)
if link:
self.write('</a>')
if bold:
self.write('</b>')
self.write('</td>')
self.table_columns_left -= 1
if not self.table_columns_left:
self.write('</tr>')
def end_table(self):
# If there was any column left, finish it before closing the table
if self.table_columns_left:
self.write('</tr>')
self.write('</table>')
def write_text(self, text):
"""Writes a paragraph of text"""
self.write('<p>')
self.write(text)
self.write('</p>')
def write_copy_button(self, text, text_to_copy):
"""Writes a button with 'text' which can be used
to copy 'text_to_copy' to clipboard when it's clicked."""
self.write_copy_script = True
self.write('<button onclick="cp(\'{}\');">{}</button>'
.format(text_to_copy, text))
def add_script(self, src='', relative_src=None):
if relative_src:
self._script += '<script src="{}"></script>'.format(relative_src)
elif src:
self._script += '<script>{}</script>'.format(src)
def end_body(self):
"""Ends the whole document. This should be called the last"""
if self.write_copy_script:
self.write(
'<textarea id="c" class="invisible"></textarea>'
'<script>'
'function cp(t){'
'var c=document.getElementById("c");'
'c.value=t;'
'c.select();'
'try{document.execCommand("copy")}'
'catch(e){}}'
'</script>')
self.write('</div>')
self.write(self._script)
self.write('</body></html>')
# "Low" level writing
def write(self, s):
"""Wrapper around handle.write"""
self.handle.write(s)
# With block
def __enter__(self):
# Sanity check
parent = os.path.dirname(self.filename)
if parent:
os.makedirs(parent, exist_ok=True)
self.handle = open(self.filename, 'w', encoding='utf-8')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.handle.close()

View File

@@ -0,0 +1,607 @@
#!/usr/bin/env python3
import os
import re
import sys
import shutil
try:
from .docs_writer import DocsWriter
except (ImportError, SystemError):
from docs_writer import DocsWriter
# Small trick so importing telethon_generator works
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from telethon_generator.parser import TLParser, TLObject
# TLObject -> Python class name
def get_class_name(tlobject):
"""Gets the class name following the Python style guidelines"""
# Courtesy of http://stackoverflow.com/a/31531797/4759433
name = tlobject.name if isinstance(tlobject, TLObject) else tlobject
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
# Replace '_' with '' once again to make sure it doesn't appear on the name
result = result[:1].upper() + result[1:].replace('_', '')
# If it's a function, let it end with "Request" to identify them more easily
if isinstance(tlobject, TLObject) and tlobject.is_function:
result += 'Request'
return result
# TLObject -> filename
def get_file_name(tlobject, add_extension=False):
"""Gets the file name in file_name_format.html for the given TLObject.
Only its name may also be given if the full TLObject is not available"""
if isinstance(tlobject, TLObject):
name = tlobject.name
else:
name = tlobject
# Courtesy of http://stackoverflow.com/a/1176023/4759433
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
if add_extension:
return result + '.html'
else:
return result
# TLObject -> from ... import ...
def get_import_code(tlobject):
kind = 'functions' if tlobject.is_function else 'types'
ns = '.' + tlobject.namespace if tlobject.namespace else ''
return 'from telethon.tl.{}{} import {}'\
.format(kind, ns, get_class_name(tlobject))
def get_create_path_for(tlobject):
"""Gets the file path (and creates the parent directories)
for the given 'tlobject', relative to nothing; only its local path"""
# Determine the output directory
out_dir = 'methods' if tlobject.is_function else 'constructors'
if tlobject.namespace:
out_dir = os.path.join(out_dir, tlobject.namespace)
# Ensure that it exists
os.makedirs(out_dir, exist_ok=True)
# Return the resulting filename
return os.path.join(out_dir, get_file_name(tlobject, add_extension=True))
def is_core_type(type_):
"""Returns "true" if the type is considered a core type"""
return type_.lower() in {
'int', 'long', 'int128', 'int256', 'double',
'vector', 'string', 'bool', 'true', 'bytes', 'date'
}
def get_path_for_type(type_, relative_to='.'):
"""Similar to getting the path for a TLObject, it might not be possible
to have the TLObject itself but rather its name (the type);
this method works in the same way, returning a relative path"""
if is_core_type(type_):
path = 'index.html#%s' % type_.lower()
elif '.' in type_:
# If it's not a core type, then it has to be a custom Telegram type
namespace, name = type_.split('.')
path = 'types/%s/%s' % (namespace, get_file_name(name, True))
else:
path = 'types/%s' % get_file_name(type_, True)
return get_relative_path(path, relative_to)
# Destination path from the current position -> relative to the given path
def get_relative_path(destination, relative_to):
if os.path.isfile(relative_to):
relative_to = os.path.dirname(relative_to)
return os.path.relpath(destination, start=relative_to)
def get_relative_paths(original, relative_to):
"""Converts the dictionary of 'original' paths to relative paths
starting from the given 'relative_to' file"""
return {k: get_relative_path(v, relative_to) for k, v in original.items()}
# Generate a index.html file for the given folder
def find_title(html_file):
"""Finds the <title> for the given HTML file, or (Unknown)"""
with open(html_file) as handle:
for line in handle:
if '<title>' in line:
# + 7 to skip len('<title>')
return line[line.index('<title>') + 7:line.index('</title>')]
return '(Unknown)'
def build_menu(docs, filename, relative_main_index):
"""Builds the menu using the given DocumentWriter up to 'filename',
which must be a file (it cannot be a directory)"""
# TODO Maybe this could be part of DocsWriter itself, "build path menu"
docs.add_menu('API', relative_main_index)
items = filename.split('/')
for i in range(len(items) - 1):
item = items[i]
link = '../' * (len(items) - (i + 2))
link += 'index.html'
docs.add_menu(item.title(), link=link)
if items[-1] != 'index.html':
docs.add_menu(os.path.splitext(items[-1])[0])
docs.end_menu()
def generate_index(folder, original_paths):
"""Generates the index file for the specified folder"""
# Determine the namespaces listed here (as sub folders)
# and the files (.html files) that we should link to
namespaces = []
files = []
for item in os.listdir(folder):
if os.path.isdir(os.path.join(folder, item)):
namespaces.append(item)
elif item != 'index.html':
files.append(item)
# We work with relative paths
paths = get_relative_paths(original_paths, relative_to=folder)
# Now that everything is setup, write the index.html file
filename = os.path.join(folder, 'index.html')
with DocsWriter(filename, type_to_path_function=get_path_for_type) as docs:
# Title should be the current folder name
docs.write_head(folder.title(), relative_css_path=paths['css'])
docs.set_menu_separator(paths['arrow'])
build_menu(docs, filename, relative_main_index=paths['index_all'])
docs.write_title(folder.title())
if namespaces:
docs.write_title('Namespaces', level=3)
docs.begin_table(4)
namespaces.sort()
for namespace in namespaces:
# For every namespace, also write the index of it
generate_index(os.path.join(folder, namespace), original_paths)
docs.add_row(namespace.title(),
link=os.path.join(namespace, 'index.html'))
docs.end_table()
docs.write_title('Available items')
docs.begin_table(2)
files = [(f, find_title(os.path.join(folder, f))) for f in files]
files.sort(key=lambda t: t[1])
for file, title in files:
docs.add_row(title, link=file)
docs.end_table()
docs.end_body()
def get_description(arg):
"""Generates a proper description for the given argument"""
desc = []
otherwise = False
if arg.can_be_inferred:
desc.append('If left unspecified, it will be inferred automatically.')
otherwise = True
elif arg.is_flag:
desc.append('This argument can be omitted.')
otherwise = True
if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}:
desc.append(
'Anything entity-like will work if the library can find its '
'<code>Input</code> version (e.g., usernames, <code>Peer</code>, '
'<code>User</code> or <code>Channel</code> objects, etc.).'
)
if arg.is_vector:
if arg.is_generic:
desc.append('A list of other Requests must be supplied.')
else:
desc.append('A list must be supplied.')
elif arg.is_generic:
desc.append('A different Request must be supplied for this argument.')
else:
otherwise = False # Always reset to false if no other text is added
if otherwise:
desc.insert(1, 'Otherwise,')
desc[-1] = desc[-1][:1].lower() + desc[-1][1:]
return ' '.join(desc).replace(
'list',
'<span class="tooltip" title="Any iterable that supports len() '
'will work too">list</span>'
)
def copy_replace(src, dst, replacements):
"""Copies the src file into dst applying the replacements dict"""
with open(src) as infile, open(dst, 'w') as outfile:
outfile.write(re.sub(
'|'.join(re.escape(k) for k in replacements),
lambda m: str(replacements[m.group(0)]),
infile.read()
))
def generate_documentation(scheme_file):
"""Generates the documentation HTML files from from scheme.tl to
/methods and /constructors, etc.
"""
original_paths = {
'css': 'css/docs.css',
'arrow': 'img/arrow.svg',
'search.js': 'js/search.js',
'404': '404.html',
'index_all': 'index.html',
'index_types': 'types/index.html',
'index_methods': 'methods/index.html',
'index_constructors': 'constructors/index.html'
}
tlobjects = tuple(TLParser.parse_file(scheme_file))
print('Generating constructors and functions documentation...')
# Save 'Type: [Constructors]' for use in both:
# * Seeing the return type or constructors belonging to the same type.
# * Generating the types documentation, showing available constructors.
# TODO Tried using 'defaultdict(list)' with strange results, make it work.
tltypes = {}
tlfunctions = {}
for tlobject in tlobjects:
# Select to which dictionary we want to store this type
dictionary = tlfunctions if tlobject.is_function else tltypes
if tlobject.result in dictionary:
dictionary[tlobject.result].append(tlobject)
else:
dictionary[tlobject.result] = [tlobject]
for tltype, constructors in tltypes.items():
tltypes[tltype] = list(sorted(constructors, key=lambda c: c.name))
for tlobject in tlobjects:
filename = get_create_path_for(tlobject)
# Determine the relative paths for this file
paths = get_relative_paths(original_paths, relative_to=filename)
with DocsWriter(filename, type_to_path_function=get_path_for_type) \
as docs:
docs.write_head(
title=get_class_name(tlobject),
relative_css_path=paths['css'])
# Create the menu (path to the current TLObject)
docs.set_menu_separator(paths['arrow'])
build_menu(docs, filename, relative_main_index=paths['index_all'])
# Create the page title
docs.write_title(get_class_name(tlobject))
# Write the code definition for this TLObject
docs.write_code(tlobject)
docs.write_copy_button('Copy import to the clipboard',
get_import_code(tlobject))
# Write the return type (or constructors belonging to the same type)
docs.write_title('Returns' if tlobject.is_function
else 'Belongs to', level=3)
generic_arg = next((arg.name for arg in tlobject.args
if arg.generic_definition), None)
if tlobject.result == generic_arg:
# We assume it's a function returning a generic type
generic_arg = next((arg.name for arg in tlobject.args
if arg.is_generic))
docs.write_text('This function returns the result of whatever '
'the result from invoking the request passed '
'through <i>{}</i> is.'.format(generic_arg))
else:
if re.search('^vector<', tlobject.result, re.IGNORECASE):
docs.write_text('A list of the following type is returned.')
_, inner = tlobject.result.split('<')
inner = inner.strip('>')
else:
inner = tlobject.result
docs.begin_table(column_count=1)
docs.add_row(inner, link=get_path_for_type(
inner, relative_to=filename
))
docs.end_table()
constructors = tltypes.get(inner, [])
if not constructors:
docs.write_text('This type has no instances available.')
elif len(constructors) == 1:
docs.write_text('This type can only be an instance of:')
else:
docs.write_text('This type can be an instance of either:')
docs.begin_table(column_count=2)
for constructor in constructors:
link = get_create_path_for(constructor)
link = get_relative_path(link, relative_to=filename)
docs.add_row(get_class_name(constructor), link=link)
docs.end_table()
# Return (or similar types) written. Now parameters/members
docs.write_title(
'Parameters' if tlobject.is_function else 'Members', level=3
)
# Sort the arguments in the same way they're sorted
# on the generated code (flags go last)
args = [
a for a in tlobject.sorted_args()
if not a.flag_indicator and not a.generic_definition
]
if args:
# Writing parameters
docs.begin_table(column_count=3)
for arg in args:
# Name row
docs.add_row(arg.name,
bold=True)
# Type row
if arg.is_generic:
docs.add_row('!' + arg.type, align='center')
else:
docs.add_row(
arg.type, align='center', link=
get_path_for_type(arg.type, relative_to=filename)
)
# Add a description for this argument
docs.add_row(get_description(arg))
docs.end_table()
else:
if tlobject.is_function:
docs.write_text('This request takes no input parameters.')
else:
docs.write_text('This type has no members.')
# TODO Bit hacky, make everything like this? (prepending '../')
depth = '../' * (2 if tlobject.namespace else 1)
docs.add_script(src='prependPath = "{}";'.format(depth))
docs.add_script(relative_src=paths['search.js'])
docs.end_body()
# Find all the available types (which are not the same as the constructors)
# Each type has a list of constructors associated to it, hence is a map
print('Generating types documentation...')
for tltype, constructors in tltypes.items():
filename = get_path_for_type(tltype)
out_dir = os.path.dirname(filename)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
# Since we don't have access to the full TLObject, split the type
if '.' in tltype:
namespace, name = tltype.split('.')
else:
namespace, name = None, tltype
# Determine the relative paths for this file
paths = get_relative_paths(original_paths, relative_to=out_dir)
with DocsWriter(filename, type_to_path_function=get_path_for_type) \
as docs:
docs.write_head(
title=get_class_name(name),
relative_css_path=paths['css'])
docs.set_menu_separator(paths['arrow'])
build_menu(docs, filename, relative_main_index=paths['index_all'])
# Main file title
docs.write_title(get_class_name(name))
# List available constructors for this type
docs.write_title('Available constructors', level=3)
if not constructors:
docs.write_text('This type has no constructors available.')
elif len(constructors) == 1:
docs.write_text('This type has one constructor available.')
else:
docs.write_text('This type has %d constructors available.' %
len(constructors))
docs.begin_table(2)
for constructor in constructors:
# Constructor full name
link = get_create_path_for(constructor)
link = get_relative_path(link, relative_to=filename)
docs.add_row(get_class_name(constructor), link=link)
docs.end_table()
# List all the methods which return this type
docs.write_title('Methods returning this type', level=3)
functions = tlfunctions.get(tltype, [])
if not functions:
docs.write_text('No method returns this type.')
elif len(functions) == 1:
docs.write_text('Only the following method returns this type.')
else:
docs.write_text(
'The following %d methods return this type as a result.' %
len(functions)
)
docs.begin_table(2)
for func in functions:
link = get_create_path_for(func)
link = get_relative_path(link, relative_to=filename)
docs.add_row(get_class_name(func), link=link)
docs.end_table()
# List all the methods which take this type as input
docs.write_title('Methods accepting this type as input', level=3)
other_methods = sorted(
(t for t in tlobjects
if any(tltype == a.type for a in t.args) and t.is_function),
key=lambda t: t.name
)
if not other_methods:
docs.write_text(
'No methods accept this type as an input parameter.')
elif len(other_methods) == 1:
docs.write_text(
'Only this method has a parameter with this type.')
else:
docs.write_text(
'The following %d methods accept this type as an input '
'parameter.' % len(other_methods))
docs.begin_table(2)
for ot in other_methods:
link = get_create_path_for(ot)
link = get_relative_path(link, relative_to=filename)
docs.add_row(get_class_name(ot), link=link)
docs.end_table()
# List every other type which has this type as a member
docs.write_title('Other types containing this type', level=3)
other_types = sorted(
(t for t in tlobjects
if any(tltype == a.type for a in t.args)
and not t.is_function
), key=lambda t: t.name
)
if not other_types:
docs.write_text(
'No other types have a member of this type.')
elif len(other_types) == 1:
docs.write_text(
'You can find this type as a member of this other type.')
else:
docs.write_text(
'You can find this type as a member of any of '
'the following %d types.' % len(other_types))
docs.begin_table(2)
for ot in other_types:
link = get_create_path_for(ot)
link = get_relative_path(link, relative_to=filename)
docs.add_row(get_class_name(ot), link=link)
docs.end_table()
docs.end_body()
# After everything's been written, generate an index.html per folder.
# This will be done automatically and not taking into account any extra
# information that we have available, simply a file listing all the others
# accessible by clicking on their title
print('Generating indices...')
for folder in ['types', 'methods', 'constructors']:
generate_index(folder, original_paths)
# Write the final core index, the main index for the rest of files
layer = TLParser.find_layer(scheme_file)
types = set()
methods = []
constructors = []
for tlobject in tlobjects:
if tlobject.is_function:
methods.append(tlobject)
else:
constructors.append(tlobject)
if not is_core_type(tlobject.result):
if re.search('^vector<', tlobject.result, re.IGNORECASE):
types.add(tlobject.result.split('<')[1].strip('>'))
else:
types.add(tlobject.result)
types = sorted(types)
methods = sorted(methods, key=lambda m: m.name)
constructors = sorted(constructors, key=lambda c: c.name)
def fmt(xs):
ys = {x: get_class_name(x) for x in xs} # cache TLObject: display
zs = {} # create a dict to hold those which have duplicated keys
for y in ys.values():
zs[y] = y in zs
return ', '.join(
'"{}.{}"'.format(x.namespace, ys[x])
if zs[ys[x]] and getattr(x, 'namespace', None)
else '"{}"'.format(ys[x]) for x in xs
)
request_names = fmt(methods)
type_names = fmt(types)
constructor_names = fmt(constructors)
def fmt(xs, formatter):
return ', '.join('"{}"'.format(formatter(x)) for x in xs)
request_urls = fmt(methods, get_create_path_for)
type_urls = fmt(types, get_path_for_type)
constructor_urls = fmt(constructors, get_create_path_for)
shutil.copy('../res/404.html', original_paths['404'])
copy_replace('../res/core.html', original_paths['index_all'], {
'{type_count}': len(types),
'{method_count}': len(methods),
'{constructor_count}': len(tlobjects) - len(methods),
'{layer}': layer,
})
os.makedirs(os.path.abspath(os.path.join(
original_paths['search.js'], os.path.pardir
)), exist_ok=True)
copy_replace('../res/js/search.js', original_paths['search.js'], {
'{request_names}': request_names,
'{type_names}': type_names,
'{constructor_names}': constructor_names,
'{request_urls}': request_urls,
'{type_urls}': type_urls,
'{constructor_urls}': constructor_urls
})
# Everything done
print('Documentation generated.')
def copy_resources():
for d in ('css', 'img'):
os.makedirs(d, exist_ok=True)
shutil.copy('../res/img/arrow.svg', 'img')
shutil.copy('../res/css/docs.css', 'css')
if __name__ == '__main__':
os.makedirs('generated', exist_ok=True)
os.chdir('generated')
try:
generate_documentation('../../telethon_generator/scheme.tl')
copy_resources()
finally:
os.chdir(os.pardir)