From f353243c19bc12f8eaf01d9ed5088deb05e9ebe7 Mon Sep 17 00:00:00 2001 From: Tieqiong Zhang Date: Fri, 27 Dec 2024 20:19:32 -0500 Subject: [PATCH] replace SCon with modern build tools, rewrite ... ... setup.py and add pyproject.toml --- SConstruct | 177 --------------------------- pyproject.toml | 80 +++++++++++++ requirements/build.txt | 2 + requirements/conda.txt | 3 + requirements/docs.txt | 4 + requirements/pip.txt | 0 requirements/test.txt | 6 + setup.py | 179 +++------------------------- src/extensions/SConscript | 69 ----------- src/extensions/SConscript.configure | 73 ------------ 10 files changed, 113 insertions(+), 480 deletions(-) delete mode 100644 SConstruct create mode 100644 pyproject.toml create mode 100644 requirements/build.txt create mode 100644 requirements/conda.txt create mode 100644 requirements/docs.txt create mode 100644 requirements/pip.txt create mode 100644 requirements/test.txt mode change 100755 => 100644 setup.py delete mode 100644 src/extensions/SConscript delete mode 100644 src/extensions/SConscript.configure diff --git a/SConstruct b/SConstruct deleted file mode 100644 index 23023b4e..00000000 --- a/SConstruct +++ /dev/null @@ -1,177 +0,0 @@ -# This SConstruct is for faster parallel builds. -# Use "setup.py" for normal installation. - -MY_SCONS_HELP = """\ -SCons rules for compiling and installing diffpy.srreal. -SCons build is much faster when run with parallel jobs (-j4). -Usage: scons [target] [var=value] - -Targets: - -module build Python extension module srreal_ext.so [default] -install install to default Python package location -develop copy extension module to src/diffpy/srreal/ directory -test execute unit tests - -Build configuration variables: -%s -Variables can be also assigned in a user script sconsvars.py. -SCons construction environment can be customized in sconscript.local script. -""" - -import os -import re -import subprocess -import platform - -def subdictionary(d, keyset): - return dict(kv for kv in d.items() if kv[0] in keyset) - -def getsyspaths(*names): - pall = sum((os.environ.get(n, '').split(os.pathsep) for n in names), []) - rv = [p for p in pall if os.path.exists(p)] - return rv - -def pyoutput(cmd): - proc = subprocess.Popen([env['python'], '-c', cmd], - stdout=subprocess.PIPE, - universal_newlines=True) - out = proc.communicate()[0] - return out.rstrip() - -def pyconfigvar(name): - cmd = ('from distutils.sysconfig import get_config_var\n' - 'print(get_config_var(%r))\n') % name - return pyoutput(cmd) - -# copy system environment variables related to compilation -DefaultEnvironment(ENV=subdictionary(os.environ, ''' - PATH PYTHONPATH GIT_DIR - CPATH CPLUS_INCLUDE_PATH LIBRARY_PATH LD_RUN_PATH - LD_LIBRARY_PATH DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH - MACOSX_DEPLOYMENT_TARGET LANG - _PYTHON_SYSCONFIGDATA_NAME - _CONDA_PYTHON_SYSCONFIGDATA_NAME - '''.split()) -) - -# Create construction environment -env = DefaultEnvironment().Clone() - -# Variables definitions below work only with 0.98 or later. -env.EnsureSConsVersion(0, 98) - -# Customizable compile variables -vars = Variables('sconsvars.py') - -vars.Add(PathVariable('prefix', - 'installation prefix directory', None)) -vars.Add(EnumVariable('build', - 'compiler settings', 'fast', - allowed_values=('debug', 'fast'))) -vars.Add(EnumVariable('tool', - 'C++ compiler toolkit to be used', 'default', - allowed_values=('default', 'intelc'))) -vars.Add(BoolVariable('profile', - 'build with profiling information', False)) -vars.Add('python', - 'Python executable to use for installation.', 'python') -vars.Update(env) -env.Help(MY_SCONS_HELP % vars.GenerateHelpText(env)) - -# Use Intel C++ compiler if requested by the user. -icpc = None -if env['tool'] == 'intelc': - icpc = env.WhereIs('icpc') - if not icpc: - print("Cannot find the Intel C/C++ compiler 'icpc'.") - Exit(1) - env.Tool('intelc', topdir=icpc[:icpc.rfind('/bin')]) - -# Figure out compilation switches, filter away C-related items. -good_python_flag = lambda n : ( - not isinstance(n, str) or - not re.match(r'(-g|-Wstrict-prototypes|-O\d|-fPIC)$', n)) -# Determine python-config script name. -pyversion = pyoutput('import sys; print("%i.%i" % sys.version_info[:2])') -pycfgname = 'python%s-config' % (pyversion if pyversion[0] == '3' else '') -pybindir = os.path.dirname(env.WhereIs(env['python'])) -pythonconfig = os.path.join(pybindir, pycfgname) -# Verify python-config comes from the same path as the target python. -xpython = env.WhereIs(env['python']) -xpythonconfig = env.WhereIs(pythonconfig) -if os.path.dirname(xpython) != os.path.dirname(xpythonconfig): - print("Inconsistent paths of %r and %r" % (xpython, xpythonconfig)) - Exit(1) -# Process the python-config flags here. -env.ParseConfig(pythonconfig + " --cflags") -env.Replace(CCFLAGS=[f for f in env['CCFLAGS'] if good_python_flag(f)]) -env.Replace(CPPDEFINES='') -# the CPPPATH directories are checked by scons dependency scanner -cpppath = getsyspaths('CPLUS_INCLUDE_PATH', 'CPATH') -env.AppendUnique(CPPPATH=cpppath) -# Insert LIBRARY_PATH explicitly because some compilers -# ignore it in the system environment. -env.PrependUnique(LIBPATH=getsyspaths('LIBRARY_PATH')) -# Add shared libraries. -# Note: libdiffpy and boost_python are added from SConscript.configure. - -fast_linkflags = ['-s'] -fast_shlinkflags = pyconfigvar('LDSHARED').split()[1:] - -# Specify minimum C++ standard. Allow later standard from sconscript.local. -# In case of multiple `-std` options the last option holds. -env.PrependUnique(CXXFLAGS='-std=c++11', delete_existing=1) - -# Platform specific intricacies. -if env['PLATFORM'] == 'darwin': - env.AppendUnique(CXXFLAGS='-ftemplate-depth-256') - darwin_shlinkflags = [n for n in env['SHLINKFLAGS'] - if n != '-dynamiclib'] - env.Replace(SHLINKFLAGS=darwin_shlinkflags) - env.AppendUnique(SHLINKFLAGS=['-bundle']) - env.AppendUnique(SHLINKFLAGS=['-undefined', 'dynamic_lookup']) - fast_linkflags[:] = [] - -# Compiler specific options -if icpc: - # options for Intel C++ compiler on hpc dev-intel07 - env.AppendUnique(CCFLAGS=['-w1', '-fp-model', 'precise']) - env.PrependUnique(LIBS=['imf']) - fast_optimflags = ['-fast', '-no-ipo'] -else: - # g++ options - env.AppendUnique(CCFLAGS=['-Wall']) - fast_optimflags = ['-ffast-math'] - -# Configure build variants -if env['build'] == 'debug': - env.AppendUnique(CCFLAGS='-g') -elif env['build'] == 'fast': - env.AppendUnique(CCFLAGS=['-O3'] + fast_optimflags) - env.AppendUnique(CPPDEFINES='NDEBUG') - env.AppendUnique(LINKFLAGS=fast_linkflags) - env.AppendUnique(SHLINKFLAGS=fast_shlinkflags) - -if env['profile']: - env.AppendUnique(CCFLAGS='-pg') - env.AppendUnique(LINKFLAGS='-pg') - -builddir = env.Dir('build/%s-%s' % (env['build'], platform.machine())) - -Export('env', 'pyconfigvar', 'pyoutput', 'pyversion') - -def GlobSources(pattern): - """Same as Glob but also require that source node is a valid file. - """ - rv = [f for f in Glob(pattern) if f.srcnode().isfile()] - return rv - -Export('GlobSources') - -if os.path.isfile('sconscript.local'): - env.SConscript('sconscript.local') - -env.SConscript('src/extensions/SConscript', variant_dir=builddir) - -# vim: ft=python diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..79c72f03 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,80 @@ +[build-system] +requires = ["setuptools>=62.0", "setuptools-git-versioning>=2.0", "numpy"] +build-backend = "setuptools.build_meta" + +[project] +name = "diffpy.srreal" +dynamic=['version', 'dependencies'] +authors = [ + { name="Simon J.L. Billinge group", email="simon.billinge@gmail.com" }, +] +maintainers = [ + { name="Simon J.L. Billinge group", email="simon.billinge@gmail.com" }, +] +description = "calculators for PDF, bond valence sum, and other quantities based on atom pair interaction" +keywords = ['PDF BVS atom overlap calculator real-space'] +readme = "README.rst" +requires-python = ">=3.11, <3.14" +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Operating System :: Unix', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Topic :: Scientific/Engineering :: Physics', + 'Topic :: Scientific/Engineering :: Chemistry', +] + +[project.urls] +Homepage = "https://github.com/diffpy/diffpy.srreal/" +Issues = "https://github.com/diffpy/diffpy.srreal/issues/" + +[tool.setuptools-git-versioning] +enabled = true +template = "{tag}" +dev_template = "{tag}" +dirty_template = "{tag}" + +[tool.setuptools.packages.find] +where = ["src"] # list of folders that contain the packages (["."] by default) +include = ["*"] # package names should match these glob patterns (["*"] by default) +exclude = [] # exclude packages matching these glob patterns (empty by default) +namespaces = false # to disable scanning PEP 420 namespaces (true by default) + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements/pip.txt"]} + +[tool.codespell] +exclude-file = ".codespell/ignore_lines.txt" +ignore-words = ".codespell/ignore_words.txt" +skip = "*.cif,*.dat" + +[tool.black] +line-length = 115 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | \.rst + | \.txt + | _build + | buck-out + | build + | dist + + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data +)/ +''' diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 00000000..f72d870d --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,2 @@ +python +setuptools diff --git a/requirements/conda.txt b/requirements/conda.txt new file mode 100644 index 00000000..5e7d3831 --- /dev/null +++ b/requirements/conda.txt @@ -0,0 +1,3 @@ +numpy +boost +diffpy.structure diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 00000000..ab17b1c8 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,4 @@ +sphinx +sphinx_rtd_theme +doctr +m2r diff --git a/requirements/pip.txt b/requirements/pip.txt new file mode 100644 index 00000000..e69de29b diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 00000000..a7277865 --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,6 @@ +flake8 +pytest +codecov +coverage +pytest-cov +pytest-env diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 6256a9f5..6bb68e4e --- a/setup.py +++ b/setup.py @@ -1,195 +1,52 @@ #!/usr/bin/env python -# Installation script for diffpy.srreal - """diffpy.srreal - calculators for PDF, bond valence sum, and other quantities based on atom pair interaction. Packages: diffpy.srreal """ -import os -import re +import numpy import sys import glob -from setuptools import setup, find_packages -from setuptools import Extension -from numpy.distutils.misc_util import get_numpy_include_dirs +from setuptools import setup, Extension +from ctypes.util import find_library -# Use this version when git data are not available, like in git zip archive. -# Update when tagging a new release. -FALLBACK_VERSION = '1.3.0.post0' +def get_boost_libraries(): + base_lib = "boost_python" + major, minor = str(sys.version_info[0]), str(sys.version_info[1]) + tags = [f"{major}{minor}", major, ""] + mttags = ["", "-mt"] + candidates = [base_lib + tag for tag in tags for mt in mttags] + [base_lib] + for lib in candidates: + if find_library(lib): + return [lib] + raise RuntimeError("Cannot find a suitable Boost.Python library.") -# define extension arguments here ext_kws = { - 'libraries' : ['diffpy'], - 'extra_compile_args' : ['-std=c++11'], + 'libraries' : ["diffpy"] + get_boost_libraries(), + 'extra_compile_args' : ["-std=c++11"], 'extra_link_args' : [], - 'include_dirs' : get_numpy_include_dirs(), + 'include_dirs' : [numpy.get_include()], } -# determine if we run with Python 3. -PY3 = (sys.version_info[0] == 3) - -# Figure out the tagged name of boost_python library. -def get_boost_libraries(): - """Check for installed boost_python shared library. - - Returns list of required boost_python shared libraries that are installed - on the system. If required libraries are not found, an Exception will be - thrown. - """ - baselib = "boost_python" - major, minor = (str(x) for x in sys.version_info[:2]) - pytags = [major + minor, major, ''] - mttags = ['', '-mt'] - boostlibtags = [(pt + mt) for mt in mttags for pt in pytags] + [''] - from ctypes.util import find_library - for tag in boostlibtags: - lib = baselib + tag - found = find_library(lib) - if found: break - - # Show warning when library was not detected. - if not found: - import platform - import warnings - ldevname = 'LIBRARY_PATH' - if platform.system() == 'Darwin': - ldevname = 'DYLD_FALLBACK_LIBRARY_PATH' - wmsg = ("Cannot detect name suffix for the %r library. " - "Consider setting %s.") % (baselib, ldevname) - warnings.warn(wmsg) - - libs = [lib] - return libs - def create_extensions(): "Initialize Extension objects for the setup function." - blibs = [n for n in get_boost_libraries() - if not n in ext_kws['libraries']] - ext_kws['libraries'] += blibs ext = Extension('diffpy.srreal.srreal_ext', glob.glob('src/extensions/*.cpp'), **ext_kws) return [ext] -# versioncfgfile holds version data for git commit hash and date. -# It must reside in the same directory as version.py. -MYDIR = os.path.dirname(os.path.abspath(__file__)) -versioncfgfile = os.path.join(MYDIR, 'src/diffpy/srreal/version.cfg') -gitarchivecfgfile = os.path.join(MYDIR, '.gitarchive.cfg') - - -def gitinfo(): - from subprocess import Popen, PIPE - kw = dict(stdout=PIPE, cwd=MYDIR, universal_newlines=True) - proc = Popen(['git', 'describe', '--match=v[[:digit:]]*'], **kw) - desc = proc.stdout.read() - proc = Popen(['git', 'log', '-1', '--format=%H %ct %ci'], **kw) - glog = proc.stdout.read() - rv = {} - rv['version'] = '.post'.join(desc.strip().split('-')[:2]).lstrip('v') - rv['commit'], rv['timestamp'], rv['date'] = glog.strip().split(None, 2) - return rv - - -def getversioncfg(): - if PY3: - from configparser import RawConfigParser - else: - from ConfigParser import RawConfigParser - vd0 = dict(version=FALLBACK_VERSION, commit='', date='', timestamp=0) - # first fetch data from gitarchivecfgfile, ignore if it is unexpanded - g = vd0.copy() - cp0 = RawConfigParser(vd0) - cp0.read(gitarchivecfgfile) - if len(cp0.get('DEFAULT', 'commit')) > 20: - g = cp0.defaults() - mx = re.search(r'\btag: v(\d[^,]*)', g.pop('refnames')) - if mx: - g['version'] = mx.group(1) - # then try to obtain version data from git. - gitdir = os.path.join(MYDIR, '.git') - if os.path.exists(gitdir) or 'GIT_DIR' in os.environ: - try: - g = gitinfo() - except OSError: - pass - # finally, check and update the active version file - cp = RawConfigParser() - cp.read(versioncfgfile) - d = cp.defaults() - rewrite = not d or (g['commit'] and ( - g['version'] != d.get('version') or g['commit'] != d.get('commit'))) - if rewrite: - cp.set('DEFAULT', 'version', g['version']) - cp.set('DEFAULT', 'commit', g['commit']) - cp.set('DEFAULT', 'date', g['date']) - cp.set('DEFAULT', 'timestamp', g['timestamp']) - with open(versioncfgfile, 'w') as fp: - cp.write(fp) - return cp - -versiondata = getversioncfg() - -with open(os.path.join(MYDIR, 'README.rst')) as fp: - long_description = fp.read() - -# define distribution +# Extensions not included in pyproject.toml setup_args = dict( - name = "diffpy.srreal", - version = versiondata.get('DEFAULT', 'version'), - packages = find_packages(os.path.join(MYDIR, 'src')), - package_dir = {'' : 'src'}, - test_suite = 'diffpy.srreal.tests', - include_package_data = True, ext_modules = [], - install_requires = [ - 'diffpy.structure', - ], - zip_safe = False, - - author = "Simon J.L. Billinge group", - author_email = "sb2896@columbia.edu", - maintainer = "Pavol Juhas", - maintainer_email = "pavol.juhas@gmail.com", - description = ("calculators for PDF, bond valence sum, and other " - "quantities based on atom pair interaction."), - long_description = long_description, - long_description_content_type = 'text/x-rst', - license = 'BSD-style license', - url = "https://github.com/diffpy/diffpy.srreal/", - keywords = "PDF BVS atom overlap calculator real-space", - classifiers = [ - # List of possible values at - # http://pypi.python.org/pypi?:action=list_classifiers - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Education', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: POSIX', - 'Operating System :: Unix', - 'Programming Language :: C++', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Scientific/Engineering :: Chemistry', - 'Topic :: Scientific/Engineering :: Physics', - 'Topic :: Software Development :: Libraries', - ], ) + if __name__ == '__main__': setup_args['ext_modules'] = create_extensions() setup(**setup_args) - -# End of file diff --git a/src/extensions/SConscript b/src/extensions/SConscript deleted file mode 100644 index 5f96f611..00000000 --- a/src/extensions/SConscript +++ /dev/null @@ -1,69 +0,0 @@ -import sys -import os - -Import('env', 'GlobSources', 'pyoutput') - -# make sure numpy headers are available -npdirs = pyoutput( - 'from numpy.distutils.misc_util import get_numpy_include_dirs\n' - 'print("\\n".join(get_numpy_include_dirs()))') -npdirs = [d.strip() for d in npdirs.split('\n')] -env.AppendUnique(CPPPATH=npdirs) - -# configure the boost_python library, which may have different extensions -if not (GetOption('clean') or env.GetOption('help')): - SConscript('SConscript.configure') - -# python extension module -module = env.SharedLibrary('srreal_ext', GlobSources('*.cpp'), - SHLIBPREFIX='', SHLIBSUFFIX='.so') -Alias('module', module) - -# update egg info when package version changes. -basedir = Dir('#').abspath -version = pyoutput( - 'import sys\n' - 'sys.path.insert(0, %r)\n' - 'from setup import versiondata\n' - 'print(versiondata.get("DEFAULT", "version"))\n' % basedir) -egginfo = env.Command(NoCache('#/src/diffpy.srreal.egg-info/PKG-INFO'), - env.Value(version), - '$python -Wignore setup.py egg_info') - -# install extension module in a development mode. -develop = Alias('develop', [egginfo, Install('#/src/diffpy/srreal', module)]) - -test = env.Alias('test', develop, - '$python -m diffpy.srreal.tests.run') -AlwaysBuild(test) - -def resolve_distutils_target(target, source, env): - tgt = pyoutput('\n'.join([ - "from setuptools import Distribution, Extension", - "ext = Extension('diffpy.srreal.srreal_ext', [])", - "attrs = dict(ext_modules=[ext])", - "dist = Distribution(attrs)", - "bcmd = dist.get_command_obj('build_ext')", - "bcmd.finalize_options()", - "print(bcmd.get_ext_fullpath(ext.name))", - ])) - env['distsofile'] = env.File(tgt) - return 0 - -c = '$python setup.py easy_install --no-deps {} .' -p = '--prefix=$prefix' if 'prefix' in env else '' -cmd_install = c.format(p) - -install = env.Alias('install', module, [ - resolve_distutils_target, - Mkdir('$distsofile.dir'), - Copy('$distsofile', '$SOURCE'), - Touch('$distsofile'), - cmd_install, - ]) -AlwaysBuild(install) - -# default targets: -Default(module) - -# vim: ft=python diff --git a/src/extensions/SConscript.configure b/src/extensions/SConscript.configure deleted file mode 100644 index 95851774..00000000 --- a/src/extensions/SConscript.configure +++ /dev/null @@ -1,73 +0,0 @@ -Import('env', 'pyconfigvar', 'pyversion') - -# Helper functions ----------------------------------------------------------- - -def CheckOptimizerFlag(context, flag): - ccflags_save = context.env['CCFLAGS'] - context.Message('Checking if compiler allows {!r}... '.format(flag)) - context.env.Replace(CCFLAGS=[flag]) - result = context.TryCompile('int a;\n', '.cpp') - context.Result(result) - if not result: - ccflags_save.remove(flag) - context.env.Replace(CCFLAGS=ccflags_save) - return result - - -def configure_boost_library(libname): - '''Add a boost library to the configured environment allowing for any - of the boostmttags name extensions. - - libname -- boost library name without any extension - - Note: CheckLib function automatically adds library to the environment. - ''' - mttags = ['', '-mt'] - boostlibtags = mttags - # check more tags for boost_python - if libname == 'boost_python': - major, minor = pyversion.split('.') - pytags = [major + minor, major, ''] - boostlibtags = [(pt + mt) for mt in mttags for pt in pytags] - # using global conf defined below - for t in boostlibtags: - libnamefull = libname + t - if conf.CheckLib(libnamefull, language='C++'): - return - # library not found here - print('This program requires %r library.' % libname) - Exit(1) - -# Start configuration -------------------------------------------------------- - -# Anaconda Python is compiled with super fancy gcc optimizer flags. -# Remove any flags that are not supported by the current compiler. - -custom_tests = {'CheckOptimizerFlag' : CheckOptimizerFlag} -conf = Configure(env, custom_tests=custom_tests) -optflags = [o for o in env['CCFLAGS'] - if o[:2] in ('-f', '-m')] -for o in optflags: - conf.CheckOptimizerFlag(o) -conf.Finish() - -# Create configuration environment that links with Python shared_library, so -# that the boost_python check does not fail due to unresolved Python symbols. -ecfg = env.Clone() -ecfg.Append(LIBS=[]) -ecfg.MergeFlags(pyconfigvar('BLDLIBRARY')) -# make sure there are no implicit dependency nodes in added LIBS -ecfg.Replace(LIBS=[str(n) for n in ecfg['LIBS']]) -newlibsindex = len(ecfg['LIBS']) - -conf = Configure(ecfg) -if not conf.CheckLib('diffpy', language='C++'): - print("This program requires 'libdiffpy' library.") - Exit(1) -configure_boost_library('boost_python') -conf.Finish() - -# Use libraries that were found in the configuration. -env.AppendUnique(LIBS=ecfg['LIBS'][newlibsindex:]) - -# vim: ft=python