Wie soll ich ein Python-Paket strukturieren, das Cython-Code enthält?


122

Ich möchte ein Python-Paket erstellen, das Cython- Code enthält. Ich habe den Cython-Code gut funktionieren lassen. Jetzt möchte ich jedoch wissen, wie ich es am besten verpacken kann.

Für die meisten Leute, die nur das Paket installieren möchten, möchte ich .cdie von Cython erstellte Datei einschließen und veranlassen, diese setup.pyzu kompilieren, um das Modul zu erstellen. Dann muss Cython nicht installiert werden, um das Paket zu installieren.

Aber für Leute , die das Paket ändern möchten, würde ich auch wie die Cython bieten .pyxDateien, und irgendwie auch erlauben setup.pysie Cython mit aufzubauen (so die Benutzer würden brauchen Cython installiert ist ).

Wie soll ich die Dateien im Paket strukturieren, um diese beiden Szenarien zu berücksichtigen?

Die Cython-Dokumentation gibt eine kleine Anleitung . Es heißt jedoch nicht, wie man eine Single erstellt setup.py, die beide Fälle mit / ohne Cython behandelt.


1
Ich sehe, dass die Frage mehr Stimmen erhält als jede der Antworten. Ich bin gespannt, warum die Leute die Antworten möglicherweise unbefriedigend finden.
Craig McQueen

4
Ich habe diesen Abschnitt der Dokumentation gefunden , der genau die Antwort gibt.
Will

Antworten:


72

Ich habe dies jetzt selbst in einem Python-Paket gemacht simplerandom( BitBucket repo - EDIT: jetzt github ) (Ich erwarte nicht, dass dies ein beliebtes Paket ist, aber es war eine gute Chance, Cython zu lernen).

Diese Methode basiert auf der Tatsache, dass beim Erstellen einer .pyxDatei mit Cython.Distutils.build_ext(zumindest mit Cython Version 0.14) immer eine .cDatei im selben Verzeichnis wie die Quelldatei .pyxerstellt wird.

Hier ist eine gekürzte Version, von setup.pyder ich hoffe, dass sie das Wesentliche zeigt:

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

Ich habe auch bearbeitet, MANIFEST.inum sicherzustellen, dass mycythonmodule.ces in einer Quelldistribution enthalten ist (eine Quelldistribution, mit der erstellt wird python setup.py sdist):

...
recursive-include cython *
...

Ich verpflichte mich nicht mycythonmodule.czur Versionskontrolle 'trunk' (oder 'default' für Mercurial). Wenn ich eine Veröffentlichung mache, muss ich daran denken, eine python setup.py build_exterste zu machen, um sicherzustellen, dass diese mycythonmodule.cfür die Verteilung des Quellcodes vorhanden und aktuell ist. Ich mache auch einen Release-Zweig und schreibe die C-Datei in den Zweig. Auf diese Weise habe ich eine historische Aufzeichnung der C-Datei, die mit dieser Version verteilt wurde.


Danke, genau das brauchte ich für ein Pyrex-Projekt, das ich gerade eröffne! Das MANIFEST.in hat mich für eine Sekunde gestolpert, aber ich brauchte nur diese eine Zeile. Ich beziehe die C-Datei aus Interesse in die Quellcodeverwaltung ein, aber ich sehe Ihren Standpunkt darin, dass dies nicht erforderlich ist.
Chmullig

Ich habe meine Antwort bearbeitet, um zu erklären, dass sich die C-Datei nicht in trunk / default befindet, sondern einem Release-Zweig hinzugefügt wird.
Craig McQueen

1
@CraigMcQueen danke für die tolle Antwort, es hat mir sehr geholfen! Ich frage mich jedoch, ob es erwünscht ist, Cython zu verwenden, wenn es verfügbar ist. Es scheint mir, dass es besser ist, standardmäßig vorgenerierte c-Dateien zu verwenden, es sei denn, der Benutzer möchte Cython ausdrücklich verwenden. In diesem Fall kann er die Umgebungsvariable oder etwas anderes festlegen. Dies würde die Installation stabiler / robuster machen, da der Benutzer möglicherweise unterschiedliche Ergebnisse erhält, je nachdem, welche Version von Cython er installiert hat. Möglicherweise ist ihm nicht einmal bewusst, dass er sie installiert hat und dass sie sich auf die Erstellung des Pakets auswirkt.
Martinsos

