diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0d4cf9c..6c521bf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,4 +22,4 @@ The optional `-- format` and `-- write` arguments (see above) attempt to correct ## Further checks: - [ ] The documentation builds: `$ nox -s docs`. - [ ] Code is commented, particularly in hard-to-understand areas. -- [ ] Tests are added that prove fix is effective or that feature works. +- [ ] Tests are added that prove fix is effective or that feature works. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c775f2d..3237e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Breaking Changes ### Chores +- Move to using `ruff` for linting and start tracking `Cython` addition issue ([#45](https://github.com/NatLabRockies/scikit-sundae/pull/45)) - Make GitHub hyperlinks reference new org name `NREL` -> `NatLabRockies` ([#42](https://github.com/NatLabRockies/scikit-sundae/pull/42)) - Allow single backticks for sphinx inline code (`default_role = 'literal'`) ([#40](https://github.com/NatLabRockies/scikit-sundae/pull/40)) - Rebrand NREL to NLR, and include name change for Alliance as well ([#39](https://github.com/NatLabRockies/scikit-sundae/pull/39)) diff --git a/noxfile.py b/noxfile.py index 6366541..ca32cf9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,7 +2,6 @@ import os import shutil -import importlib import nox @@ -12,7 +11,6 @@ @nox.session(name='cleanup', python=False) def run_cleanup(_) -> None: """Use os/shutil to remove some files/directories""" - if os.path.exists('.coverage'): os.remove('.coverage') @@ -23,22 +21,27 @@ def run_cleanup(_) -> None: @nox.session(name='linter', python=False) -def run_flake8(session: nox.Session) -> None: +def run_ruff(session: nox.Session) -> None: """ - Run flake8 with the github config file + Run ruff to check for linting errors. - Use the optional 'format' argument to run autopep8 prior to the linter. + Use the optional 'format' or 'format-unsafe' arguments to run ruff with the + --fix or --unsafe-fixes option prior to the linter. You can also use 'stats' + to show a summary of the found errors rather than a full report. """ + session.run('pip', 'install', '--upgrade', '--quiet', 'ruff') - session.run('pip', 'install', '--upgrade', '--quiet', 'flake8') - session.run('pip', 'install', '--upgrade', '--quiet', 'autopep8') + command = ['ruff', 'check'] + if 'stats' in session.posargs: + command.append('--statistics') if 'format' in session.posargs: - session.run('autopep8', '.', '--in-place', '--recursive', - '--global-config=.github/linters/.flake8') + command.append('--fix') + elif 'format-unsafe' in session.posargs: + command.extend(['--fix', '--unsafe-fixes']) - session.run('flake8', '--config=.github/linters/.flake8') + session.run(*command) @nox.session(name='codespell', python=False) @@ -50,7 +53,6 @@ def run_codespell(session: nox.Session) -> None: the files. Otherwise, you will only see a summary of the found errors. """ - session.run('pip', 'install', '--upgrade', '--quiet', 'codespell') command = ['codespell', '--config=.github/linters/.codespellrc'] @@ -69,7 +71,6 @@ def run_spellcheck(session: nox.Session) -> None: the files. Otherwise, you will only see a summary of the found errors. """ - run_codespell(session) command = ['codespell', '--config=.github/linters/.codespellrc'] @@ -89,20 +90,25 @@ def run_pytest(session: nox.Session) -> None: you can specify the number of workers using an int, e.g., parallel=4. """ - - package = importlib.util.find_spec('sksundae') - coverage_folder = os.path.dirname(package.origin) - if 'no-reports' in session.posargs: command = [ 'pytest', - f'--cov={coverage_folder}', # for editable or site-packages + '--cov=sksundae', + 'tests/', + ] + elif 'codecov' in session.posargs: + command = [ + 'pytest', + '--cov=sksundae', + '--cov-report=xml', 'tests/', ] else: + os.makedirs('reports', exist_ok=True) + command = [ 'pytest', - '--cov=src/sksundae', + '--cov=sksundae', '--cov-report=html:reports/htmlcov', '--cov-report=xml:reports/coverage.xml', '--junitxml=reports/junit.xml', @@ -117,15 +123,13 @@ def run_pytest(session: nox.Session) -> None: elif arg.startswith('parallel'): command[1:1] = ['-n', 'auto'] + os.environ['MPLBACKEND'] = 'Agg' session.run(*command) - run_cleanup(session) - @nox.session(name='badges', python=False) def run_genbadge(session: nox.Session) -> None: """Run genbadge to make test/coverage badges""" - session.run( 'genbadge', 'coverage', '-l', '-i', 'reports/coverage.xml', @@ -150,7 +154,6 @@ def run_sphinx(session: nox.Session) -> None: are not showing new pages. In general, try without 'clean' first. """ - if 'clean' in session.posargs: os.chdir('docs') session.run('make', 'clean') @@ -173,12 +176,11 @@ def run_pre_commit(session: nox.Session) -> None: """ Run all linters/tests and make new badges - Order of sessions: flake8, spellcheck, pytest, genbadge. Using 'format' for + Order of sessions: ruff, spellcheck, pytest, genbadge. Using 'format' for linter, 'write' for spellcheck, and/or 'parallel' for pytest is permitted. """ - - run_flake8(session) + run_ruff(session) run_spellcheck(session) run_pytest(session) @@ -197,6 +199,5 @@ def run_build_ext(session: nox.Session) -> None: extension files (.pyx, .pxd) are updated. """ - session.run('pip', 'install', '--upgrade', '--quiet', 'cython') session.run('python', 'setup.py', 'build_ext', '--inplace') diff --git a/pyproject.toml b/pyproject.toml index fd80f52..8f30a0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,31 @@ version = {attr = "sksundae.__version__"} [tool.setuptools.packages.find] where = ["src"] +[tool.ruff] +line-length = 80 +exclude = ["build", "docs", "images", "reports", "C_programs"] + +[tool.ruff.lint] +preview = true +select = [ + "E", # All pycodestyle errors + "W", # All pycodestyle warnings + "F", # All pyflakes errors + "B", # All flake8-bugbear checks + "D", # All pydocstyle checks +] +extend-select = [ + "E302", "E305", # Expected blank lines (requires preview=true, above) +] +ignore = [ + "B007", "B009", "B010", "B028", "B905", + "D1", "D203", "D205", "D212", "D400", "D401", "D415", + "E201", "E202", "E226", "E241", "E731", +] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*" = ["D"] + [project.optional-dependencies] docs = [ "sphinx", @@ -62,8 +87,7 @@ tests = [ ] dev = [ "nox", - "flake8", - "autopep8", + "ruff", "codespell", "genbadge[all]", "scikit-sundae[docs,tests]", diff --git a/scripts/version_checker.py b/scripts/version_checker.py index 0d0d564..94981a8 100644 --- a/scripts/version_checker.py +++ b/scripts/version_checker.py @@ -28,7 +28,6 @@ def get_latest_version(package: str, prefix: str = None) -> str: Failed to fetch PyPI data for requested package. """ - url = f"https://pypi.org/pypi/{package}/json" response = requests.get(url) @@ -73,7 +72,6 @@ def check_against_pypi(pypi: str, local: str) -> None: Local package is older than PyPI. """ - pypi = Version(pypi) local = Version(local) @@ -104,7 +102,6 @@ def check_against_tag(tag: str, local: str) -> None: Version mismatch: tag differs from local. """ - tag = Version(tag) local = Version(local) diff --git a/src/sksundae/cvode/_jactimes.py b/src/sksundae/cvode/_jactimes.py index 28f929a..78ee550 100644 --- a/src/sksundae/cvode/_jactimes.py +++ b/src/sksundae/cvode/_jactimes.py @@ -50,7 +50,6 @@ def __init__(self, setupfn: Callable | None, solvefn: Callable) -> None: 'rhsfn' -> 'setupfn' -> 'solvefn' for each integration step. """ - if setupfn is None: pass elif not isinstance(setupfn, Callable): diff --git a/src/sksundae/cvode/_precond.py b/src/sksundae/cvode/_precond.py index d9443e9..0144ac0 100644 --- a/src/sksundae/cvode/_precond.py +++ b/src/sksundae/cvode/_precond.py @@ -92,7 +92,6 @@ def psolvefn(t, y, yp, rvec, zvec, gamma, delta, lr, userdata): cvode/Usage/index.html#preconditioner-solve-iterative-linear-solvers """ - if setupfn is None: pass elif not isinstance(setupfn, Callable): diff --git a/src/sksundae/cvode/_solver.py b/src/sksundae/cvode/_solver.py index 478d705..ba35bd7 100644 --- a/src/sksundae/cvode/_solver.py +++ b/src/sksundae/cvode/_solver.py @@ -15,7 +15,7 @@ class CVODE: def __init__(self, rhsfn: Callable, **options) -> None: """ - This class wraps the C-based variable-coefficient ordinary differential + A class to wrap the C-based variable-coefficient ordinary differential equations (CVODE) solver from SUNDIALS [1]_ [2]_. Parameters @@ -138,7 +138,7 @@ def __init__(self, rhsfn: Callable, **options) -> None: Notes ----- - Return values from all user-defined function (e.g., 'rhsfn', 'eventsfn', + Return values from user-defined functions (e.g., 'rhsfn', 'eventsfn', and 'jacfn') are ignored by the solver. Instead the solver directly reads from pre-allocated memory. Output arrays (e.g., 'yp', 'events', and 'JJ') from each user-defined callable should be filled within each @@ -338,10 +338,14 @@ class CVODEResult(_CVODEResult): def __init__(self, **kwargs) -> None: """ Inherits from :class:`~sksundae.common.RichResult`. The solution class - groups output from :class:`CVODE` into an object with the fields: + groups output from :class:`CVODE` into an object. Descriptions of the + fields are given below. Parameters ---------- + **kwargs : dict + Keyword arguments for the result fields. The full list of fields is + given below. message : str Human-readable description of the status value. success : bool @@ -382,7 +386,7 @@ def __init__(self, **kwargs) -> None: Notes ----- Terminal events are appended to the end of 't' and 'y'. However, if an - event was not terminal then it will only appear in '\\*_events' outputs + event was not terminal then it will only appear in `*_events` outputs and not within the main output arrays. 'nfev' and 'njev' are cumulative for stepwise solution approaches. The diff --git a/src/sksundae/ida/_jactimes.py b/src/sksundae/ida/_jactimes.py index 93fc377..7422cd7 100644 --- a/src/sksundae/ida/_jactimes.py +++ b/src/sksundae/ida/_jactimes.py @@ -50,7 +50,6 @@ def __init__(self, setupfn: Callable | None, solvefn: Callable) -> None: 'resfn' -> 'setupfn' -> 'solvefn' for each integration step. """ - if setupfn is None: pass elif not isinstance(setupfn, Callable): diff --git a/src/sksundae/ida/_precond.py b/src/sksundae/ida/_precond.py index 4b914c2..71740d7 100644 --- a/src/sksundae/ida/_precond.py +++ b/src/sksundae/ida/_precond.py @@ -63,7 +63,6 @@ def __init__(self, setupfn: Callable | None, solvefn: Callable) -> None: ida/Usage/index.html#preconditioner-setup-iterative-linear-solvers """ - if setupfn is None: pass elif not isinstance(setupfn, Callable): diff --git a/src/sksundae/ida/_solver.py b/src/sksundae/ida/_solver.py index 59b4ff5..04bef9d 100644 --- a/src/sksundae/ida/_solver.py +++ b/src/sksundae/ida/_solver.py @@ -15,7 +15,7 @@ class IDA: def __init__(self, resfn: Callable, **options) -> None: """ - This class wraps the implicit differential algebraic (IDA) solver from + A class to wrap the implicit differential algebraic (IDA) solver from SUNDIALS [1]_ [2]_. IDA solves both ordinary differential equations (ODEs) and differiential agebraic equations (DAEs). @@ -144,7 +144,7 @@ def __init__(self, resfn: Callable, **options) -> None: Notes ----- - Return values from all user-defined function (e.g., 'resfn', 'eventsfn', + Return values from user-defined functions (e.g., 'resfn', 'eventsfn', and 'jacfn') are ignored by the solver. Instead the solver directly reads from pre-allocated memory. Output arrays (e.g., 'res', 'events', and 'JJ') from each user-defined callable should be filled within each @@ -363,10 +363,14 @@ class IDAResult(_IDAResult): def __init__(self, **kwargs) -> None: """ Inherits from :class:`~sksundae.common.RichResult`. The solution class - groups output from :class:`IDA` into an object with the fields: + groups output from :class:`IDA` into an object. Descriptions of the + fields are given below. Parameters ---------- + **kwargs : dict + Keyword arguments for the result fields. The full list of fields is + given below. message : str Human-readable description of the status value. success : bool @@ -413,7 +417,7 @@ def __init__(self, **kwargs) -> None: Notes ----- Terminal events are appended to the end of 't', 'y', and 'yp'. However, - if an event was not terminal then it will only appear in '\\*_events' + if an event was not terminal then it will only appear in `*_events` outputs and not within the main output arrays. 'nfev' and 'njev' are cumulative for stepwise solution approaches. The diff --git a/src/sksundae/jacband.py b/src/sksundae/jacband.py index f4b8b27..1282c9c 100644 --- a/src/sksundae/jacband.py +++ b/src/sksundae/jacband.py @@ -21,7 +21,6 @@ def _cvode_pattern(rhsfn: Callable, t0: float, y0: ndarray, userdata: Any = None) -> ndarray: """Jacobian pattern for CVODE functions. Access via j_pattern().""" - # wrap rhsfn for cases w/ and w/o userdata signature = inspect.signature(rhsfn) @@ -76,7 +75,6 @@ def j_pattern(j): def _ida_pattern(resfn: Callable, t0: float, y0: ndarray, yp0: ndarray = None, userdata: Any = None) -> ndarray: """Jacobian pattern for IDA functions. Access via j_pattern().""" - # wrap resfn for cases w/ and w/o userdata signature = inspect.signature(resfn) @@ -175,7 +173,6 @@ def j_pattern(rhsfn: Callable, t0: float, y0: ndarray, yp0: ndarray = None, either is or is not dependedent on variable `y_j`, respectively. """ - if yp0 is None: y0 = np.asarray(y0, dtype=float) return _cvode_pattern(rhsfn, t0, y0, userdata) diff --git a/src/sksundae/utils.py b/src/sksundae/utils.py index 64f605b..bd9717f 100644 --- a/src/sksundae/utils.py +++ b/src/sksundae/utils.py @@ -1,7 +1,4 @@ -""" -General-purpose module for shared utilities across the package. - -""" +"""General-purpose module for shared utilities across the package.""" import numpy as np @@ -16,7 +13,7 @@ class RichResult: def __init__(self, **kwargs): """ - This class is a based off the `_RichResult` class in the `scipy` + A container class based off the `_RichResult` class in the `scipy` library. It combines a series of formatting functions to make the printed 'repr' easy to read. Use this class directly by passing in any number of keyword arguments, or use it as a base class to have @@ -105,14 +102,12 @@ def sorter(d): def _indenter(s, n=0): """Ensures lines after the first are indented by the specified amount.""" - split = s.split('\n') return ('\n' + ' '*n).join(split) def _format_float_10(x): """Returns string representation of floats with exactly ten characters.""" - if np.isposinf(x): return ' inf' elif np.isneginf(x): @@ -124,7 +119,6 @@ def _format_float_10(x): def _format_dict(d, n=0, mplus=1, sorter=None): """Pretty printer for dictionaries.""" - if isinstance(d, dict): m = max(map(len, list(d.keys()))) + mplus # width to print keys