Comment structurer un package Python contenant du code Cython

122

Je voudrais créer un package Python contenant du code Cython . J'ai le code Cython qui fonctionne bien. Cependant, je veux maintenant savoir comment le conditionner au mieux.

Pour la plupart des gens qui souhaitent simplement installer le package, j'aimerais inclure le .cfichier créé par Cython et prendre des dispositions pour setup.pyle compiler pour produire le module. Ensuite, l'utilisateur n'a pas besoin d'installer Cython pour installer le package.

Mais pour les personnes qui souhaitent modifier le package, j'aimerais également fournir les .pyxfichiers Cython , et autoriser d'une manière ou d'une autre setup.pyà les construire en utilisant Cython (donc ces utilisateurs auraient besoin de Cython installé).

Comment dois-je structurer les fichiers du package pour répondre à ces deux scénarios?

La documentation Cython donne quelques conseils . Mais il ne dit pas comment en créer un setup.pyqui gère à la fois les cas avec / sans Cython.

Craig McQueen
la source
1
Je vois que la question reçoit plus de votes positifs que n'importe laquelle des réponses. Je suis curieux de savoir pourquoi les gens peuvent trouver les réponses insatisfaisantes.
Craig McQueen
4
J'ai trouvé cette section de la documentation , qui donne exactement la réponse.
Will

Réponses:

72

Je l'ai fait moi-même maintenant, dans un package Python simplerandom( BitBucket repo - EDIT: now github ) (je ne m'attends pas à ce que ce soit un package populaire, mais c'était une bonne chance d'apprendre Cython).

Cette méthode repose sur le fait que la construction d'un .pyxfichier avec Cython.Distutils.build_ext(au moins avec Cython version 0.14) semble toujours créer un .cfichier dans le même répertoire que le .pyxfichier source .

Voici une version réduite setup.pydont je l'espère montre l'essentiel:

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,
    ...
)

J'ai également édité MANIFEST.inpour m'assurer qu'il mycythonmodule.cest inclus dans une distribution source (une distribution source créée avec python setup.py sdist):

...
recursive-include cython *
...

Je ne m'engage pas mycythonmodule.csur le contrôle de version «trunk» (ou «default» pour Mercurial). Lorsque je fais une version, je dois me rappeler de faire une python setup.py build_extpremière, pour m'assurer qu'elle mycythonmodule.cest présente et à jour pour la distribution du code source. Je crée également une branche de publication et je valide le fichier C dans la branche. De cette façon, j'ai un enregistrement historique du fichier C qui a été distribué avec cette version.

Craig McQueen
la source
Merci, c'est exactement ce dont j'avais besoin pour un projet Pyrex que j'ouvre! Le MANIFEST.in m'a fait trébucher pendant une seconde, mais j'avais juste besoin de cette ligne. J'inclus le fichier C dans le contrôle de code source par intérêt, mais je vois votre point qu'il est inutile.
chmullig
J'ai modifié ma réponse pour expliquer comment le fichier C n'est pas dans le tronc / par défaut, mais est ajouté à une branche de publication.
Craig McQueen
1
@CraigMcQueen merci pour la bonne réponse, cela m'a beaucoup aidé! Je me demande cependant si le comportement souhaité est d'utiliser Cython lorsqu'il est disponible? Il me semble qu'il serait préférable d'utiliser par défaut des fichiers c pré-générés, à moins que l'utilisateur ne veuille explicitement utiliser Cython, auquel cas il peut définir la variable d'environnement ou quelque chose. Cela rendrait l'installation plus stable / robuste, car l'utilisateur peut obtenir des résultats différents en fonction de la version de Cython qu'il a installée - il peut même ne pas être conscient qu'il l'a installé et que cela affecte la construction du package.
Martinsos
20

Ajout à la réponse de Craig McQueen: voir ci-dessous comment remplacer la sdistcommande pour que Cython compile automatiquement vos fichiers source avant de créer une distribution source.

De cette façon, vous ne courez aucun risque de distribuer accidentellement des Csources obsolètes . Cela aide également dans le cas où vous avez un contrôle limité sur le processus de distribution, par exemple lors de la création automatique de distributions à partir d'une intégration continue, 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
la source
19

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

Il est fortement recommandé de distribuer les fichiers .c générés ainsi que vos sources Cython, afin que les utilisateurs puissent installer votre module sans avoir besoin de disposer de Cython.

Il est également recommandé que la compilation Cython ne soit pas activée par défaut dans la version que vous distribuez. Même si l'utilisateur a installé Cython, il ne veut probablement pas l'utiliser uniquement pour installer votre module. En outre, la version dont il dispose peut ne pas être la même que celle que vous avez utilisée et peut ne pas compiler vos sources correctement.

Cela signifie simplement que le fichier setup.py que vous livrez sera juste un fichier distutils normal sur les fichiers .c générés, pour l'exemple de base que nous aurions à la place:

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

Le plus simple est d'inclure les deux mais d'utiliser simplement le fichier c? L'inclusion du fichier .pyx est bien, mais ce n'est pas nécessaire une fois que vous avez le fichier .c de toute façon. Les personnes qui souhaitent recompiler le .pyx peuvent installer Pyrex et le faire manuellement.

