Come dovrei strutturare un pacchetto Python che contiene codice Cython


122

Vorrei creare un pacchetto Python contenente del codice Cython . Ho il codice Cython che funziona bene. Tuttavia, ora voglio sapere come impacchettarlo al meglio.

Per la maggior parte delle persone che vogliono solo installare il pacchetto, vorrei includere il .cfile creato da Cython e fare in modo setup.pydi compilarlo per produrre il modulo. Quindi l'utente non ha bisogno che Cython sia installato per installare il pacchetto.

Ma per le persone che potrebbero voler modificare il pacchetto, vorrei anche fornire i .pyxfile Cython e in qualche modo consentire anche setup.pydi crearli usando Cython (quindi quegli utenti avrebbero bisogno di Cython installato).

Come devo strutturare i file nel pacchetto per soddisfare entrambi questi scenari?

La documentazione di Cython fornisce una piccola guida . Ma non dice come fare un singolo setup.pyche gestisca entrambi i casi con / senza Cython.


1
Vedo che la domanda sta ottenendo più voti positivi rispetto a qualsiasi risposta. Sono curioso di sapere perché le persone potrebbero trovare le risposte insoddisfacenti.
Craig McQueen

4
Ho trovato questa sezione della documentazione , che fornisce esattamente la risposta.
Sarà l'

Risposte:


72

L'ho fatto io stesso ora, in un pacchetto Python simplerandom( BitBucket repo - EDIT: now github ) (non mi aspetto che questo sia un pacchetto popolare, ma è stata una buona occasione per imparare Cython).

Questo metodo si basa sul fatto che la creazione di un .pyxfile con Cython.Distutils.build_ext(almeno con Cython versione 0.14) sembra sempre creare un .cfile nella stessa directory del .pyxfile sorgente .

Ecco una versione ridotta di setup.pycui spero mostri gli elementi essenziali:

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

Ho anche modificato MANIFEST.inper assicurarmi che mycythonmodule.csia incluso in una distribuzione di origine (una distribuzione di origine creata con python setup.py sdist):

...
recursive-include cython *
...

Non mi impegno mycythonmodule.cper il controllo della versione "trunk" (o "default" per Mercurial). Quando creo un rilascio, devo ricordarmi di farlo python setup.py build_extprima, per assicurarmi che mycythonmodule.csia presente e aggiornato per la distribuzione del codice sorgente. Creo anche un ramo di rilascio e inserisco il file C nel ramo. In questo modo ho un record storico del file C che è stato distribuito con quella versione.


Grazie, questo è esattamente ciò di cui avevo bisogno per un progetto Pyrex che sto aprendo! Il MANIFEST.in mi ha fatto scattare per un secondo, ma avevo solo bisogno di quella riga. Includo il file C nel controllo del codice sorgente per interesse, ma vedo il tuo punto che non è necessario.
chmullig

Ho modificato la mia risposta per spiegare come il file C non è in trunk / default, ma viene aggiunto a un ramo di rilascio.
Craig McQueen

1
@CraigMcQueen grazie per l'ottima risposta, mi ha aiutato molto! Mi chiedo tuttavia, è un comportamento desiderato utilizzare Cython quando disponibile? Mi sembra che sarebbe meglio usare per impostazione predefinita i file c pre-generati, a meno che l'utente non voglia esplicitamente usare Cython, nel qual caso può impostare la variabile d'ambiente o qualcosa del genere. Ciò renderebbe l'installazione più stabile / robusta, perché l'utente potrebbe ottenere risultati diversi in base alla versione di Cython che ha installato - potrebbe anche non essere consapevole di averlo installato e che sta influenzando la creazione del pacchetto.
Martinsos

20

In aggiunta alla risposta di Craig McQueen: vedi sotto per come sovrascrivere il sdistcomando per fare in modo che Cython compili automaticamente i tuoi file sorgente prima di creare una distribuzione sorgente.

In questo modo non corri il rischio di distribuire accidentalmente Cfonti obsolete . Aiuta anche nel caso in cui si abbia un controllo limitato sul processo di distribuzione, ad esempio quando si creano automaticamente distribuzioni dall'integrazione continua, ecc.

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

Si consiglia vivamente di distribuire i file .c generati così come i sorgenti Cython, in modo che gli utenti possano installare il modulo senza la necessità di avere Cython disponibile.

Si consiglia inoltre di non abilitare la compilazione Cython per impostazione predefinita nella versione distribuita. Anche se l'utente ha installato Cython, probabilmente non vuole usarlo solo per installare il tuo modulo. Inoltre, la versione che ha potrebbe non essere la stessa che hai usato e potrebbe non compilare correttamente i tuoi sorgenti.

Ciò significa semplicemente che il file setup.py fornito con sarà solo un normale file distutils sui file .c generati, per l'esempio di base avremmo invece:

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

7

