-
Notifications
You must be signed in to change notification settings - Fork 51
Automatically generate Python Typing stub files for Cython extensions (_p4p, _gw) #212
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
base: master
Are you sure you want to change the base?
Changes from 24 commits
f1fd91d
4ccfb7c
df902e4
6c45ed9
b3b7a36
ac97302
800e3ec
8e1b84a
2a598d7
1dadd13
cf3d483
d4fc7a4
621ad67
1ad0299
b53aaa8
d9b7979
d0baf1c
c331d2d
9a950c0
3714a7b
34a52ff
d14fc67
f472700
d1a9544
8b48d02
d46fa98
f374d3f
d7f69ec
63aab2e
527bea7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,13 @@ | |
| from __future__ import print_function | ||
|
|
||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import sysconfig | ||
|
|
||
| from setuptools_dso import Extension, setup, cythonize | ||
| from setuptools import Command | ||
| from setuptools_dso import Extension, setup, cythonize, build_ext as _build_ext | ||
|
|
||
| import numpy | ||
|
|
||
|
|
@@ -31,15 +35,13 @@ def get_numpy_include_dirs(): | |
| if get_config_var('CMPLR_CLASS') in ('gcc', 'clang'): | ||
| cxxflags += ['-std=c++11'] | ||
| ldflags = [] | ||
| import sys | ||
| import platform | ||
| if sys.platform=='linux2' and not sysconfig.get_config_var('Py_DEBUG'): | ||
| if sys.platform.startswith('linux') and not sysconfig.get_config_var('Py_DEBUG'): | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears to fix a small bug in the existing versions of setup.py.
|
||
| # c++ debug symbols size is huge. ~20x code size. | ||
| # So we choose to only emit debug symbols when building for an interpreter | ||
| # with debugging enabled (aka 'python-dbg' on debian). | ||
| cxxflags += ['-g0'] | ||
|
|
||
| elif platform.system()=='Darwin': | ||
| elif sys.platform == 'darwin': | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made consistent with other usages. Removes the need to import |
||
| # avoid later failure where install_name_tool may run out of space. | ||
| # install_name_tool: changing install names or rpaths can't be redone for: | ||
| # ... because larger updated load commands do not fit (the program must be relinked, | ||
|
|
@@ -51,6 +53,170 @@ def get_numpy_include_dirs(): | |
| # return c++ types. | ||
| cppflags = get_config_var('CPPFLAGS') + [('__PYX_EXTERN_C','extern')] | ||
|
|
||
| # Fully-qualified module names for which .pyi stubs are generated. | ||
| # Must match the Extension name= values below. | ||
| _STUB_MODULES = ['p4p._p4p', 'p4p._gw'] | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't sure whether to use a glob here ( |
||
|
|
||
|
|
||
| class GenerateStubs(Command): | ||
| """Generate .pyi stub files for compiled extensions using mypy stubgen. | ||
|
|
||
| Invoked automatically by build_ext. To regenerate stubs without | ||
| rebuilding the extensions:: | ||
|
|
||
| python setup.py generate_stubs | ||
| """ | ||
|
|
||
| description = 'generate .pyi stub files for compiled extensions' | ||
| user_options = [] | ||
|
|
||
| def initialize_options(self): pass | ||
| def finalize_options(self): pass | ||
|
|
||
| def _find_stubgen(self): | ||
| """Locate the stubgen console script installed alongside this Python. | ||
|
|
||
| The scripts directory is checked first so that the environment-local | ||
| stubgen is preferred over any system-wide installation. | ||
| """ | ||
| stubgen = shutil.which('stubgen', path=sysconfig.get_path('scripts')) \ | ||
| or shutil.which('stubgen') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I've switched to using |
||
|
|
||
| if stubgen is None: | ||
| print( | ||
| "WARNING: stubgen not found; skipping stub generation. " | ||
| "Typing stub files (*.pyi) will not be generated", | ||
| file=sys.stderr, | ||
| ) | ||
| return None | ||
|
|
||
| return stubgen | ||
|
|
||
| def _make_env(self): | ||
| """Return a copy of os.environ with pvxslibs/epicscorelibs lib dirs | ||
| prepended to the platform DSO search path variable, so that stubgen | ||
| subprocesses can load the extensions in editable installs where the | ||
| relative RUNPATH embedded in the .so/.pyd does not resolve correctly. | ||
| """ | ||
| # pvxslibs exposes no public lib_path attribute; | ||
| # path derived by convention. | ||
| lib_dirs = [d for d in [ | ||
| os.path.join(os.path.dirname(pvxslibs.__file__), 'lib'), | ||
| epicscorelibs.path.lib_path, | ||
| ] if os.path.isdir(d)] | ||
| env = os.environ.copy() | ||
| if sys.platform == 'win32': | ||
| path_var = 'PATH' | ||
| elif sys.platform == 'darwin': | ||
| path_var = 'DYLD_LIBRARY_PATH' | ||
| else: | ||
| path_var = 'LD_LIBRARY_PATH' | ||
| existing = env.get(path_var, '') | ||
| env[path_var] = os.pathsep.join( | ||
| lib_dirs + ([existing] if existing else []) | ||
| ) | ||
|
|
||
| return env | ||
|
|
||
| @staticmethod | ||
| def _stub_src_path(mod): | ||
| """Return the source-tree path for a module's generated .pyi file.""" | ||
| return os.path.join('src', mod.replace('.', os.sep) + '.pyi') | ||
|
|
||
| def run(self): | ||
| """Verify extensions are importable, then invoke stubgen to write | ||
| .pyi files. | ||
| """ | ||
| stubgen = self._find_stubgen() | ||
| if stubgen is None: | ||
| return | ||
| env = self._make_env() | ||
|
|
||
| # For non-inplace builds (e.g. wheel), extensions land in | ||
| # build_lib rather than the source tree, so add it to PYTHONPATH | ||
| # for the subprocesses. | ||
| build_ext_cmd = self.get_finalized_command('build_ext') | ||
| if not build_ext_cmd.inplace: | ||
| existing = env.get('PYTHONPATH', '') | ||
| env['PYTHONPATH'] = os.pathsep.join( | ||
| [build_ext_cmd.build_lib] + ([existing] if existing else []) | ||
| ) | ||
|
|
||
| # Verify extensions are importable under the same env stubgen will use. | ||
| # Done in a subprocess so the current process's DSO search path (which | ||
| # may not include pvxslibs/lib for editable installs) is not a factor. | ||
| for mod in _STUB_MODULES: | ||
| check = subprocess.run( | ||
| [sys.executable, '-c', 'import %s' % mod], | ||
| env=env, capture_output=True, check=False, | ||
| ) | ||
| if check.returncode: | ||
| print( | ||
| "WARNING: extension %r not importable; " | ||
| "skipping stub generation. Build it first with:\n" | ||
| " python setup.py build_ext --inplace" % mod, | ||
| file=sys.stderr, | ||
| ) | ||
| return | ||
|
|
||
| # Run stubgen in a subprocess for DSO search path isolation -- | ||
| # the same reason as the importability check above. | ||
| cmd = [stubgen, '--include-docstrings', '--output', 'src'] | ||
| for mod in _STUB_MODULES: | ||
| cmd += ['-m', mod] | ||
|
|
||
| result = subprocess.run(cmd, env=env, check=False) | ||
| if result.returncode: | ||
| print( | ||
| "WARNING: stubgen failed with code %s" % result.returncode, | ||
| file=sys.stderr, | ||
| ) | ||
| return | ||
|
|
||
| missing = [ | ||
| path for path in | ||
| (self._stub_src_path(mod) for mod in _STUB_MODULES) | ||
| if not os.path.isfile(path) | ||
| ] | ||
| if missing: | ||
| print( | ||
| "WARNING: expected stubs not found:\n" + '\n'.join(missing), | ||
| file=sys.stderr, | ||
| ) | ||
| return | ||
|
|
||
| # build_py runs before build_ext, so *.pyi files generated above | ||
| # are not yet present when package_data is copied into build_lib. | ||
| # Copy them now. | ||
| if not build_ext_cmd.inplace: | ||
| for mod in _STUB_MODULES: | ||
| stub_src = self._stub_src_path(mod) | ||
| if os.path.isfile(stub_src): | ||
| stub_dst = os.path.join( | ||
| build_ext_cmd.build_lib, | ||
| mod.replace('.', os.sep) + '.pyi', | ||
| ) | ||
| shutil.copy2(stub_src, stub_dst) | ||
|
|
||
|
|
||
| class BuildExt(_build_ext): | ||
| """Extends the standard build_ext to invoke generate_stubs | ||
| after building. | ||
| """ | ||
|
|
||
| def run(self): | ||
| _build_ext.run(self) | ||
| if sys.version_info >= (3, 0): | ||
| print("Generating .pyi stubs...", file=sys.stderr) | ||
| self.run_command('generate_stubs') | ||
| else: | ||
| print( | ||
| "WARNING: skipping .pyi stub generation; " | ||
| "Python 2 not supported.", | ||
| file=sys.stderr, | ||
| ) | ||
|
|
||
|
|
||
| exts = cythonize([ | ||
| Extension( | ||
| name='p4p._p4p', | ||
|
|
@@ -145,7 +311,8 @@ def get_numpy_include_dirs(): | |
| 'p4p.asLib', | ||
| ], | ||
| package_dir={'':'src'}, | ||
| package_data={'p4p': ['*.conf', '*.service']}, | ||
| package_data={'p4p': ['*.conf', '*.service', '*.pyi']}, | ||
| cmdclass={'build_ext': BuildExt, 'generate_stubs': GenerateStubs}, | ||
| ext_modules = exts, | ||
| install_requires = install_requires, | ||
| extras_require={ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| # distutils: language = c++ | ||
| #cython: language_level=2 | ||
| #cython: binding=True | ||
|
|
||
| cimport cython | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| # distutils: language = c++ | ||
| #cython: language_level=2 | ||
| #cython: binding=True | ||
|
|
||
| cimport cython | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pip understands conditionals already, no need for special machinery.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps I'm misunderstanding the syntax here, but isn't this part of
build.ymlnot using the information inpyproject.toml?Experimenting shows that
produces a GitHub CI/CD error warning of
ERROR: You need Python 3.5 or later to use mypy.I could replace this with
pip download .but that would presumably interfere with the matrix?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pip download ...accepts the same syntax as would be found inpyproject.toml.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
D'oh! I understand; I've made the change now.