Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mypyc] Introduce subcommand to mypyc command line interface #10395

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypyc/test/test_commandline.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
try:
# Compile program
cmd = subprocess.run([sys.executable,
os.path.join(base_path, 'scripts', 'mypyc')] + args,
os.path.join(base_path, 'scripts', 'mypyc'), 'build'] + args,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd='tmp')
if 'ErrorOutput' in testcase.name or cmd.returncode != 0:
out += cmd.stdout
Expand Down
136 changes: 127 additions & 9 deletions scripts/mypyc
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import subprocess
import sys
import tempfile
import time
import argparse
from typing import Dict, List
from typing_extensions import Final
import shutil

base_path = os.path.join(os.path.dirname(__file__), '..')

setup_format = """\
from distutils.core import setup
Expand All @@ -27,29 +30,144 @@ from mypyc.build import mypycify
setup(name='mypyc_output',
ext_modules=mypycify({}, opt_level="{}"),
)
"""
""" # type: Final

def main() -> None:
build_dir = 'build' # can this be overridden??

MODE_BUILD = 'build' # type: Final
MODE_RUN = 'run' # type: Final
MODE_CLEAN = 'clean' # type: Final

mode_mapping = {
'build': MODE_BUILD,
'run': MODE_RUN,
'clean': MODE_CLEAN
} # type: Dict[str, str]


class Options:
def __init__(self) -> None:
self.files = [] # type: List[str]
self.build_dir = '' # type: str
self.mode = MODE_BUILD # type: str
self.opt_level = '3' # type: str


def parse_options() -> Options:
options = Options()
# keep default mypyc configs
options.opt_level = os.getenv("MYPYC_OPT_LEVEL", '3')
options.build_dir = 'build' # can this be overridden??

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='mypyc subcommand', dest='subcommand')
subparsers.required = True

# create subparsers
build_parser = subparsers.add_parser('build', help='compile python files')
run_parser = subparsers.add_parser('run', help='compile and run a single python file')
clean_parser = subparsers.add_parser('clean', help='clean build directory')

# construct build subcommand's extra param
build_parser.add_argument(metavar='files', nargs='+', dest='files',
help="Compile given files")

# construct run subcommand's extra param
run_parser.add_argument('file', type=str, help="Compile and run given file")


args = parser.parse_args()
options.mode = mode_mapping[args.subcommand]

if options.mode == MODE_BUILD:
options.files = args.files
elif options.mode == MODE_RUN:
options.files = [args.file]
else:
options.files = []
return options


def mypyc_compile(build_dir: str,
paths: List[str],
opt_level: str) -> int:
try:
os.mkdir(build_dir)
except FileExistsError:
pass

opt_level = os.getenv("MYPYC_OPT_LEVEL", '3')

setup_file = os.path.join(build_dir, 'setup.py')
with open(setup_file, 'w') as f:
f.write(setup_format.format(sys.argv[1:], opt_level))

f.write(setup_format.format(paths, opt_level))
# We don't use run_setup (like we do in the test suite) because it throws
# away the error code from distutils, and we don't care about the slight
# performance loss here.
env = os.environ.copy()
base_path = os.path.join(os.path.dirname(__file__), '..')
env['PYTHONPATH'] = base_path + os.pathsep + env.get('PYTHONPATH', '')
cmd = subprocess.run([sys.executable, setup_file, 'build_ext', '--inplace'], env=env)
return cmd.returncode


def mypyc_build(options: Options) -> None:
if len(options.files) < 1:
sys.exit("no source files provided")
returncode = mypyc_compile(options.build_dir, options.files, options.opt_level)
sys.exit(returncode)


def mypyc_run(options: Options) -> None:
if len(options.files) < 1:
sys.exit("no source file provided")
returncode = mypyc_compile(options.build_dir, options.files[0:1], options.opt_level)
if returncode != 0:
sys.exit(returncode)
module_name = os.path.basename(options.files[0])
module_name = os.path.splitext(module_name)[0]
import_command = "import {}".format(module_name)
# TODO: well this is dumb, we'd come up with a better way
env = os.environ.copy()
base_path = os.path.join(os.path.dirname(__file__), '..')
env['PYTHONPATH'] = base_path + os.pathsep + env.get('PYTHONPATH', '')
# TODO: How do we guarantee that we run the compiled version when both .py and .so files
# are in the same directory?
cmd = subprocess.run([sys.executable, '-c', import_command], env=env)
sys.exit(cmd.returncode)


def mypyc_clean(options: Options) -> None:
if os.path.exists(options.build_dir):
files = []
directories = []
# TODO: this simply hardcodes what generates now
# a better solution is to use some configuration files
# maybe use a `.mypyc_cache` folder to store them
with os.scandir(options.build_dir) as entries:
for entry in entries:
if entry.is_dir() and entry.path.find("temp") != -1:
directories.append(entry.path)
if entry.is_file() and (entry.path.endswith('.c') or entry.path.endswith('.h')
or entry.path.find('ops.txt') != -1):
files.append(entry.path)
for file in files:
try:
os.remove(file)
except OSError as e:
print('Error "{}" occurred when removing {}'.format(e.strerror, file))
for directory in directories:
shutil.rmtree(directory)
sys.exit(0)
else:
sys.exit("Build directory '{}' does not exists".format(options.build_dir))


def main() -> None:
options = parse_options()
if options.mode == MODE_BUILD:
mypyc_build(options)
elif options.mode == MODE_RUN:
mypyc_run(options)
elif options.mode == MODE_CLEAN:
mypyc_clean(options)


if __name__ == '__main__':
main()