¿Cómo debo estructurar un paquete de Python que contiene código de Cython?

122

Me gustaría hacer un paquete de Python que contenga algún código de Cython . Tengo el código de Cython funcionando bien. Sin embargo, ahora quiero saber cómo empaquetarlo mejor.

Para la mayoría de las personas que solo desean instalar el paquete, me gustaría incluir el .carchivo que crea Cython y organizar la setup.pycompilación para producir el módulo. Entonces el usuario no necesita Cython instalado para instalar el paquete.

Pero para las personas que quieran modificar el paquete, me gustaría también como para proporcionar la Cython .pyxarchivos, y de alguna manera también permiten setup.pyconstruirlos usando Cython (por lo que los usuarios podrían necesitar instalar Cython).

¿Cómo debo estructurar los archivos en el paquete para atender estos dos escenarios?

La documentación de Cython da una pequeña guía . Pero no dice cómo hacer un single setup.pyque maneje ambos casos con / sin Cython.

Craig McQueen
fuente
1
Veo que la pregunta está obteniendo más votos positivos que cualquiera de las respuestas. Tengo curiosidad por saber por qué las personas pueden encontrar las respuestas insatisfactorias.
Craig McQueen
44
Encontré esta sección de la documentación , que da la respuesta exacta.
Será el

Respuestas:

72

Lo hice yo mismo ahora, en un paquete de Python simplerandom( BitBucket repo - EDIT: ahora github ) (no espero que sea un paquete popular, pero fue una buena oportunidad para aprender Cython).

Este método se basa en el hecho de que construir un .pyxarchivo con Cython.Distutils.build_ext(al menos con la versión 0.14 de Cython) siempre parece crear un .carchivo en el mismo directorio que el .pyxarchivo fuente .

Aquí hay una versión reducida de la setup.pyque espero muestre lo esencial:

from distutils.core import setup
from distutils.extension import Extension

try:
    from Cython.Distutils import build_ext
except ImportError:
    use_cython = False
else:
    use_cython = True

cmdclass = {}
ext_modules = []

if use_cython:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.pyx"]),
    ]
    cmdclass.update({'build_ext': build_ext})
else:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.c"]),
    ]

setup(
    name='mypackage',
    ...
    cmdclass=cmdclass,
    ext_modules=ext_modules,
    ...
)

También edité MANIFEST.inpara asegurarme de que mycythonmodule.cesté incluido en una distribución fuente (una distribución fuente que se crea con python setup.py sdist):

...
recursive-include cython *
...

No me comprometo mycythonmodule.ccon el control de versiones 'trunk' (o 'default' para Mercurial). Cuando hago un lanzamiento, necesito recordar hacer python setup.py build_extprimero, para asegurarme de que mycythonmodule.cesté presente y actualizado para la distribución del código fuente. También hago una rama de lanzamiento y confirmo el archivo C en la rama. De esa manera tengo un registro histórico del archivo C que se distribuyó con esa versión.

Craig McQueen
fuente
¡Gracias, esto es exactamente lo que necesitaba para un proyecto de Pyrex que estoy abriendo! El MANIFEST.in me hizo tropezar por un segundo, pero solo necesitaba esa línea. Incluyo el archivo C en el control de origen por interés, pero veo su punto de que es innecesario.
chmullig
Edité mi respuesta para explicar cómo el archivo C no está en troncal / predeterminado, sino que se agrega a una rama de lanzamiento.
Craig McQueen
1
@ CraigMcQueen gracias por la gran respuesta, ¡me ayudó mucho! Sin embargo, me pregunto si es un comportamiento deseado usar Cython cuando esté disponible. Me parece que sería mejor usar de forma predeterminada archivos c pregenerados, a menos que el usuario explícitamente quiera usar Cython, en cuyo caso puede establecer la variable de entorno o algo así. Eso haría que la instalación sea más estable / robusta, ya que el usuario puede obtener resultados diferentes según la versión de Cython que haya instalado; es posible que ni siquiera sepa que la tiene instalada y que está afectando la construcción del paquete.
Martinsos
20

Agregando a la respuesta de Craig McQueen: vea a continuación cómo anular el sdistcomando para que Cython compile automáticamente sus archivos fuente antes de crear una distribución fuente.

De esta forma, no corre el riesgo de distribuir accidentalmente Cfuentes obsoletas . También ayuda en el caso de que tenga un control limitado sobre el proceso de distribución, por ejemplo, al crear distribuciones automáticamente a partir de la integración continua, etc.

from distutils.command.sdist import sdist as _sdist

...