20

Hinzufügen zu Craig McQueens Antwort: Im Folgenden sdisterfahren Sie, wie Sie den Befehl überschreiben , damit Cython Ihre Quelldateien automatisch kompiliert, bevor Sie eine Quelldistribution erstellen.

Auf diese Weise besteht kein Risiko, dass veraltete CQuellen versehentlich verteilt werden . Dies ist auch hilfreich, wenn Sie nur begrenzte Kontrolle über den Verteilungsprozess haben, z. B. wenn Sie automatisch Verteilungen aus der kontinuierlichen Integration usw. erstellen.

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

19

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

Es wird dringend empfohlen, die generierten .c-Dateien sowie Ihre Cython-Quellen zu verteilen, damit Benutzer Ihr Modul installieren können, ohne dass Cython verfügbar sein muss.

Es wird außerdem empfohlen, die Cython-Kompilierung in der von Ihnen verteilten Version nicht standardmäßig zu aktivieren. Selbst wenn der Benutzer Cython installiert hat, möchte er es wahrscheinlich nicht nur zur Installation Ihres Moduls verwenden. Außerdem ist die Version, die er hat, möglicherweise nicht die gleiche, die Sie verwendet haben, und Ihre Quellen werden möglicherweise nicht korrekt kompiliert.

Dies bedeutet einfach, dass die Datei setup.py, mit der Sie geliefert werden, nur eine normale distutils-Datei in den generierten .c-Dateien ist. Für das grundlegende Beispiel hätten wir stattdessen:

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

7

Am einfachsten ist es, beide einzuschließen, aber nur die C-Datei zu verwenden? Das Einfügen der .pyx-Datei ist nett, wird aber nicht benötigt, sobald Sie die .c-Datei haben. Personen, die die .pyx-Datei neu kompilieren möchten, können Pyrex installieren und manuell ausführen.

Andernfalls benötigen Sie einen benutzerdefinierten Befehl build_ext für distutils, der zuerst die C-Datei erstellt. Cython enthält bereits eine. http://docs.cython.org/src/userguide/source_files_and_compilation.html

Was diese Dokumentation nicht tut, ist zu sagen, wie man dies bedingt, aber

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

Sollte damit umgehen.


1
Danke für deine Antwort. Das ist vernünftig, obwohl ich es vorziehen würde, wenn das bei der Installation von Cython setup.pydirekt aus der .pyxDatei erstellt werden kann . Meine Antwort hat das auch umgesetzt.
Craig McQueen

Nun, das ist der springende Punkt meiner Antwort. Es war einfach keine vollständige setup.py.
Lennart Regebro

4

Das Einschließen von (Cython) generierten .c-Dateien ist ziemlich seltsam. Besonders wenn wir das in git aufnehmen. Ich würde lieber setuptools_cython verwenden . Wenn Cython nicht verfügbar ist, erstellt es ein Ei mit integrierter Cython-Umgebung und erstellt dann Ihren Code mithilfe des Eies.

Ein mögliches Beispiel: https://github.com/douban/greenify/blob/master/setup.py


Update (05.01.2017):

Da setuptools 18.0gibt es keine Notwendigkeit zu verwenden setuptools_cython. Hier ist ein Beispiel, um ein Cython-Projekt von Grund auf ohne zu erstellen setuptools_cython.


Behebt dies das Problem, dass Cython nicht installiert wird, obwohl Sie es in setup_requires angegeben haben?
Kamil Sindi

ist es auch nicht möglich, 'setuptools>=18.0'setup_requires einzugeben, anstatt die Methode zu erstellen is_installed?
Kamil Sindi

