Skip to content

Commit b7fa63c

Browse files
flowergrassdpgeorge
authored andcommitted
tools: Add gen-cpydiff.py to generate docs differences.
This patch introduces the a small framework to track differences between uPy and CPython. The framework consists of: - A set of "tests" which test for an individual feature that differs between uPy and CPy. Each test is like a normal uPy test in the test suite, but has a special comment at the start with some meta-data: a category (eg syntax, core language), a human-readable description of the difference, a cause, and a workaround. Following the meta-data there is a short code snippet which demonstrates the difference. See tests/cpydiff directory for the initial set of tests. - A program (this patch) which runs all the tests (on uPy and CPy) and generates nicely-formated .rst documenting the differences. - Integration into the docs build so that everything is automatic, and the differences appear in a way that is easy for users to read/reference (see latter commits). The idea with using this new framework is: - When a new difference is found it's easy to write a short test for it, along with a description, and add it to the existing ones. It's also easy for contributors to submit tests for differences they find. - When something is no longer different the tool will give an error and difference can be removed (or promoted to a proper feature test).
1 parent 86c7507 commit b7fa63c

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
MicroPython Differences from CPython
2+
====================================
3+
4+
The operations listed in this section produce conflicting results in MicroPython when compared to standard Python.
5+
6+
.. toctree::
7+
:maxdepth: 2
8+