class sdist(_sdist):
    def run(self):
        # Make sure the compiled Cython files in the distribution are up-to-date
        from Cython.Build import cythonize
        cythonize(['cython/mycythonmodule.pyx'])
        _sdist.run(self)
cmdclass['sdist'] = sdist
kynan
fuente
19

http://docs.cython.org/en/latest/src/userguide/source_files_and_compilation.html#distributing-cython-modules

Se recomienda encarecidamente que distribuya los archivos .c generados, así como las fuentes de Cython, para que los usuarios puedan instalar su módulo sin necesidad de tener Cython disponible.

También se recomienda que la compilación de Cython no esté habilitada de forma predeterminada en la versión que distribuya. Incluso si el usuario tiene Cython instalado, probablemente no quiera usarlo solo para instalar su módulo. Además, es posible que la versión que tiene no sea la misma que usó y que no compile sus fuentes correctamente.

Esto simplemente significa que el archivo setup.py con el que se envía será simplemente un archivo distutils normal en los archivos .c generados, para el ejemplo básico que tendríamos en su lugar:

from distutils.core import setup
from distutils.extension import Extension
 
setup(
    ext_modules = [Extension("example", ["example.c"])]
)
Coronel Panic
fuente
7

¿Lo más fácil es incluir ambos pero solo usar el archivo c? Incluir el archivo .pyx es bueno, pero no es necesario una vez que tenga el archivo .c de todos modos. Las personas que desean recompilar el .pyx pueden instalar Pyrex y hacerlo manualmente.

De lo contrario, debe tener un comando personalizado build_ext para distutils que primero compila el archivo C. Cython ya incluye uno. http://docs.cython.org/src/userguide/source_files_and_compilation.html

Lo que no hace esa documentación es decir cómo hacer que esto sea condicional, pero

try:
     from Cython.distutils import build_ext
except ImportError:
     from distutils.command import build_ext

Debería manejarlo.

Lennart Regebro
fuente
1
Gracias por tu respuesta. Eso es razonable, aunque prefiero que se setup.pypueda construir directamente desde el .pyxarchivo cuando se instala Cython. Mi respuesta también lo ha implementado.
Craig McQueen
Bueno, ese es el punto central de mi respuesta. Simplemente no era un setup.py completo.
Lennart Regebro
4

Incluyendo (Cython) los archivos .c generados son bastante raros. Especialmente cuando incluimos eso en git. Prefiero usar setuptools_cython . Cuando Cython no está disponible, creará un huevo que tiene un entorno Cython incorporado, y luego construirá su código usando el huevo.

Un posible ejemplo: https://github.com/douban/greenify/blob/master/setup.py


Actualización (2017-01-05):

Desde entonces setuptools 18.0, no hay necesidad de usar setuptools_cython. Aquí hay un ejemplo para construir un proyecto Cython desde cero sin él setuptools_cython.

McKelvin
fuente
¿soluciona esto el problema de que Cython no se instale aunque lo especifique en setup_requires?
Kamil Sindi
¿tampoco es posible poner 'setuptools>=18.0'setup_requires en lugar de crear el método is_installed?
Kamil Sindi
1
@capitalistpug primer lugar usted necesita para asegurarse de que setuptools>=18.0se ha instalado, a continuación, sólo tiene que poner 'Cython >= 0.18'en setup_requires, y Cython se instalará durante la instalación progreso. Pero si está utilizando setuptools <18.0, incluso su cython específico en setup_requires, no se instalará, en este caso, debe considerar su uso setuptools_cython.
McKelvin
Gracias @ McKelvin, ¡esta parece ser una gran solución! ¿Hay alguna razón por la que deberíamos usar el otro enfoque, con cythonizing de los archivos de origen por adelantado, al lado de esto? Intenté su enfoque y parece ser algo lento cuando se instala (tarda un minuto en instalarse pero se construye en un segundo).
Martinsos
1
@Martinsos pip install wheel. Entonces debe ser la razón 1. Instale la rueda primero e intente nuevamente.
McKelvin
2

Este es un script de configuración que escribí que hace que sea más fácil incluir directorios anidados dentro de la compilación. Uno necesita ejecutarlo desde la carpeta dentro de un paquete.

Estructura Givig como esta:

__init__.py
setup.py
test.py
subdir/
      __init__.py
      anothertest.py

setup.py

from setuptools import setup, Extension
from Cython.Distutils import build_ext
# from os import path
ext_names = (
    'test',
    'subdir.anothertest',       
) 

cmdclass = {'build_ext': build_ext}
# for modules in main dir      
ext_modules = [
    Extension(
        ext,
        [ext + ".py"],            
    ) 
    for ext in ext_names if ext.find('.') < 0] 
