Merge pull request #8706 from dcbaker/wip/2021-04/cython-language

1st class Cython language support
pull/8844/head
Jussi Pakkanen 4 years ago committed by GitHub
commit 40e8a67a83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 33
      docs/markdown/Cython.md
  2. 3
      docs/markdown/Reference-tables.md
  3. 18
      docs/markdown/snippets/first-class-cython.md
  4. 1
      docs/sitemap.txt
  5. 77
      mesonbuild/backend/ninjabackend.py
  6. 32
      mesonbuild/build.py
  7. 2
      mesonbuild/compilers/__init__.py
  8. 2
      mesonbuild/compilers/compilers.py
  9. 79
      mesonbuild/compilers/cython.py
  10. 28
      mesonbuild/environment.py
  11. 5
      mesonbuild/interpreter/interpreter.py
  12. 5
      run_project_tests.py
  13. 19
      test cases/cython/1 basic/cytest.py
  14. 9
      test cases/cython/1 basic/libdir/cstorer.pxd
  15. 8
      test cases/cython/1 basic/libdir/meson.build
  16. 24
      test cases/cython/1 basic/libdir/storer.c
  17. 8
      test cases/cython/1 basic/libdir/storer.h
  18. 16
      test cases/cython/1 basic/libdir/storer.pyx
  19. 20
      test cases/cython/1 basic/meson.build
  20. 2
      test cases/cython/2 generated sources/configure.pyx.in
  21. 2
      test cases/cython/2 generated sources/g.in
  22. 14
      test cases/cython/2 generated sources/gen.py
  23. 12
      test cases/cython/2 generated sources/generator.py
  24. 61
      test cases/cython/2 generated sources/meson.build
  25. 13
      test cases/cython/2 generated sources/test.py

@ -0,0 +1,33 @@
---
title: Cython
short-description: Support for Cython in Meson
...
# Cython
Meson provides native support for cython programs starting with version 0.59.0.
This means that you can include it as a normal language, and create targets like
any other supported language:
```meson
lib = static_library(
'foo',
'foo.pyx',
)
```
Generally Cython is most useful when combined with the python module's
extension_module method:
```meson
project('my project', 'cython')
py = import('python')
dep_py3 = py.dependency()
py.extension_module(
'foo',
'foo.pyx',
dependencies : dep_py,
)
```

