diff --git a/+euphonic/install_python_modules.m b/+euphonic/install_python_modules.m index f556a3c..4e95aba 100644 --- a/+euphonic/install_python_modules.m +++ b/+euphonic/install_python_modules.m @@ -6,6 +6,9 @@ function install_python_modules() % >> euphonic.install_python_modules req_mods = required_modules; + if isempty(req_mods) + return + end pipe = py.subprocess.PIPE; kwargs = pyargs('stdout', pipe, 'stderr', pipe); diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index ed3fbef..f24ff1a 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -14,14 +14,13 @@ jobs: matrix: os: [windows-latest, ubuntu-latest] matlab_version: [latest] - python_version: [3.9] + python_version: [3.11] euphonic_version: [''] # Test lowest supported Python/Euphonic versions, and lowest available Matlab include: - os: ubuntu-latest - matlab_version: R2020a - python_version: 3.7 - euphonic_version: '--version 1.2.0' + matlab_version: R2021b + python_version: 3.9 fail-fast: false runs-on: ${{ matrix.os }} defaults: @@ -29,54 +28,48 @@ jobs: shell: bash -l {0} # Needed to use conda environments steps: - name: Check out Horace-Euphonic-Interface - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: true - name: Set up MATLAB - uses: matlab-actions/setup-matlab@v1 # v1.1.0 required for Windows/MacOS support + uses: matlab-actions/setup-matlab@v2 # v1.1.0 required for Windows/MacOS support with: release: ${{ matrix.matlab_version }} - name: Set up Python - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python_version }} - - name: Set Python environment variable for access by Matlab - if: ${{ matrix.os != 'windows-latest' }} - run: echo "PYTHON_EX_PATH=`which python`" >> $GITHUB_ENV - - name: Set Python environment variable for access by Matlab (Windows) + auto-update-conda: true + conda-solver: libmamba + channels: conda-forge + - name: Set up Python environment (Windows) if: ${{ matrix.os == 'windows-latest' }} shell: powershell run: | + python -m pip install requests psutil numpy==1.26.4 brille==0.7.0 euphonic[phonopy_reader] $pypath = (Get-Command python).Path echo "PYTHON_EX_PATH=$pypath" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - - name: Update pip and create Matlab toolbox + - name: Set up Python environment (Linux) + if: ${{ matrix.os == 'ubuntu-latest' }} run: | - python -m pip install --upgrade pip && - python -m pip install requests && - python create_mltbx.py - - name: Install base Euphonic - run: | - python -m pip install numpy && - python euphonic_sqw_models/apply_requirements.py ${{ matrix.euphonic_version }} - python create_mltbx.py + python -m pip install requests psutil numpy==1.26.4 euphonic[phonopy_reader,brille] + echo "PYTHON_EX_PATH=`which python`" >> $GITHUB_ENV + - name: Create Matlab toolbox + run: python create_mltbx.py + - name: create_toolbox + uses: matlab-actions/run-command@v2 + with: + command: cd('mltbx'); create_mltbx() - name: Run tests with base Euphonic - uses: matlab-actions/run-command@v1 + uses: matlab-actions/run-command@v2 with: command: cd('test'); set_up_dependencies; run_tests('not', 'phonopy_reader', 'not', 'brille') - - name: Install Euphonic phonopy_reader - if: always() - run: python euphonic_sqw_models/apply_requirements.py ${{ matrix.euphonic_version}} --extras phonopy_reader - name: Run tests with Euphonic and phonopy_reader - if: always() - uses: matlab-actions/run-command@v1 + uses: matlab-actions/run-command@v2 with: command: cd('test'); set_up_dependencies; run_tests('phonopy_reader') - - name: Install brille - if: always() - run: python euphonic_sqw_models/apply_requirements.py ${{ matrix.euphonic_version}} --extras brille - name: Run tests with Euphonic and Brille - if: always() - uses: matlab-actions/run-command@v1 + uses: matlab-actions/run-command@v2 with: command: cd('test'); set_up_dependencies; run_tests('brille') - uses: codecov/codecov-action@v3 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3b5c5bb --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,35 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: doc/source/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: doc/requirements.txt diff --git a/create_mltbx.py b/create_mltbx.py index 502750d..1f8b354 100644 --- a/create_mltbx.py +++ b/create_mltbx.py @@ -1,58 +1,79 @@ -import os +import fileinput import re -import subprocess import shutil -import glob +import subprocess +from pathlib import Path -import versioneer import update_module_versions +import versioneer __version__ = versioneer.get_version() -HELPDOCSTR = '\n' \ - ' % Overloaded help command to display Python help in Matlab\n' \ - ' % To use it, please type\n' \ - ' %\n' \ - ' % >> import euphonic.help\n' \ - ' % >> help \n' \ - ' %\n' \ - ' % where is a Python class or method which has been wrapped for use in Matlab.\n' \ - ' % If the topic is not wrapped, the normal Matlab help is displayed.\n' \ - -def replace_matlab_docstring(filename, replacement_str): - with open(filename) as f: - txt = f.read() - cm = [m.start() for m in re.finditer(r'\n\s*%', txt)] - nl = [m.start() for m in re.finditer(r'\n', txt)] - idx = [cm[idx] for idx in range(len(cm)) if cm[idx] == nl[idx]] +HELPDOCSTR = """ + % Overloaded help command to display Python help in Matlab + % To use it, please type + % + % >> import euphonic.help + % >> help + % + % where is a Python class or method which has been wrapped for use in Matlab. + % If the topic is not wrapped, the normal Matlab help is displayed. +""" + + +def replace_matlab_docstring(filename: Path, replacement_str: str): + txt = filename.read_text(encoding="utf-8") + comment = [m.start() for m in re.finditer(r'\n\s*%', txt)] + newline = [m.start() for m in re.finditer(r'\n', txt)] + idx = [cm for cm, nl in zip(comment, newline) if cm == nl] newtxt = txt[:idx[0]] + replacement_str + txt[idx[-1]:] - with open(filename, 'w') as f: - f.write(newtxt) - -def create_mltbx(): - import fileinput - # replace version string - version = __version__.split('+')[0] if '+' in __version__ else __version__ # Matlab only accepts numbers - with fileinput.FileInput('mltbx/horace_euphonic_interface.prj', inplace=True) as prj: + filename.write_text(newtxt, encoding="utf-8") + + +def create_mltbx(base_path: Path): + """ + Create toolbox assuming files relative to `base_path` + """ + + # replace version string as MATLAB only accepts numbers + version = __version__.split('+')[0] if '+' in __version__ else __version__ + base_path = base_path.absolute() + + lpw_src = base_path / "light_python_wrapper" + eup_src = base_path / "+euphonic" + mdl_src = base_path / "euphonic_sqw_models" / "euphonic_sqw_models" + mltbx_path = base_path / 'mltbx' + lpw_dest = mltbx_path / "+light_python_wrapper" + eup_dest = mltbx_path / "+euphonic" + mdl_dest = mltbx_path / "euphonic_sqw_models" / "euphonic_sqw_models" + + with fileinput.FileInput(mltbx_path / 'horace_euphonic_interface.prj', inplace=True) as prj: for line in prj: - # FileInput redirect stdout to the file, for inplace replacement; end='' means don't add extra newlines - print(line.replace('1.0', f'{version}'), end='') + # FileInput redirects stdout to the file, for inplace replacement + print(line.replace('1.0', + f'{version}'), end='') + update_module_versions.update_module_versions() # shutil.copytree expects destination to not exist for dest_folder in ['+light_python_wrapper', 'euphonic_sqw_models', '+euphonic']: - if os.path.isdir('mltbx/' + dest_folder): shutil.rmtree('mltbx/' + dest_folder) - shutil.copyfile('LICENSE', 'mltbx/LICENSE') - shutil.copyfile('CITATION.cff', 'mltbx/CITATION.cff') - shutil.copytree('light_python_wrapper/+light_python_wrapper', 'mltbx/+light_python_wrapper') - shutil.copytree('euphonic_sqw_models/euphonic_sqw_models', 'mltbx/euphonic_sqw_models/euphonic_sqw_models') - shutil.copytree('+euphonic', 'mltbx/+euphonic') - for fil in glob.glob('light_python_wrapper/helputils/*.m'): shutil.copy(fil, 'mltbx/+euphonic') - for fil in glob.glob('light_python_wrapper/helputils/private/*.m'): shutil.copy(fil, 'mltbx/+euphonic/private') - replace_matlab_docstring('mltbx/+euphonic/help.m', HELPDOCSTR) - replace_matlab_docstring('mltbx/+euphonic/doc.m', HELPDOCSTR.replace('help', 'doc')) - subprocess.run(['matlab', '-batch', 'create_mltbx'], cwd='mltbx') - print('.mltbx created') + if (dest := mltbx_path / dest_folder).is_dir(): + shutil.rmtree(dest) -if __name__ == '__main__': - create_mltbx() + for file in ('LICENSE', 'CITATION.cff'): + shutil.copy(file, mltbx_path) + shutil.copytree(lpw_src / "+light_python_wrapper", lpw_dest) + shutil.copytree(mdl_src, mdl_dest) + shutil.copytree(eup_src, eup_dest) + for fil in (lpw_src / "helputils").glob("*.m"): + shutil.copy(fil, eup_dest) + for fil in (lpw_src / "helputils/private").glob("*.m"): + shutil.copy(fil, eup_dest / "private") + + replace_matlab_docstring(eup_dest / "help.m", HELPDOCSTR) + replace_matlab_docstring(eup_dest / "doc.m", HELPDOCSTR.replace('help', 'doc')) + + +if __name__ == '__main__': + curr_path = Path(__file__).parent + create_mltbx(curr_path) diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..1aef75d --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,2 @@ +sphinx==7.4.7 +sphinx_rtd_theme==2.0.0 diff --git a/doc/source/conf.py b/doc/source/conf.py index 5ea79ea..a5ff24d 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -14,6 +14,19 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) +# -- RTD setup --------------------------------------------------------------- +# See: https://about.readthedocs.com/blog/2024/07/addons-by-default/ +import os + +# Define the canonical URL if you are using a custom domain on Read the Docs +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") + +# Tell Jinja2 templates the build is running on Read the Docs +if os.environ.get("READTHEDOCS", "") == "True": + if "html_context" not in globals(): + html_context = {} + html_context["READTHEDOCS"] = True + # -- Project information ----------------------------------------------------- diff --git a/light_python_wrapper b/light_python_wrapper index 684dc30..69a91c4 160000 --- a/light_python_wrapper +++ b/light_python_wrapper @@ -1 +1 @@ -Subproject commit 684dc30c5667d203946165ff5ef11c819ceb0cd7 +Subproject commit 69a91c4ad07a47cbfd788ff2a2c6dd9b89573b2c diff --git a/test/EuphonicHelpTest.m b/test/EuphonicHelpTest.m index 64c85dc..9fb4a74 100644 --- a/test/EuphonicHelpTest.m +++ b/test/EuphonicHelpTest.m @@ -57,11 +57,17 @@ function run_euphonic_doc_tests(testCase) import matlab.unittest.constraints.IsFalse txt_fc_noimport = eval_doc('doc euphonic.ForceConstants'); txt_fc_import = eval_doc_import('doc euphonic.ForceConstants'); - % Checks both cases called "web" with the "-helpbrowser" argument - testCase.verifySubstring(txt_fc_noimport{2}, 'helpbrowser'); - testCase.verifySubstring(txt_fc_import{2}, 'helpbrowser'); - % Checks the two text are different - testCase.verifyThat(strcmp(txt_fc_noimport{1}, txt_fc_import{1}), IsFalse); + if verLessThan('matlab', '9.13') + % Checks both cases called "web" with the "-helpbrowser" argument + testCase.verifySubstring(txt_fc_noimport{2}, 'helpbrowser'); + testCase.verifySubstring(txt_fc_import{2}, 'helpbrowser'); + % Checks the two text are different + testCase.verifyThat(strcmp(txt_fc_noimport{1}, txt_fc_import{1}), IsFalse); + else + % Doc system changed in R2020b does not use the "web" command any more. + testCase.verifySubstring(txt_fc_noimport{1}.Topic, 'ForceConstants'); + testCase.verifySubstring(txt_fc_import{2}, 'helpbrowser'); + end % Checks we still have hyperlinks in the imported version testCase.verifyThat(contains(txt_fc_import{1}, 'href'), IsTrue); % Checks that __init__ method is included diff --git a/test/mymockfuncs/+matlab/+internal/+doc/+reference/getReferencePage.m b/test/mymockfuncs/+matlab/+internal/+doc/+reference/getReferencePage.m new file mode 100644 index 0000000..e02e82b --- /dev/null +++ b/test/mymockfuncs/+matlab/+internal/+doc/+reference/getReferencePage.m @@ -0,0 +1,9 @@ +function [docPage, displayText, primitive] = getReferencePage(varargin) + % Function to overload built-in "getReferencePage" function + global web_called_with; + web_called_with = varargin; + displayText = string.empty; + primitive = true; + docPage = []; +end + diff --git a/test/set_up_dependencies.m b/test/set_up_dependencies.m index ce1489c..d6514f7 100644 --- a/test/set_up_dependencies.m +++ b/test/set_up_dependencies.m @@ -28,4 +28,4 @@ % Set flags on Linux to avoid segfault with libraries if ~ispc py.sys.setdlopenflags(int32(10)) -end \ No newline at end of file +end diff --git a/update_module_versions.py b/update_module_versions.py index 5019354..686dbce 100644 --- a/update_module_versions.py +++ b/update_module_versions.py @@ -4,11 +4,12 @@ def get_module_versions(): # gets the required module versions from `min_requirements.txt` file curdir = os.path.dirname(os.path.abspath(__file__)) - req_file = os.path.join( - curdir, 'euphonic_sqw_models', 'min_requirements.txt') - if not os.path.isfile(req_file): - from update_dependencies import update_submodules - update_submodules('euphonic_sqw_models') + for submodule in [['light_python_wrapper', '+light_python_wrapper'], + ['euphonic_sqw_models', 'min_requirements.txt']]: + if not os.path.isfile(os.path.join(curdir, *submodule)): + from update_dependencies import update_submodules + update_submodules(submodule[0]) + req_file = os.path.join(curdir, 'euphonic_sqw_models', 'min_requirements.txt') with open(req_file, 'r') as minreq: reqstrs = minreq.read().splitlines() reqmods = []