Repository: xgcm/xgcm

Issue:
Rename `_connections` to `_face_connections`. 
We currently store the input from `Grid(..., **face_connection=**)` in the private property [`_connections`](https://github.com/xgcm/xgcm/blob/61dca56eecde7c3aabb3eaf4577a39c69fc86cfa/xgcm/grid.py#L227) which seems inconsistent. I propose to rename this to `_face_connections` for consistency

Discussion:

Solution patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 380ab29..cbf5ce3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,23 +8,20 @@ repos:
       - id: trailing-whitespace
       - id: end-of-file-fixer
       - id: check-yaml
-  - repo: https://github.com/PyCQA/isort
-    rev: 5.13.2
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    # Ruff version.
+    rev: v0.6.9
     hooks:
-      - id: isort
-  - repo: https://github.com/psf/black
-    rev: 24.4.2 #make sure to sync with blackdoc beneath
-    hooks:
-      - id: black
+      # Run the linter.
+      - id: ruff
+        args: [ --fix ]
+      # Run the formatter.
+      - id: ruff-format
   - repo: https://github.com/keewis/blackdoc
     rev: v0.3.9
     hooks:
       - id: blackdoc
         additional_dependencies: ["black==22.3.0"]
-  - repo: https://github.com/PyCQA/flake8
-    rev: 7.1.0
-    hooks:
-      - id: flake8
   - repo: https://github.com/pre-commit/mirrors-mypy
     rev: v1.11.0
     hooks:
diff --git a/doc/installation.rst b/doc/installation.rst
index dce1003..281df92 100644
--- a/doc/installation.rst
+++ b/doc/installation.rst
@@ -32,19 +32,16 @@ Installation from GitHub
 
 xgcm is under active development. To obtain the latest development version,
 you may clone the `source repository <https://github.com/xgcm/xgcm>`_
-and install it::
-
-    git clone https://github.com/xgcm/xgcm.git
-    cd xgcm
-    python setup.py install
-
-or simply::
+and install it using pip::
 
     pip install git+https://github.com/xgcm/xgcm.git
 
+More comprehensive instructions for installing a development environment can be found in the [Contributor Guide](https://xgcm.readthedocs.io/en/latest/contributor_guide.html).
+
 Users are encouraged to `fork <https://help.github.com/articles/fork-a-repo/>`_
 xgcm and submit issues_ and `pull requests`_.
 
+
 .. _dask: http://dask.pydata.org
 .. _numpy: https://numpy.org
 .. _xarray: http://xarray.pydata.org
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..af3cb8b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,104 @@
+# pyproject.toml
+[build-system]
+requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=6.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "xgcm"
+dynamic = ["version"]
+requires-python = ">=3.9"
+description = "General Circulation Model Postprocessing with xarray"
+readme = { file = "README.rst", content-type = "text/x-rst" }
+authors = [
+  { name = "xgcm Developers", email = "julius@ldeo.columbia.edu" }
+]
+classifiers = [
+        "Development Status :: 5 - Production/Stable",
+        "Intended Audience :: Science/Research",
+        "Topic :: Scientific/Engineering",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+]
+license = { text = "MIT" }
+keywords = ["pangeo", "simulations", "grids"]
+
+dependencies = [
+    "xarray>=0.20.0",
+    "dask",
+    "numpy",
+    "future"
+]
+
+[project.optional-dependencies]
+dev = [
+    "pytest",
+    "pytest-cov",
+    "scipy",
+    "flake8",
+    "black",
+    "codecov"
+]
+
+[project.urls]
+Homepage =  "https://github.com/xgcm/xgcm"
+Tracker =  "https://github.com/xgcm/xgcm/issues"
+
+[tool.setuptools.packages.find]
+exclude = ["docs", "tests", "tests.*", "docs.*"]
+
+[tool.setuptools_scm]
+write_to = "xgcm/_version.py"
+write_to_template = '__version__ = "{version}"'
+
+[tool.pytest.ini_options]
+addopts = "-v"
+
+[tool.ruff]
+exclude = [
+    "__init__.py",
+    ".eggs",
+    "doc"
+]
+# Same as Black.
+line-length = 88
+indent-width = 4
+
+[tool.ruff.lint]
+extend-select = ["T201"]
+# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`)  codes by default.
+# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
+# McCabe complexity (`C901`) by default.
+select = ["E4", "E7", "E9", "F"]
+ignore = [
+    "E203",  # whitespace before ':'
+    "E402",  # module level import not at top of file
+    "E501",  # line too long
+    "E731",  # do not assign a lambda expression, use a def
+    "E265",  # block comment should start with ‘# ’
+    "F811",  # redefinition of unused name from import
+    "F722",  # allows type hinting as Gridded[DataArray, "(X:center)"]
+]
+
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.ruff.format]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 9fbc327..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,29 +0,0 @@
-[aliases]
-test=pytest
-
-[flake8]
-exclude = __init__.py,.eggs,doc
-ignore =
-    # whitespace before ':' - doesn't work well with black
-    E203
-    E402
-    # line too long - let black worry about that
-    E501
-    # do not assign a lambda expression, use a def
-    E731
-    # line break before binary operator
-    W503
-    E265
-    F811
-    # Allows type hinting as Gridded[DataArray, "(X:center)"], where we did `from typing import Annotated as Gridded`
-    F722
-    E704
-
-[isort]
-profile = black
-skip_gitignore = true
-force_to_top = true
-default_section = THIRDPARTY
-known_first_party = xgcm
-skip=
-    doc/conf.py
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 0f84adb..0000000
--- a/setup.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import os
-
-from setuptools import find_packages, setup
-
-here = os.path.dirname(__file__)
-with open(os.path.join(here, "README.rst"), encoding="utf-8") as f:
-    long_description = f.read()
-
-install_requires = [
-    "xarray>=0.20.0",
-    "dask",
-    "numpy",
-    "future",
-]
-
-dev_require = install_requires + [
-    "pytest",
-    "pytest-cov",
-    "scipy",
-    "flake8",
-    "black",
-    "codecov",
-]
-
-setup(
-    name="xgcm",
-    description="General Circulation Model Postprocessing with xarray",
-    url="https://github.com/xgcm/xgcm",
-    author="xgcm Developers",
-    author_email="rpa@ldeo.columbia.edu",
-    license="MIT",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Science/Research",
-        "Topic :: Scientific/Engineering",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-    ],
-    packages=find_packages(exclude=["docs", "tests", "tests.*", "docs.*"]),
-    install_requires=install_requires,
-    extras_require={"dev": dev_require},
-    python_requires=">=3.9",
-    long_description=long_description,
-    long_description_content_type="text/x-rst",
-    setup_requires="setuptools_scm",
-    use_scm_version={
-        "write_to": "xgcm/_version.py",
-        "write_to_template": '__version__ = "{version}"',
-        "tag_regex": r"^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$",
-    },
-)
diff --git a/xgcm/grid.py b/xgcm/grid.py
index e0fa298..098727b 100644
--- a/xgcm/grid.py
+++ b/xgcm/grid.py
@@ -143,7 +143,6 @@ class Grid:
 
             # Loop over input kwargs. If None and parsed alternative available
             # then replace local variable with autoparsed. If conflict raise error.
