Skip to content

Commit c2b4755

Browse files
committed
CPython docs database for module items
1 parent 72e9423 commit c2b4755

File tree

9 files changed

+2191
-13
lines changed

9 files changed

+2191
-13
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_functools.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,6 @@ def wrapper():
717717
with self.assertRaises(AttributeError):
718718
functools.update_wrapper(wrapper, f, assign, update)
719719

720-
# TODO: RUSTPYTHON
721-
@unittest.expectedFailure
722720
@support.requires_docstrings
723721
@unittest.skipIf(sys.flags.optimize >= 2,
724722
"Docstrings are omitted with -O2 and above")

derive/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ proc-macro2 = "1.0"
1818
rustpython-compiler = { path = "../compiler/porcelain", version = "0.1.1" }
1919
rustpython-bytecode = { path = "../bytecode", version = "0.1.1" }
2020
maplit = "1.0"
21-
once_cell = "1.3.1"
21+
once_cell = "1.8.0"
2222
textwrap = "0.13.4"
2323
indexmap = "^1"
24+
serde_json = "1.0.68"

derive/docs.json

Lines changed: 2030 additions & 0 deletions
Large diffs are not rendered by default.

derive/src/doc.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
pub(crate) type Result = std::result::Result<Option<&'static str>, ()>;
2+
3+
pub(crate) fn try_read(path: &str) -> Result {
4+
static DATABASE: once_cell::sync::OnceCell<std::collections::HashMap<String, Option<String>>> =
5+
once_cell::sync::OnceCell::new();
6+
let db = DATABASE.get_or_init(|| {
7+
let raw = include_str!("../docs.json");
8+
serde_json::from_str(raw).expect("docs.json must be a valid json file")
9+
});
10+
let data = db.get(path).ok_or(())?;
11+
Ok(data.as_ref().map(|s| s.as_str()))
12+
}
13+
14+
pub(crate) fn try_module_item(module: &str, item: &str) -> Result {
15+
try_read(&format!("{}.{}", module, item))
16+
}
17+
18+
#[cfg(test)]
19+
mod test {
20+
use super::*;
21+
22+
#[test]
23+
fn test_module_item() {
24+
let doc = try_module_item("array", "_array_reconstructor").unwrap();
25+
assert!(doc.is_some());
26+
}
27+
}

derive/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod error;
1313
mod util;
1414

1515
mod compile_bytecode;
16+
mod doc;
1617
mod from_args;
1718
mod pyclass;
1819
mod pymodule;