tools/gen-cpydiff.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# This file is part of the MicroPython project, http://micropython.org/
2+
#
3+
# The MIT License (MIT)
4+
#
5+
# Copyright (c) 2016 Rami Ali
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in
15+
# all copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
# THE SOFTWARE.
24+
25+
""" gen-cpydiff generates documentation which outlines operations that differ between MicroPython
26+
and CPython. This script is called by the docs Makefile for html and Latex and may be run
27+
manually using the command make gen-cpydiff. """
28+
29+
import os
30+
import errno
31+
import subprocess
32+
import time
33+
import re
34+
from collections import namedtuple
35+
36+
TESTPATH = '../tests/cpydiff/'
37+
UPYPATH = '../unix/micropython'
38+
DOCPATH = '../docs/genrst/'
39+
INDEXTEMPLATE = '../docs/differences/index_template.txt'
40+
INDEX = 'index.rst'
41+
42+
HEADER = '.. This document was generated by tools/gen-cpydiff.py\n\n'
43+
UIMPORTLIST = {'struct', 'collections', 'json'}
44+
CLASSMAP = {'Core': 'Core Language', 'Types': 'Builtin Types'}
45+
INDEXPRIORITY = ['syntax', 'core_language', 'builtin_types', 'modules']
46+
RSTCHARS = ['=', '-', '~', '`', ':']
47+
SPLIT = '"""\n|categories: |description: |cause: |workaround: '
48+
TAB = ' '
49+
50+
Output = namedtuple('output', ['name', 'class_', 'desc', 'cause', 'workaround', 'code',
51+
'output_cpy', 'output_upy', 'status'])
52+
53+
def readfiles():
54+
""" Reads test files """
55+
tests = list(filter(lambda x: x.endswith('.py'), os.listdir(TESTPATH)))
56+
tests.sort()
57+
files = []
58+
59+
for test in tests:
60+
text = open(TESTPATH + test, 'r').read()
61+
62+
try:
63+
class_, desc, cause, workaround, code = [x.rstrip() for x in \
64+
list(filter(None, re.split(SPLIT, text)))]
65+
output = Output(test, class_, desc, cause, workaround, code, '', '', '')
66+
files.append(output)
67+
except IndexError:
68+
print('Incorrect format in file ' + TESTPATH + test)
69+
70+
return files
71+
72+
def uimports(code):
73+
""" converts CPython module names into MicroPython equivalents """
74+
for uimport in UIMPORTLIST:
75+
uimport = bytes(uimport, 'utf8')
76+
code = code.replace(uimport, b'u' + uimport)
77+
return code
78+
79+
def run_tests(tests):
80+
""" executes all tests """
81+
results = []
82+
for test in tests:
83+
with open(TESTPATH + test.name, 'rb') as f:
84+
input_cpy = f.read()
85+
input_upy = uimports(input_cpy)
86+
87+
process = subprocess.Popen('python', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
88+
output_cpy = [com.decode('utf8') for com in process.communicate(input_cpy)]
89+
90+
process = subprocess.Popen(UPYPATH, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
91+
output_upy = [com.decode('utf8') for com in process.communicate(input_upy)]
92+
93+
if output_cpy[0] == output_upy[0] and output_cpy[1] == output_upy[1]:
94+
status = 'Supported'
95+
print('Supported operation!\nFile: ' + TESTPATH + test.name)
96+
else:
97+
status = 'Unsupported'
98+
99+
output = Output(test.name, test.class_, test.desc, test.cause,
100+
test.workaround, test.code, output_cpy, output_upy, status)
101+
results.append(output)
102+
103+
results.sort(key=lambda x: x.class_)
104+
return results
105+
106+
def indent(block, spaces):
107+
""" indents paragraphs of text for rst formatting """
108+
new_block = ''
109+
for line in block.split('\n'):
110+
new_block += spaces + line + '\n'
111+
return new_block
112+
113+
def gen_table(contents):
114+
""" creates a table given any set of columns """
115+
xlengths = []
116+
ylengths = []
117+
for column in contents:
118+
col_len = 0
119+
for entry in column:
120+
lines = entry.split('\n')
121+
for line in lines:
122+
col_len = max(len(line) + 2, col_len)
123+
xlengths.append(col_len)
124+
for i in range(len(contents[0])):
125+
ymax = 0
126+
for j in range(len(contents)):
127+
ymax = max(ymax, len(contents[j][i].split('\n')))
128+
ylengths.append(ymax)
129+
130+
table_divider = '+' + ''.join(['-' * i + '+' for i in xlengths]) + '\n'
131+
table = table_divider
132+
for i in range(len(ylengths)):
133+
row = [column[i] for column in contents]
134+
row = [entry + '\n' * (ylengths[i]-len(entry.split('\n'))) for entry in row]
135+
row = [entry.split('\n') for entry in row]
136+
for j in range(ylengths[i]):
137+
k = 0
138+
for entry in row:
139+
width = xlengths[k]
140+
table += ''.join(['| {:{}}'.format(entry[j], width - 1)])
141+
k += 1
142+
table += '|\n'
143+
table += table_divider
144+
return table + '\n'
145+
146+
def gen_rst(results):
147+
""" creates restructured text documents to display tests """
148+
149+
# make sure the destination directory exists
150+
try:
151+
os.mkdir(DOCPATH)
152+
except OSError as e:
153+
if e.args[0] != errno.EEXIST and e.args[0] != errno.EISDIR:
154+
raise
155+
156+
toctree = []
157+
class_ = []
158+
for output in results:
159+
section = output.class_.split(',')
160+
for i in range(len(section)):
161+
section[i] = section[i].rstrip()
162+
if section[i] in CLASSMAP:
163+
section[i] = CLASSMAP[section[i]]
164+
if i >= len(class_) or section[i] != class_[i]:
165+
if i == 0:
166+
filename = section[i].replace(' ', '_').lower()
167+
rst = open(DOCPATH + filename + '.rst', 'w')
168+
rst.write(HEADER)
169+
rst.write(section[i] + '\n')
170+
rst.write(RSTCHARS[0] * len(section[i]))
171+
rst.write(time.strftime("\nGenerated %a %d %b %Y %X UTC\n\n", time.gmtime()))
172+
toctree.append(filename)
173+
else:
174+
rst.write(section[i] + '\n')
175+
rst.write(RSTCHARS[min(i, len(RSTCHARS)-1)] * len(section[i]))
176+
rst.write('\n\n')
177+
class_ = section
178+
rst.write('**' + output.desc + '**\n\n')
179+
if output.cause != 'Unknown':
180+
rst.write('**Cause:** ' + output.cause + '\n\n')
181+
if output.workaround != 'Unknown':
182+
rst.write('**Workaround:** ' + output.workaround + '\n\n')
183+
184+
rst.write('Sample code::\n\n' + indent(output.code, TAB) + '\n')
185+
output_cpy = indent(''.join(output.output_cpy[0:2]), TAB).rstrip()
186+
output_cpy = ('::\n\n' if output_cpy != '' else '') + output_cpy
187+
output_upy = indent(''.join(output.output_upy[0:2]), TAB).rstrip()
188+
output_upy = ('::\n\n' if output_upy != '' else '') + output_upy
189+
table = gen_table([['CPy output:', output_cpy], ['uPy output:', output_upy]])
190+
rst.write(table)
191+
192+
template = open(INDEXTEMPLATE, 'r')
193+
index = open(DOCPATH + INDEX, 'w')
194+
index.write(HEADER)
195+
index.write(template.read())
196+
for section in INDEXPRIORITY:
197+
if section in toctree:
198+
index.write(indent(section + '.rst', TAB))
199+
toctree.remove(section)
200+
for section in toctree:
201+
index.write(indent(section + '.rst', TAB))
202+
203+
def main():
204+
""" Main function """
205+
206+
# clear search path to make sure tests use only builtin modules
207+
os.environ['MICROPYPATH'] = ''
208+
209+
files = readfiles()
210+
results = run_tests(files)
211+
gen_rst(results)
212+
213+
main()

0 commit comments

Comments
 (0)