使用numpy进行打包和测试套件

10

介绍

免责声明:我对使用distutils进行Python打包非常新,到目前为止,我只是将所有内容手动存储到模块和包中,并在此基础上开发。我以前从未编写过setup.py文件。

我有一个Fortran模块,我想在我的Python代码中与numpy一起使用它。我想到的最好的方法是使用f2py,因为它包含在numpy中。为了自动化构建流程,我想使用distutils和相应的numpy增强功能,其中包括方便的f2py包装器函数。

我不明白应该如何组织我的文件,以及如何包含我的测试套件。

我想要的是能够使用./setup.py进行构建、安装、测试和开发的可能性。

我的目录结构如下:

volterra
├── setup.py
└── volterra
    ├── __init__.py
    ├── integral.f90
    ├── test
    │   ├── __init__.py
    │   └── test_volterra.py
    └── volterra.f90

setup.py 文件包含以下内容:

def configuration(parent_package='', top_path=None):
    from numpy.distutils.misc_util import Configuration
    config = Configuration('volterra', parent_package, top_path)
    config.add_extension('_volterra',
                         sources=['volterra/integral.f90', 'volterra/volterra.f90'])
    return config


if __name__ == '__main__':
    from numpy.distutils.core import setup
    setup(**configuration(top_path='').todict())

在运行./setup.py build之后,我得到了:

build/lib.linux-x86_64-2.7/
└── volterra
    └── _volterra.so

其中不包括__init__.py文件和测试。

问题

  • 是否真的需要为每个扩展源文件(即volterra/integral.f90)添加路径?我不能给一个参数,说在volterra/中寻找东西吗? top_pathpackage_dir参数行不通。
  • 目前构建中没有包含__init__.py文件。为什么会这样?
  • 如何在这种设置中运行我的测试?
  • 在这种环境下进行开发的最佳工作流程是什么?我不想每次更改时都要安装我的软件包。当您需要编译某些扩展模块时,在源目录中进行开发的最佳方法是什么?

其中不包括__init__.py文件和测试。

问题

  • 是否真的需要为每个扩展源文件(即volterra/integral.f90)添加路径?我不能给一个参数,说在volterra/中寻找东西吗? top_pathpackage_dir参数行不通。
  • 目前构建中没有包含__init__.py文件。为什么会这样?
  • 如何在这种设置中运行我的测试?
  • 在这种环境下进行开发的最佳工作流程是什么?我不想每次更改时都要安装我的软件包。当您需要编译某些扩展模块时,在源目录中进行开发的最佳方法是什么?

