Package only binary compiled .so files of a python library compiled with Cython

前端 未结 4 988
说谎
说谎 2020-12-14 03:47

I have a package named mypack which inside has a module mymod.py, and the __init__.py. For some reason that is not in debate, I need

相关标签:
4条回答
  • 2020-12-14 04:19

    Unfortunately, the accepted answer with setting packages=[] is wrong and may break a lot of stuff, as can e.g. be seen in this question. Don't use it. Instead of excluding all packages from the dist, you should exclude only the python files that will be cythonized and compiled to shared objects.

    Below is a working example; it uses my recipe from the question Exclude single source file from python bdist_egg or bdist_wheel. The example project contains package spam with two modules, spam.eggs and spam.bacon, and a subpackage spam.fizz with one module spam.fizz.buzz:

    root
    ├── setup.py
    └── spam
        ├── __init__.py
        ├── bacon.py
        ├── eggs.py
        └── fizz
            ├── __init__.py
            └── buzz.py
    

    The module lookup is being done in the build_py command, so it is the one you need to subclass with custom behaviour.

    Simple case: compile all source code, make no exceptions

    If you are about to compile every .py file (including __init__.pys), it is already sufficient to override build_py.build_packages method, making it a noop. Because build_packages doesn't do anything, no .py file will be collected at all and the dist will include only cythonized extensions:

    import fnmatch
    from setuptools import find_packages, setup, Extension
    from setuptools.command.build_py import build_py as build_py_orig
    from Cython.Build import cythonize
    
    
    extensions = [
        # example of extensions with regex
        Extension('spam.*', ['spam/*.py']),
        # example of extension with single source file
        Extension('spam.fizz.buzz', ['spam/fizz/buzz.py']),
    ]
    
    
    class build_py(build_py_orig):
        def build_packages(self):
            pass
    
    
    setup(
        name='...',
        version='...',
        packages=find_packages(),
        ext_modules=cythonize(extensions),
        cmdclass={'build_py': build_py},
    )
    

    Complex case: mix cythonized extensions with source modules

    If you want to compile only selected modules and leave the rest untouched, you will need a bit more complex logic; in this case, you need to override module lookup. In the below example, I still compile spam.bacon, spam.eggs and spam.fizz.buzz to shared objects, but leave __init__.py files untouched, so they will be included as source modules:

    import fnmatch
    from setuptools import find_packages, setup, Extension
    from setuptools.command.build_py import build_py as build_py_orig
    from Cython.Build import cythonize
    
    
    extensions = [
        Extension('spam.*', ['spam/*.py']),
        Extension('spam.fizz.buzz', ['spam/fizz/buzz.py']),
    ]
    cython_excludes = ['**/__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='...',
        version='...',
        packages=find_packages(),
        ext_modules=cythonize(extensions, exclude=cython_excludes),
        cmdclass={'build_py': build_py},
    )
    
    0 讨论(0)
  • 2020-12-14 04:28

    While packaging as a wheel is definitely what you want, the original question was about excluding .py source files from the package. This is addressed in Using Cython to protect a Python codebase by @Teyras, but his solution uses a hack: it removes the packages argument from the call to setup(). This prevents the build_py step from running which does, indeed, exclude the .py files but it also excludes any data files you want included in the package. (For example my package has a data file called VERSION which contains the package version number.) A better solution would be replacing the build_py setup command with a custom command which only copies the data files.

    You also need the __init__.py file as described above. So the custom build_py command should create the __init_.py file. I found that the compiled __init__.so runs when the package is imported so all that is needed is an empty __init__.py file to tell Python that the directory is a module which is ok to import.

    Your custom build_py class would look like:

    import os
    from setuptools.command.build_py import build_py
    
    class CustomBuildPyCommand(build_py):
        def run(self):
            # package data files but not .py files
            build_py.build_package_data(self)
            # create empty __init__.py in target dirs
            for pdir in self.packages:
                open(os.path.join(self.build_lib, pdir, '__init__.py'), 'a').close()
    

    And configure setup to override the original build_py command:

    setup(
       ...
       cmdclass={'build_py': CustomBuildPyCommand},
    )
    
    0 讨论(0)
  • 2020-12-14 04:30

    This was exactly the sort of problem the Python wheels format – described in PEP 427 – was developed to address.

    Wheels are a replacement for Python eggs (which were/are problematic for a bunch of reasons) – they are supported by pip, can contain architecture-specific private binaries (here is one example of such an arrangement) and are accepted generally by the Python communities who have stakes in these kind of things.

    Here is one setup.py snippet from the aforelinked Python on Wheels article, showing how one sets up a binary distribution:

    import os
    from setuptools import setup
    from setuptools.dist import Distribution
    
    class BinaryDistribution(Distribution):
        def is_pure(self):
            return False
    
    setup(
        ...,
        include_package_data=True,
        distclass=BinaryDistribution,
    )
    

    … in leu of the older (but probably somehow still canonically supported) setuptools classes you are using. It’s very straightforward to make Wheels for your distribution purposes, as outlined – as I recall from experience, either the wheel modules’ build process is somewhat cognizant of virtualenv, or it’s very easy to use one within the other.

    In any case, trading in the setuptools egg-based APIs for wheel-based tooling should save you some serious pain, I should think.

    0 讨论(0)
  • 2020-12-14 04:37

    I suggest you use the wheel format (as suggested by fish2000). Then, in your setup.py, set the packages argument to []. Your Cython extension will still build and the resulting .so files will be included in the resulting wheel package.

    If your __init__.py is not included in the wheel, you can override the run method of build_ext class shipped by Cython and copy the file from your source tree to the build folder (the path can be found in self.build_lib).

    0 讨论(0)
提交回复
热议问题