-            print(f"coords = {coords}")
             duplicates = []
             if "coords" in parsed_kwargs:
                 if coords is None:
@@ -243,7 +242,7 @@ class Grid:
 
         # Set properties on grid object.
         self._facedim = list(face_connections.keys())[0] if face_connections else None
-        self._connections = face_connections if face_connections else None
+        self._face_connections = face_connections if face_connections else None
         # TODO: I think of the face connection data as grid not axes properties, since they almost by defintion
         # TODO: involve multiple axes. In a future PR we should remove this info from the axes
         # TODO: but make sure to properly port the checking functionality!
@@ -387,7 +386,7 @@ class Grid:
 
         for axis, axis_links in axis_connections.items():
             self.axes[axis]._facedim = facedim
-            self.axes[axis]._connections = axis_links
+            self.axes[axis]._face_connections = axis_links
 
     def set_metrics(self, key, value, overwrite=False):
         metric_axes = frozenset(_maybe_promote_str_to_list(key))
diff --git a/xgcm/grid_ufunc.py b/xgcm/grid_ufunc.py
index c3f11e0..c631f3b 100644
--- a/xgcm/grid_ufunc.py
+++ b/xgcm/grid_ufunc.py
@@ -44,7 +44,7 @@ _SIGNATURE = f"^{_ARGUMENT_LIST}->{_ARGUMENT_LIST}$"
 
 
 def _maybe_unpack_vector_component(
-    data: Union[xr.DataArray, Dict[str, xr.DataArray]]
+    data: Union[xr.DataArray, Dict[str, xr.DataArray]],
 ) -> xr.DataArray:
     if isinstance(data, dict):
         [da] = list(data.values())  # this will raise if more than one element