# for modules in subdir ONLY ONE LEVEL DOWN!! 
# modify it if you need more !!!
ext_modules += [
    Extension(
        ext,
        ["/".join(ext.split('.')) + ".py"],     
    )
    for ext in ext_names if ext.find('.') > 0]

setup(
    name='name',
    ext_modules=ext_modules,
    cmdclass=cmdclass,
    packages=["base", "base.subdir"],
)
#  Build --------------------------
#  python setup.py build_ext --inplace

Compilación feliz;)

zzart
fuente
2

El simple truco que se me ocurrió:

from distutils.core import setup

try:
    from Cython.Build import cythonize
except ImportError:
    from pip import pip

    pip.main(['install', 'cython'])

    from Cython.Build import cythonize


setup(…)

Simplemente instale Cython si no se pudo importar. Probablemente no se deba compartir este código, pero para mis propias dependencias es lo suficientemente bueno.

kay - SE es malvado
fuente
2

Todas las demás respuestas dependen de

  • distutils
  • importar desde Cython.Build, lo que crea un problema de huevo y gallina entre requerir cython vía setup_requirese importarlo.

Una solución moderna es usar setuptools en su lugar, vea esta respuesta (el manejo automático de extensiones de Cython requiere setuptools 18.0, es decir, está disponible desde hace muchos años). Un estándar moderno setup.pycon manejo de requisitos, un punto de entrada y un módulo cython podría verse así:

from setuptools import setup, Extension

with open('requirements.txt') as f:
    requirements = f.read().splitlines()

setup(
    name='MyPackage',
    install_requires=requirements,
    setup_requires=[
        'setuptools>=18.0',  # automatically handles Cython extensions
        'cython>=0.28.4',
    ],
    entry_points={
        'console_scripts': [
            'mymain = mypackage.main:main',
        ],
    },
    ext_modules=[
        Extension(
            'mypackage.my_cython_module',
            sources=['mypackage/my_cython_module.pyx'],
        ),
    ],
)
bluenote10
fuente
Importar desde Cython.Buildel momento de la configuración me causa ImportError. Tener herramientas de configuración para compilar pyx es la mejor manera de hacerlo.
Carson Ip
1

La forma más fácil que encontré usando solo herramientas de configuración en lugar de la característica limitada distutils es

from setuptools import setup
from setuptools.extension import Extension
try:
    from Cython.Build import cythonize
except ImportError:
    use_cython = False
else:
    use_cython = True

ext_modules = []
if use_cython:
    ext_modules += cythonize('package/cython_module.pyx')
else:
    ext_modules += [Extension('package.cython_module',
                              ['package/cython_modules.c'])]

setup(name='package_name', ext_modules=ext_modules)
LSchueler
fuente
De hecho, con setuptools no hay necesidad de la importación explícita try / catched desde Cython.Build, vea mi respuesta.
bluenote10
0

Creo que encontré una forma bastante buena de hacerlo proporcionando un build_extcomando personalizado . La idea es la siguiente:

  1. Agrego los encabezados numpy anulando finalize_options()y haciendo import numpyen el cuerpo de la función, lo que evita muy bien el problema de que numpy no esté disponible antes de setup()instalarlo.

  2. Si cython está disponible en el sistema, se engancha en el check_extensions_list()método del comando y por cythoniza todos los módulos de cython desactualizados, reemplazándolos con extensiones C que luego puede manejar el build_extension() método. También proporcionamos la última parte de la funcionalidad en nuestro módulo: esto significa que si cython no está disponible pero tenemos una extensión C presente, todavía funciona, lo que le permite hacer distribuciones de origen.

Aquí está el código:

import re, sys, os.path
from distutils import dep_util, log
from setuptools.command.build_ext import build_ext

try:
    import Cython.Build
    HAVE_CYTHON = True
except ImportError:
    HAVE_CYTHON = False

class BuildExtWithNumpy(build_ext):
    def check_cython(self, ext):
        c_sources = []
        for fname in ext.sources:
            cname, matches = re.subn(r"(?i)\.pyx$", ".c", fname, 1)
            c_sources.append(cname)
            if matches and dep_util.newer(fname, cname):
                if HAVE_CYTHON:
                    return ext
                raise RuntimeError("Cython and C module unavailable")
        ext.sources = c_sources
        return ext

    def check_extensions_list(self, extensions):
        extensions = [self.check_cython(ext) for ext in extensions]
        return build_ext.check_extensions_list(self, extensions)

    def finalize_options(self):
        import numpy as np
        build_ext.finalize_options(self)
        self.include_dirs.append(np.get_include())

Esto le permite a uno escribir los setup()argumentos sin preocuparse por las importaciones y si uno tiene cython disponible:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
summentier
fuente