@ -34,6 +34,7 @@ These are return values of the `get_id` (Compiler family) and
| sun | Sun Fortran compiler | |
| valac | Vala compiler | |
| xc16 | Microchip XC16 C compiler | |
| cython | The Cython compiler | |
## Linker ids
@ -160,6 +161,7 @@ These are the parameter names for passing language specific arguments to your bu
| Objective C++ | objcpp_args | objcpp_link_args |
| Rust | rust_args | rust_link_args |
| Vala | vala_args | vala_link_args |
| Cython | cython_args | cython_link_args |
All these `<lang>_*` options are specified per machine. See in
[specifying options per
@ -186,6 +188,7 @@ arguments](#language-arguments-parameter-names) instead.
| DFLAGS | Flags for the D compiler |
| VALAFLAGS | Flags for the Vala compiler |
| RUSTFLAGS | Flags for the Rust compiler |
| CYTHONFLAGS | Flags for the Cython compiler |
| LDFLAGS | The linker flags, used for all languages |
N.B. these settings are specified per machine, and so the environment

@ -0,0 +1,18 @@
## Cython as as first class language
Meson now supports Cython as a first class language. This means you can write:
```meson
project('my project', 'cython')
py = import('python')
dep_py3 = py.dependency()
py.extension_module(
'foo',
'foo.pyx',
dependencies : dep_py,
)
```
And avoid the step through a generator that was previously required.

@ -59,6 +59,7 @@ index.md
Java.md
Vala.md
D.md
Cython.md
IDE-integration.md
Custom-build-targets.md
Build-system-converters.md

@ -749,6 +749,9 @@ int dummy;
# C/C++ sources, objects, generated libs, and unknown sources now.
target_sources, generated_sources, \
transpiled_sources = self.generate_vala_compile(target)
elif 'cython' in target.compilers:
target_sources, generated_sources, \
transpiled_sources = self.generate_cython_transpile(target)
else:
target_sources = self.get_target_sources(target)
generated_sources = self.get_target_generated_sources(target)
@ -1544,6 +1547,66 @@ int dummy;
self.create_target_source_introspection(target, valac, args, all_files, [])
return other_src[0], other_src[1], vala_c_src
def generate_cython_transpile(self, target: build.BuildTarget) -> \
T.Tuple[T.MutableMapping[str, File], T.MutableMapping[str, File], T.List[str]]:
"""Generate rules for transpiling Cython files to C or C++
XXX: Currently only C is handled.
"""
static_sources: T.MutableMapping[str, File] = OrderedDict()
generated_sources: T.MutableMapping[str, File] = OrderedDict()
cython_sources: T.List[str] = []
cython = target.compilers['cython']
opt_proxy = self.get_compiler_options_for_target(target)
args: T.List[str] = []
args += cython.get_always_args()
args += cython.get_buildtype_args(self.get_option_for_target(OptionKey('buildtype'), target))
args += cython.get_debug_args(self.get_option_for_target(OptionKey('debug'), target))
args += cython.get_optimization_args(self.get_option_for_target(OptionKey('optimization'), target))
args += cython.get_option_compile_args(opt_proxy)
args += self.build.get_global_args(cython, target.for_machine)
args += self.build.get_project_args(cython, target.subproject, target.for_machine)
for src in target.get_sources():
if src.endswith('.pyx'):
output = os.path.join(self.get_target_private_dir(target), f'{src}.c')
args = args.copy()
args += cython.get_output_args(output)
element = NinjaBuildElement(
self.all_outputs, [output],
self.compiler_to_rule_name(cython),
[src.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir())])
element.add_item('ARGS', args)
self.add_build(element)
# TODO: introspection?
cython_sources.append(output)
else:
static_sources[src.rel_to_builddir(self.build_to_src)] = src
for gen in target.get_generated_sources():
for ssrc in gen.get_outputs():
if isinstance(gen, GeneratedList):
ssrc = os.path.join(self.get_target_private_dir(target) , ssrc)
if ssrc.endswith('.pyx'):
args = args.copy()
output = os.path.join(self.get_target_private_dir(target), f'{ssrc}.c')
args += cython.get_output_args(output)
element = NinjaBuildElement(
self.all_outputs, [output],
self.compiler_to_rule_name(cython),
[ssrc])
element.add_item('ARGS', args)
self.add_build(element)
# TODO: introspection?
cython_sources.append(output)
else:
generated_sources[ssrc] = mesonlib.File.from_built_file(gen.subdir, ssrc)
return static_sources, generated_sources, cython_sources
def generate_rust_target(self, target: build.BuildTarget) -> None:
rustc = target.compilers['rust']
# Rust compiler takes only the main file as input and
@ -1889,10 +1952,7 @@ int dummy;
for for_machine in MachineChoice:
complist = self.environment.coredata.compilers[for_machine]
for langname, compiler in complist.items():
if langname == 'java' \
or langname == 'vala' \
or langname == 'rust' \
or langname == 'cs':
if langname in {'java', 'vala', 'rust', 'cs', 'cython'}:
continue
rule = '{}_LINKER{}'.format(langname, self.get_rule_suffix(for_machine))
command = compiler.get_linker_exelist()
@ -1940,6 +2000,12 @@ int dummy;
description = 'Compiling Vala source $in'
self.add_rule(NinjaRule(rule, command, [], description, extra='restat = 1'))
def generate_cython_compile_rules(self, compiler: 'Compiler') -> None:
rule = self.compiler_to_rule_name(compiler)
command = compiler.get_exelist() + ['$ARGS', '$in']
description = 'Compiling Cython source $in'
self.add_rule(NinjaRule(rule, command, [], description, extra='restat = 1'))
def generate_rust_compile_rules(self, compiler):
rule = self.compiler_to_rule_name(compiler)
command = compiler.get_exelist() + ['$ARGS', '$in']
@ -2012,6 +2078,9 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485'''))
if self.environment.machines.matches_build_machine(compiler.for_machine):
self.generate_swift_compile_rules(compiler)
return
if langname == 'cython':
self.generate_cython_compile_rules(compiler)
return
crstr = self.get_rule_suffix(compiler.for_machine)
if langname == 'fortran':
self.generate_fortran_dep_hack(crstr)

@ -41,9 +41,10 @@ from .interpreterbase import FeatureNew
if T.TYPE_CHECKING:
from ._typing import ImmutableListProtocol, ImmutableSetProtocol
from .interpreter.interpreter import Test, SourceOutputs
from .interpreter.interpreter import Test, SourceOutputs, Interpreter
from .mesonlib import FileMode, FileOrString
from .backend.backends import Backend
from .interpreter.interpreterobjects import GeneratorHolder
pch_kwargs = {'c_pch', 'cpp_pch'}
@ -63,6 +64,7 @@ lang_arg_kwargs = {
'rust_args',
'vala_args',
'cs_args',
'cython_args',
}
vala_kwargs = {'vala_header', 'vala_gir', 'vala_vapi'}
@ -810,7 +812,7 @@ class BuildTarget(Target):
# If all our sources are Vala, our target also needs the C compiler but
# it won't get added above.
if 'vala' in self.compilers and 'c' not in self.compilers:
if ('vala' in self.compilers or 'cython' in self.compilers) and 'c' not in self.compilers:
self.compilers['c'] = compilers['c']
def validate_sources(self):
@ -1564,7 +1566,7 @@ class Generator:
raise InvalidArguments('Depends entries must be build targets.')
self.depends.append(d)
def get_base_outnames(self, inname):
def get_base_outnames(self, inname) -> T.List[str]:
plainname = os.path.basename(inname)
basename = os.path.splitext(plainname)[0]
bases = [x.replace('@BASENAME@', basename).replace('@PLAINNAME@', plainname) for x in self.outputs]
@ -1586,7 +1588,7 @@ class Generator:
relpath = pathlib.PurePath(trial).relative_to(parent)
return relpath.parts[0] != '..' # For subdirs we can only go "down".
def process_files(self, name, files, state, preserve_path_from=None, extra_args=None):
def process_files(self, name, files, state: 'Interpreter', preserve_path_from=None, extra_args=None):
new = False
output = GeneratedList(self, state.subdir, preserve_path_from, extra_args=extra_args if extra_args is not None else [])
#XXX
@ -1621,14 +1623,14 @@ class Generator:
class GeneratedList:
def __init__(self, generator, subdir, preserve_path_from=None, extra_args=None):
def __init__(self, generator: 'GeneratorHolder', subdir: str, preserve_path_from=None, extra_args=None):
self.generator = unholder(generator)
self.name = self.generator.exe
self.depends = set() # Things this target depends on (because e.g. a custom target was used as input)
self.subdir = subdir
self.infilelist = []
self.outfilelist = []
self.outmap = {}
self.infilelist: T.List['File'] = []
self.outfilelist: T.List[str] = []
self.outmap: T.Dict['File', str] = {}
self.extra_depends = []
self.depend_files = []
self.preserve_path_from = preserve_path_from
@ -1642,17 +1644,17 @@ class GeneratedList:
# know the absolute path of
self.depend_files.append(File.from_absolute_file(path))
def add_preserved_path_segment(self, infile, outfiles, state):
result = []
def add_preserved_path_segment(self, infile: 'File', outfiles: T.List[str], state: 'Interpreter') -> T.List[str]:
result: T.List[str] = []
in_abs = infile.absolute_path(state.environment.source_dir, state.environment.build_dir)
assert(os.path.isabs(self.preserve_path_from))
assert os.path.isabs(self.preserve_path_from)
rel = os.path.relpath(in_abs, self.preserve_path_from)
path_segment = os.path.dirname(rel)
for of in outfiles:
result.append(os.path.join(path_segment, of))
return result
def add_file(self, newfile, state):
def add_file(self, newfile: 'File', state: 'Interpreter') -> None:
self.infilelist.append(newfile)
outfiles = self.generator.get_base_outnames(newfile.fname)
if self.preserve_path_from:
@ -1660,16 +1662,16 @@ class GeneratedList:
self.outfilelist += outfiles
self.outmap[newfile] = outfiles
def get_inputs(self):
def get_inputs(self) -> T.List['File']:
return self.infilelist
def get_outputs(self) -> T.List[str]:
return self.outfilelist
def get_outputs_for(self, filename):
def get_outputs_for(self, filename: 'File') -> T.List[str]:
return self.outmap[filename]
def get_generator(self):
def get_generator(self) -> 'Generator':
return self.generator
def get_extra_args(self):

@ -107,6 +107,7 @@ __all__ = [
'VisualStudioCCompiler',
'VisualStudioCPPCompiler',
'CLikeCompiler',
'CythonCompiler',
]
# Bring symbols from each module into compilers sub-package namespace
@ -213,3 +214,4 @@ from .mixins.gnu import GnuCompiler, GnuLikeCompiler
from .mixins.intel import IntelGnuLikeCompiler, IntelVisualStudioLikeCompiler
from .mixins.clang import ClangCompiler
from .mixins.clike import CLikeCompiler
from .cython import CythonCompiler

@ -64,6 +64,7 @@ lang_suffixes = {
'cs': ('cs',),
'swift': ('swift',),
'java': ('java',),
'cython': ('pyx', ),
} # type: T.Dict[str, T.Tuple[str, ...]]
all_languages = lang_suffixes.keys()
cpp_suffixes = lang_suffixes['cpp'] + ('h',) # type: T.Tuple[str, ...]
@ -97,6 +98,7 @@ CFLAGS_MAPPING: T.Mapping[str, str] = {
'd': 'DFLAGS',
'vala': 'VALAFLAGS',
'rust': 'RUSTFLAGS',
'cython': 'CYTHONFLAGS',
}
CEXE_MAPPING: T.Mapping = {

@ -0,0 +1,79 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2021 Intel Corporation
"""Abstraction for Cython language compilers."""
import typing as T
from .. import coredata
from ..mesonlib import EnvironmentException, OptionKey
from .compilers import Compiler
if T.TYPE_CHECKING:
from ..coredata import KeyedOptionDictType
from ..environment import Environment
class CythonCompiler(Compiler):
"""Cython Compiler."""
language = 'cython'
id = 'cython'
def needs_static_linker(self) -> bool:
# We transpile into C, so we don't need any linker
return False
def get_always_args(self) -> T.List[str]:
return ['--fast-fail']
def get_werror_args(self) -> T.List[str]:
return ['-Werror']
def get_output_args(self, outputname: str) -> T.List[str]:
return ['-o', outputname]
def get_optimization_args(self, optimization_level: str) -> T.List[str]:
# Cython doesn't have optimization levels itself, the underlying
# compiler might though
return []
def sanity_check(self, work_dir: str, environment: 'Environment') -> None:
code = 'print("hello world")'
with self.cached_compile(code, environment.coredata) as p:
if p.returncode != 0:
raise EnvironmentException(f'Cython compiler {self.id!r} cannot compile programs')
def get_buildtype_args(self, buildtype: str) -> T.List[str]:
# Cython doesn't implement this, but Meson requires an implementation
return []
def get_pic_args(self) -> T.List[str]:
# We can lie here, it's fine
return []
def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str],
build_dir: str) -> T.List[str]:
new: T.List[str] = []
for i in parameter_list:
new.append(i)
return new
def get_options(self) -> 'KeyedOptionDictType':
opts = super().get_options()
opts.update({
OptionKey('version', machine=self.for_machine, lang=self.language): coredata.UserComboOption(
'Python version to target',
['2', '3'],
'3',
)
})
return opts
def get_option_compile_args(self, options: 'KeyedOptionDictType') -> T.List[str]:
args: T.List[str] = []
key = options[OptionKey('version', machine=self.for_machine, lang=self.language)]
args.append(f'-{key.value}')
return args

@ -87,6 +87,7 @@ from .compilers import (
ClangObjCPPCompiler,
ClangClCCompiler,
ClangClCPPCompiler,
CythonCompiler,
FlangFortranCompiler,
G95FortranCompiler,
GnuCCompiler,
@ -732,6 +733,7 @@ class Environment:
self.default_rust = ['rustc']
self.default_swift = ['swiftc']
self.default_vala = ['valac']
self.default_cython = [['cython']]
self.default_static_linker = ['ar', 'gar']
self.default_strip = ['strip']
self.vs_static_linker = ['lib']
@ -1757,6 +1759,30 @@ class Environment:
self._handle_exceptions(popen_exceptions, compilers)
def detect_cython_compiler(self, for_machine: MachineChoice) -> CythonCompiler:
"""Search for a cython compiler."""
compilers = self.lookup_binary_entry(for_machine, 'cython')
is_cross = self.is_cross_build(for_machine)
info = self.machines[for_machine]
if compilers is None:
# TODO support fallback
compilers = [self.default_cython[0]]
popen_exceptions: T.Dict[str, Exception] = {}
for comp in compilers:
try:
err = Popen_safe(comp + ['-V'])[2]
except OSError as e:
popen_exceptions[' '.join(comp + ['-V'])] = e
continue
version = search_version(err)
if 'Cython' in err:
comp_class = CythonCompiler
self.coredata.add_lang_args(comp_class.language, comp_class, for_machine, self)
return comp_class(comp, version, for_machine, info, is_cross=is_cross)
self._handle_exceptions(popen_exceptions, compilers)
def detect_vala_compiler(self, for_machine):
exelist = self.lookup_binary_entry(for_machine, 'vala')
is_cross = self.is_cross_build(for_machine)
@ -2023,6 +2049,8 @@ class Environment:
comp = self.detect_fortran_compiler(for_machine)
elif lang == 'swift':
comp = self.detect_swift_compiler(for_machine)
elif lang == 'cython':
comp = self.detect_cython_compiler(for_machine)
else:
comp = None
return comp

@ -1193,8 +1193,9 @@ external dependencies (including libraries) must go to "dependencies".''')
args = [a.lower() for a in args]
langs = set(self.coredata.compilers[for_machine].keys())
langs.update(args)
if 'vala' in langs and 'c' not in langs:
FeatureNew('Adding Vala language without C', '0.59.0').use(self.subproject)
if ('vala' in langs or 'cython' in langs) and 'c' not in langs:
if 'vala' in langs:
FeatureNew.single_use('Adding Vala language without C', '0.59.0', self.subproject)
args.append('c')
success = True

