Convert whats_left into a CPython script

Co-Authored-By: Jeong YunWon <jeong@youknowone.org>
This commit is contained in:
boris
2021-02-11 03:36:03 -05:00
committed by Jeong YunWon
parent 9e9d61022f
commit 65f0399ba0
2 changed files with 119 additions and 87 deletions

177
extra_tests/not_impl_gen.py → whats_left.py Normal file → Executable file
View File

@@ -1,20 +1,56 @@
# It's recommended to run this with `python3 -I not_impl_gen.py`, to make sure
# that nothing in your global Python environment interferes with what's being
# extracted here.
#
#!/usr/bin/env python3 -I
# This script generates Lib/snippets/whats_left_data.py with these variables defined:
# expected_methods - a dictionary mapping builtin objects to their methods
# cpymods - a dictionary mapping module names to their contents
# libdir - the location of RustPython's Lib/ directory.
import inspect
import io
#
# TODO: include this:
# which finds all modules it has available and
# creates a Python dictionary mapping module names to their contents, which is
# in turn used to generate a second Python script that also finds which modules
# it has available and compares that against the first dictionary we generated.
# We then run this second generated script with RustPython.
import argparse
import re
import os
import re
import sys
import json
import warnings
import inspect
import subprocess
import platform
from pydoc import ModuleScanner
GENERATED_FILE = "extra_tests/snippets/not_impl.py"
implementation = platform.python_implementation()
if implementation != "CPython":
sys.exit("whats_left.py must be run under CPython, got {implementation} instead")
def parse_args():
parser = argparse.ArgumentParser(description="Process some integers.")
parser.add_argument(
"--signature",
action="store_true",
help="print functions whose signatures don't match CPython's",
)
parser.add_argument(
"--json",
action="store_true",
help="print output as JSON (instead of line by line)",
)
args = parser.parse_args()
return args
args = parse_args()
# modules suggested for deprecation by PEP 594 (www.python.org/dev/peps/pep-0594/)
# some of these might be implemented, but they are not a priority
@@ -54,7 +90,7 @@ CPYTHON_SPECIFIC_MODS = {
'_testbuffer', '_testcapi', '_testimportmultiple', '_testinternalcapi', '_testmultiphase',
}
IGNORED_MODULES = {'this', 'antigravity'} | PEP_594_MODULES | CPYTHON_SPECIFIC_MODS
IGNORED_MODULES = {"this", "antigravity"} | PEP_594_MODULES | CPYTHON_SPECIFIC_MODS
sys.path = [
path
@@ -188,6 +224,7 @@ def scan_modules():
def import_module(module_name):
import io
from contextlib import redirect_stdout
# Importing modules causes ('Constant String', 2, None, 4) and
# "Hello world!" to be printed to stdout.
f = io.StringIO()
@@ -203,6 +240,7 @@ def import_module(module_name):
def is_child(module, item):
import inspect
item_mod = inspect.getmodule(item)
return item_mod is module
@@ -250,7 +288,7 @@ output = """\
output += gen_methods()
output += f"""
cpymods = {gen_modules()!r}
libdir = {os.path.abspath("../Lib/").encode('utf8')!r}
libdir = {os.path.abspath("Lib/").encode('utf8')!r}
"""
@@ -260,7 +298,7 @@ REUSED = [
extra_info,
dir_of_mod_or_error,
import_module,
is_child
is_child,
]
for fn in REUSED:
output += "".join(inspect.getsourcelines(fn)[0]) + "\n\n"
@@ -278,7 +316,7 @@ def compare():
import sys
import warnings
from contextlib import redirect_stdout
import json
import platform
def method_incompatability_reason(typ, method_name, real_method_value):
@@ -288,7 +326,7 @@ def compare():
is_inherited = not attr_is_not_inherited(typ, method_name)
if is_inherited:
return "inherited"
return "(inherited)"
value = extra_info(getattr(typ, method_name))
if value != real_method_value:
@@ -321,16 +359,20 @@ def compare():
rustpymods = {mod: dir_of_mod_or_error(mod) for mod in mod_names}
not_implemented = {}
failed_to_import = {}
missing_items = {}
mismatched_items = {}
result = {
"cpython_modules": {},
"implemented": {},
"not_implemented": {},
"failed_to_import": {},
"missing_items": {},
"mismatched_items": {},
}
for modname, cpymod in cpymods.items():
rustpymod = rustpymods.get(modname)
if rustpymod is None:
not_implemented[modname] = None
result["not_implemented"][modname] = None
elif isinstance(rustpymod, Exception):
failed_to_import[modname] = rustpymod
result["failed_to_import"][modname] = rustpymod.__class__.__name__ + str(rustpymod)
else:
implemented_items = sorted(set(cpymod) & set(rustpymod))
mod_missing_items = set(cpymod) - set(rustpymod)
@@ -343,48 +385,18 @@ def compare():
if rustpymod[item] != cpymod[item]
and not isinstance(cpymod[item], Exception)
]
if mod_missing_items:
missing_items[modname] = mod_missing_items
if mod_mismatched_items:
mismatched_items[modname] = mod_mismatched_items
if mod_missing_items or mod_mismatched_items:
if mod_missing_items:
result["missing_items"][modname] = mod_missing_items
if mod_mismatched_items:
result["mismatched_items"][modname] = mod_mismatched_items
else:
result["implemented"][modname] = None
# missing entire module
print("# modules")
for modname in not_implemented:
print(modname, "(entire module)")
for modname, exception in failed_to_import.items():
print(f"{modname} (exists but not importable: {exception})")
result["cpython_modules"] = cpymods
result["not_implementeds"] = not_implementeds
# missing from builtins
print("\n# builtin items")
for module, missing_methods in not_implementeds.items():
for method, reason in missing_methods.items():
print(f"{module}.{method}" + (f" ({reason})" if reason else ""))
# missing from modules
print("\n# stdlib items")
for modname, missing in missing_items.items():
for item in missing:
print(item)
print("\n# mismatching signatures (warnings)")
for modname, mismatched in mismatched_items.items():
for (item, rustpy_value, cpython_value) in mismatched:
if cpython_value == "ValueError('no signature found')":
continue # these items will never match
print(f"{item} {rustpy_value} != {cpython_value}")
result = {
"not_implemented": not_implemented,
"failed_to_import": failed_to_import,
"missing_items": missing_items,
"mismatched_items": mismatched_items,
}
print()
print("# out of", len(cpymods), "modules:")
for error_type, modules in result.items():
print("# ", error_type, len(modules))
print(json.dumps(result))
def remove_one_indent(s):
@@ -395,5 +407,54 @@ def remove_one_indent(s):
compare_src = inspect.getsourcelines(compare)[0][1:]
output += "".join(remove_one_indent(line) for line in compare_src)
with open("not_impl.py", "w") as f:
with open(GENERATED_FILE, "w") as f:
f.write(output + "\n")
subprocess.run(["cargo", "build", "--release", "--features=ssl"], check=True)
result = subprocess.run(
["cargo", "run", "--release", "--features=ssl", "-q", "--", GENERATED_FILE],
env={**os.environ.copy(), "RUSTPYTHONPATH": "Lib"},
text=True,
capture_output=True,
)
# The last line should be json output, the rest of the lines can contain noise
# because importing certain modules can print stuff to stdout/stderr
result = json.loads(result.stdout.splitlines()[-1])
if args.json:
print(json.dumps(result))
sys.exit()
# missing entire modules
print("# modules")
for modname in result["not_implemented"]:
print(modname, "(entire module)")
for modname, exception in result["failed_to_import"].items():
print(f"{modname} (exists but not importable: {exception})")
# missing from builtins
print("\n# builtin items")
for module, missing_methods in result["not_implementeds"].items():
for method, reason in missing_methods.items():
print(f"{module}.{method}" + (f" {reason}" if reason else ""))
# missing from modules
print("\n# stdlib items")
for modname, missing in result["missing_items"].items():
for item in missing:
print(item)
if args.signature:
print("\n# mismatching signatures (warnings)")
for modname, mismatched in result["mismatched_items"].items():
for (item, rustpy_value, cpython_value) in mismatched:
if cpython_value == "ValueError('no signature found')":
continue # these items will never match
print(f"{item} {rustpy_value} != {cpython_value}")
print()
print("# summary")
for error_type, modules in result.items():
print("# ", error_type, len(modules))

View File

@@ -1,29 +0,0 @@
#!/bin/bash
set -e
# This script runs a Python script which finds all modules it has available and
# creates a Python dictionary mapping module names to their contents, which is
# in turn used to generate a second Python script that also finds which modules
# it has available and compares that against the first dictionary we generated.
# We then run this second generated script with RustPython.
cd "$(dirname "$0")"
export RUSTPYTHONPATH=Lib
(
cd extra_tests
# -I means isolate from environment; we don't want any pip packages to be listed
python3 -I not_impl_gen.py
)
# This takes a while
if command -v black &> /dev/null; then
black -q extra_tests/not_impl.py
fi
# show the building first, so people aren't confused why it's taking so long to
# run whats_left
cargo build --release --features=ssl
cargo run --release --features=ssl -q -- extra_tests/not_impl.py