要包含Fortran文件,您可以使用一个MANIFEST.in文件(http://docs.python.org/3/distutils/sourcedist.html#manifest),其中包含类似于“include * .f90”的内容。 - rtrwalker
但是,特别是__init__.py文件是包的一部分,应该自动包含,而无需我在清单中手动指定,对吧?否则,如果自动生成的清单甚至不包括包,那么自动清单生成的意义何在? - Lemming
我相信你确实需要包含每个源文件。今晚我会尝试从家里确认一下(我曾在以前的工作中使用NumPy和F2Py)。 - K.Niemczyk
我收回之前的说法,我不认为disutils使用清单。从numpy.setup.py中可以看到#在导入distutils之前,删除MANIFEST。当目录内容发生更改时,distutils无法正确更新它。 - rtrwalker
2个回答

2
这是我制作的项目的setup.py文件。我发现弄清楚setup.py/打包很令人沮丧,没有确定的答案,而且绝对不是pythonic的,因为没有一个明显的方法来完成某些事情。希望这会有所帮助。
您可能会发现以下几点有用:
- `find_packages`可以消除包含大量文件或处理生成清单的繁琐工作。 - `package_data`允许您轻松指定要包含的非.py文件 - `install_requires` / `tests_require`
如果您还没有distribute_setup.py的源代码,您需要找到它。
“是否真的有必要添加扩展名的每个源文件的路径?(例如:volterra/integral.f90)不能我给出一个参数来告诉你在volterra/ ? top_path和package_dir参数没起作用。”
“目前,init.py文件没有包含在内。为什么?”
希望`find_packages()`能解决这两个问题。我没有太多打包经验,但我还没有被迫手动包含它。
“如何在此设置中运行测试?”
我认为这可能是一个不同的问题,有很多答案取决于你如何进行测试。也许你可以把它单独问一下?
顺便说一下,我认为标准是将测试目录放在顶层位置。即`volterra/volterra` 和 `volterra/tests`。
“在这种环境中进行开发的最佳工作流是什么?我不想为每个更改安装软件包。当您需要编译某些扩展模块时,如何在源目录中进行开发?”
这可能值得再问一次。我不明白为什么您需要为每个更改安装软件包。如果您正在上传软件包,请勿在开发系统上安装它(除了测试安装之外),并直接从开发副本中进行工作。也许我的理解有误,因为我不使用已编译的扩展。
try:
    from setuptools import setup, find_packages
except ImportError:
    from distribute_setup import use_setuptools
    use_setuptools()
    from setuptools import setup, find_packages


setup(
    # ... other stuff
    py_modules=['distribute_setup'],
    packages=find_packages(),
    package_data={'': ['*.png']},  # for me to include anything with png
    install_requires=['numpy', 'treenode', 'investigators'],
    tests_require=['mock', 'numpy', 'treenode', 'investigators'],
)

感谢您的回答。我选择了rtrwalker的解决方案,因为它更加详细。一旦我有时间,我会考虑按照您的建议将一些子问题发布为新的单独问题。 - Lemming

2
这是我用的一个可行的 setup.py:
# pkg - A fancy software package
# Copyright (C) 2013  author (email)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see http://www.gnu.org/licenses/gpl.html.
"""pkg: a software suite for 

Hey look at me I'm a long description
But how long am I?

"""

from __future__ import division, print_function

#ideas for setup/f2py came from:
#    -numpy setup.py: https://github.com/numpy/numpy/blob/master/setup.py 2013-11-07
#    -winpython setup.py: http://code.google.com/p/winpython/source/browse/setup.py 2013-11-07
#    -needing to use 
#        import setuptools; from numpy.distutils.core import setup, Extension: 
#        http://comments.gmane.org/gmane.comp.python.f2py.user/707 2013-11-07
#    -wrapping FORTRAN code with f2py: http://www2-pcmdi.llnl.gov/cdat/tutorials/f2py-wrapping-fortran-code 2013-11-07
#    -numpy disutils: http://docs.scipy.org/doc/numpy/reference/distutils.html 2013-11-07
#    -manifest files in disutils: 
#        'distutils doesn't properly update MANIFEST. when the contents of directories change.'
#        https://github.com/numpy/numpy/blob/master/setup.py         
#    -if things are not woring try deleting build, sdist, egg directories  and try again: 
#        https://stackoverflow.com/a/9982133/2530083 2013-11-07
#    -getting fortran extensions to be installed in their appropriate sub package
#        i.e. "my_ext = Extension(name = 'my_pack._fortran', sources = ['my_pack/code.f90'])" 
#        Note that sources is a list even if one file: 
#        http://numpy-discussion.10968.n7.nabble.com/f2py-and-setup-py-how-can-I-specify-where-the-so-file-goes-tp34490p34497.html 2013-11-07
#    -install fortran source files into their appropriate sub-package 
#        i.e. "package_data={'': ['*.f95','*.f90']}# Note it's a dict and list":
#        https://dev59.com/yGIk5IYBdhLWcg3wadgD#19373744 2013-11-07
#    -Chapter 9 Fortran Programming with NumPy Arrays: 
#        Langtangen, Hans Petter. 2013. Python Scripting for Computational Science. 3rd edition. Springer.
#    -Hitchhikers guide to packaging :
#        http://guide.python-distribute.org/
#    -Python Packaging: Hate, hate, hate everywhere : 
#        http://lucumr.pocoo.org/2012/6/22/hate-hate-hate-everywhere/
#    -How To Package Your Python Code: 
#        http://www.scotttorborg.com/python-packaging/
#    -install testing requirements: 
#        https://dev59.com/Qm445IYBdhLWcg3ws8Zp#7747140 2013-11-07

import setuptools
from numpy.distutils.core import setup, Extension
import os
import os.path as osp

def readme(filename='README.rst'):
    with open('README.rst') as f:
        text=f.read()
    f.close()
    return text

def get_package_data(name, extlist):
    """Return data files for package *name* with extensions in *extlist*"""
    #modified slightly from taken from http://code.google.com/p/winpython/source/browse/setup.py 2013-11-7
    flist = []
    # Workaround to replace os.path.relpath (not available until Python 2.6):
    offset = len(name)+len(os.pathsep)
    for dirpath, _dirnames, filenames in os.walk(name):
        for fname in filenames:            
            if not fname.startswith('.') and osp.splitext(fname)[1] in extlist:
#                flist.append(osp.join(dirpath, fname[offset:]))
                flist.append(osp.join(dirpath, fname))
    return flist

DOCLINES = __doc__.split("\n")
CLASSIFIERS = """\
Development Status :: 1 - Planning
License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
Programming Language :: Python :: 2.7
Topic :: Scientific/Engineering
"""

NAME = 'pkg'
MAINTAINER = "me"
MAINTAINER_EMAIL = "me@me.com"
DESCRIPTION = DOCLINES[0]
LONG_DESCRIPTION = "\n".join(DOCLINES[2:])#readme('readme.rst')
URL = "http://meeeee.mmemem"
DOWNLOAD_URL = "https://github.com/rtrwalker/geotecha.git"
LICENSE = 'GNU General Public License v3 or later (GPLv3+)'
CLASSIFIERS = [_f for _f in CLASSIFIERS.split('\n') if _f]
KEYWORDS=''
AUTHOR = "me"
AUTHOR_EMAIL = "me.com"
PLATFORMS = ["Windows"]#, "Linux", "Solaris", "Mac OS-X", "Unix"]
MAJOR = 0
MINOR = 1
MICRO = 0
ISRELEASED = False
VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO)