@ -54,8 +54,8 @@ from run_tests import guess_backend
ALL_TESTS = ['cmake', 'common', 'native', 'warning-meson', 'failing-meson', 'failing-build', 'failing-test',
'keyval', 'platform-osx', 'platform-windows', 'platform-linux',
'java', 'C#', 'vala', 'rust', 'd', 'objective c', 'objective c++',
'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm'
'java', 'C#', 'vala', 'cython', 'rust', 'd', 'objective c', 'objective c++',
'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm',
]
@ -1016,6 +1016,7 @@ def detect_tests_to_run(only: T.Dict[str, T.List[str]], use_tmp: bool) -> T.List
TestCategory('java', 'java', backend is not Backend.ninja or mesonlib.is_osx() or not have_java()),
TestCategory('C#', 'csharp', skip_csharp(backend)),
TestCategory('vala', 'vala', backend is not Backend.ninja or not shutil.which(os.environ.get('VALAC', 'valac'))),
TestCategory('cython', 'cython', backend is not Backend.ninja or not shutil.which(os.environ.get('CYTHON', 'cython'))),
TestCategory('rust', 'rust', should_skip_rust(backend)),
TestCategory('d', 'd', backend is not Backend.ninja or not have_d_compiler()),
TestCategory('objective c', 'objc', backend not in (Backend.ninja, Backend.xcode) or not have_objc_compiler(options.use_tmpdir)),

