From b7e37e00a055d294b54563048234b9c75e4835a8 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 8 Oct 2025 09:03:30 -0600 Subject: [PATCH 1/4] use ruff as linter and formatter --- .ci/azure/style.yml | 19 +--- .github/workflows/ruff.yml | 8 ++ .gitignore | 2 + .pre-commit-config.yaml | 26 ++---- discretize/base/base_mesh.py | 8 +- discretize/base/base_regular_mesh.py | 5 +- discretize/base/base_tensor_mesh.py | 6 +- discretize/mixins/mesh_io.py | 4 +- .../operators/differential_operators.py | 4 +- discretize/operators/inner_products.py | 22 +++-- discretize/tests.py | 2 +- discretize/utils/mesh_utils.py | 2 +- docs/conf.py | 7 +- examples/plot_slicer_demo.py | 3 +- pyproject.toml | 86 +++---------------- tests/base/test_operators.py | 10 ++- tests/base/test_tests.py | 1 - tests/cyl/test_cylOperators.py | 8 +- tests/cyl/test_cyl_innerproducts.py | 8 +- tests/tree/test_safeguards.py | 1 - tutorials/pde/1_poisson.py | 1 - 21 files changed, 82 insertions(+), 151 deletions(-) create mode 100644 .github/workflows/ruff.yml diff --git a/.ci/azure/style.yml b/.ci/azure/style.yml index 3604f9e45..9db42452e 100644 --- a/.ci/azure/style.yml +++ b/.ci/azure/style.yml @@ -1,6 +1,6 @@ jobs: - job: - displayName: Run style checks with Black + displayName: Run style checks with ruff pool: vmImage: ubuntu-latest steps: @@ -9,18 +9,5 @@ jobs: versionSpec: "3.11" - bash: .ci/install_style.sh displayName: "Install dependencies to run the checks" - - script: black --check . - displayName: "Run black" - - - job: - displayName: Run (permissive) style checks with flake8 - pool: - vmImage: ubuntu-latest - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: "3.11" - - bash: .ci/install_style.sh - displayName: "Install dependencies to run the checks" - - script: flake8 - displayName: "Run flake8" \ No newline at end of file + - script: ruff --check . + displayName: "Run black" \ No newline at end of file diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 000000000..0d1e50c5a --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,8 @@ +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0d67a8104..847dee800 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,5 @@ discretize/version.py .idea/ docs/sg_execution_times.rst + +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e33561e47..88f252ab8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,10 @@ repos: - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.3.0 - hooks: - - id: black - language_version: python3.11 - - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - language_version: python3.11 - additional_dependencies: - - flake8-bugbear==23.12.2 - - flake8-builtins==2.2.0 - - flake8-mutable==1.2.0 - - flake8-rst-docstrings==0.3.0 - - flake8-docstrings==1.7.0 - - flake8-pyproject==1.2.3 \ No newline at end of file +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.14.0 + hooks: + # Run the linter. + - id: ruff-check + args: [ --fix ] + # Run the formatter. + - id: ruff-format \ No newline at end of file diff --git a/discretize/base/base_mesh.py b/discretize/base/base_mesh.py index 90fb2e331..08f6c8eb9 100644 --- a/discretize/base/base_mesh.py +++ b/discretize/base/base_mesh.py @@ -2796,7 +2796,7 @@ def get_face_inner_product_surface_deriv( elif invert_model: dMdprop = A * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = sdiag(-MI.diagonal() ** 2) * A + dMdprop = sdiag(-(MI.diagonal() ** 2)) * A elif tensorType == 1: # isotropic, variable in space if not invert_matrix and not invert_model: @@ -2806,7 +2806,7 @@ def get_face_inner_product_surface_deriv( elif invert_model: dMdprop = A * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = sdiag(-MI.diagonal() ** 2) * A + dMdprop = sdiag(-(MI.diagonal() ** 2)) * A if dMdprop is not None: @@ -2901,7 +2901,7 @@ def get_edge_inner_product_line_deriv( elif invert_model: dMdprop = L * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = sdiag(-MI.diagonal() ** 2) * L + dMdprop = sdiag(-(MI.diagonal() ** 2)) * L elif tensorType == 1: # isotropic, variable in space if not invert_matrix and not invert_model: @@ -2911,7 +2911,7 @@ def get_edge_inner_product_line_deriv( elif invert_model: dMdprop = L * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = sdiag(-MI.diagonal() ** 2) * L + dMdprop = sdiag(-(MI.diagonal() ** 2)) * L if dMdprop is not None: diff --git a/discretize/base/base_regular_mesh.py b/discretize/base/base_regular_mesh.py index b4918ccd6..2e626680c 100644 --- a/discretize/base/base_regular_mesh.py +++ b/discretize/base/base_regular_mesh.py @@ -955,8 +955,9 @@ def switchKernal(xx): if dimName in out_type: if self.dim <= dim: raise ValueError( - "Dimensions of mesh not great enough for " - "{}_{}".format(x_type, dimName) + "Dimensions of mesh not great enough for {}_{}".format( + x_type, dimName + ) ) if xx.size != np.sum(nn): raise ValueError("Vector is not the right size.") diff --git a/discretize/base/base_tensor_mesh.py b/discretize/base/base_tensor_mesh.py index 58375b19c..1248dfc05 100644 --- a/discretize/base/base_tensor_mesh.py +++ b/discretize/base/base_tensor_mesh.py @@ -924,7 +924,7 @@ def _fastInnerProductDeriv( elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * V) + dMdprop = n_elements * (sdiag(-(MI.diagonal() ** 2)) * Av.T * V) elif tensorType == 1: # isotropic, variable in space Av = getattr(self, "ave" + projection_type + "2CC") @@ -938,7 +938,7 @@ def _fastInnerProductDeriv( elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * V) + dMdprop = n_elements * (sdiag(-(MI.diagonal() ** 2)) * Av.T * V) elif tensorType == 2: # anisotropic Av = getattr(self, "ave" + projection_type + "2CCV") @@ -967,7 +967,7 @@ def _fastInnerProductDeriv( elif invert_model: dMdprop = Av.T * P * V * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = sdiag(-MI.diagonal() ** 2) * Av.T * P * V + dMdprop = sdiag(-(MI.diagonal() ** 2)) * Av.T * P * V if dMdprop is not None: diff --git a/discretize/mixins/mesh_io.py b/discretize/mixins/mesh_io.py index a9218db0b..9adc420b5 100644 --- a/discretize/mixins/mesh_io.py +++ b/discretize/mixins/mesh_io.py @@ -194,9 +194,7 @@ def _readModelUBC_2D(mesh, file_name): if not len(model) == mesh.nC: raise Exception( """Something is not right, expected size is {:d} - but unwrap vector is size {:d}""".format( - mesh.nC, len(model) - ) + but unwrap vector is size {:d}""".format(mesh.nC, len(model)) ) return model.reshape(mesh.vnC, order="F")[:, ::-1].reshape(-1, order="F") diff --git a/discretize/operators/differential_operators.py b/discretize/operators/differential_operators.py index a7dbbdaad..cee02f650 100644 --- a/discretize/operators/differential_operators.py +++ b/discretize/operators/differential_operators.py @@ -2318,7 +2318,7 @@ def get_BC_projections(self, BC, discretization="CC"): """ if discretization != "CC": raise NotImplementedError( - "Boundary conditions only implemented" "for CC discretization." + "Boundary conditions only implemented for CC discretization." ) if isinstance(BC, str): @@ -2411,7 +2411,7 @@ def get_BC_projections_simple(self, discretization="CC"): """Create weak form boundary condition projection matrices for mixed boundary condition.""" if discretization != "CC": raise NotImplementedError( - "Boundary conditions only implemented" "for CC discretization." + "Boundary conditions only implemented for CC discretization." ) def projBC(n): diff --git a/discretize/operators/inner_products.py b/discretize/operators/inner_products.py index eb5976391..aebd579f0 100644 --- a/discretize/operators/inner_products.py +++ b/discretize/operators/inner_products.py @@ -379,7 +379,7 @@ def get_edge_inner_product_surface_deriv( # NOQA D102 elif invert_model: dMdprop = n_elements * Av.T * A * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * A) + dMdprop = n_elements * (sdiag(-(MI.diagonal() ** 2)) * Av.T * A) elif tensorType == 1: # isotropic, variable in space if not invert_matrix and not invert_model: @@ -391,7 +391,7 @@ def get_edge_inner_product_surface_deriv( # NOQA D102 elif invert_model: dMdprop = n_elements * Av.T * A * sdiag(-1.0 / model**2) elif invert_matrix: - dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * A) + dMdprop = n_elements * (sdiag(-(MI.diagonal() ** 2)) * Av.T * A) if dMdprop is not None: @@ -819,17 +819,29 @@ def Pxxx(xEdge, yEdge, zEdge): posX = ( [0, 0] if xEdge == "eX0" - else [1, 0] if xEdge == "eX1" else [0, 1] if xEdge == "eX2" else [1, 1] + else [1, 0] + if xEdge == "eX1" + else [0, 1] + if xEdge == "eX2" + else [1, 1] ) posY = ( [0, 0] if yEdge == "eY0" - else [1, 0] if yEdge == "eY1" else [0, 1] if yEdge == "eY2" else [1, 1] + else [1, 0] + if yEdge == "eY1" + else [0, 1] + if yEdge == "eY2" + else [1, 1] ) posZ = ( [0, 0] if zEdge == "eZ0" - else [1, 0] if zEdge == "eZ1" else [0, 1] if zEdge == "eZ2" else [1, 1] + else [1, 0] + if zEdge == "eZ1" + else [0, 1] + if zEdge == "eZ2" + else [1, 1] ) ind1 = sub2ind(M.vnEx, np.c_[ii, jj + posX[0], kk + posX[1]]) diff --git a/discretize/tests.py b/discretize/tests.py index 8fdbc6416..56f2e612e 100644 --- a/discretize/tests.py +++ b/discretize/tests.py @@ -926,7 +926,7 @@ def random(size, iscomplex): print( f"Adjoint test {'PASSED' if passed else 'FAILED'} :: " - f"{abs(rhs-lhs):.3e} < {atol+rtol*abs(lhs):.3e} :: " + f"{abs(rhs - lhs):.3e} < {atol + rtol * abs(lhs):.3e} :: " f"|rhs-lhs| < atol + rtol|lhs|" ) diff --git a/discretize/utils/mesh_utils.py b/discretize/utils/mesh_utils.py index ab1221097..a12bde15b 100644 --- a/discretize/utils/mesh_utils.py +++ b/discretize/utils/mesh_utils.py @@ -931,7 +931,7 @@ def refine_tree_xyz( else: raise NotImplementedError( - "Only method= 'radial', 'surface'" " or 'box' have been implemented" + "Only method= 'radial', 'surface' or 'box' have been implemented" ) return mesh diff --git a/docs/conf.py b/docs/conf.py index 9ec52daae..7083e13d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,12 +13,13 @@ import os import sys -from pathlib import Path from datetime import datetime from packaging.version import parse import discretize import shutil from importlib.metadata import version +import math +import pyvista # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -218,8 +219,6 @@ def linkcode_resolve(domain, info): plot_include_source = True plot_formats = [("png", 100), "pdf"] -import math - phi = (math.sqrt(5) + 1) / 2 plot_rcparams = { @@ -457,7 +456,7 @@ def linkcode_resolve(domain, info): ] # -- pyvista configuration --------------------------------------------------- -import pyvista + # Manage errors pyvista.set_error_output_file("errors.txt") diff --git a/examples/plot_slicer_demo.py b/examples/plot_slicer_demo.py index bcd5f19a9..e8e8bd830 100644 --- a/examples/plot_slicer_demo.py +++ b/examples/plot_slicer_demo.py @@ -117,8 +117,7 @@ def beautify(title, fig=None): mesh.plot_3d_slicer(Lpout, 370000, 6002500, -2500, transparent=[[-0.02, 0.1]]) beautify( - "mesh.plot_3d_slicer(" - "\nLpout, 370000, 6002500, -2500, transparent=[[-0.02, 0.1]])" + "mesh.plot_3d_slicer(\nLpout, 370000, 6002500, -2500, transparent=[[-0.02, 0.1]])" ) plt.show() diff --git a/pyproject.toml b/pyproject.toml index 8186b25cf..8b66ffb58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,14 +79,7 @@ test = [ ] # when changing these, make sure to keep it consistent with .pre-commit-config. style = [ - "black==24.3.0", - "flake8==7.0.0", - "flake8-bugbear==23.12.2", - "flake8-builtins==2.2.0", - "flake8-mutable==1.2.0", - "flake8-rst-docstrings==0.3.0", - "flake8-docstrings==1.7.0", - "flake8-pyproject==1.2.3", + "ruff=0.14.0", ] build = [ "meson-python>=0.14.0", @@ -150,74 +143,23 @@ exclude_also = [ # Don't complain about abstract methods, they aren't run: "@(abc\\.)?abstractmethod", ] +[tool.ruff] +required-version='0.14.0' +target-version = 'py311' +#extend-exclude = [ +# 'docs/examples/*', +# 'docs/tutorials/*', +#] -[tool.black] -required-version = '24.3.0' -target-version = ['py38', 'py39', 'py310', 'py311'] - -[tool.flake8] +[tool.ruff.lint] extend-ignore = [ - # Too many leading '#' for block comment - 'E266', - # Line too long (82 > 79 characters) - 'E501', # Do not use variables named 'I', 'O', or 'l' 'E741', - # Line break before binary operator (conflicts with black) - 'W503', - # Ignore spaces before a colon (Black handles it) - 'E203', - # Ignore spaces around an operator (Black handles it) - 'E225', - # Ignore rst warnings for start and end, due to *args and **kwargs being invalid rst, but good for numpydoc - 'RST210', - 'RST213', - # ignore undocced __init__ - 'D107', -] -exclude = [ - '.git', - '.eggs', - '__pycache__', - '.ipynb_checkpoints', - 'docs/examples/*', - 'docs/tutorials/*', - 'docs/*', - 'discretize/_extensions/*.py', - '.ci/*' -] -per-file-ignores = [ - # disable unused-imports errors on __init__.py - # Automodule used for __init__ scripts' description - '__init__.py: F401, D204, D205, D400', - # do not check for assigned lambdas in tests - # do not check for missing docstrings in tests - 'tests/*: E731, D', - 'tutorials/*: D', - 'examples/*: D', -] -exclude-from-doctest = [ - # Only check discretize for docstring style - 'tests', - 'tutorials', - 'examples', ] -rst-roles = [ - 'class', - 'func', - 'mod', - 'meth', - 'attr', - 'ref', - 'data', - # Python programming language: - 'py:func','py:mod','py:attr','py:meth', -] +[tool.ruff.lint.extend-per-file-ignores] +"tests/*" = ["E731"] # Ignore assigning lambdas in tests +"__init__.py" = ["F401"] # Ignore unused import in __init__.py -rst-directives = [ - # These are sorted alphabetically - but that does not matter - 'autosummary', - 'currentmodule', - 'deprecated', -] +[tool.ruff.lint.pydocstyle] +convention = "numpy" diff --git a/tests/base/test_operators.py b/tests/base/test_operators.py index b5fc225a3..f5e23c166 100644 --- a/tests/base/test_operators.py +++ b/tests/base/test_operators.py @@ -783,8 +783,9 @@ def test_DivCurl(self): rel_err = np.linalg.norm(divcurlv) / np.linalg.norm(v) passed = rel_err < self.tol print( - "Testing Div * Curl on {} : |Div Curl v| / |v| = {} " - "... {}".format(meshType, rel_err, "FAIL" if not passed else "ok") + "Testing Div * Curl on {} : |Div Curl v| / |v| = {} ... {}".format( + meshType, rel_err, "FAIL" if not passed else "ok" + ) ) def test_CurlGrad(self): @@ -797,8 +798,9 @@ def test_CurlGrad(self): rel_err = np.linalg.norm(curlgradv) / np.linalg.norm(v) passed = rel_err < self.tol print( - "Testing Curl * Grad on {} : |Curl Grad v| / |v|= {} " - "... {}".format(meshType, rel_err, "FAIL" if not passed else "ok") + "Testing Curl * Grad on {} : |Curl Grad v| / |v|= {} ... {}".format( + meshType, rel_err, "FAIL" if not passed else "ok" + ) ) diff --git a/tests/base/test_tests.py b/tests/base/test_tests.py index 62ca679fb..41bfeb1dd 100644 --- a/tests/base/test_tests.py +++ b/tests/base/test_tests.py @@ -175,7 +175,6 @@ def test_import_time(): def test_random_test_warning(): - match = r"You are running a pytest without setting a random seed.*" with pytest.warns(UserWarning, match=match): _warn_random_test() diff --git a/tests/cyl/test_cylOperators.py b/tests/cyl/test_cylOperators.py index 4486fca19..eac6589b3 100644 --- a/tests/cyl/test_cylOperators.py +++ b/tests/cyl/test_cylOperators.py @@ -67,9 +67,7 @@ def sol(self): ans = sympy.integrate( sympy.integrate(sympy.integrate(r * jTSj, (r, 0, 1)), (t, 0, 2 * sympy.pi)), (z, 0, 1), - )[ - 0 - ] # The `[0]` is to make it a number rather than a matrix + )[0] # The `[0]` is to make it a number rather than a matrix return ans @@ -119,9 +117,7 @@ def sol(self): ans = sympy.integrate( sympy.integrate(sympy.integrate(r * hTSh, (r, 0, 1)), (t, 0, 2 * sympy.pi)), (z, 0, 1), - )[ - 0 - ] # The `[0]` is to make it a scalar + )[0] # The `[0]` is to make it a scalar return ans diff --git a/tests/cyl/test_cyl_innerproducts.py b/tests/cyl/test_cyl_innerproducts.py index 875b54140..f22072b14 100644 --- a/tests/cyl/test_cyl_innerproducts.py +++ b/tests/cyl/test_cyl_innerproducts.py @@ -36,9 +36,7 @@ def sol(self): ans = sympy.integrate( sympy.integrate(sympy.integrate(r * jTSj, (r, 0, 1)), (t, 0, 2 * sympy.pi)), (z, 0, 1), - )[ - 0 - ] # The `[0]` is to make it an int. + )[0] # The `[0]` is to make it an int. return ans @@ -112,9 +110,7 @@ def sol(self): ans = sympy.integrate( sympy.integrate(sympy.integrate(r * hTSh, (r, 0, 1)), (t, 0, 2 * sympy.pi)), (z, 0, 1), - )[ - 0 - ] # The `[0]` is to make it an int. + )[0] # The `[0]` is to make it an int. return ans def vectors(self, mesh): diff --git a/tests/tree/test_safeguards.py b/tests/tree/test_safeguards.py index 63d19c478..ab6ef3ed2 100644 --- a/tests/tree/test_safeguards.py +++ b/tests/tree/test_safeguards.py @@ -83,7 +83,6 @@ def refine_mesh(mesh): class TestSafeGuards: - @pytest.mark.parametrize("prop_name", PROPERTIES) @pytest.mark.parametrize("refine", [True, False], ids=["refined", "non-refined"]) def test_errors(self, mesh, prop_name, refine): diff --git a/tutorials/pde/1_poisson.py b/tutorials/pde/1_poisson.py index d8665d1b7..9922b7126 100644 --- a/tutorials/pde/1_poisson.py +++ b/tutorials/pde/1_poisson.py @@ -90,7 +90,6 @@ # Here we import the packages required for this tutorial. # - from discretize import TensorMesh from scipy.sparse.linalg import spsolve import matplotlib.pyplot as plt From bc7529238dfea171e2e3fe3fc8d4d88fc2260ae8 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 8 Oct 2025 09:10:35 -0600 Subject: [PATCH 2/4] specify version in ruff action --- .github/workflows/ruff.yml | 5 ++++- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 0d1e50c5a..7d7991ba9 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -5,4 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/ruff-action@v3 \ No newline at end of file + - uses: astral-sh/ruff-action@v3 + with: + version: "0.14.0" + checksum: "28fe06f700caf99eee235f90e6e349f48b7f9a4b0d42e3ee5b3686f9259649a3" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8b66ffb58..a6e91a2bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,7 @@ exclude_also = [ "@(abc\\.)?abstractmethod", ] [tool.ruff] -required-version='0.14.0' +required-version = '0.14.0' target-version = 'py311' #extend-exclude = [ # 'docs/examples/*', From 06427d8b44838d6ff749fa7ec64d9582df162eb1 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 8 Oct 2025 09:11:36 -0600 Subject: [PATCH 3/4] fix version equality --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6e91a2bd..25e2d6553 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ test = [ ] # when changing these, make sure to keep it consistent with .pre-commit-config. style = [ - "ruff=0.14.0", + "ruff==0.14.0", ] build = [ "meson-python>=0.14.0", From b21bc32261741c5561149445b38c1714e7a983fb Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 8 Oct 2025 09:14:13 -0600 Subject: [PATCH 4/4] fix in script --- .ci/azure/style.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure/style.yml b/.ci/azure/style.yml index 9db42452e..4704a00c7 100644 --- a/.ci/azure/style.yml +++ b/.ci/azure/style.yml @@ -9,5 +9,5 @@ jobs: versionSpec: "3.11" - bash: .ci/install_style.sh displayName: "Install dependencies to run the checks" - - script: ruff --check . - displayName: "Run black" \ No newline at end of file + - script: ruff check . + displayName: "Run ruff" \ No newline at end of file