Sinon, vous devez avoir une commande build_ext personnalisée pour distutils qui construit d'abord le fichier C. Cython en inclut déjà un. http://docs.cython.org/src/userguide/source_files_and_compilation.html

Ce que cette documentation ne fait pas, c'est dire comment rendre cela conditionnel, mais

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

Devrait le gérer.

Lennart Regebro
la source
1
Merci pour votre réponse. C'est raisonnable, bien que je préfère que le setup.pypuisse construire directement à partir du .pyxfichier lorsque Cython est installé. Ma réponse a également mis en œuvre cela.
Craig McQueen
Eh bien, c'est tout le but de ma réponse. Ce n'était tout simplement pas un setup.py complet.
Lennart Regebro
4

L'inclusion de fichiers .c générés par (Cython) est assez étrange. Surtout quand nous incluons cela dans git. Je préfère utiliser setuptools_cython . Lorsque Cython n'est pas disponible, il construira un œuf avec un environnement Cython intégré, puis construira votre code à l'aide de l'oeuf.

Un exemple possible: https://github.com/douban/greenify/blob/master/setup.py


Mise à jour (2017-01-05):

Depuis setuptools 18.0, il n'y a plus besoin d'utiliser setuptools_cython. Voici un exemple pour créer un projet Cython à partir de zéro sans setuptools_cython.

McKelvin
la source
est-ce que cela résout le problème de Cython qui n'est pas installé même si vous le spécifiez dans setup_requires?
Kamil Sindi le
n'est pas non plus possible de mettre 'setuptools>=18.0'setup_requires au lieu de créer la méthode is_installed?
Kamil Sindi le
1
@capitalistpug D' abord , vous devez vous assurer setuptools>=18.0a été installé, il vous suffit de mettre 'Cython >= 0.18'en setup_requireset Cython sera installé lors de l' installation des progrès. Mais si vous utilisez setuptools <18.0, même si vous spécifiez cython dans setup_requires, il ne sera pas installé, dans ce cas, vous devriez envisager de l'utiliser setuptools_cython.
McKelvin
Merci @McKelvin, cela semble être une excellente solution! Y a-t-il une raison pour laquelle devrions-nous utiliser l'autre approche, avec cythonisation des fichiers sources à l'avance, à côté de cela? J'ai essayé votre approche et cela semble être un peu lent lors de l'installation (cela prend une minute à installer mais se construit en une seconde).
Martinsos
1
@Martinsos pip install wheel. Alors ce doit être la raison 1. Veuillez d'abord installer la roue et réessayer.
McKelvin
2

C'est un script de configuration que j'ai écrit qui facilite l'inclusion de répertoires imbriqués dans la construction. Il faut l'exécuter à partir d'un dossier dans un package.

Structure de Givig comme celle-ci:

__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

Bonne compilation;)

zzart
la source
2

Le simple hack que j'ai trouvé:

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(…)

Installez simplement Cython s'il n'a pas pu être importé. On ne devrait probablement pas partager ce code, mais pour mes propres dépendances, c'est assez bon.

kay - SE est le mal
la source
2

Toutes les autres réponses reposent sur

  • distutils
  • importing from Cython.Build, ce qui crée un problème de poulet et d'œuf entre la demande de cython via setup_requireset son importation.

Une solution moderne consiste à utiliser setuptools à la place, voir cette réponse (la gestion automatique des extensions Cython nécessite setuptools 18.0, c'est-à-dire qu'elle est déjà disponible depuis de nombreuses années). Une norme moderne setup.pyavec gestion des exigences, un point d'entrée et un module cython pourrait ressembler à ceci:

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
la source
L'importation à partir Cython.Builddu moment de l'installation provoque ImportError pour moi. Avoir setuptools pour compiler pyx est la meilleure façon de le faire.
Carson Ip
1

Le moyen le plus simple que j'ai trouvé en utilisant uniquement setuptools au lieu des fonctionnalités limitées distutils est

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
la source
En fait, avec setuptools, il n'est pas nécessaire de procéder à l'importation explicite try / catched Cython.Build, voir ma réponse.
bluenote10
0

Je pense avoir trouvé un très bon moyen de faire cela en fournissant une build_extcommande personnalisée . L'idée est la suivante:

  1. J'ajoute les en-têtes numpy en remplaçant finalize_options()et en faisant import numpydans le corps de la fonction, ce qui évite joliment le problème de la non disponibilité de numpy avant de l' setup()installer.

  2. Si cython est disponible sur le système, il se connecte à la commande check_extensions_list() connecte à méthode de et cythonise tous les modules cython obsolètes, en les remplaçant par des extensions C qui peuvent plus tard être gérées par la build_extension() méthode. Nous fournissons simplement la dernière partie de la fonctionnalité dans notre module: cela signifie que si cython n'est pas disponible mais que nous avons une extension C présente, cela fonctionne toujours, ce qui vous permet de faire des distributions de sources.

Voici le code:

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())

Cela permet d'écrire simplement les setup()arguments sans se soucier des importations et de savoir si cython est disponible:

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