@@ -252,7 +252,7 @@ def _parse_signature_from_string(
 
 
 def _parse_signature_from_type_hints(
-    hints: Dict[str, Any]
+    hints: Dict[str, Any],
 ) -> Tuple[T_AX_POS_LIST, T_AX_POS_LIST, T_AX_POS_LIST, T_AX_POS_LIST]:
     """
     Parse signatures from type annotations for a grid-aware universal function.
@@ -953,7 +953,7 @@ def _map_func_over_core_dims(
     single_dim_chunktype = Tuple[int, ...]
 
     def _dict_to_numbered_axes(
-        sizes: Mapping[str, single_dim_chunktype]
+        sizes: Mapping[str, single_dim_chunktype],
     ) -> Tuple[single_dim_chunktype, ...]:
         """This implicitly crystallises the order of the given mapping"""
         return tuple(sizes.values())
diff --git a/xgcm/padding.py b/xgcm/padding.py
index 4ddbcd3..62eef10 100644
--- a/xgcm/padding.py
+++ b/xgcm/padding.py
@@ -76,7 +76,7 @@ def _pad_face_connections(
     other_component: Optional[Dict[str, xr.DataArray]] = None,
 ):
     facedim = grid._facedim
-    connections = grid._connections
+    connections = grid._face_connections
 
     if isinstance(da, dict):
         isvector = True
@@ -412,7 +412,7 @@ def pad(
     data = _strip_all_coords(data)
 
     # If any axis has connections we need to use the complex padding
-    if grid._connections:
+    if grid._face_connections:
         da_padded = _pad_face_connections(
             data,
             grid,


Test patch:
diff --git a/xgcm/test/test_faceconnections.py b/xgcm/test/test_faceconnections.py
index 78e7331..a344db3 100644
--- a/xgcm/test/test_faceconnections.py
+++ b/xgcm/test/test_faceconnections.py
@@ -146,10 +146,10 @@ def test_create_connected_grid(ds, ds_face_connections_x_to_x, face_coord_dim):
     # this is a bad test because it tests the details of the implementation,
     # not the behavior. But it is useful for now
     assert xaxis._facedim == "face"
-    assert xaxis._connections[0][1][0] == 1
-    assert xaxis._connections[0][1][1] is xaxis
-    assert xaxis._connections[1][0][0] == 0
-    assert xaxis._connections[1][0][1] is xaxis
+    assert xaxis._face_connections[0][1][0] == 1
+    assert xaxis._face_connections[0][1][1] is xaxis
+    assert xaxis._face_connections[1][0][0] == 0
+    assert xaxis._face_connections[1][0][1] is xaxis
 
 
 def test_create_connected_grid_error_wrong_facedim(ds, ds_face_connections_x_to_x):
@@ -249,7 +249,7 @@ def test_vector_diff_interp_connected_grid_x_to_y(
             ds,
             **comodo_grid_kwargs,
             face_connections=ds_face_connections_x_to_y,
-            autoparse_metadata=False
+            autoparse_metadata=False,
         )
     else:
         # simplest scenario with one face connection
diff --git a/xgcm/test/test_grid.py b/xgcm/test/test_grid.py
index 11dc937..5b6a337 100644
--- a/xgcm/test/test_grid.py
+++ b/xgcm/test/test_grid.py
@@ -25,7 +25,7 @@ def _assert_axes_equal(ax1, ax2):
     assert ax1._default_shifts == ax2._default_shifts
     # TODO: make this work...
     # assert ax1._facedim == ax2._facedim
-    # assert ax1._connections == ax2._connections
+    # assert ax1._face_connections == ax2._face_connections
 
 
 class TestInvalidGrid:
diff --git a/xgcm/test/test_grid_ufunc.py b/xgcm/test/test_grid_ufunc.py
index 5c4a628..9e770ae 100644
--- a/xgcm/test/test_grid_ufunc.py
+++ b/xgcm/test/test_grid_ufunc.py
@@ -114,7 +114,7 @@ class TestParseSignatureFromTypeHints:
     def test_annotated_args(self):
         @as_grid_ufunc()
         def ufunc(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:center"]:
             return np.array([])
 
@@ -122,7 +122,7 @@ class TestParseSignatureFromTypeHints:
 
         @as_grid_ufunc()
         def ufunc(
-            a: Annotated[np.ndarray, "X:center,Y:center"]
+            a: Annotated[np.ndarray, "X:center,Y:center"],
         ) -> Annotated[np.ndarray, "X:center"]:
             return np.array([])
 
@@ -139,7 +139,7 @@ class TestParseSignatureFromTypeHints:
 
         @as_grid_ufunc()
         def ufunc(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:left,Y:right"]:
             return np.array([])
 
@@ -147,7 +147,7 @@ class TestParseSignatureFromTypeHints:
 
         @as_grid_ufunc()
         def ufunc(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Tuple[Annotated[np.ndarray, "X:left"], Annotated[np.ndarray, "Y:right"]]:
             return np.array([]), np.array([])
 
@@ -161,7 +161,7 @@ class TestParseSignatureFromTypeHints:
 
             @as_grid_ufunc()
             def ufunc(
-                a: Annotated[np.ndarray, "nonsense"]  # type: ignore
+                a: Annotated[np.ndarray, "nonsense"],  # type: ignore
             ) -> Annotated[np.ndarray, "X:center"]:
                 return np.array([])
 
@@ -169,7 +169,7 @@ class TestParseSignatureFromTypeHints:
 
             @as_grid_ufunc()
             def ufunc(
-                a: Annotated[np.ndarray, "X:Mars"]
+                a: Annotated[np.ndarray, "X:Mars"],
             ) -> Annotated[np.ndarray, "X:center"]:
                 return np.array([])
 
@@ -181,7 +181,7 @@ class TestParseSignatureFromTypeHints:
 
             @as_grid_ufunc()
             def ufunc(
-                a: Annotated[np.ndarray, "X:center"]
+                a: Annotated[np.ndarray, "X:center"],
             ) -> Annotated[np.ndarray, "X:Venus"]:
                 return np.array([])
 
@@ -192,7 +192,7 @@ class TestParseSignatureFromTypeHints:
 
             @as_grid_ufunc(signature="(X:center)->(X:left)")
             def ufunc(
-                a: Annotated[np.ndarray, "X:center"]
+                a: Annotated[np.ndarray, "X:center"],
             ) -> Annotated[np.ndarray, "X:left"]:
                 return np.array([])
 
@@ -201,15 +201,15 @@ class TestParseSignatureFromTypeHints:
         @as_grid_ufunc()
         def ufunc1(a: Annotated[str, "X:center"]) -> Annotated[np.ndarray, "X:center"]:
             # np.ndarray has a .strides method but str doesn't (and nor does xr.DataArray)
-            print(a.strides)  # type: ignore
+            assert isinstance(a, np.ndarray)
             return a  # type: ignore
 
         # This should pass mypy without raising any errors
         @as_grid_ufunc()
         def ufunc3(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:center"]:
-            print(a.strides)
+            assert isinstance(a, np.ndarray)
             return a
 
 
@@ -301,7 +301,7 @@ class TestGridUFuncNoPadding:
 
         @as_grid_ufunc()
         def diff_center_to_left(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:left"]:
             return a - np.roll(a, shift=-1)
 
@@ -372,7 +372,7 @@ class TestGridUFuncNoPadding:
         # Test decorator
         @as_grid_ufunc()
         def diff_center_to_left(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:left"]:
             return a - np.roll(a, shift=-1)
 
@@ -416,7 +416,7 @@ class TestGridUFuncNoPadding:
         # Test decorator
         @as_grid_ufunc(dask="parallelized")
         def interp_center_to_inner(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:inner"]:
             return 0.5 * (a[:-1] + a[1:])
 
@@ -465,7 +465,7 @@ class TestGridUFuncNoPadding:
         # Test decorator
         @as_grid_ufunc(dask="allowed")
         def diff_overlap(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:left"]:
             return map_overlap(diff_center_to_left, a, depth=1, boundary="periodic")
 
@@ -504,7 +504,7 @@ class TestGridUFuncNoPadding:
         # Test decorator
         @as_grid_ufunc()
         def diff_center_to_left(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:left"]:
             return a - np.roll(a, shift=-1, axis=-1)
 
@@ -594,7 +594,9 @@ class TestGridUFuncNoPadding:
 
         # Test decorator
         @as_grid_ufunc()
-        def grad_to_inner(a: Annotated[np.ndarray, "X:center,Y:center"]) -> Tuple[
+        def grad_to_inner(
+            a: Annotated[np.ndarray, "X:center,Y:center"],
+        ) -> Tuple[
             Annotated[np.ndarray, "X:inner,Y:center"],
             Annotated[np.ndarray, "X:center,Y:inner"],
         ]:
@@ -901,7 +903,7 @@ class TestDaskOverlap:
             map_overlap=True,
         )
         def diff_center_to_left(
-            a: Annotated[np.ndarray, "X:center"]
+            a: Annotated[np.ndarray, "X:center"],
         ) -> Annotated[np.ndarray, "X:left"]:
             return a[..., 1:] - a[..., :-1]
 
@@ -963,7 +965,7 @@ class TestDaskOverlap:
             map_overlap=True,
         )
         def diff_outer_to_center(
-            a: Annotated[np.ndarray, "X:outer"]
+            a: Annotated[np.ndarray, "X:outer"],
         ) -> Annotated[np.ndarray, "X:center"]:
             """Mocking up a function which can only act on in-memory arrays, and requires no padding"""
             if isinstance(a, np.ndarray):
diff --git a/xgcm/test/test_transform.py b/xgcm/test/test_transform.py
index 209faf0..c9fc9cd 100644
--- a/xgcm/test/test_transform.py
+++ b/xgcm/test/test_transform.py
@@ -781,7 +781,7 @@ def test_mid_level_linear(linear_cases):
                 source_dim,
                 source_dim,
                 target_dim,
-                **transform_kwargs
+                **transform_kwargs,
             )
     else:
         interpolated = linear_interpolation(
@@ -791,7 +791,7 @@ def test_mid_level_linear(linear_cases):
             source_dim,
             source_dim,
             target_dim,
-            **transform_kwargs
+            **transform_kwargs,
         )
         xr.testing.assert_allclose(interpolated, expected[output_name])
         assert interpolated.name == output_name
@@ -833,7 +833,7 @@ def test_mid_level_conservative(conservative_cases):
                 source_dim,
                 bounds_dim,
                 target_dim,
-                **transform_kwargs
+                **transform_kwargs,
             )
     else:
         transformed = conservative_interpolation(
@@ -843,7 +843,7 @@ def test_mid_level_conservative(conservative_cases):
             source_dim,
             bounds_dim,
             target_dim,
-            **transform_kwargs
+            **transform_kwargs,
         )
         xr.testing.assert_allclose(transformed, expected[output_name])
         # make sure that the extensive quantitiy is actually conserved
@@ -999,7 +999,7 @@ def test_grid_transform_bypass_checks(bypass_checks):
         target,
         target_data=target_data,
         bypass_checks=bypass_checks,
-        **transform_kwargs
+        **transform_kwargs,
     )
 
     xr.testing.assert_allclose(transformed, expected.data)