Como devo estruturar um pacote Python que contém código Cython

122

Eu gostaria de fazer um pacote Python contendo algum código Cython . Eu tenho o código Cython funcionando bem. No entanto, agora eu quero saber a melhor forma de empacotá-lo.

Para a maioria das pessoas que apenas deseja instalar o pacote, gostaria de incluir o .carquivo que o Cython cria e providenciar setup.pya compilação para produzir o módulo. Em seguida, o usuário não precisa do Cython instalado para instalar o pacote.

Mas para as pessoas que podem querer modificar o pacote, eu também gostaria de fornecer os Cython .pyxarquivos, e de alguma forma também permitem setup.pypara construí-los usando Cython (para aqueles usuários que precisam Cython instalado).

Como devo estruturar os arquivos no pacote para atender a esses dois cenários?

A documentação do Cython fornece um pouco de orientação . Mas não diz como criar um single setup.pyque lide com os casos com / sem Cython.

Craig McQueen
fonte
1
Vejo que a pergunta está recebendo mais votos positivos do que qualquer uma das respostas. Estou curioso para saber por que as pessoas podem achar as respostas insatisfatórias.
Craig McQueen
4
Encontrei esta seção da documentação , que fornece a resposta exata.
Will

Respostas:

72

Eu já fiz isso agora, em um pacote Python simplerandom( repositório BitBucket - EDIT: agora github ) (não espero que seja um pacote popular, mas foi uma boa chance de aprender Cython).

Este método baseia-se no fato de que a criação de um .pyxarquivo com Cython.Distutils.build_ext(pelo menos com o Cython versão 0.14) sempre parece criar um .carquivo no mesmo diretório que o .pyxarquivo de origem .

Aqui está uma versão resumida da setup.pyqual espero que mostre o essencial:

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

Também editei MANIFEST.inpara garantir que isso mycythonmodule.cseja incluído em uma distribuição de origem (uma distribuição de origem criada com python setup.py sdist):

...
recursive-include cython *
...

Não me comprometo mycythonmodule.ccom o controle de versão 'trunk' (ou 'padrão' para o Mercurial). Ao fazer um lançamento, lembre-se de fazer o python setup.py build_extprimeiro, para garantir que mycythonmodule.cesteja presente e atualizado para a distribuição do código-fonte. Também faço uma ramificação de liberação e submeto o arquivo C na ramificação. Dessa forma, eu tenho um registro histórico do arquivo C que foi distribuído com essa versão.

Craig McQueen
fonte
Obrigado, é exatamente isso que eu precisava para um projeto Pyrex que estou abrindo! O MANIFEST.in me tropeçou por um segundo, mas eu só precisava dessa linha. Estou incluindo o arquivo C no controle de origem por interesse, mas entendo que é desnecessário.
23411 chmullig
Eu editei minha resposta para explicar como o arquivo C não está no tronco / padrão, mas é adicionado a uma ramificação de lançamento.
Craig McQueen
1
@CraigMcQueen obrigado pela ótima resposta, isso me ajudou muito! Eu estou querendo saber, no entanto, é o comportamento desejado para usar Cython quando disponível? Parece-me que seria melhor usar, por padrão, arquivos c pré-gerados, a menos que o usuário explicitamente queira usar o Cython; nesse caso, ele pode definir a variável de ambiente ou algo assim. Isso tornaria a instalação mais estável / robusta, porque o usuário pode obter resultados diferentes com base em qual versão do Cython ele instalou - ele pode nem estar ciente de que a instalou e está afetando a construção do pacote.
Martinsos 17/02
20

Adicionando à resposta de Craig McQueen: veja abaixo como substituir o sdistcomando para que o Cython compile automaticamente seus arquivos de origem antes de criar uma distribuição de origem.

Dessa forma, você não corre o risco de distribuir acidentalmente Cfontes desatualizadas . Também ajuda no caso de você ter controle limitado sobre o processo de distribuição, por exemplo, ao criar automaticamente distribuições a partir de integração contínua 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
fonte
19

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

É altamente recomendável que você distribua os arquivos .c gerados, bem como as fontes do Cython, para que os usuários possam instalar o módulo sem precisar do Cython disponível.

Também é recomendável que a compilação Cython não seja ativada por padrão na versão que você distribui. Mesmo que o usuário tenha o Cython instalado, ele provavelmente não deseja usá-lo apenas para instalar o seu módulo. Além disso, a versão que ele possui pode não ser a mesma que você usou e pode não compilar suas fontes corretamente.

Isso significa simplesmente que o arquivo setup.py enviado com você será apenas um arquivo distutils normal nos arquivos .c gerados, como exemplo básico que teríamos:

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

O mais fácil é incluir os dois, mas basta usar o arquivo c? Incluir o arquivo .pyx é bom, mas não é necessário depois que você tiver o arquivo .c. As pessoas que desejam recompilar o .pyx podem instalar o Pyrex e fazê-lo manualmente.

