diff --git a/.github/workflows/codespell_and_flake.yml b/.github/workflows/codespell_and_flake.yml deleted file mode 100644 index e191caa25d1..00000000000 --- a/.github/workflows/codespell_and_flake.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: 'codespell_and_flake' -# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#concurrency -# https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types#pullrequestevent -# workflow name, PR number (empty on push), push ref (empty on PR) -concurrency: - group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} - cancel-in-progress: true -on: - push: - branches: - - '*' - pull_request: - branches: - - '*' - -jobs: - style: - name: 'codespell and flake' - runs-on: ubuntu-20.04 - env: - CODESPELL_DIRS: 'mne/ doc/ tutorials/ examples/' - CODESPELL_SKIPS: 'doc/_build,doc/auto_*,*.fif,*.eve,*.gz,*.tgz,*.zip,*.mat,*.stc,*.label,*.w,*.bz2,*.annot,*.sulc,*.log,*.local-copy,*.orig_avg,*.inflated_avg,*.gii,*.pyc,*.doctree,*.pickle,*.inv,*.png,*.edf,*.touch,*.thickness,*.nofix,*.volume,*.defect_borders,*.mgh,lh.*,rh.*,COR-*,FreeSurferColorLUT.txt,*.examples,.xdebug_mris_calc,bad.segments,BadChannels,*.hist,empty_file,*.orig,*.js,*.map,*.ipynb,searchindex.dat,install_mne_c.rst,plot_*.rst,*.rst.txt,c_EULA.rst*,*.html,gdf_encodes.txt,*.svg,references.bib,*.css,*.edf,*.bdf,*.vhdr' - - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.9' - architecture: 'x64' - - run: | - python -m pip install --upgrade pip setuptools wheel - python -m pip install flake8 - name: 'Install dependencies' - - uses: rbialon/flake8-annotations@v1 - name: 'Setup flake8 annotations' - - run: make flake - name: 'Run flake8' - - uses: codespell-project/actions-codespell@v1.0 - with: - path: ${{ env.CODESPELL_DIRS }} - skip: ${{ env.CODESPELL_SKIPS }} - builtin: 'clear,rare,informal,names' - ignore_words_file: 'ignore_words.txt' - uri_ignore_words_list: 'bu' - name: 'Run codespell' diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml new file mode 100644 index 00000000000..4638064b646 --- /dev/null +++ b/.github/workflows/precommit.yml @@ -0,0 +1,14 @@ +name: Pre-commit + +on: [push, pull_request] + +jobs: + style: + name: Pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - uses: pre-commit/action@v3.0.0 diff --git a/.gitignore b/.gitignore index 40c64c7bb65..c73ee6d5257 100644 --- a/.gitignore +++ b/.gitignore @@ -92,5 +92,5 @@ cover venv/ *.json .hypothesis/ - +.ruff_cache/ .ipynb_checkpoints/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..4814c23d8eb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +default_language_version: + python: python3.11 + +repos: +# - repo: https://github.com/psf/black +# rev: 23.1.0 +# hooks: +# - id: black +# args: [--quiet] + +# Ruff mne +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.262 + hooks: + - id: ruff + name: ruff mne + files: ^mne/ + +# Ruff tutorials and examples +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.262 + hooks: + - id: ruff + name: ruff tutorials and examples + # D103: missing docstring in public function + # D400: docstring first line must end with period + args: ["--ignore=D103,D400"] + files: ^tutorials/|^examples/ + +# Codespell +- repo: https://github.com/codespell-project/codespell + rev: v2.2.3 + hooks: + - id: codespell + files: ^mne/|^doc/|^examples/|^tutorials/ + types_or: [python, bib, rst, inc] diff --git a/MANIFEST.in b/MANIFEST.in index 6c1aa9ff47f..e00b86e3e79 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -57,6 +57,7 @@ exclude tools exclude Makefile exclude .coveragerc exclude *.yml +exclude *.yaml exclude ignore_words.txt exclude .mailmap exclude codemeta.json diff --git a/Makefile b/Makefile index a162617cd0a..c0e47ada7fb 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ PYTHON ?= python PYTESTS ?= py.test -CTAGS ?= ctags CODESPELL_SKIPS ?= "doc/_build,doc/auto_*,*.fif,*.eve,*.gz,*.tgz,*.zip,*.mat,*.stc,*.label,*.w,*.bz2,*.annot,*.sulc,*.log,*.local-copy,*.orig_avg,*.inflated_avg,*.gii,*.pyc,*.doctree,*.pickle,*.inv,*.png,*.edf,*.touch,*.thickness,*.nofix,*.volume,*.defect_borders,*.mgh,lh.*,rh.*,COR-*,FreeSurferColorLUT.txt,*.examples,.xdebug_mris_calc,bad.segments,BadChannels,*.hist,empty_file,*.orig,*.js,*.map,*.ipynb,searchindex.dat,install_mne_c.rst,plot_*.rst,*.rst.txt,c_EULA.rst*,*.html,gdf_encodes.txt,*.svg,references.bib,*.css,*.edf,*.bdf,*.vhdr" CODESPELL_DIRS ?= mne/ doc/ tutorials/ examples/ all: clean inplace test test-doc @@ -25,13 +24,6 @@ clean-cache: clean: clean-build clean-pyc clean-so clean-ctags clean-cache -in: inplace # just a shortcut -inplace: - $(PYTHON) setup.py build_ext -i - -wheel: - $(PYTHON) setup.py sdist bdist_wheel - wheel_quiet: $(PYTHON) setup.py -q sdist bdist_wheel @@ -43,22 +35,6 @@ testing_data: pytest: test -test: in - rm -f .coverage - $(PYTESTS) -m 'not ultraslowtest' mne - -test-verbose: in - rm -f .coverage - $(PYTESTS) -m 'not ultraslowtest' mne --verbose - -test-fast: in - rm -f .coverage - $(PYTESTS) -m 'not slowtest' mne - -test-full: in - rm -f .coverage - $(PYTESTS) mne - test-no-network: in sudo unshare -n -- sh -c 'MNE_SKIP_NETWORK_TESTS=1 py.test mne' @@ -66,56 +42,20 @@ test-no-testing-data: in @MNE_SKIP_TESTING_DATASET_TESTS=true \ $(PYTESTS) mne -test-no-sample-with-coverage: in testing_data - rm -rf coverage .coverage - $(PYTESTS) --cov=mne --cov-report html:coverage - test-doc: sample_data testing_data $(PYTESTS) --doctest-modules --doctest-ignore-import-errors --doctest-glob='*.rst' ./doc/ --ignore=./doc/auto_examples --ignore=./doc/auto_tutorials --ignore=./doc/_build --ignore=./doc/conf.py --ignore=doc/sphinxext --fulltrace -test-coverage: testing_data - rm -rf coverage .coverage - $(PYTESTS) --cov=mne --cov-report html:coverage -# what's the difference with test-no-sample-with-coverage? - -test-mem: in testing_data - ulimit -v 1097152 && $(PYTESTS) mne - -trailing-spaces: - find . -name "*.py" | xargs perl -pi -e 's/[ \t]*$$//' +pre-commit: + @pre-commit run -a -ctags: - # make tags for symbol based navigation in emacs and vim - # Install with: sudo apt-get install exuberant-ctags - $(CTAGS) -R * - -upload-pipy: - python setup.py sdist bdist_egg register upload - -flake: - @if command -v flake8 > /dev/null; then \ - echo "Running flake8"; \ - flake8 --count; \ - else \ - echo "flake8 not found, please install it!"; \ - exit 1; \ - fi; - @echo "flake8 passed" +# Aliases for stuff we used to support or users might think of +ruff: pre-commit +flake: pre-commit +pep: pre-commit codespell: # running manually @codespell --builtin clear,rare,informal,names,usage -w -i 3 -q 3 -S $(CODESPELL_SKIPS) --ignore-words=ignore_words.txt --uri-ignore-words-list=bu $(CODESPELL_DIRS) -codespell-error: # running on travis - @codespell --builtin clear,rare,informal,names,usage -i 0 -q 7 -S $(CODESPELL_SKIPS) --ignore-words=ignore_words.txt --uri-ignore-words-list=bu $(CODESPELL_DIRS) - -pydocstyle: - @echo "Running pydocstyle" - @pydocstyle mne - -docstring: - @echo "Running docstring tests" - @$(PYTESTS) --doctest-modules mne/tests/test_docstring_parameters.py - check-manifest: check-manifest -q --ignore .circleci/config.yml,doc,logo,mne/io/*/tests/data*,mne/io/tests/data,mne/preprocessing/tests/data,.DS_Store,mne/_version.py @@ -125,26 +65,3 @@ check-readme: clean wheel_quiet nesting: @echo "Running import nesting tests" @$(PYTESTS) mne/tests/test_import_nesting.py - -pep: - @$(MAKE) -k flake pydocstyle docstring codespell-error check-manifest nesting check-readme - -manpages: - @echo "I: generating manpages" - set -e; mkdir -p _build/manpages && \ - cd bin && for f in mne*; do \ - descr=$$(grep -h -e "^ *'''" -e 'DESCRIP =' $$f -h | sed -e "s,.*' *\([^'][^']*\)'.*,\1,g" | head -n 1); \ - PYTHONPATH=../ \ - help2man -n "$$descr" --no-discard-stderr --no-info --version-string "$(uver)" ./$$f \ - >| ../_build/manpages/$$f.1; \ - done - -build-doc-dev: - cd doc; make clean - cd doc; DISPLAY=:1.0 xvfb-run -n 1 -s "-screen 0 1280x1024x24 -noreset -ac +extension GLX +render" make html_dev - -build-doc-stable: - cd doc; make clean - cd doc; DISPLAY=:1.0 xvfb-run -n 1 -s "-screen 0 1280x1024x24 -noreset -ac +extension GLX +render" make html_stable - -docstyle: pydocstyle diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 186c081f036..e27e056aa3f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -39,7 +39,7 @@ stages: pool: vmImage: 'ubuntu-latest' variables: - PYTHON_VERSION: '3.9' + PYTHON_VERSION: '3.11' PYTHON_ARCH: 'x64' steps: - bash: echo $(COMMIT_MSG) @@ -50,23 +50,14 @@ stages: addToPath: true displayName: 'Get Python' - bash: | - set -e + set -eo pipefail python -m pip install --progress-bar off --upgrade pip setuptools wheel python -m pip install --progress-bar off -r requirements_base.txt -r requirements_hdf5.txt -r requirements_testing.txt + pre-commit install --install-hooks displayName: Install dependencies - bash: | - make flake - displayName: make flake - - bash: | - make codespell-error - displayName: make codespell - - bash: | - make pydocstyle - displayName: make pydocstyle - condition: always() - - bash: | - make docstring - displayName: make docstring + make pre-commit + displayName: make ruff condition: always() - bash: | make nesting diff --git a/doc/install/contributing.rst b/doc/install/contributing.rst index d1419ef80f3..f7b278fe7d7 100644 --- a/doc/install/contributing.rst +++ b/doc/install/contributing.rst @@ -694,11 +694,16 @@ Adhere to standard Python style guidelines All contributions to MNE-Python are checked against style guidelines described in `PEP 8`_. We also check for common coding errors (such as variables that are defined but never used). We allow very few exceptions to these guidelines, and -use tools such as pep8_, pyflakes_, and flake8_ to check code style +use tools such as ruff_ to check code style automatically. From the :file:`mne-python` root directory, you can check for -style violations by running:: +style violations by first installing our pre-commit hook:: - $ make flake + $ pip install pre-commit + $ pre-commit install --install-hooks + +Then running:: + + $ make ruff # alias for `pre-commit run -a` in the shell. Several text editors or IDEs also have Python style checking, which can highlight style errors while you code (and train you to make those @@ -748,7 +753,7 @@ but complete docstrings are appropriate when private functions/methods are relatively complex. To run some basic tests on documentation, you can use:: $ pytest mne/tests/test_docstring_parameters.py - $ make docstyle + $ make ruff Cross-reference everywhere @@ -1097,8 +1102,7 @@ it can serve as a useful example of what to expect from the PR review process. .. linting .. _PEP 8: https://www.python.org/dev/peps/pep-0008/ -.. _pyflakes: https://pypi.org/project/pyflakes -.. _Flake8: http://flake8.pycqa.org/ +.. _ruff: https://beta.ruff.rs/docs .. misc diff --git a/examples/simulation/simulate_evoked_data.py b/examples/simulation/simulate_evoked_data.py index 0d4cff6a6c3..b906d2df265 100644 --- a/examples/simulation/simulate_evoked_data.py +++ b/examples/simulation/simulate_evoked_data.py @@ -55,7 +55,7 @@ def data_fun(times): - """Function to generate random source time courses""" + """Generate random source time courses.""" return (50e-9 * np.sin(30. * times) * np.exp(- (times - 0.15 + 0.05 * rng.randn(1)) ** 2 / 0.01)) diff --git a/examples/simulation/simulated_raw_data_using_subject_anatomy.py b/examples/simulation/simulated_raw_data_using_subject_anatomy.py index b78db66c965..0edb33e7d0f 100644 --- a/examples/simulation/simulated_raw_data_using_subject_anatomy.py +++ b/examples/simulation/simulated_raw_data_using_subject_anatomy.py @@ -124,8 +124,7 @@ def data_fun(times, latency, duration): - """Function to generate source time courses for evoked responses, - parametrized by latency and duration.""" + """Generate source time courses for evoked responses.""" f = 15 # oscillating frequency, beta band [Hz] sigma = 0.375 * duration sinusoid = np.sin(2 * np.pi * f * (times - latency)) diff --git a/examples/visualization/topo_customized.py b/examples/visualization/topo_customized.py index cc284431246..e9106a1e8d2 100644 --- a/examples/visualization/topo_customized.py +++ b/examples/visualization/topo_customized.py @@ -47,7 +47,8 @@ def my_callback(ax, ch_idx): - """ + """Handle axes callback. + This block of code is executed once you click on one of the channel axes in the plot. To work with the viz internals, this function should only take two parameters, the axis and the channel or data index. diff --git a/mne/beamformer/tests/test_resolution_matrix.py b/mne/beamformer/tests/test_resolution_matrix.py index d033eaf6b67..6d6730e3b9e 100755 --- a/mne/beamformer/tests/test_resolution_matrix.py +++ b/mne/beamformer/tests/test_resolution_matrix.py @@ -85,9 +85,9 @@ def test_resolution_matrix_lcmv(): # Some rows are off by about 0.1 - not yet clear why corr = [] - for (f, l) in zip(resmat_fwd, resmat_lcmv): + for (f, lf) in zip(resmat_fwd, resmat_lcmv): - corr.append(np.corrcoef(f, l)[0, 1]) + corr.append(np.corrcoef(f, lf)[0, 1]) # all row correlations should at least be above ~0.8 assert_allclose(corr, 1., atol=0.2) diff --git a/mne/chpi.py b/mne/chpi.py index cdfc9b558ae..b57477deb29 100644 --- a/mne/chpi.py +++ b/mne/chpi.py @@ -1212,8 +1212,8 @@ def compute_chpi_locs(info, chpi_amplitudes, t_step_max=1., too_close='raise', # check if data has sufficiently changed if last['sin_fit'] is not None: # first iteration corrs = np.array( - [np.corrcoef(s, l)[0, 1] - for s, l in zip(sin_fit, last['sin_fit'])]) + [np.corrcoef(s, lst)[0, 1] + for s, lst in zip(sin_fit, last['sin_fit'])]) corrs *= corrs # check to see if we need to continue if fit_time - last['coil_fit_time'] <= t_step_max - 1e-7 and \ diff --git a/mne/io/base.py b/mne/io/base.py index 96e4e0f2549..df198af4033 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -570,7 +570,10 @@ def time_as_index(self, times, use_rounding=False, origin=None): @property def _raw_lengths(self): - return [l - f + 1 for f, l in zip(self._first_samps, self._last_samps)] + return [ + last - first + 1 + for first, last in zip(self._first_samps, self._last_samps) + ] @property def annotations(self): # noqa: D401 diff --git a/mne/utils/check.py b/mne/utils/check.py index 6a66fb20edc..e351184680f 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -1122,6 +1122,7 @@ def _to_rgb(*args, name='color', alpha=False): @deprecated('has_nibabel is deprecated and will be removed in 1.5') def has_nibabel(): + """Check if nibabel is installed.""" return check_version('nibabel') # pragma: no cover diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..b8b664ab193 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[tool.codespell] +ignore-words = "ignore_words.txt" +uri-ignore-words-list = "bu" +builtin = "clear,rare,informal,names,usage" +skip = "doc/references.bib" + +[tool.ruff] +select = ["E", "F", "W", "D"] +exclude = ["__init__.py", "constants.py", "fixes.py", "resources.py"] +ignore = [ + "D100", # Missing docstring in public module + "D104", # Missing docstring in public package + "D413", # Missing blank line after last section +] + +[tool.ruff.pydocstyle] +convention = "numpy" +ignore-decorators = [ + "property", + "setter", + "mne.utils.copy_function_doc_to_method_doc", + "mne.utils.copy_doc", + "mne.utils.deprecated" +] + +[tool.ruff.per-file-ignores] +"tutorials/time-freq/10_spectrum_class.py" = [ + "E501" # line too long +] +"mne/datasets/*/*.py" = [ + "D103", # Missing docstring in public function +] +"mne/utils/tests/test_docs.py" = [ + "D101", # Missing docstring in public class + "D410", # Missing blank line after section + "D411", # Missing blank line before section + "D414", # Section has no content +] +"examples/*/*.py" = [ + "D205", # 1 blank line required between summary line and description +] + +[tool.pytest.ini_options] +addopts = """--durations=20 --doctest-modules -ra --cov-report= --tb=short \ + --doctest-ignore-import-errors --junit-xml=junit-results.xml \ + --ignore=doc --ignore=logo --ignore=examples --ignore=tutorials \ + --ignore=mne/gui/_*.py --ignore=mne/icons --ignore=tools \ + --ignore=mne/report/js_and_css \ + --color=yes --capture=sys""" +junit_family = "xunit2" diff --git a/requirements_testing.txt b/requirements_testing.txt index 3344f4b409e..c8ff7b5c5fb 100644 --- a/requirements_testing.txt +++ b/requirements_testing.txt @@ -3,11 +3,10 @@ pytest pytest-cov pytest-timeout pytest-harvest -flake8 -flake8-array-spacing +ruff numpydoc codespell -pydocstyle check-manifest twine wheel +pre-commit diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 396a8906cc0..00000000000 --- a/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[aliases] -release = egg_info -RDb '' -# Make sure the sphinx docs are built each time we do a dist. -# bdist = build_sphinx bdist -# sdist = build_sphinx sdist -# Make sure a zip file is created each time we build the sphinx docs -# build_sphinx = generate_help build_sphinx zip_help -# Make sure the docs are uploaded when we do an upload -# upload = upload upload_help - -[egg_info] -# tag_build = .dev - -[bdist_rpm] -doc_files = doc - -[flake8] -exclude = __init__.py,constants.py,fixes.py,resources.py,*doc/auto_*,*doc/_build*,build/* -ignore = W503,W504,I100,I101,I201,N806,E201,E202,E221,E222,E241 -# We add A for the array-spacing plugin, and ignore the E ones it covers above -select = A,E,F,W,C -# 10_spectrum_class.py has a wide rST table -per-file-ignores = - tutorials/time-freq/10_spectrum_class.py:E501 - -[tool:pytest] -addopts = - --durations=20 --doctest-modules -ra --cov-report= --tb=short - --doctest-ignore-import-errors --junit-xml=junit-results.xml - --ignore=doc --ignore=logo --ignore=examples --ignore=tutorials - --ignore=mne/gui/_*.py --ignore=mne/icons --ignore=tools - --ignore=mne/report/js_and_css - --color=yes --capture=sys -junit_family = xunit2 - -[pydocstyle] -convention = pep257 -match_dir = ^(?!\.|doc|tutorials|examples|logo|icons).*$ -match = (?!tests/__init__\.py|fixes).*\.py -add-ignore = D100,D104,D107,D413 -add-select = D214,D215,D404,D405,D406,D407,D408,D409,D410,D411 -ignore-decorators = ^(copy_.*_doc_to_|on_trait_change|cached_property|deprecated|property|.*setter).*