Bagaimana saya harus menyusun paket Python yang berisi kode Cython

122

Saya ingin membuat paket Python yang berisi beberapa kode Cython . Saya mendapatkan kode Cython yang berfungsi dengan baik. Namun, sekarang saya ingin tahu cara terbaik untuk mengemasnya.

Bagi kebanyakan orang yang hanya ingin menginstal paket, saya ingin menyertakan .cfile yang dibuat Cython, dan mengaturnya untuk setup.pymengkompilasinya untuk menghasilkan modul. Maka pengguna tidak perlu menginstal Cython untuk menginstal paket.

Tetapi bagi orang-orang yang mungkin ingin mengubah paket, saya juga ingin menyediakan .pyxfile Cython , dan entah bagaimana juga mengizinkan untuk setup.pymembangunnya menggunakan Cython (sehingga pengguna tersebut perlu menginstal Cython).

Bagaimana saya harus menyusun file dalam paket untuk memenuhi kedua skenario ini?

The dokumentasi Cython memberikan sedikit pengarahan . Tapi itu tidak mengatakan bagaimana membuat single setup.pyyang menangani kasus dengan / tanpa Cython.

Craig McQueen
sumber
1
Saya melihat pertanyaannya adalah mendapatkan lebih banyak suara daripada jawaban mana pun. Saya penasaran untuk mengetahui mengapa orang mungkin menemukan jawaban yang tidak memuaskan.
Craig McQueen
4
Saya menemukan bagian dokumentasi ini , yang memberikan jawabannya dengan tepat.
Akankah

Jawaban:

72

Saya telah melakukan ini sendiri sekarang, dalam paket Python simplerandom( BitBucket repo - EDIT: now github ) (Saya tidak berharap ini menjadi paket yang populer, tetapi ini adalah kesempatan bagus untuk mempelajari Cython).

Metode ini bergantung pada fakta bahwa membangun .pyxfile dengan Cython.Distutils.build_ext(setidaknya dengan versi Cython 0.14) sepertinya selalu membuat .cfile di direktori yang sama dengan .pyxfile sumber .

Berikut adalah versi cut-down setup.pyyang saya harap menunjukkan hal-hal penting:

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

Saya juga mengedit MANIFEST.inuntuk memastikan itu mycythonmodule.ctermasuk dalam distribusi sumber (distribusi sumber yang dibuat dengan python setup.py sdist):

...
recursive-include cython *
...

Saya tidak berkomitmen mycythonmodule.cuntuk kontrol versi 'trunk' (atau 'default' untuk Mercurial). Ketika saya membuat rilis, saya harus ingat untuk melakukan yang python setup.py build_extpertama, untuk memastikan bahwa mycythonmodule.cada dan terbaru untuk distribusi kode sumber. Saya juga membuat cabang rilis, dan memasukkan file C ke dalam cabang. Dengan cara itu saya memiliki catatan sejarah dari file C yang didistribusikan dengan rilis tersebut.

Craig McQueen
sumber
Terima kasih, inilah yang saya butuhkan untuk proyek Pyrex yang saya buka! MANIFEST.in membuat saya tersandung sesaat, tetapi saya hanya membutuhkan satu baris itu. Saya menyertakan file C dalam kontrol sumber karena minat, tetapi saya melihat maksud Anda bahwa itu tidak perlu.
chmullig
Saya telah mengedit jawaban saya untuk menjelaskan bagaimana file C tidak ada di trunk / default, tetapi ditambahkan ke cabang rilis.
Craig McQueen
1
@CraigMcQueen terima kasih atas jawaban yang bagus, itu sangat membantu saya! Namun saya bertanya-tanya, apakah itu perilaku yang diinginkan untuk menggunakan Cython bila tersedia? Menurut saya akan lebih baik secara default menggunakan file c yang sudah dibuat sebelumnya, kecuali pengguna secara eksplisit ingin menggunakan Cython, dalam hal ini dia dapat mengatur variabel lingkungan atau sesuatu. Itu akan membuat penginstalan lebih stabil / kuat, karena pengguna mungkin mendapatkan hasil yang berbeda berdasarkan versi Cython mana yang telah ia instal - ia bahkan mungkin tidak menyadari bahwa ia telah menginstalnya dan itu memengaruhi pembuatan paket.
Martinsos
20

Menambahkan jawaban Craig McQueen: lihat di bawah untuk cara menimpa sdistperintah agar Cython secara otomatis mengkompilasi file sumber Anda sebelum membuat distribusi sumber.

Dengan cara itu, Anda tidak berisiko menyebarkan Csumber-sumber yang sudah usang secara tidak sengaja . Ini juga membantu dalam kasus di mana Anda memiliki kendali terbatas atas proses distribusi misalnya ketika secara otomatis membuat distribusi dari integrasi berkelanjutan dll.

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
sumber
19

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

Sangat disarankan agar Anda mendistribusikan file .c yang dihasilkan serta sumber Cython Anda, sehingga pengguna dapat menginstal modul Anda tanpa perlu menyediakan Cython.

Juga disarankan agar kompilasi Cython tidak diaktifkan secara default dalam versi yang Anda distribusikan. Bahkan jika pengguna menginstal Cython, dia mungkin tidak ingin menggunakannya hanya untuk menginstal modul Anda. Juga, versinya mungkin tidak sama dengan yang Anda gunakan, dan mungkin tidak mengkompilasi sumber Anda dengan benar.

Ini berarti bahwa file setup.py yang Anda kirimkan hanya akan menjadi file distutils normal pada file .c yang dihasilkan, untuk contoh dasar kita akan memiliki:

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