@ -0,0 +1,19 @@
#!/usr/bin/env python3
from storer import Storer
s = Storer()
if s.get_value() != 0:
raise SystemExit('Initial value incorrect.')
s.set_value(42)
if s.get_value() != 42:
raise SystemExit('Setting value failed.')
try:
s.set_value('not a number')
raise SystemExit('Using wrong argument type did not fail.')
except TypeError:
pass

@ -0,0 +1,9 @@
cdef extern from "storer.h":
ctypedef struct Storer:
pass
Storer* storer_new();
void storer_destroy(Storer *s);
int storer_get_value(Storer *s);
void storer_set_value(Storer *s, int v);

@ -0,0 +1,8 @@
slib = py3.extension_module(
'storer',
'storer.pyx',
'storer.c',
dependencies : py3_dep
)
pydir = meson.current_build_dir()

@ -0,0 +1,24 @@
#include"storer.h"
#include<stdlib.h>
struct _Storer {
int value;
};
Storer* storer_new() {
Storer *s = malloc(sizeof(struct _Storer));
s->value = 0;
return s;
}
void storer_destroy(Storer *s) {
free(s);
}
int storer_get_value(Storer *s) {
return s->value;
}
void storer_set_value(Storer *s, int v) {
s->value = v;
}

@ -0,0 +1,8 @@
#pragma once
typedef struct _Storer Storer;
Storer* storer_new();
void storer_destroy(Storer *s);
int storer_get_value(Storer *s);
void storer_set_value(Storer *s, int v);

