Skip to content

Commit aa7e741

Browse files
authored
Merge pull request #194 from v923z/diag
added diagonal and updated extract_pyi from circuitpython
2 parents 4dcaaf1 + 475c0ae commit aa7e741

File tree

6 files changed

+275
-48
lines changed

6 files changed

+275
-48
lines changed

code/ulab.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
#include "user/user.h"
3333
#include "vector/vectorise.h"
3434

35-
#define ULAB_VERSION 1.0.0
35+
#define ULAB_VERSION 1.1.0
3636
#define xstr(s) str(s)
3737
#define str(s) #s
3838
#if ULAB_NUMPY_COMPATIBILITY
@@ -126,6 +126,9 @@ STATIC const mp_map_elem_t ulab_globals_table[] = {
126126
#if ULAB_CREATE_HAS_CONCATENATE
127127
{ MP_ROM_QSTR(MP_QSTR_concatenate), (mp_obj_t)&create_concatenate_obj },
128128
#endif
129+
#if ULAB_CREATE_HAS_DIAGONAL
130+
{ MP_ROM_QSTR(MP_QSTR_diagonal), (mp_obj_t)&create_diagonal_obj },
131+
#endif
129132
#if ULAB_MAX_DIMS > 1
130133
#if ULAB_CREATE_HAS_EYE
131134
{ MP_ROM_QSTR(MP_QSTR_eye), (mp_obj_t)&create_eye_obj },

code/ulab.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
// module constant
124124
#define ULAB_CREATE_HAS_ARANGE (1)
125125
#define ULAB_CREATE_HAS_CONCATENATE (1)
126+
#define ULAB_CREATE_HAS_DIAGONAL (1)
126127
#define ULAB_CREATE_HAS_EYE (1)
127128
#define ULAB_CREATE_HAS_FULL (1)
128129
#define ULAB_CREATE_HAS_LINSPACE (1)

code/ulab_create.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,67 @@ mp_obj_t create_concatenate(size_t n_args, const mp_obj_t *pos_args, mp_map_t *k
271271
MP_DEFINE_CONST_FUN_OBJ_KW(create_concatenate_obj, 1, create_concatenate);
272272
#endif
273273

274+
#if ULAB_CREATE_HAS_DIAGONAL
275+
//| def diagonal(a: ulab.array, *, offset: int = 0) -> ulab.array:
276+
//| """
277+
//| .. param: a
278+
//| an ndarray
279+
//| .. param: offset
280+
//| Offset of the diagonal from the main diagonal. Can be positive or negative.
281+
//|
282+
//| Return specified diagonals."""
283+
//| ...
284+
//|
285+
mp_obj_t create_diagonal(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
286+
static const mp_arg_t allowed_args[] = {
287+
{ MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ, { .u_rom_obj = mp_const_none } },
288+
{ MP_QSTR_offset, MP_ARG_KW_ONLY | MP_ARG_INT, { .u_int = 0 } },
289+
};
290+
291+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
292+
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
293+
294+
if(!MP_OBJ_IS_TYPE(args[0].u_obj, &ulab_ndarray_type)) {
295+
mp_raise_TypeError(translate("input must be an ndarray"));
296+
}
297+
ndarray_obj_t *source = MP_OBJ_TO_PTR(args[0].u_obj);
298+
if(source->ndim != 2) {
299+
mp_raise_TypeError(translate("input must be a tensor of rank 2"));
300+
}
301+
int32_t offset = args[1].u_int;
302+
size_t len = 0;
303+
uint8_t *sarray = (uint8_t *)source->array;
304+
if(offset < 0) { // move the pointer "vertically"
305+
sarray -= offset * source->strides[ULAB_MAX_DIMS - 2];
306+
if(-offset < (int32_t)source->shape[ULAB_MAX_DIMS - 2]) {
307+
len = source->shape[ULAB_MAX_DIMS - 1] + offset;
308+
}
309+
} else { // move the pointer "horizontally"
310+
if(offset < (int32_t)source->shape[ULAB_MAX_DIMS - 1]) {
311+
len = source->shape[ULAB_MAX_DIMS - 1] - offset;
312+
}
313+
sarray += offset * source->strides[ULAB_MAX_DIMS - 1];
314+
}
315+
316+
if(len == 0) {
317+
mp_raise_ValueError(translate("offset is too large"));
318+
}
319+
320+
ndarray_obj_t *target = ndarray_new_linear_array(len, source->dtype);
321+
uint8_t *tarray = (uint8_t *)target->array;
322+
323+
for(size_t i=0; i < len; i++) {
324+
memcpy(tarray, sarray, source->itemsize);
325+
sarray += source->strides[ULAB_MAX_DIMS - 2];
326+
sarray += source->strides[ULAB_MAX_DIMS - 1];
327+
tarray += source->itemsize;
328+
}
329+
return MP_OBJ_FROM_PTR(target);
330+
}
331+
332+
MP_DEFINE_CONST_FUN_OBJ_KW(create_diagonal_obj, 1, create_diagonal);
333+
#endif /* ULAB_CREATE_HAS_DIAGONAL */
334+
274335
#if ULAB_MAX_DIMS > 1
275336
#if ULAB_CREATE_HAS_EYE
276337
//| def eye(size: int, *, M: Optional[int] = None, k: int = 0, dtype: _DType = ulab.float) -> ulab.array:

code/ulab_create.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ mp_obj_t create_concatenate(size_t , const mp_obj_t *, mp_map_t *);
2525
MP_DECLARE_CONST_FUN_OBJ_KW(create_concatenate_obj);
2626
#endif
2727

28+
#if ULAB_CREATE_HAS_DIAGONAL
29+
mp_obj_t create_diagonal(size_t , const mp_obj_t *, mp_map_t *);
30+
MP_DECLARE_CONST_FUN_OBJ_KW(create_diagonal_obj);
31+
#endif
32+
2833
#if ULAB_MAX_DIMS > 1
2934
#if ULAB_CREATE_HAS_EYE
3035
mp_obj_t create_eye(size_t , const mp_obj_t *, mp_map_t *);

docs/manual/extract_pyi.py

Lines changed: 183 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,86 +2,222 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5+
# Run with 'python tools/extract_pyi.py shared-bindings/ path/to/stub/dir
6+
# You can also test a specific library in shared-bindings by putting the path
7+
# to that directory instead
8+
9+
import ast
510
import os
11+
import re
612
import sys
7-
import astroid
813
import traceback
914

10-
top_level = sys.argv[1].strip("/")
11-
stub_directory = sys.argv[2]
15+
import isort
16+
import black
17+
18+
19+
IMPORTS_IGNORE = frozenset({'int', 'float', 'bool', 'str', 'bytes', 'tuple', 'list', 'set', 'dict', 'bytearray', 'slice', 'file', 'buffer', 'range', 'array', 'struct_time'})
20+
IMPORTS_TYPING = frozenset({'Any', 'Optional', 'Union', 'Tuple', 'List', 'Sequence', 'NamedTuple', 'Iterable', 'Iterator', 'Callable', 'AnyStr', 'overload', 'Type'})
21+
IMPORTS_TYPES = frozenset({'TracebackType'})
22+
CPY_TYPING = frozenset({'ReadableBuffer', 'WriteableBuffer', 'AudioSample', 'FrameBuffer'})
23+
24+
25+
def is_typed(node, allow_any=False):
26+
if node is None:
27+
return False
28+
if allow_any:
29+
return True
30+
elif isinstance(node, ast.Name) and node.id == "Any":
31+
return False
32+
elif isinstance(node, ast.Attribute) and type(node.value) == ast.Name \
33+
and node.value.id == "typing" and node.attr == "Any":
34+
return False
35+
return True
36+
37+
38+
def find_stub_issues(tree):
39+
for node in ast.walk(tree):
40+
if isinstance(node, ast.AnnAssign):
41+
if not is_typed(node.annotation):
42+
yield ("WARN", f"Missing attribute type on line {node.lineno}")
43+
if isinstance(node.value, ast.Constant) and node.value.value == Ellipsis:
44+
yield ("WARN", f"Unnecessary Ellipsis assignment (= ...) on line {node.lineno}.")
45+
elif isinstance(node, ast.Assign):
46+
if isinstance(node.value, ast.Constant) and node.value.value == Ellipsis:
47+
yield ("WARN", f"Unnecessary Ellipsis assignment (= ...) on line {node.lineno}.")
48+
elif isinstance(node, ast.arguments):
49+
allargs = list(node.args + node.kwonlyargs)
50+
if sys.version_info >= (3, 8):
51+
allargs.extend(node.posonlyargs)
52+
for arg_node in allargs:
53+
if not is_typed(arg_node.annotation) and (arg_node.arg != "self" and arg_node.arg != "cls"):
54+
yield ("WARN", f"Missing argument type: {arg_node.arg} on line {arg_node.lineno}")
55+
if node.vararg and not is_typed(node.vararg.annotation, allow_any=True):
56+
yield ("WARN", f"Missing argument type: *{node.vararg.arg} on line {node.vararg.lineno}")
57+
if node.kwarg and not is_typed(node.kwarg.annotation, allow_any=True):
58+
yield ("WARN", f"Missing argument type: **{node.kwarg.arg} on line {node.kwarg.lineno}")
59+
elif isinstance(node, ast.FunctionDef):
60+
if not is_typed(node.returns):
61+
yield ("WARN", f"Missing return type: {node.name} on line {node.lineno}")
62+
63+
64+
def extract_imports(tree):
65+
modules = set()
66+
typing = set()
67+
types = set()
68+
cpy_typing = set()
69+
70+
def collect_annotations(anno_tree):
71+
if anno_tree is None:
72+
return
73+
for node in ast.walk(anno_tree):
74+
if isinstance(node, ast.Name):
75+
if node.id in IMPORTS_IGNORE:
76+
continue
77+
elif node.id in IMPORTS_TYPING:
78+
typing.add(node.id)
79+
elif node.id in IMPORTS_TYPES:
80+
types.add(node.id)
81+
elif node.id in CPY_TYPING:
82+
cpy_typing.add(node.id)
83+
elif isinstance(node, ast.Attribute):
84+
if isinstance(node.value, ast.Name):
85+
modules.add(node.value.id)
86+
87+
for node in ast.walk(tree):
88+
if isinstance(node, (ast.AnnAssign, ast.arg)):
89+
collect_annotations(node.annotation)
90+
elif isinstance(node, ast.Assign):
91+
collect_annotations(node.value)
92+
elif isinstance(node, ast.FunctionDef):
93+
collect_annotations(node.returns)
94+
for deco in node.decorator_list:
95+
if isinstance(deco, ast.Name) and (deco.id in IMPORTS_TYPING):
96+
typing.add(deco.id)
97+
98+
return {
99+
"modules": sorted(modules),
100+
"typing": sorted(typing),
101+
"types": sorted(types),
102+
"cpy_typing": sorted(cpy_typing),
103+
}
104+
105+
106+
def find_references(tree):
107+
for node in ast.walk(tree):
108+
if isinstance(node, ast.arguments):
109+
for node in ast.walk(node):
110+
if isinstance(node, ast.Attribute):
111+
if isinstance(node.value, ast.Name) and node.value.id[0].isupper():
112+
yield node.value.id
113+
12114

13115
def convert_folder(top_level, stub_directory):
14116
ok = 0
15117
total = 0
16118
filenames = sorted(os.listdir(top_level))
17-
pyi_lines = []
119+
stub_fragments = []
120+
references = set()
121+
18122
for filename in filenames:
19123
full_path = os.path.join(top_level, filename)
20124
file_lines = []
21125
if os.path.isdir(full_path):
22-
mok, mtotal = convert_folder(full_path, os.path.join(stub_directory, filename))
126+
(mok, mtotal) = convert_folder(full_path, os.path.join(stub_directory, filename))
23127
ok += mok
24128
total += mtotal
25129
elif filename.endswith(".c"):
26-
with open(full_path, "r") as f:
130+
with open(full_path, "r", encoding="utf-8") as f:
27131
for line in f:
132+
line = line.rstrip()
28133
if line.startswith("//|"):
29-
if line[3] == " ":
134+
if len(line) == 3:
135+
line = ""
136+
elif line[3] == " ":
30137
line = line[4:]
31-
elif line[3] == "\n":
32-
line = line[3:]
33138
else:
34-
continue
139+
line = line[3:]
140+
print("[WARN] There must be at least one space after '//|'")
35141
file_lines.append(line)
36142
elif filename.endswith(".pyi"):
37143
with open(full_path, "r") as f:
38-
file_lines.extend(f.readlines())
144+
file_lines.extend(line.rstrip() for line in f)
145+
146+
fragment = "\n".join(file_lines).strip()
147+
try:
148+
tree = ast.parse(fragment)
149+
except SyntaxError as e:
150+
print(f"[ERROR] Failed to parse a Python stub from {full_path}")
151+
traceback.print_exception(type(e), e, e.__traceback__)
152+
return (ok, total + 1)
153+
references.update(find_references(tree))
39154

40-
# Always put the contents from an __init__ first.
41-
if filename.startswith("__init__."):
42-
pyi_lines = file_lines + pyi_lines
43-
else:
44-
pyi_lines.extend(file_lines)
155+
if fragment:
156+
name = os.path.splitext(os.path.basename(filename))[0]
157+
if name == "__init__" or (name in references):
158+
stub_fragments.insert(0, fragment)
159+
else:
160+
stub_fragments.append(fragment)
45161

46-
if not pyi_lines:
47-
return ok, total
162+
if not stub_fragments:
163+
return (ok, total)
48164

49165
stub_filename = os.path.join(stub_directory, "__init__.pyi")
50166
print(stub_filename)
51-
stub_contents = "".join(pyi_lines)
167+
stub_contents = "\n\n".join(stub_fragments)
168+
169+
# Validate the stub code.
170+
try:
171+
tree = ast.parse(stub_contents)
172+
except SyntaxError as e:
173+
traceback.print_exception(type(e), e, e.__traceback__)
174+
return (ok, total)
175+
176+
error = False
177+
for (level, msg) in find_stub_issues(tree):
178+
if level == "ERROR":
179+
error = True
180+
print(f"[{level}] {msg}")
181+
182+
total += 1
183+
if not error:
184+
ok += 1
185+
186+
# Add import statements
187+
imports = extract_imports(tree)
188+
import_lines = ["from __future__ import annotations"]
189+
if imports["types"]:
190+
import_lines.append("from types import " + ", ".join(imports["types"]))
191+
if imports["typing"]:
192+
import_lines.append("from typing import " + ", ".join(imports["typing"]))
193+
if imports["cpy_typing"]:
194+
import_lines.append("from _typing import " + ", ".join(imports["cpy_typing"]))
195+
import_lines.extend(f"import {m}" for m in imports["modules"])
196+
import_body = "\n".join(import_lines)
197+
m = re.match(r'(\s*""".*?""")', stub_contents, flags=re.DOTALL)
198+
if m:
199+
stub_contents = m.group(1) + "\n\n" + import_body + "\n\n" + stub_contents[m.end():]
200+
else:
201+
stub_contents = import_body + "\n\n" + stub_contents
202+
203+
# Code formatting
204+
stub_contents = isort.code(stub_contents)
205+
stub_contents = black.format_str(stub_contents, mode=black.FileMode(is_pyi=True))
206+
52207
os.makedirs(stub_directory, exist_ok=True)
53208
with open(stub_filename, "w") as f:
54209
f.write(stub_contents)
55210

56-
# Validate that the module is a parseable stub.
57-
total += 1
58-
try:
59-
tree = astroid.parse(stub_contents)
60-
for i in tree.body:
61-
if 'name' in i.__dict__:
62-
print(i.__dict__['name'])
63-
for j in i.body:
64-
if isinstance(j, astroid.scoped_nodes.FunctionDef):
65-
if None in j.args.__dict__['annotations']:
66-
print(f"Missing parameter type: {j.__dict__['name']} on line {j.__dict__['lineno']}\n")
67-
if j.returns:
68-
if 'Any' in j.returns.__dict__.values():
69-
print(f"Missing return type: {j.__dict__['name']} on line {j.__dict__['lineno']}")
70-
elif isinstance(j, astroid.node_classes.AnnAssign):
71-
if 'name' in j.__dict__['annotation'].__dict__:
72-
if j.__dict__['annotation'].__dict__['name'] == 'Any':
73-
print(f"missing attribute type on line {j.__dict__['lineno']}")
211+
return (ok, total)
74212

75-
ok += 1
76-
except astroid.exceptions.AstroidSyntaxError as e:
77-
e = e.__cause__
78-
traceback.print_exception(type(e), e, e.__traceback__)
79-
print()
80-
return ok, total
81213

82-
ok, total = convert_folder(top_level, stub_directory)
214+
if __name__ == "__main__":
215+
top_level = sys.argv[1].strip("/")
216+
stub_directory = sys.argv[2]
217+
218+
(ok, total) = convert_folder(top_level, stub_directory)
83219

84-
print(f"{ok} ok out of {total}")
220+
print(f"Parsing .pyi files: {total - ok} failed, {ok} passed")
85221

86-
if ok != total:
87-
sys.exit(total - ok)
222+
if ok != total:
223+
sys.exit(total - ok)

0 commit comments

Comments
 (0)