diff --git a/.github/environment.yml b/.github/environment.yml index 07ce6355..ce8c294a 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -4,6 +4,7 @@ dependencies: - numpy >=2.0 - swig - meson >=1.3.2 + - meson-python - compilers - pkg-config - pip diff --git a/doc/contribute.rst b/doc/contribute.rst index bcfce24c..34ddfdaa 100644 --- a/doc/contribute.rst +++ b/doc/contribute.rst @@ -8,6 +8,22 @@ If you have an issue with pyOptSparse, a bug to report, or a feature to request, This lets other users know about the issue. If you are comfortable fixing the issue, please do so and submit a pull request. +Editable Installs +----------------- +Due to the use of ``meson-python`` as the backend, the typical process of using ``pip install -e .`` to generate an editable install cannot be used. +Instead, based on the instructions `here `__, +you must first install the `build dependencies` yourself. +This can be done by looking at the ``requires`` field of the ``[build-system]`` section of the ``pyproject.toml`` file, or via +``pip install .[dev]`` + +Then, do the following: + +.. prompt:: bash + + pip install --no-build-isolation --editable . + +To run tests, ensure that the testing dependencies specified in the ``pyproject.toml`` file are also installed. + Coding style ------------ We use `ruff `_ and `pre-commit `_ for linting and formatting. @@ -49,6 +65,9 @@ When you add code or functionality, add tests that cover the new or modified cod These may be units tests for individual components or regression tests for entire models that use the new functionality. All the existing tests can be found under the ``test`` folder. +To run tests, ensure that the testing dependencies have been installed (see `pyproject.toml`). + + Pull requests ------------- Finally, after adding or modifying code, and making sure the steps above are followed, submit a pull request via the GitHub interface. diff --git a/doc/install.rst b/doc/install.rst index e3347ade..6c92ca11 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -98,11 +98,7 @@ If you encounter a ``no module named tkinter`` error when trying to run optview, Testing ------- pyOptSparse provides a set of unit and regression tests to verify the installation. -To run these tests, first install ``testflo`` which is a testing framework developed by the OpenMDAO team: - -.. prompt:: bash - - pip install testflo +To run these tests, first install testing dependencies via ``pip install .[testing]``. Then, in the project root directory, type: diff --git a/meson.build b/meson.build index a2219cd3..abfb31c1 100644 --- a/meson.build +++ b/meson.build @@ -1,24 +1,33 @@ -# Much of this is from SciPy - project( 'pyoptsparse', 'c', 'cpp', -# unnecessary metadata commented out until Meson supports PEP517 and installation with pip -# version: 'x.x.x', -# license: 'GPL-3', - meson_version: '>= 0.60', + version: run_command( + 'python', '-c', + ''' +import re +from pathlib import Path +init_file = Path("pyoptsparse/__init__.py") +match = re.search(r'__version__ = ["\\\']([\d\\.]+)["\\\']', init_file.read_text()) +print(match.group(1)) + ''' + ).stdout().strip(), + meson_version: '>= 0.64', default_options: [ 'buildtype=debugoptimized', - 'c_std=c99', - 'cpp_std=c++14', + 'b_ndebug=if-release', + 'c_std=c17', + 'cpp_std=c++17', ], ) -fortranobject_c = '../fortranobject.c' +# + +# install python sources +install_subdir('pyoptsparse', + exclude_directories: [ + 'pyCONMIN/source', + 'pyCONMIN/README', + 'pyNLPQLP/source', + 'pyNLPQLP/README', + 'pyNSGA2/source', + 'pyNSGA2/README', + 'pyPSQP/source', + 'pyPSQP/README', + 'pySLSQP/source', + 'pySLSQP/README', + 'pySNOPT/source', + 'pySNOPT/README' + ], + install_dir: py3.get_install_dir()) + +# install non-python sources subdir('pyoptsparse') diff --git a/pyoptsparse/meson.build b/pyoptsparse/meson.build index 7f1d811b..c2518d48 100644 --- a/pyoptsparse/meson.build +++ b/pyoptsparse/meson.build @@ -1,69 +1,56 @@ -# NumPy include directory - needed in all submodules -incdir_numpy = get_option('incdir_numpy') -if incdir_numpy == '' - incdir_numpy = run_command(py3_target, - [ - '-c', - 'import os; os.chdir(".."); import numpy; print(numpy.get_include())' - ], - check: true - ).stdout().strip() +# subdir('pySNOPT') subdir('pySLSQP') @@ -71,27 +58,3 @@ subdir('pyCONMIN') subdir('pyNLPQLP') subdir('pyNSGA2') subdir('pyPSQP') -#subdir('pyALPSO') -#subdir('pyParOpt') -#subdir('postprocessing') - -# test imports -# envdata = environment() -# python_paths = [join_paths(meson.current_build_dir(), '..')] -# envdata.prepend('PYTHONPATH', python_paths) - -# progs = [['SLSQP', 'pySLSQP', 'slsqp'], -# ['CONMIN', 'pyCONMIN', 'conmin'], -# ['PSQP', 'pyPSQP', 'psqp'], -# ['NSGA2', 'pyNSGA2', 'nsga2']] - - -# foreach p : progs -# import_command = 'from pyoptsparse.' + p[1] + ' import '+p[2]+'; print('+p[2]+'.__file__)' -# test( -# 'import test for '+p[0], -# py3_command, -# args: ['-c', import_command], -# env: envdata -# ) -# endforeach diff --git a/pyoptsparse/postprocessing/meson.build b/pyoptsparse/postprocessing/meson.build deleted file mode 100644 index 22e7114e..00000000 --- a/pyoptsparse/postprocessing/meson.build +++ /dev/null @@ -1,27 +0,0 @@ - -python_sources = [ - 'OptView.py', - 'OptView_baseclass.py', - 'OptView_dash.py', - '__init__.py', - 'view_saved_figure.py' -] - -py3_target.install_sources( - python_sources, - pure: true, - subdir: 'pyoptsparse/postprocessing' -) - -asset_sources = [ - 'assets/OptViewIcon.gif', - 'assets/base-styles.css', - 'assets/custom-styles.css', - 'assets/logo.png' -] - -py3_target.install_sources( - asset_sources, - pure: true, - subdir: 'pyoptsparse/postprocessing/assets' -) diff --git a/pyoptsparse/pyALPSO/meson.build b/pyoptsparse/pyALPSO/meson.build deleted file mode 100644 index 3a967556..00000000 --- a/pyoptsparse/pyALPSO/meson.build +++ /dev/null @@ -1,13 +0,0 @@ -python_sources = [ - '__init__.py', - 'alpso.py', - 'alpso_ext.py', - 'pyALPSO.py', - 'LICENSE' -] - -py3_target.install_sources( - python_sources, - pure: true, - subdir: 'pyoptsparse/pyALPSO' -) diff --git a/pyoptsparse/pyCONMIN/meson.build b/pyoptsparse/pyCONMIN/meson.build index a2eb7e5c..9d9b905f 100644 --- a/pyoptsparse/pyCONMIN/meson.build +++ b/pyoptsparse/pyCONMIN/meson.build @@ -2,11 +2,11 @@ conmin_source = custom_target('conminmodule.c', input : ['source/f2py/conmin.pyf', ], output : ['conminmodule.c', 'conmin-f2pywrappers.f'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pyCONMIN'] ) -py3_target.extension_module('conmin', +py3.extension_module('conmin', 'source/openunit.f', 'source/cnmn00.f', 'source/cnmn01.f', @@ -21,21 +21,7 @@ py3_target.extension_module('conmin', 'source/conmin.f', 'source/closeunit.f', conmin_source, - fortranobject_c, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies: [fortranobject_dep], subdir: 'pyoptsparse/pyCONMIN', - install : false, + install: true, build_rpath: '') - -#python_sources = [ -# '__init__.py', -# 'pyCONMIN.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyCONMIN' -#) diff --git a/pyoptsparse/pyIPOPT/meson.build b/pyoptsparse/pyIPOPT/meson.build deleted file mode 100644 index 7ea72e61..00000000 --- a/pyoptsparse/pyIPOPT/meson.build +++ /dev/null @@ -1,10 +0,0 @@ -# python_sources = [ -# '__init__.py', -# 'pyIPOPT.py', -# ] - -# py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyIPOPT' -# ) diff --git a/pyoptsparse/pyNLPQLP/meson.build b/pyoptsparse/pyNLPQLP/meson.build index 352d9bc6..3e09e538 100644 --- a/pyoptsparse/pyNLPQLP/meson.build +++ b/pyoptsparse/pyNLPQLP/meson.build @@ -11,31 +11,17 @@ if HAS_NLPQLP nlpqlp_source = custom_target('nlpqlpmodule.c', input : ['source/f2py/nlpqlp.pyf'], output : ['nlpqlpmodule.c'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pyNLPQLP'] ) - py3_target.extension_module('nlpqlp', + py3.extension_module('nlpqlp', 'source/wrapper.F90', 'source/NLPQLP.F', 'source/QL.F', nlpqlp_source, - fortranobject_c, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies: [fortranobject_dep], subdir: 'pyoptsparse/pyNLPQLP', - install : false + install: true ) endif - -#python_sources = [ -# '__init__.py', -# 'pyNLPQLP.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyNLPQLP' -#) diff --git a/pyoptsparse/pyNSGA2/meson.build b/pyoptsparse/pyNSGA2/meson.build index 48faac1a..219bea0f 100644 --- a/pyoptsparse/pyNSGA2/meson.build +++ b/pyoptsparse/pyNSGA2/meson.build @@ -1,12 +1,12 @@ swig = find_program('swig', required: true) nsga2_source = custom_target('nsga2_wrap.c', - input : ['source/swig/nsga2.i'], - output : ['nsga2_wrap.c'], - command: ['swig', '-o', 'pyoptsparse/pyNSGA2/nsga2_wrap.c', '-python', '-interface', 'nsga2', '@INPUT@'] - ) + input : ['source/swig/nsga2.i'], + output : ['nsga2_wrap.c'], + command: ['swig', '-o', 'pyoptsparse/pyNSGA2/nsga2_wrap.c', '-python', '-interface', 'nsga2', '@INPUT@'] + ) -py3_target.extension_module('nsga2', +py3.extension_module('nsga2', 'source/allocate.c', 'source/auxiliary.c', 'source/crossover.c', @@ -30,16 +30,4 @@ py3_target.extension_module('nsga2', dependencies : py3_dep, subdir: 'pyoptsparse/pyNSGA2', link_language: 'c', - install : false) - -#python_sources = [ -# '__init__.py', -# 'pyNSGA2.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyNSGA2' -#) + install: true) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 1af2a389..68d788f3 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -622,8 +622,12 @@ def import_module( try: module = importlib.import_module(module_name) except ImportError as e: - if on_error.lower() == "raise": - raise e - else: - module = e + try: + full_module_name = f"pyoptsparse.{path[0].split('/')[-1]}.{module_name}" + module = importlib.import_module(full_module_name) + except (ImportError, IndexError): + if on_error.lower() == "raise": + raise e from e + else: + module = e return module diff --git a/pyoptsparse/pyPSQP/meson.build b/pyoptsparse/pyPSQP/meson.build index 47bc59b0..1cf920ec 100644 --- a/pyoptsparse/pyPSQP/meson.build +++ b/pyoptsparse/pyPSQP/meson.build @@ -1,11 +1,11 @@ psqp_source = custom_target('psqpmodule.c', input : ['source/f2py/psqp.pyf'], output : ['psqpmodule.c', 'psqp-f2pywrappers.f'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pyPSQP', '--no-wrap-functions'] ) -py3_target.extension_module('psqp', +py3.extension_module('psqp', 'source/closeunit.f', 'source/mqsubs.f', 'source/openunit.f', @@ -13,20 +13,6 @@ py3_target.extension_module('psqp', 'source/psqp.f', 'source/psqp_wrap.f90', psqp_source, - fortranobject_c, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies: [fortranobject_dep], subdir: 'pyoptsparse/pyPSQP', - install : false) - -#python_sources = [ -# '__init__.py', -# 'pyPSQP.py', -# 'LICENSE' -#] -# -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pyPSQP' -#) + install: true) diff --git a/pyoptsparse/pyParOpt/meson.build b/pyoptsparse/pyParOpt/meson.build deleted file mode 100644 index 4500d203..00000000 --- a/pyoptsparse/pyParOpt/meson.build +++ /dev/null @@ -1,10 +0,0 @@ -python_sources = [ - '__init__.py', - 'ParOpt.py', -] - -py3_target.install_sources( - python_sources, - pure: true, - subdir: 'pyoptsparse/pyParOpt' -) diff --git a/pyoptsparse/pySLSQP/meson.build b/pyoptsparse/pySLSQP/meson.build index e898636b..29717b7c 100644 --- a/pyoptsparse/pySLSQP/meson.build +++ b/pyoptsparse/pySLSQP/meson.build @@ -1,11 +1,11 @@ slsqp_source = custom_target('slsqpmodule.c', input : ['source/f2py/slsqp.pyf'], output : ['slsqpmodule.c'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pySLSQP'] ) -py3_target.extension_module('slsqp', +py3.extension_module('slsqp', 'source/closeunit.f', 'source/daxpy.f', 'source/dcopy.f', @@ -24,20 +24,6 @@ py3_target.extension_module('slsqp', 'source/slsqp.f', 'source/slsqpb.f', slsqp_source, - fortranobject_c, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies: [fortranobject_dep], subdir: 'pyoptsparse/pySLSQP', - install : false) - -#python_sources = [ -# '__init__.py', -# 'pySLSQP.py', -# 'LICENSE' -#] - -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pySLSQP' -#) + install: true) diff --git a/pyoptsparse/pySNOPT/meson.build b/pyoptsparse/pySNOPT/meson.build index 61edfdb0..e93b266c 100644 --- a/pyoptsparse/pySNOPT/meson.build +++ b/pyoptsparse/pySNOPT/meson.build @@ -14,30 +14,16 @@ if HAS_SNOPT snopt_source = custom_target('snoptmodule.c', input : ['source/f2py/snopt.pyf'], output : ['snoptmodule.c'], - command: [py3_command, '-m', 'numpy.f2py', '@INPUT@', + command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', 'pyoptsparse/pySNOPT'] ) - py3_target.extension_module('snopt', + py3.extension_module('snopt', snopt_source, - fortranobject_c, snopt_source_files, - include_directories: [inc_np, inc_f2py], - dependencies : py3_dep, + dependencies : [fortranobject_dep], subdir: 'pyoptsparse/pySNOPT', - install : false, + install: true, fortran_args: '-ffixed-line-length-80' ) endif - -#python_sources = [ -# '__init__.py', -# 'pySNOPT.py', -# 'LICENSE' -#] - -#py3_target.install_sources( -# python_sources, -# pure: false, -# subdir: 'pyoptsparse/pySNOPT' -#) diff --git a/pyproject.toml b/pyproject.toml index ce02acea..7405b275 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,48 @@ [build-system] -requires = ["setuptools>=42", "meson>=0.60.0", "ninja", "numpy>=2.0", "swig"] -build-backend = "setuptools.build_meta" +requires = ["meson-python", "ninja", "numpy>=2.0", "swig"] +build-backend = "mesonpy" + +[project] +name = "pyoptsparse" +description = "Python package for formulating and solving nonlinear constrained optimization problems" +readme = "README.md" +license = {file = "LICENSE"} +requires-python = ">=3.9" +dependencies = [ + "packaging", + "sqlitedict>=1.6", + "numpy>=1.21", + "scipy>=1.7", + "mdolab-baseclasses>=1.3.1" +] +dynamic = ["version"] # version is dynamically populated from meson project + +[project.optional-dependencies] +optview = [ + "dash", + "plotly", + "matplotlib" +] +docs = [ + "sphinx", + "sphinx_rtd_theme" +] +testing = [ + "testflo>=1.4.5", + "parameterized", +] +dev = [ + "meson-python", + "ninja", + "numpy", + "swig", + "testflo>=1.4.5", + "parameterized", +] + +[project.urls] +Homepage = "https://github.com/mdolab/pyoptsparse" + +[project.scripts] +optview = "pyoptsparse.postprocessing.OptView:main" +optview_dash = "pyoptsparse.postprocessing.OptView_dash:main" diff --git a/setup.py b/setup.py deleted file mode 100644 index c100711e..00000000 --- a/setup.py +++ /dev/null @@ -1,135 +0,0 @@ -import os -import re -import shutil -import setuptools -import subprocess - - -def run_meson_build(): - prefix = os.path.join(os.getcwd(), staging_dir) - purelibdir = "." - - # check if meson extra args are specified - meson_args = "" - if "MESON_ARGS" in os.environ: - meson_args = os.environ["MESON_ARGS"] - - # configure - meson_path = shutil.which("meson") - meson_call = ( - f"{meson_path} setup {staging_dir} --prefix={prefix} " - + f"-Dpython.purelibdir={purelibdir} -Dpython.platlibdir={purelibdir} {meson_args}" - ) - sysargs = meson_call.split(" ") - sysargs = [arg for arg in sysargs if arg != ""] - p1 = subprocess.run(sysargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - print(p1.stdout.decode()) - setup_log = os.path.join(staging_dir, "setup.log") - with open(setup_log, "wb") as f: - f.write(p1.stdout) - if p1.returncode != 0: - raise OSError(sysargs, f"The meson setup command failed! Check the log at {setup_log} for more information.") - - # build - meson_call = f"{meson_path} compile -C {staging_dir}" - sysargs = meson_call.split(" ") - p2 = subprocess.run(sysargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - print(p2.stdout.decode()) - compile_log = os.path.join(staging_dir, "compile.log") - with open(compile_log, "wb") as f: - f.write(p2.stdout) - if p2.returncode != 0: - raise OSError( - sysargs, f"The meson compile command failed! Check the log at {compile_log} for more information." - ) - - -def copy_shared_libraries(): - build_path = os.path.join(staging_dir, "pyoptsparse") - for root, _dirs, files in os.walk(build_path): - for file in files: - # move pyoptsparse to just under staging_dir - if file.endswith((".so", ".lib", ".pyd", ".pdb", ".dylib")): - if ".so.p" in root or ".pyd.p" in root: # excludes intermediate object files - continue - file_path = os.path.join(root, file) - new_path = str(file_path) - match = re.search(staging_dir, new_path) - new_path = new_path[match.span()[1] + 1 :] - print(f"Copying build file {file_path} -> {new_path}") - shutil.copy(file_path, new_path) - - -if __name__ == "__main__": - # This is where the meson build system will install to, it is then - # used as the sources for setuptools - staging_dir = "meson_build" - - # this keeps the meson build system from running more than once - if "dist" not in str(os.path.abspath(__file__)) and not os.path.isdir(staging_dir): - cwd = os.getcwd() - run_meson_build() - os.chdir(cwd) - copy_shared_libraries() - - docs_require = "" - req_txt = os.path.join("doc", "requirements.txt") - if os.path.isfile(req_txt): - with open(req_txt) as f: - docs_require = f.read().splitlines() - - init_file = os.path.join("pyoptsparse", "__init__.py") - __version__ = re.findall( - r"""__version__ = ["']+([0-9\.]*)["']+""", - open(init_file).read(), - )[0] - - setuptools.setup( - name="pyoptsparse", - version=__version__, - description="Python package for formulating and solving nonlinear constrained optimization problems", - long_description="pyOptSparse is a Python package for formulating and solving nonlinear constrained optimization problems", - platforms=["Linux"], - keywords="optimization", - install_requires=[ - "packaging", - "sqlitedict>=1.6", - "numpy>=1.21", - "scipy>=1.7", - "mdolab-baseclasses>=1.3.1", - ], - extras_require={ - "optview": [ - "dash", - "plotly", - "matplotlib", - ], - "docs": docs_require, - "testing": ["testflo>=1.4.5", "parameterized"], - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Science/Research", - "Intended Audience :: Developers", - "Intended Audience :: Education", - "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python", - "Topic :: Scientific/Engineering", - "Topic :: Software Development", - "Topic :: Education", - ], - package_dir={"": "."}, - packages=setuptools.find_packages(where="."), - package_data={ - "": ["*.so", "*.lib", "*.pyd", "*.pdb", "*.dylib", "assets/*", "LICENSE"], - }, - python_requires=">=3.9", - entry_points={ - "gui_scripts": [ - "optview = pyoptsparse.postprocessing.OptView:main", - "optview_dash = pyoptsparse.postprocessing.OptView_dash:main", - ] - }, - )