Jak mam ustrukturyzować pakiet Pythona, który zawiera kod Cython


122

Chciałbym stworzyć pakiet Pythona zawierający kod Cythona . Kod Cythona działa dobrze. Jednak teraz chcę wiedzieć, jak najlepiej to zapakować.

Dla większości ludzi, którzy chcą tylko zainstalować pakiet, chciałbym dołączyć .cplik, który tworzy Cython i zorganizować setup.pyjego kompilację w celu wyprodukowania modułu. Wtedy użytkownik nie potrzebuje zainstalowanego Cythona, aby zainstalować pakiet.

Ale dla osób, które mogą chcieć zmodyfikować pakiet, chciałbym również dostarczyć .pyxpliki Cython , a także w jakiś sposób umożliwić setup.pyich zbudowanie przy użyciu Cythona (więc ci użytkownicy musieliby zainstalować Cython).

Jak należy uporządkować pliki w pakiecie, aby zaspokoić oba te scenariusze?

Dokumentacja Cythona zawiera niewielkie wskazówki . Ale nie mówi, jak stworzyć singiel, setup.pyktóry będzie obsługiwał zarówno sprawy z Cythonem, jak i bez nich.


1
Widzę, że pytanie zdobywa więcej głosów pozytywnych niż którakolwiek z odpowiedzi. Ciekawi mnie, dlaczego ludzie mogą uważać odpowiedzi za niezadowalające.
Craig McQueen

4
Znalazłem tę sekcję dokumentacji , która daje dokładną odpowiedź.
Will

Odpowiedzi:


72

Zrobiłem to teraz sam, w pakiecie Pythona simplerandom( repozytorium BitBucket - EDIT: teraz github ) (nie spodziewam się, że będzie to popularny pakiet, ale była to dobra okazja, aby nauczyć się Cythona).

Ta metoda opiera się na fakcie, że tworzenie .pyxpliku w programie Cython.Distutils.build_ext(przynajmniej w wersji Cython 0.14) zawsze wydaje się tworzyć .cplik w tym samym katalogu, co .pyxplik źródłowy .

Oto skrócona wersja, setup.pyktóra, mam nadzieję, pokazuje najważniejsze:

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

Edytowałem również, MANIFEST.inaby upewnić się, że mycythonmodule.cjest uwzględniony w dystrybucji źródłowej (dystrybucji źródłowej utworzonej za pomocą python setup.py sdist):

...
recursive-include cython *
...

Nie zobowiązuję się mycythonmodule.cdo kontroli wersji „trunk” (lub „default” dla Mercurial). Kiedy robię wydanie, muszę pamiętać o zrobieniu python setup.py build_extpierwszego, aby upewnić się, że mycythonmodule.cjest obecny i aktualny dla dystrybucji kodu źródłowego. Tworzę także gałąź Release i zatwierdzam plik C do gałęzi. W ten sposób mam historyczny zapis pliku C, który był dystrybuowany z tym wydaniem.


Dzięki, właśnie tego potrzebowałem do projektu Pyrex, który otwieram! MANIFEST.in wyłączył mnie na sekundę, ale potrzebowałem tylko tej jednej linii. Włączam plik C do kontroli źródła z powodu zainteresowania, ale widzę, że jest to niepotrzebne.
chmullig

Zredagowałem moją odpowiedź, aby wyjaśnić, dlaczego plik C nie znajduje się w głównej / domyślnej wersji, ale został dodany do gałęzi wydania.
Craig McQueen

1
@CraigMcQueen dzięki za świetną odpowiedź, bardzo mi to pomogło! Zastanawiam się jednak, czy pożądanym zachowaniem jest używanie Cythona, gdy jest dostępny? Wydaje mi się, że lepiej byłoby domyślnie używać wstępnie wygenerowanych plików c, chyba że użytkownik wyraźnie chce użyć Cythona, w którym to przypadku może ustawić zmienną środowiskową lub coś w tym rodzaju. To uczyniłoby instalację bardziej stabilną / solidną, ponieważ użytkownik może uzyskać różne wyniki w zależności od zainstalowanej wersji Cythona - może nawet nie być świadomy, że ma ją zainstalowaną i że ma to wpływ na budowanie pakietu.
Martinsos

20

Dodając do odpowiedzi Craiga McQueena: zobacz poniżej, jak zastąpić sdistpolecenie, aby Cython automatycznie kompilował pliki źródłowe przed utworzeniem dystrybucji źródłowej.

W ten sposób nie ryzykujesz przypadkowej dystrybucji przestarzałych Cźródeł. Pomaga również w przypadku, gdy masz ograniczoną kontrolę nad procesem dystrybucji np. Podczas automatycznego tworzenia dystrybucji z ciągłej integracji itp.

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

Zdecydowanie zaleca się rozpowszechnianie wygenerowanych plików .c, a także źródeł Cython, aby użytkownicy mogli zainstalować moduł bez konieczności udostępniania Cython.

Zaleca się również, aby kompilacja Cython nie była domyślnie włączona w rozpowszechnianej wersji. Nawet jeśli użytkownik ma zainstalowany Cython, prawdopodobnie nie chce go używać tylko do instalacji Twojego modułu. Ponadto wersja, którą ma, może nie być tą samą, której używałeś, i może nie kompilować poprawnie twoich źródeł.

Oznacza to po prostu, że plik setup.py, z którym wysyłasz, będzie po prostu zwykłym plikiem distutils w wygenerowanych plikach .c, dla podstawowego przykładu, który mielibyśmy zamiast tego:

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