Yang termudah adalah memasukkan keduanya tetapi cukup gunakan file-c? Menyertakan file .pyx memang bagus, tetapi tidak diperlukan setelah Anda memiliki file .c. Orang yang ingin mengkompilasi ulang .pyx dapat menginstal Pyrex dan melakukannya secara manual.

Jika tidak, Anda perlu memiliki perintah build_ext kustom untuk distutils yang membuat file C terlebih dahulu. Cython sudah termasuk satu. http://docs.cython.org/src/userguide/source_files_and_compilation.html

Apa yang dokumentasi tidak lakukan adalah mengatakan bagaimana membuat ini bersyarat, tapi

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

Harus menanganinya.

Lennart Regebro
sumber
1
Terima kasih atas jawaban anda. Itu masuk akal, meskipun saya lebih suka jika setup.pydapat membangun langsung dari .pyxfile ketika Cython diinstal. Jawaban saya telah menerapkannya juga.
Craig McQueen
Nah, itulah inti dari jawaban saya. Itu bukan setup.py lengkap.
Lennart Regebro
4

Menyertakan file .c yang dihasilkan (Cython) cukup aneh. Terutama jika kita memasukkannya ke dalam git. Saya lebih suka menggunakan setuptools_cython . Ketika Cython tidak tersedia, itu akan membuat telur yang memiliki lingkungan Cython built-in, dan kemudian membangun kode Anda menggunakan telur.

Contoh yang memungkinkan: https://github.com/douban/greenify/blob/master/setup.py


Perbarui (2017-01-05):

Karena setuptools 18.0, tidak perlu menggunakan setuptools_cython. Berikut adalah contoh untuk membangun proyek Cython dari awal tanpa setuptools_cython.

McKelvin
sumber
apakah ini memperbaiki masalah Cython yang tidak diinstal meskipun Anda menentukannya di setup_requires?
Kamil Sindi
juga tidak mungkin untuk memasukkan 'setuptools>=18.0'setup_requires daripada membuat metode is_installed?
Kamil Sindi
1
@capitalistpug Pertama Anda perlu memastikan setuptools>=18.0telah terpasang, maka Anda hanya perlu menempatkan 'Cython >= 0.18'di setup_requires, dan Cython akan dipasang selama instalasi berlangsung. Tetapi jika Anda menggunakan setuptools <18.0, meskipun Anda menggunakan cython tertentu di setup_requires, itu tidak akan diinstal, dalam hal ini, Anda harus mempertimbangkan untuk menggunakannya setuptools_cython.
McKelvin
Terima kasih @McKelvin, ini sepertinya solusi yang bagus! Apakah ada alasan mengapa kita harus menggunakan pendekatan lain, dengan melakukan cythonisasi file sumber terlebih dahulu, di samping ini? Saya mencoba pendekatan Anda dan tampaknya agak lambat saat menginstal (membutuhkan satu menit untuk menginstal tetapi membangun dalam satu detik).
Martinsos
1
@Martinsos pip install wheel. Maka itu pasti alasan 1. Silakan pasang roda terlebih dahulu dan coba lagi.
McKelvin
2

Ini adalah skrip pengaturan yang saya tulis yang membuatnya lebih mudah untuk menyertakan direktori bersarang di dalam build. Seseorang perlu menjalankannya dari folder dalam sebuah paket.

Struktur Givig seperti ini:

__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

Selamat menyusun;)

zzart
sumber
2

Peretasan sederhana yang saya buat:

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

Instal saja Cython jika tidak bisa diimpor. Seseorang mungkin tidak boleh membagikan kode ini, tetapi untuk dependensi saya sendiri itu cukup baik.

kay - SE jahat
sumber
2

Semua jawaban lain bergantung pada

  • distutils
  • mengimpor dari Cython.Build, yang menciptakan masalah ayam-dan-telur antara membutuhkan cython melalui setup_requiresdan mengimpornya.

Solusi modern adalah dengan menggunakan setuptools, lihat jawaban ini (penanganan otomatis ekstensi Cython memerlukan setuptools 18.0, yaitu sudah tersedia selama bertahun-tahun). Standar modern setup.pydengan penanganan persyaratan, titik masuk, dan modul cython bisa terlihat seperti ini:

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
sumber
Mengimpor dari Cython.Buildpada waktu penyiapan menyebabkan ImportError bagi saya. Memiliki setuptools untuk mengkompilasi pyx adalah cara terbaik untuk melakukannya.
Carson Ip
1

Cara termudah yang saya temukan dengan hanya menggunakan setuptools daripada fitur distutils terbatas adalah

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
sumber
Faktanya, dengan setuptools tidak perlu impor coba / tangkap eksplisit dari Cython.Build, lihat jawaban saya.
bluenote10
0

Saya rasa saya menemukan cara yang cukup bagus untuk melakukan ini dengan memberikan build_extperintah khusus . Idenya adalah sebagai berikut:

  1. Saya menambahkan header numpy dengan menimpa finalize_options()dan melakukan import numpydi badan fungsi, yang dengan baik menghindari masalah numpy tidak tersedia sebelum setup()menginstalnya.

  2. Jika cython tersedia di sistem, itu akan menghubungkan ke metode perintah check_extensions_list()dan dengan melakukan cython semua modul cython yang sudah kadaluwarsa, menggantinya dengan ekstensi C yang nantinya dapat ditangani oleh build_extension() metode. Kami hanya menyediakan bagian terakhir dari fungsionalitas dalam modul kami juga: ini berarti bahwa jika cython tidak tersedia tetapi kami memiliki ekstensi C, ini masih berfungsi, yang memungkinkan Anda untuk melakukan distribusi sumber.

Berikut kodenya:

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

Ini memungkinkan seseorang untuk hanya menulis setup()argumen tanpa khawatir tentang impor dan apakah ada cython yang tersedia:

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