derive/src/pymodule.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,19 @@ impl ModuleItem for FunctionItem {
261261
let sig_doc = text_signature(func.sig(), &py_name);
262262

263263
let item = {
264-
let doc = args.attrs.doc().map_or_else(TokenStream::new, |mut doc| {
265-
doc = format!("{}\n--\n\n{}", sig_doc, doc);
266-
quote!(.with_doc(#doc.to_owned(), &vm.ctx))
267-
});
268264
let module = args.module_name();
265+
let doc = args.attrs.doc().or_else(|| {
266+
crate::doc::try_module_item(module, &py_name)
267+
.ok() // TODO: doc must exist at least one of code or CPython
268+
.flatten()
269+
.map(str::to_owned)
270+
});
271+
let doc = if let Some(doc) = doc {
272+
format!("{}\n--\n\n{}", sig_doc, doc)
273+
} else {
274+
sig_doc
275+
};
276+
let doc = quote!(.with_doc(#doc.to_owned(), &vm.ctx));
269277
let new_func = quote_spanned!(ident.span()=>
270278
vm.ctx.make_funcdef(#py_name, #ident)
271279
#doc

extra_tests/docs_gen.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import sys
2+
import warnings
3+
from pydoc import ModuleScanner
4+
5+
6+
def scan_modules():
7+
"""taken from the source code of help('modules')
8+
9+
https://github.com/python/cpython/blob/63298930fb531ba2bb4f23bc3b915dbf1e17e9e1/Lib/pydoc.py#L2178"""
10+
modules = {}
11+
12+
def callback(path, modname, desc, modules=modules):
13+
if modname and modname[-9:] == ".__init__":
14+
modname = modname[:-9] + " (package)"
15+
if modname.find(".") < 0:
16+
modules[modname] = 1
17+
18+
def onerror(modname):
19+
callback(None, modname, None)
20+
21+
with warnings.catch_warnings():
22+
# ignore warnings from importing deprecated modules
23+
warnings.simplefilter("ignore")
24+
ModuleScanner().run(callback, onerror=onerror)
25+
return list(modules.keys())
26+
27+
28+
def import_module(module_name):
29+
import io
30+
from contextlib import redirect_stdout
31+
# Importing modules causes ('Constant String', 2, None, 4) and
32+
# "Hello world!" to be printed to stdout.
33+
f = io.StringIO()
34+
with warnings.catch_warnings(), redirect_stdout(f):
35+
# ignore warnings caused by importing deprecated modules
36+
warnings.filterwarnings("ignore", category=DeprecationWarning)
37+
try:
38+
module = __import__(module_name)
39+
except Exception as e:
40+
return e
41+
return module
42+
43+
44+
def is_child(module, item):
45+
import inspect
46+
item_mod = inspect.getmodule(item)
47+
return item_mod is module
48+
49+
50+
def traverse(module, names, item):
51+
import inspect
52+
is_builtin = inspect.ismodule(item) or inspect.isbuiltin(item)
53+
if is_builtin and isinstance(item.__doc__, str):
54+
yield names, item.__doc__
55+
attr_names = dir(item)
56+
for name in attr_names:
57+
if name in ['__class__', '__dict__', '__doc__', '__objclass__', '__name__', '__qualname__']:
58+
continue
59+
try:
60+
attr = getattr(item, name)
61+
except AttributeError:
62+
assert name == '__abstractmethods__'
63+
continue
64+
65+
if module is item and not is_child(module, attr):
66+
continue
67+
68+
is_type_or_module = (type(attr) is type) or (type(attr) is type(__builtins__))
69+
new_names = names.copy()
70+
new_names.append(name)
71+
72+
if item == attr:
73+
pass
74+
elif not inspect.ismodule(item) and inspect.ismodule(attr):
75+
pass
76+
elif is_type_or_module:
77+
yield from traverse(module, new_names, attr)
78+
elif callable(attr) or not issubclass(type(attr), type) or type(attr).__name__ in ('getset_descriptor', 'member_descriptor'):
79+
if inspect.isbuiltin(attr):
80+
yield new_names, attr.__doc__
81+
else:
82+
assert False, (module, new_names, attr, type(attr).__name__)
83+
84+
85+
def traverse_all():
86+
for module_name in scan_modules():
87+
if module_name in ('this', 'antigravity'):
88+
continue
89+
module = import_module(module_name)
90+
if hasattr(module, '__cached__'): # python module
91+
continue
92+
yield from traverse(module, [module_name], module)
93+
94+
95+
def docs():
96+
docs = {'.'.join(names): doc for names, doc in traverse_all()}
97+
return docs
98+
99+
if __name__ == '__main__':
100+
import json
101+
print(json.dumps(docs(), indent=4, sort_keys=True))

extra_tests/not_impl_gen.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import re
1414
import sys
1515
import warnings
16-
from contextlib import redirect_stdout
1716
from pydoc import ModuleScanner
1817

1918

@@ -172,7 +171,9 @@ def onerror(modname):
172171
return list(modules.keys())
173172

174173

175-
def dir_of_mod_or_error(module_name, keep_other=True):
174+
def import_module(module_name):
175+
import io
176+
from contextlib import redirect_stdout
176177
# Importing modules causes ('Constant String', 2, None, 4) and
177178
# "Hello world!" to be printed to stdout.
178179
f = io.StringIO()
@@ -183,13 +184,23 @@ def dir_of_mod_or_error(module_name, keep_other=True):
183184
module = __import__(module_name)
184185
except Exception as e:
185186
return e
187+
return module
188+
189+
190+
def is_child(module, item):
191+
import inspect
192+
item_mod = inspect.getmodule(item)
193+
return item_mod is module
194+
195+
196+
def dir_of_mod_or_error(module_name, keep_other=True):
197+
module = import_module(module_name)
186198
item_names = sorted(set(dir(module)))
187199
result = {}
188200
for item_name in item_names:
189201
item = getattr(module, item_name)
190-
item_mod = inspect.getmodule(item)
191202
# don't repeat items imported from other modules
192-
if keep_other or item_mod is module or item_mod is None:
203+
if keep_other or is_child(module, item) or inspect.getmodule(item) is None:
193204
result[item_name] = extra_info(item)
194205
return result
195206

0 commit comments

Comments
 (0)