1
@capitalistpug Zuerst müssen Sie sicherstellen setuptools>=18.0installiert wurde, dann müssen Sie nur setzen 'Cython >= 0.18'in setup_requiresund Cython wird während der Installation Fortschritte installiert werden. Wenn Sie jedoch setuptools <18.0 verwenden, selbst wenn Sie in setup_requires ein bestimmtes Cython verwenden, wird es nicht installiert. In diesem Fall sollten Sie die Verwendung in Betracht ziehen setuptools_cython.
McKelvin

Danke @McKelvin, dies scheint eine großartige Lösung zu sein! Gibt es einen Grund, warum wir den anderen Ansatz verwenden sollten, bei dem die Quelldateien im Voraus zythonisiert werden? Ich habe Ihren Ansatz ausprobiert und es scheint bei der Installation etwas langsam zu sein (die Installation dauert eine Minute, wird aber in einer Sekunde erstellt).
Martinsos

1
@ Martinsos pip install wheel. Dann muss es Grund 1 sein. Bitte zuerst das Rad einbauen und es erneut versuchen.
McKelvin

2

Dies ist ein Setup-Skript, das ich geschrieben habe und das es einfacher macht, verschachtelte Verzeichnisse in den Build aufzunehmen. Man muss es aus einem Ordner innerhalb eines Pakets ausführen.

Gegebene Struktur wie folgt:

__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

Viel Spaß beim Kompilieren;)


2

Der einfache Hack, den ich mir ausgedacht habe:

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

Installieren Sie einfach Cython, wenn es nicht importiert werden konnte. Man sollte diesen Code wahrscheinlich nicht teilen, aber für meine eigenen Abhängigkeiten ist er gut genug.


2

Alle anderen Antworten stützen sich entweder auf

  • distutils
  • Importieren aus Cython.Build, wodurch ein Henne-Ei-Problem zwischen dem Erfordernis von Cython über setup_requiresund dem Importieren entsteht.

Eine moderne Lösung besteht darin, stattdessen Setuptools zu verwenden (siehe diese Antwort) (für die automatische Behandlung von Cython-Erweiterungen sind Setuptools 18.0 erforderlich, dh sie sind bereits seit vielen Jahren verfügbar). Ein moderner Standard setup.pymit Anforderungshandhabung, einem Einstiegspunkt und einem Cython-Modul könnte folgendermaßen aussehen:

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'],
        ),
    ],
)

Der Import von Cython.Buildzur Setup-Zeit verursacht für mich ImportError. Setuptools zum Kompilieren von Pyx sind der beste Weg, dies zu tun.
Carson Ip

1

Der einfachste Weg, den ich gefunden habe, nur Setuptools anstelle der Feature-Limited Distutils zu verwenden, ist

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)

Tatsächlich ist bei setuptools der explizite try / catched-Import von nicht erforderlich Cython.Build, siehe meine Antwort.
bluenote10

0

Ich denke, ich habe einen ziemlich guten Weg gefunden, dies zu tun, indem ich einen benutzerdefinierten build_extBefehl bereitgestellt habe . Die Idee ist die folgende:

  1. Ich füge die Numpy-Header hinzu, indem ich sie überschreibe finalize_options()und import numpyim Hauptteil der Funktion ausführe, wodurch das Problem vermieden wird, dass Numpy vor der setup()Installation nicht verfügbar ist.

  2. Wenn Cython auf dem System verfügbar ist, wird es in die check_extensions_list()Methode des Befehls eingebunden und alle veralteten Cython-Module cythonisiert und durch C-Erweiterungen ersetzt, die später von der build_extension() Methode verarbeitet werden können. Wir stellen nur den letzten Teil der Funktionalität in unserem Modul zur Verfügung: Dies bedeutet, dass Cython, wenn es nicht verfügbar ist, aber eine C-Erweiterung vorhanden ist, weiterhin funktioniert, sodass Sie Quelldistributionen durchführen können.

Hier ist der 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())

Dies ermöglicht es einem, nur die setup()Argumente zu schreiben , ohne sich um Importe zu kümmern und ob man Cython zur Verfügung hat:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.