7

Najłatwiej jest uwzględnić oba, ale po prostu użyj pliku c? Dołączenie pliku .pyx jest fajne, ale i tak nie jest potrzebne, gdy już masz plik .c. Osoby, które chcą ponownie skompilować .pyx, mogą zainstalować Pyrex i zrobić to ręcznie.

W przeciwnym razie musisz mieć niestandardowe polecenie build_ext dla distutils, które najpierw budują plik C. Cython już zawiera jeden. http://docs.cython.org/src/userguide/source_files_and_compilation.html

To, czego ta dokumentacja nie robi, to powiedzieć, jak uczynić to warunkowym, ale

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

Powinienem to załatwić.


1
Dziękuję za odpowiedź. To rozsądne, chociaż wolę, aby setup.pymożna było budować bezpośrednio z .pyxpliku po zainstalowaniu Cythona. Moja odpowiedź również to zaimplementowała.
Craig McQueen

Cóż, o to właśnie chodzi w mojej odpowiedzi. Po prostu nie był to kompletny plik setup.py.
Lennart Regebro

4

Dołączanie plików .c wygenerowanych przez (Cython) jest dość dziwne. Zwłaszcza jeśli włączymy to do git. Wolałbym użyć setuptools_cython . Gdy Cython nie jest dostępny, zbuduje jajko, które ma wbudowane środowisko Cython, a następnie zbuduje kod za pomocą jajka.

Możliwy przykład: https://github.com/douban/greenify/blob/master/setup.py


Aktualizacja (2017-01-05):

Ponieważ setuptools 18.0nie ma potrzeby używania setuptools_cython. Oto przykład zbudowania projektu Cython od zera bez setuptools_cython.


czy to rozwiązuje problem braku instalacji Cythona, mimo że określisz to w setup_requires?
Kamil Sindi,

również nie można wstawić 'setuptools>=18.0'setup_requires zamiast tworzenia metody is_installed?
Kamil Sindi

1
@capitalistpug Najpierw trzeba się upewnić, setuptools>=18.0został zainstalowany, a następnie wystarczy umieścić 'Cython >= 0.18'w setup_requires, i będą instalowane podczas instalacji Cython postępy. Ale jeśli używasz setuptools <18.0, nawet jeśli masz określony cython w setup_requires, nie zostanie on zainstalowany, w tym przypadku powinieneś rozważyć użycie setuptools_cython.
McKelvin,

Dzięki @McKelvin, to wydaje się być świetnym rozwiązaniem! Czy jest jakiś powód, dla którego powinniśmy zastosować inne podejście, z wcześniejszą cytonizacją plików źródłowych, obok tego? Wypróbowałem twoje podejście i wydaje się, że jest nieco powolny podczas instalacji (instalacja zajmuje minutę, ale kompiluje się w sekundę).
Martinsos

1
@Martinsos pip install wheel. To musi być powód 1. Najpierw zainstaluj koło i spróbuj ponownie.
McKelvin,

2

To jest skrypt instalacyjny, który napisałem, który ułatwia dołączanie zagnieżdżonych katalogów do kompilacji. Trzeba go uruchomić z folderu w pakiecie.

Podaj taką strukturę:

__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

Miłej kompilacji;)


2

Prosty hack, który wymyśliłem:

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

Po prostu zainstaluj Cython, jeśli nie można go zaimportować. Prawdopodobnie nie powinno się udostępniać tego kodu, ale dla moich własnych zależności jest wystarczająco dobry.


2

Wszystkie inne odpowiedzi polegają na

  • distutils
  • importowanie z Cython.Build, co stwarza problem typu kura i jajko między wymaganiem cythonu via setup_requiresa importem.

Nowoczesnym rozwiązaniem jest użycie zamiast tego setuptools, zobacz tę odpowiedź (automatyczna obsługa rozszerzeń Cython wymaga setuptools 18.0, czyli jest dostępna już od wielu lat). Nowoczesny standard setup.pyz obsługą wymagań, punktem wejścia i modułem cython mógłby wyglądać następująco:

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

Importowanie z poziomu Cython.Buildw czasie konfiguracji powoduje u mnie ImportError. Posiadanie setuptools do skompilowania pyxa to najlepszy sposób na zrobienie tego.
Carson Ip,

1

Najłatwiejszym sposobem, jaki znalazłem, używając tylko setuptools zamiast ograniczonych funkcji, jest

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)

W rzeczywistości w przypadku setuptools nie ma potrzeby jawnego importowania metodą try / catched Cython.Build, zobacz moją odpowiedź.
bluenote10

0

Myślę, że znalazłem całkiem dobry sposób na zrobienie tego, udostępniając niestandardowe build_extpolecenie. Pomysł jest następujący:

  1. Dodaje nagłówki numpy, nadpisując finalize_options()i wykonując import numpyw treści funkcji, co ładnie unika problemu braku dostępności numpy przed setup()jej zainstalowaniem.

  2. Jeśli cython jest dostępny w systemie, check_extensions_list()podłącza się do metody polecenia i cytonizuje wszystkie nieaktualne moduły cython, zastępując je rozszerzeniami C, które mogą być później obsługiwane przez tę build_extension() metodę. Po prostu zapewniamy drugą część funkcjonalności również w naszym module: oznacza to, że jeśli cython nie jest dostępny, ale mamy obecne rozszerzenie C, nadal działa, co pozwala na dystrybucje źródeł.

Oto kod:

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

Pozwala to po prostu napisać setup()argumenty bez martwienia się o import i dostępność cythonu:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.