INSTALL_REQUIRES=[]
ZIP_SAFE=False
TEST_SUITE='nose.collector'
TESTS_REQUIRE=['nose']

DATA_FILES = [(NAME, ['LICENSE.txt','README.rst'])]
PACKAGES=setuptools.find_packages()
PACKAGES.remove('tools')

PACKAGE_DATA={'': ['*.f95','*f90']}               
ext_files = get_package_data(NAME,['.f90', '.f95','.F90', '.F95'])
ext_module_names = ['.'.join(osp.splitext(v)[0].split(osp.sep)) for v in ext_files]
EXT_MODULES = [Extension(name=x,sources=[y]) for x, y in zip(ext_module_names, ext_files)]      


setup(
    name=NAME,
    version=VERSION,
    maintainer=MAINTAINER,
    maintainer_email=MAINTAINER_EMAIL,
    description=DESCRIPTION,
    long_description=LONG_DESCRIPTION,
    url=URL,
    download_url=DOWNLOAD_URL,
    license=LICENSE,
    classifiers=CLASSIFIERS,
    author=AUTHOR,
    author_email=AUTHOR_EMAIL,
    platforms=PLATFORMS,
    packages=PACKAGES,
    data_files=DATA_FILES,
    install_requires=INSTALL_REQUIRES,
    zip_safe=ZIP_SAFE,
    test_suite=TEST_SUITE,
    tests_require=TESTS_REQUIRE,
    package_data=PACKAGE_DATA,    
    ext_modules=EXT_MODULES,
    )

安装时,在命令行中使用以下命令:
python setup.py install
python setup.py clean --all

我唯一似乎有的问题是一个小问题。当我在site-packages中寻找我的包时,它被安装在egg文件夹内:C:\Python27\Lib\site-packages\pkg-0.1.0-py2.7-win32.egg\pkg。我在那里看到的大多数其他包都有一个与egg文件夹分开的C:\Python27\Lib\site-packages\pkg文件夹。有人知道如何获得这种分离吗?
至于测试,在安装后,我在命令行中键入以下内容:
nosetests package_name -v

尝试研究一下 python setup.py developPython setup.py develop vs install),这样每次更改后就不必重新安装软件包。
正如我在代码中所评论的那样,我发现以下内容非常有用:

非常感谢您提供如此详细的答案。我进行了适当的调整,现在它可以满足我的使用需求。我已经放弃了f2py,并成功地改用Cython进行了重写。对于Cython的设置,我使用了这个答案。 - Lemming

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接