Generate pyi docs

This commit is contained in:
0xd4d 2020-12-16 20:47:21 +01:00
parent d5388ec9bd
commit 6ed7b7c006
6 changed files with 8411 additions and 814 deletions

View File

@ -41,6 +41,7 @@ namespace Generator.Misc.Python {
Args,
}
[DebuggerDisplay("Count = {Attributes.Count}")]
sealed class RustAttributes {
public readonly List<RustAttribute> Attributes = new List<RustAttribute>();
@ -71,6 +72,7 @@ namespace Generator.Misc.Python {
Args,
Raises,
Returns,
Note,
TestCode,
TestOutput,
}
@ -132,6 +134,11 @@ namespace Generator.Misc.Python {
public ReturnsDocCommentSection(TypeAndDocs returns) => Returns = returns;
}
sealed class NoteDocCommentSection : DocCommentSection {
public readonly string[] Lines;
public NoteDocCommentSection(string[] lines) => Lines = lines;
}
sealed class TestCodeDocCommentSection : DocCommentSection {
public readonly string[] Lines;
public TestCodeDocCommentSection(string[] lines) => Lines = lines;

View File

@ -713,6 +713,7 @@ namespace Generator.Misc.Python {
"Args:" => DocCommentKind.Args,
"Raises:" => DocCommentKind.Raises,
"Returns:" => DocCommentKind.Returns,
"Note:" => DocCommentKind.Note,
".. testcode::" => DocCommentKind.TestCode,
".. testoutput::" => DocCommentKind.TestOutput,
_ => DocCommentKind.Text,
@ -790,6 +791,12 @@ namespace Generator.Misc.Python {
docs.Sections.Add(new ReturnsDocCommentSection(returns.Value));
break;
case DocCommentKind.Note:
if (!TryGetDescLines(lines, ref i, out descLines, out error))
return false;
docs.Sections.Add(new NoteDocCommentSection(descLines.ToArray()));
break;
case DocCommentKind.TestCode:
if (!TryReadExampleLines(lines, ref i, out var exampleLines, out error))
return false;

View File

@ -0,0 +1,247 @@
/*
Copyright (C) 2018-2019 de4dot@gmail.com
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
namespace Generator.Misc.Python {
// The docs in the *.rs files are in a format understood by sphinx but it's not
// supported by most Python language servers. This class converts the docs to
// something that is accepted by most Python language servers.
//
// Tested: VSCode(Jedi, Pylance, Microsoft)
sealed class PyiDocGen {
readonly List<string> converted = new List<string>();
const string HeaderPrefix = "###";
public List<string> Convert(DocComments docComments) {
converted.Clear();
bool addEmptyLine = false;
var colDefs = new List<(int start, int length)>();
var tableLines = new List<List<string>>();
var columnLengths = new List<int>();
var sb = new StringBuilder();
foreach (var section in docComments.Sections) {
if (addEmptyLine)
converted.Add(string.Empty);
addEmptyLine = true;
switch (section) {
case TextDocCommentSection text:
for (int i = 0; i < text.Lines.Length; i++) {
var line = text.Lines[i];
// Convert tables to code blocks. They're not converted to
// markdown tables because Jedi doesn't support tables in
// tooltips. Instead, we create a text block that will be
// rendered with a mono spaced font.
if (IsTableLine(line)) {
GetColumnDefs(colDefs, line);
if (colDefs.Count < 2)
throw new InvalidOperationException($"Invalid table, expected at least 2 columns. First line: {line}");
i++;
if (!TryGetNextTableLine(text.Lines, ref i, out var tblLine) || IsTableLine(tblLine))
throw new InvalidOperationException("Invalid table");
tableLines.Add(GetColumns(colDefs, tblLine));
if (!TryGetNextTableLine(text.Lines, ref i, out tblLine) || !IsTableLine(tblLine))
throw new InvalidOperationException("Invalid table");
while (TryGetNextTableLine(text.Lines, ref i, out tblLine) && !IsTableLine(tblLine))
tableLines.Add(GetColumns(colDefs, tblLine));
foreach (var tableCols in tableLines) {
for (int j = 0; j < tableCols.Count; j++)
tableCols[j] = FixTableColumn(tableCols[j], j == 0);
}
columnLengths.Clear();
for (int j = 0; j < colDefs.Count; j++)
columnLengths.Add(0);
foreach (var tableCols in tableLines) {
if (tableCols.Count != columnLengths.Count)
throw new InvalidOperationException();
for (int j = 0; j < columnLengths.Count; j++)
columnLengths[j] = Math.Max(columnLengths[j], tableCols[j].Length);
}
const int colSepLen = 2;
for (int j = 0; j < columnLengths.Count - 1; j++)
columnLengths[j] += colSepLen;
converted.Add("```text");
for (int j = 0; j < tableLines.Count; j++) {
var tableCols = tableLines[j];
sb.Clear();
for (int k = 0; k < tableCols.Count; k++) {
var col = tableCols[k];
sb.Append(FixCodeBlockLine(col));
int colLen = columnLengths[k];
if (col.Length > colLen)
throw new InvalidOperationException();
if (k + 1 != tableCols.Count)
sb.Append(' ', colLen - col.Length);
}
converted.Add(sb.ToString());
if (j == 0) {
sb.Clear();
sb.Append('-', columnLengths.Sum() - columnLengths[^1] + tableLines[0][^1].Length);
converted.Add(sb.ToString());
}
}
converted.Add("```");
}
else
converted.Add(FixDocLine(line));
}
break;
case ArgsDocCommentSection args:
converted.Add($"{HeaderPrefix} Args:");
converted.Add(string.Empty);
foreach (var arg in args.Args)
converted.Add(FixDocLine($"- `{arg.Name}` ({arg.SphinxType}): {arg.Documentation}"));
break;
case RaisesDocCommentSection raises:
converted.Add($"{HeaderPrefix} Raises:");
converted.Add(string.Empty);
foreach (var raise in raises.Raises)
converted.Add(FixDocLine($"- {raise.SphinxType}: {raise.Documentation}"));
break;
case ReturnsDocCommentSection returns:
converted.Add($"{HeaderPrefix} Returns:");
converted.Add(string.Empty);
converted.Add(FixDocLine($"- {returns.Returns.SphinxType}: {returns.Returns.Documentation}"));
break;
case NoteDocCommentSection note:
converted.Add($"{HeaderPrefix} Note:");
converted.Add(string.Empty);
foreach (var line in note.Lines)
converted.Add(FixDocLine($"- {line}"));
break;
case TestCodeDocCommentSection testCode:
converted.Add("```python");
foreach (var line in testCode.Lines)
converted.Add(FixCodeBlockLine(line));
converted.Add("```");
break;
case TestOutputDocCommentSection testOutput:
converted.Add("```text");
foreach (var line in testOutput.Lines)
converted.Add(FixCodeBlockLine(line));
converted.Add("```");
break;
default:
throw new InvalidOperationException();
}
}
return converted;
}
static bool IsTableLine(string line) =>
line.StartsWith("===", StringComparison.Ordinal) &&
line.EndsWith("===", StringComparison.Ordinal);
static bool TryGetNextTableLine(string[] strings, ref int i, [NotNullWhen(true)] out string? tblLine) {
if (i >= strings.Length) {
tblLine = null;
return false;
}
else {
tblLine = strings[i];
i++;
return true;
}
}
static List<string> GetColumns(List<(int start, int length)> colDefs, string line) {
var cols = new List<string>();
for (int i = 0; i < colDefs.Count; i++) {
bool isLastCol = i + 1 == colDefs.Count;
var colDef = colDefs[i];
if (colDef.start > 0 && line[colDef.start - 1] != ' ')
throw new InvalidOperationException($"Invalid column #{i}, line: {line}");
if (!isLastCol &&
colDef.start + colDef.length < line.Length && line[colDef.start + colDef.length] != ' ') {
throw new InvalidOperationException($"Invalid column #{i}, line: {line}");
}
string col;
if (isLastCol)
col = line.Substring(colDef.start).Trim();
else
col = line.Substring(colDef.start, colDef.length).Trim();
cols.Add(col);
}
return cols;
}
static void GetColumnDefs(List<(int start, int length)> colDefs, string line) {
colDefs.Clear();
for (int index = 0; ;) {
while (index < line.Length && line[index] == ' ')
index++;
if (index >= line.Length)
break;
int startIndex = index;
index = line.IndexOf(' ', index);
if (index < 0)
index = line.Length;
colDefs.Add((startIndex, index - startIndex));
}
}
static string FixDocLine(string line) {
line = line.Replace(@"\", @"\\");
line = line.Replace("\"\"\"", "\\\"\\\"\\\"");
line = line.Replace(@"``", @"`");
line = line.Replace(@":class:", string.Empty);
if (line == "Examples:")
line = HeaderPrefix + " " + line;
return line;
}
static string FixCodeBlockLine(string line) {
line = line.Replace(@"\", @"\\");
line = line.Replace("\"\"\"", "\\\"\\\"\\\"");
return line;
}
static string FixTableColumn(string line, bool isFirst) {
if (isFirst && line == @"\")
line = string.Empty;
line = line.Replace(@"``", @"`");
line = line.Replace(@":class:", string.Empty);
return line;
}
}
}

View File

@ -169,26 +169,29 @@ namespace Generator.Misc.Python {
writer.WriteLine($"class {pythonName}({baseClass}): ...");
}
var docGen = new PyiDocGen();
foreach (var classStr in classOrder) {
var pyClass = toClass[classStr];
toClass.Remove(classStr);
writer.WriteLine();
writer.WriteLine($"class {idConverter.Type(pyClass.Name)}:");
using (writer.Indent()) {
WriteDocs(writer, docGen.Convert(pyClass.DocComments));
int defCount = 0;
foreach (var member in GetMembers(pyClass)) {
switch (member) {
case PyMethod method:
var docComments = method.Attributes.Any(AttributeKind.New) == true ?
pyClass.DocComments : method.DocComments;
Write(writer, idConverter, pyClass, method, docComments, toEnumType);
Write(writer, docGen, idConverter, pyClass, method, docComments, toEnumType);
defCount++;
break;
case PyProperty property:
Write(writer, idConverter, pyClass, property.Getter, property.Getter.DocComments, toEnumType);
Write(writer, docGen, idConverter, pyClass, property.Getter, property.Getter.DocComments, toEnumType);
defCount++;
if (property.Setter is not null) {
Write(writer, idConverter, pyClass, property.Setter, property.Getter.DocComments, toEnumType);
Write(writer, docGen, idConverter, pyClass, property.Setter, property.Getter.DocComments, toEnumType);
defCount++;
}
break;
@ -205,7 +208,26 @@ namespace Generator.Misc.Python {
}
}
static void Write(FileWriter writer, IdentifierConverter idConverter, PyClass pyClass, PyMethod method, DocComments docComments, Dictionary<string, EnumType> toEnumType) {
static void WriteDocs(FileWriter writer, List<string> docs) {
if (docs.Count == 0)
throw new InvalidOperationException();
const string docQuotes = "\"\"\"";
if (docs.Count == 1)
writer.WriteLine($"{docQuotes}{docs[0]}{docQuotes}");
else {
writer.WriteLine(docQuotes);
foreach (var doc in docs) {
if (doc == string.Empty)
writer.WriteLineNoIndent(string.Empty);
else
writer.WriteLine(doc);
}
writer.WriteLine(docQuotes);
}
}
static void Write(FileWriter writer, PyiDocGen docGen, IdentifierConverter idConverter, PyClass pyClass, PyMethod method, DocComments docComments, Dictionary<string, EnumType> toEnumType) {
if (method.Attributes.Any(AttributeKind.ClassMethod) == true)
writer.WriteLine("@classmethod");
if (method.Attributes.Any(AttributeKind.StaticMethod) == true)
@ -291,7 +313,15 @@ namespace Generator.Misc.Python {
writer.Write(GetReturnType(pyClass, method.Name, method.RustReturnType, sphinxReturnType));
else
writer.Write("None");
writer.WriteLine(": ...");
if (method.DocComments.Sections.Count == 0)
writer.WriteLine(": ...");
else {
writer.WriteLine(":");
using (writer.Indent()) {
WriteDocs(writer, docGen.Convert(method.DocComments));
writer.WriteLine("...");
}
}
}
static bool TryGetValueStr(IdentifierConverter idConverter, string typeStr, string defaultValueStr, Dictionary<string, EnumType> toEnumType, [NotNullWhen(true)] out string? valueStr) {
@ -395,7 +425,7 @@ namespace Generator.Misc.Python {
}
}
IEnumerable<object> GetMembers(PyClass pyClass) {
static IEnumerable<object> GetMembers(PyClass pyClass) {
var setters = pyClass.Methods.Where(a => a.Attributes.Any(AttributeKind.Setter) == true).ToDictionary(a => a.Name, a => a, StringComparer.Ordinal);
foreach (var method in pyClass.Methods) {
if (method.Attributes.Any(AttributeKind.Setter) == true)

File diff suppressed because it is too large Load Diff

View File

@ -363,8 +363,7 @@ impl Instruction {
/// You can also call `len(instr)` to get this value.
///
/// Note:
/// This is just informational. If you modify the instruction or create a new one,
/// this method could return the wrong value.
/// This is just informational. If you modify the instruction or create a new one, this method could return the wrong value.
#[getter]
fn len(&self) -> usize {
self.instr.len()