Il modo più semplice è includerli entrambi ma usare solo il file c? Includere il file .pyx è carino, ma non è comunque necessario una volta che hai il file .c. Le persone che vogliono ricompilare il .pyx possono installare Pyrex e farlo manualmente.

Altrimenti è necessario disporre di un comando build_ext personalizzato per distutils che compili prima il file C. Cython ne include già uno. http://docs.cython.org/src/userguide/source_files_and_compilation.html

Quello che quella documentazione non fa è dire come renderlo condizionale, ma

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

Dovrebbe gestirlo.


1
Grazie per la tua risposta. È ragionevole, anche se preferisco che setup.pyil .pyxfile possa essere compilato direttamente dal file quando Cython è installato. La mia risposta ha implementato anche questo.
Craig McQueen

Bene, questo è il punto centrale della mia risposta. Semplicemente non era un setup.py completo.
Lennart Regebro

4

Includere i file .c generati da (Cython) è piuttosto strano. Soprattutto quando lo includiamo in git. Preferisco usare setuptools_cython . Quando Cython non è disponibile, costruirà un uovo con l'ambiente Cython integrato, quindi creerà il tuo codice usando l'uovo.

Un possibile esempio: https://github.com/douban/greenify/blob/master/setup.py


Aggiornamento (2017/01/05):

Da allora setuptools 18.0, non è necessario utilizzare setuptools_cython. Ecco un esempio per costruire il progetto Cython da zero senza setuptools_cython.


questo risolve il problema della mancata installazione di Cython anche se lo specifichi in setup_requires?
Kamil Sindi

inoltre non è possibile inserire 'setuptools>=18.0'setup_requires invece di creare il metodo is_installed?
Kamil Sindi

1
@capitalistpug Per prima cosa è necessario assicurarsi che setuptools>=18.0è stato installato, allora avete solo bisogno di mettere 'Cython >= 0.18'in setup_requires, e Cython verrà installato durante l'installazione progresso. Ma se stai usando setuptools <18.0, anche tu specifico cython in setup_requires, non verrà installato, in questo caso, dovresti considerare l'uso setuptools_cython.
McKelvin

Grazie @McKelvin, questa sembra un'ottima soluzione! C'è qualche motivo per cui dovremmo usare l'altro approccio, con cythonizing i file sorgente in anticipo, accanto a questo? Ho provato il tuo approccio e sembra essere un po 'lento durante l'installazione (richiede un minuto per l'installazione ma si costruisce in un secondo).
Martinsos,

1
@Martinsos pip install wheel. Quindi deve essere il motivo 1. Installare prima la ruota e riprovare.
McKelvin

2

Questo è uno script di installazione che ho scritto che semplifica l'inclusione di directory nidificate all'interno della build. È necessario eseguirlo dalla cartella all'interno di un pacchetto.

Givig struttura come questa:

__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

Buona compilazione;)


2

Il semplice trucco che mi è venuto in mente:

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

Basta installare Cython se non può essere importato. Probabilmente non si dovrebbe condividere questo codice, ma per le mie dipendenze è abbastanza buono.


2

Tutte le altre risposte si basano su

  • distutils
  • importazione da Cython.Build, il che crea un problema con l'uovo e la gallina tra la richiesta di cython tramite setup_requirese l'importazione.

Una soluzione moderna è invece quella di utilizzare setuptools, vedi questa risposta (la gestione automatica delle estensioni Cython richiede setuptools 18.0, cioè è disponibile già da molti anni). Uno standard moderno setup.pycon gestione dei requisiti, un punto di ingresso e un modulo cython potrebbe essere simile a questo:

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

L'importazione da Cython.Buildal momento dell'installazione causa ImportError per me. Avere setuptools per compilare pyx è il modo migliore per farlo.
Carson Ip

1

Il modo più semplice che ho trovato usando solo setuptools invece della funzionalità limitata distutils è

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)

In effetti, con setuptools non è necessario l'importazione esplicita try / catched da Cython.Build, vedere la mia risposta.
bluenote10

0

Penso di aver trovato un modo abbastanza buono per farlo fornendo un build_extcomando personalizzato . L'idea è la seguente:

  1. Aggiungo le intestazioni numpy sovrascrivendo finalize_options()e facendo import numpynel corpo della funzione, il che evita piacevolmente il problema di numpy non essere disponibile prima di setup()installarlo.

  2. Se cython è disponibile sul sistema, si aggancia al check_extensions_list()metodo del comando e cythonizza tutti i moduli cython scaduti, sostituendoli con estensioni C che possono essere successivamente gestite dal build_extension() metodo. Forniamo solo l'ultima parte della funzionalità anche nel nostro modulo: questo significa che se cython non è disponibile ma abbiamo un'estensione C presente, funziona ancora, il che ti permette di fare distribuzioni di sorgenti.

Ecco il codice:

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

Ciò consente di scrivere solo gli setup()argomenti senza preoccuparsi delle importazioni e se si dispone di cython disponibile:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.