Caso contrário, você precisará ter um comando build_ext personalizado para distutils que constrói o arquivo C primeiro. Cython já inclui um. http://docs.cython.org/src/userguide/source_files_and_compilation.html

O que essa documentação não faz é dizer como tornar isso condicional, mas

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

Deve lidar com isso.

Lennart Regebro
fonte
1
Obrigado pela sua resposta. Isso é razoável, embora eu prefira se o setup.pypode construir diretamente a partir do .pyxarquivo quando o Cython está instalado. Minha resposta implementou isso também.
Craig McQueen
Bem, esse é o ponto principal da minha resposta. Não era apenas um setup.py completo.
Lennart Regebro
4

Incluir arquivos .c gerados (Cython) é bem estranho. Especialmente quando incluímos isso no git. Eu preferiria usar setuptools_cython . Quando o Cython não estiver disponível, ele criará um ovo que possui o ambiente Cython interno e, em seguida, seu código usando o ovo.

Um possível exemplo: https://github.com/douban/greenify/blob/master/setup.py


Atualização (05-01-2017):

Desde setuptools 18.0, não há necessidade de usar setuptools_cython. Aqui está um exemplo para criar o projeto Cython do zero sem setuptools_cython.

McKelvin
fonte
isso corrige o problema do Cython que não está sendo instalado, mesmo que você o especifique em setup_requires?
Kamil Sindi
também não é possível colocar 'setuptools>=18.0'setup_requires em vez de criar o método is_installed?
Kamil Sindi
1
@capitalistpug Primeiro você precisa ter certeza de setuptools>=18.0ter sido instalado, então você só precisa colocar 'Cython >= 0.18'em setup_requirese Cython será instalado durante a instalação progresso. Mas se você estiver usando o setuptools <18.0, mesmo o cython específico em setup_requires, ele não será instalado; nesse caso, você deve considerar o uso setuptools_cython.
McKelvin
Obrigado @McKelvin, esta parece ser uma ótima solução! Existe alguma razão para usarmos a outra abordagem, com a codificação antecipada dos arquivos de origem, além disso? Eu tentei sua abordagem e ela parece um pouco lenta durante a instalação (leva um minuto para instalar, mas é compilada em um segundo).
Martinsos 16/02
1
@Martinsos pip install wheel. Então deve ser o motivo 1. Instale a roda primeiro e tente novamente.
21717 McKelvin
2

Este é um script de configuração que escrevi que facilita a inclusão de diretórios aninhados dentro da compilação. É necessário executá-lo da pasta dentro de um pacote.

Estrutura Givig como esta:

__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

Compilação feliz;)

zzart
fonte
2

O hack simples que eu criei:

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 instalar o Cython se não puder ser importado. Provavelmente não se deve compartilhar esse código, mas para minhas próprias dependências é bom o suficiente.

kay - SE é mau
fonte
2

Todas as outras respostas contam com

  • distutils
  • importar de Cython.Build, o que cria um problema de galinha e ovo entre exigir o cython via setup_requirese importá-lo.

Uma solução moderna é usar o setuptools, veja esta resposta (o manuseio automático das extensões Cython requer o setuptools 18.0, ou seja, já está disponível há muitos anos). Um padrão moderno setup.pycom manipulação de requisitos, um ponto de entrada e um módulo cython poderia ter esta aparência:

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
fonte
Importar de Cython.Buildno momento da instalação causa ImportError para mim. Ter setuptools para compilar pyx é a melhor maneira de fazer isso.
Carson Ip
1

A maneira mais fácil que encontrei usando apenas as ferramentas de instalação em vez dos recursos distutils limitados é

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
fonte
De fato, com o setuptools, não há necessidade da importação explícita try / catch from Cython.Build, veja minha resposta.
bluenote10
0

Acho que encontrei uma maneira muito boa de fazer isso fornecendo um build_extcomando personalizado . A ideia é a seguinte:

  1. Eu adiciono os cabeçalhos numpy substituindo finalize_options()e executando import numpyno corpo da função, o que evita o problema de o numpy não estar disponível antes da setup()instalação.

  2. Se o cython estiver disponível no sistema, ele se conecta ao check_extensions_list()método do comando e cythoniza todos os módulos cython desatualizados, substituindo-os por extensões C que podem ser tratadas posteriormente pelo build_extension() método. Também fornecemos a última parte da funcionalidade em nosso módulo: isso significa que, se o cython não estiver disponível, mas tivermos uma extensão C presente, ele ainda funcionará, o que permite fazer distribuições de código-fonte.

Aqui está o código:

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

Isso permite que você escreva os setup()argumentos sem se preocupar com as importações e se existe um cython disponível:

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