@ -0,0 +1,16 @@
cimport cstorer
cdef class Storer:
cdef cstorer.Storer* _c_storer
def __cinit__(self):
self._c_storer = cstorer.storer_new()
def __dealloc__(self):
cstorer.storer_destroy(self._c_storer)
cpdef int get_value(self):
return cstorer.storer_get_value(self._c_storer)
cpdef set_value(self, int value):
cstorer.storer_set_value(self._c_storer, value)

@ -0,0 +1,20 @@
project(
'basic cython project',
['cython', 'c'],
default_options : ['warning_level=3']
)
py_mod = import('python')
py3 = py_mod.find_installation()
py3_dep = py3.dependency(required : false)
if not py3_dep.found()
error('MESON_SKIP_TEST: Python library not found.')
endif
subdir('libdir')
test('cython tester',
py3,
args : files('cytest.py'),
env : ['PYTHONPATH=' + pydir]
)

@ -0,0 +1,2 @@
cpdef func():
return "Hello, World!"

@ -0,0 +1,2 @@
cpdef func():
return "Hello, World!"

@ -0,0 +1,14 @@
# SPDX-License-Identifier: Apache-2.0
import argparse
import textwrap
parser = argparse.ArgumentParser()
parser.add_argument('output')
args = parser.parse_args()
with open(args.output, 'w') as f:
f.write(textwrap.dedent('''\
cpdef func():
return "Hello, World!"
'''))

