Given a company-internal git repository for a pure Python3 application (multiple packages inside single root package, fwiw) is it possible to support the following development/deployment usecases with a single unified setup.py
? Even if not, I’m thankful for constructive directions as to how to handle such use cases in a sane manner; please bear in mind that I’m new to this forum and I’m not a terribly experienced Python aficionado.
Note bene: this is a “pure” Python3 source code base without any external C/C++ modules; cythonization is only used for container deployment to “obfuscate” the sources.
-
in-place development:
pip3 install -e .
inside the repository base; for working on the source code. -
source install:
pip3 install .
inside the repository base; to install the unobfuscated sources into the system/venv, no cythonization, no “external modules”. -
cythonized install: install only the obfuscated cythonized files into the system/venv, but not any sources.
-
build binary wheel:
python3 setup.py bdist_wheel
, then install the wheel and use only the installed binary files in the final container image.
So far, I’ve managed to get use cases 1. and 4. working, where 4. is basically done via a custom build_py
class.
# https://stackoverflow.com/a/56043918
from setuptools.command.build_py import build_py as build_py_orig
try:
from Cython.Build import cythonize
except:
cythonize = None
# https://stackoverflow.com/a/56043918
extensions = [
Extension('spam.*', ['spam/**/*.py'],
extra_compile_args=["-O3", "-Wall"]),
]
cython_excludes = ['spam/**/__init__.py']
def not_cythonized(tup):
(package, module, filepath) = tup
return any(
fnmatch.fnmatchcase(filepath, pat=pattern) for pattern in cython_excludes
) or not any(
fnmatch.fnmatchcase(filepath, pat=pattern)
for ext in extensions
for pattern in ext.sources
)
class build_py(build_py_orig):
def find_modules(self):
modules = super().find_modules()
return list(filter(not_cythonized, modules))
def find_package_modules(self, package, package_dir):
modules = super().find_package_modules(package, package_dir)
return list(filter(not_cythonized, modules))
setup(
name='spam',
packages=find_packages(),
ext_modules=cythonize(
extensions,
exclude=cython_excludes,
compiler_directives={
"language_level": 3,
"always_allow_keywords": True,
},
build_dir="build", # needs to be explicitly set, otherwise pollutes package sources
) if cythonize is not None else [],
cmdclass={
'build_py': build_py,
},
include_package_data=True,
install_requires=[...]
)
How can I support also use case 2 and 3? I would suspect to use a custom install
class but then I’m stuck: how do I prevent setuptools’ install
class from trying to build a source wheel which then always includes both the source as well as the cythonized shared libraries? I’m fine if there is only use case 2 possible at a time and then document that otherwise for use case 3 two steps are required: python3 setup.py bdist_wheel && pip3 install spam-*.whl
.
How to properly deal with these multiple use cases? Admittedly, this is most probably not a much-needed situation, but specific to sometimes obfuscated private python projects. Any help, remedies?