@ -0,0 +1,12 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('output')
args = parser.parse_args()
with open(args.input, 'r') as i, open(args.output, 'w') as o:
o.write(i.read())

@ -0,0 +1,61 @@
project(
'generated cython sources',
['cython'],
)
py_mod = import('python')
py3 = py_mod.find_installation('python3')
py3_dep = py3.dependency(required : false)
if not py3_dep.found()
error('MESON_SKIP_TEST: Python library not found.')
endif
ct = custom_target(
'ct',
input : 'gen.py',
output : 'ct.pyx',
command : [py3, '@INPUT@', '@OUTPUT@'],
)
ct_ext = py3.extension_module('ct', ct, dependencies : py3_dep)
test(
'custom target',
py3,
args : [files('test.py'), 'ct'],
env : ['PYTHONPATH=' + meson.current_build_dir()]
)
cf = configure_file(
input : 'configure.pyx.in',
output : 'cf.pyx',
copy : true,
)
cf_ext = py3.extension_module('cf', cf, dependencies : py3_dep)
test(
'configure file',
py3,
args : [files('test.py'), 'cf'],
env : ['PYTHONPATH=' + meson.current_build_dir()]
)
gen = generator(
find_program('generator.py'),
arguments : ['@INPUT@', '@OUTPUT@'],
output : '@BASENAME@.pyx',
)
g_ext = py3.extension_module(
'g',
gen.process('g.in'),
dependencies : py3_dep,
)
test(
'generator',
py3,
args : [files('test.py'), 'g'],
env : ['PYTHONPATH=' + meson.current_build_dir()]
)

@ -0,0 +1,13 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
import argparse
import importlib
parser = argparse.ArgumentParser()
parser.add_argument('mod')
args = parser.parse_args()
mod = importlib.import_module(args.mod)
assert mod.func() == 'Hello, World!'
Loading…
Cancel
Save