Repository: ImperialCollegeLondon/pycsvy (language: python)

Issue:
Updates linters and formatters to use `ruff`
As that has become a more popular option, we should adopt it. 

This repository shows a configuration we would like to adopt: https://github.com/ImperialCollegeLondon/poetry_template_2

Essentially we need to:
- Update `pyproject.toml` to get the right dependencies and remove the old ones.
- Update `.pre-commit.config.yml` to use `ruff`.

Run everything to make sure things pass, and fix issues along the way.


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a9bf045..d6e80e9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,19 +4,14 @@ repos:
     hooks:
       - id: check-merge-conflict
       - id: debug-statements
-  - repo: https://github.com/PyCQA/isort
-    rev: "5.12.0"
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.6.8
     hooks:
-      - id: isort
-        additional_dependencies: [toml]
-  - repo: https://github.com/psf/black
-    rev: "23.1.0"
-    hooks:
-      - id: black
-  # - repo: https://github.com/PyCQA/flake8
-  #   rev: 6.0.0
-  #   hooks:
-  #     - id: flake8
+      - id: ruff
+        types_or: [python]
+        args: [--fix]
+      - id: ruff-format
+        types_or: [python]
   - repo: https://github.com/pre-commit/mirrors-mypy
     rev: "v0.991"
     hooks:
diff --git a/csvy/__init__.py b/csvy/__init__.py
index adb2a2b..9955d89 100644
--- a/csvy/__init__.py
+++ b/csvy/__init__.py
@@ -1,6 +1,5 @@
-"""
-Python reader/writer for CSV files with YAML header information.
-"""
+"""Python reader/writer for CSV files with YAML header information."""
+
 __version__ = "0.2.2"
 from .readers import (  # noqa: F401
     read_header,
diff --git a/csvy/readers.py b/csvy/readers.py
index e51bcd9..ba9803e 100644
--- a/csvy/readers.py
+++ b/csvy/readers.py
@@ -1,6 +1,8 @@
+"""A collection of functions for parsing CSVY files."""
+
 import logging
 from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple, Union
+from typing import Any
 
 import yaml
 
@@ -45,8 +47,8 @@ def get_comment(line: str, marker: str = "---") -> str:
 
 
 def read_header(
-    filename: Union[Path, str], marker: str = "---", **kwargs: Any
-) -> Tuple[Dict[str, Any], int, str]:
+    filename: Path | str, marker: str = "---", **kwargs: Any
+) -> tuple[dict[str, Any], int, str]:
     """Read the yaml-formatted header from a file.
 
     Args:
@@ -80,8 +82,8 @@ def read_header(
 
 
 def read_metadata(
-    filename: Union[Path, str], marker: str = "---", **kwargs: Any
-) -> Dict[str, Any]:
+    filename: Path | str, marker: str = "---", **kwargs: Any
+) -> dict[str, Any]:
     """Read the yaml-formatted metadata from a file.
 
     Args:
@@ -96,11 +98,11 @@ def read_metadata(
 
 
 def read_to_array(
-    filename: Union[Path, str],
+    filename: Path | str,
     marker: str = "---",
-    csv_options: Optional[Dict[str, Any]] = None,
-    yaml_options: Optional[Dict[str, Any]] = None,
-) -> Tuple[NDArray, Dict[str, Any]]:
+    csv_options: dict[str, Any] | None = None,
+    yaml_options: dict[str, Any] | None = None,
+) -> tuple[NDArray, dict[str, Any]]:
     """Reads a CSVY file into dict with the header and array with the data.
 
     Args:
@@ -131,11 +133,11 @@ def read_to_array(
 
 
 def read_to_dataframe(
-    filename: Union[Path, str],
+    filename: Path | str,
     marker: str = "---",
-    csv_options: Optional[Dict[str, Any]] = None,
-    yaml_options: Optional[Dict[str, Any]] = None,
-) -> Tuple[DataFrame, Dict[str, Any]]:
+    csv_options: dict[str, Any] | None = None,
+    yaml_options: dict[str, Any] | None = None,
+) -> tuple[DataFrame, dict[str, Any]]:
     """Reads a CSVY file into dict with the header and a DataFrame with the data.
 
     Possible 'skiprows' and 'comment' argument provided in the 'csv_options' dictionary
@@ -169,11 +171,11 @@ def read_to_dataframe(
 
 
 def read_to_list(
-    filename: Union[Path, str],
+    filename: Path | str,
     marker: str = "---",
-    csv_options: Optional[Dict[str, Any]] = None,
-    yaml_options: Optional[Dict[str, Any]] = None,
-) -> Tuple[List[List], Dict[str, Any]]:
+    csv_options: dict[str, Any] | None = None,
+    yaml_options: dict[str, Any] | None = None,
+) -> tuple[list[list], dict[str, Any]]:
     """Reads a CSVY file into a list with the header and a nested list with the data.
 
     Args:
@@ -196,7 +198,7 @@ def read_to_list(
     options = csv_options.copy() if csv_options is not None else {}
 
     data = []
-    with open(filename, "r", newline="") as csvfile:
+    with open(filename, newline="") as csvfile:
         csvreader = csv.reader(csvfile, **options)
 
         for _ in range(nlines):
diff --git a/csvy/writers.py b/csvy/writers.py
index 68234cd..60584ec 100644
--- a/csvy/writers.py
+++ b/csvy/writers.py
@@ -1,29 +1,40 @@
+"""A collection of functions for writing CSVY files."""
+
 from __future__ import annotations
 
 import csv
 import logging
+from collections.abc import Callable, Iterable
 from io import TextIOBase
 from pathlib import Path
-from typing import Any, Callable, Dict, Iterable, List, Optional, Union
+from typing import Any
 
 import yaml
 
-KNOWN_WRITERS: List[Callable[[Union[Path, str], Any, str], bool]] = []
+KNOWN_WRITERS: list[Callable[[Path | str, Any, str], bool]] = []
+
 
+def register_writer(fun: Callable[[Path | str, Any, str], bool]) -> Callable:
+    """Register a file writer.
+
+    Args:
+        fun (Callable): The writer function.
 
-def register_writer(fun: Callable[[Union[Path, str], Any, str], bool]) -> Callable:
+    Returns:
+        Callable: the writer function.
+    """
     if fun not in KNOWN_WRITERS:
         KNOWN_WRITERS.append(fun)
     return fun
 
 
 def write(
-    filename: Union[Path, str],
+    filename: Path | str,
     data: Any,
-    header: Dict[str, Any],
+    header: dict[str, Any],
     comment: str = "",
-    csv_options: Optional[Dict[str, Any]] = None,
-    yaml_options: Optional[Dict[str, Any]] = None,
+    csv_options: dict[str, Any] | None = None,
+    yaml_options: dict[str, Any] | None = None,
 ) -> None:
     """Writes the data and header in a CSV file, formating the header as yaml.
 
@@ -54,11 +65,11 @@ class Writer:
 
     def __init__(
         self,
-        filename: Union[Path, str],
-        header: Dict[str, Any],
+        filename: Path | str,
+        header: dict[str, Any],
         comment: str = "",
-        csv_options: Optional[Dict[str, Any]] = None,
-        yaml_options: Optional[Dict[str, Any]] = None,
+        csv_options: dict[str, Any] | None = None,
+        yaml_options: dict[str, Any] | None = None,
         line_buffering: bool = False,
     ) -> None:
         """Create a new Writer.
@@ -72,7 +83,6 @@ def __init__(
                 writing the header.
             line_buffering: Line buffering instead of chunk buffering (default False).
         """
-
         if not csv_options:
             csv_options = {}
         if not yaml_options:
@@ -88,9 +98,11 @@ def __init__(
         self._writer = csv.writer(self._file, **csv_options)
 
     def __enter__(self) -> Writer:
+        """Enter the context manager."""
         return self
 
     def __exit__(self, *_: Any) -> None:
+        """Exit the context manager."""
         self._file.close()
 
     def close(self) -> None:
@@ -107,8 +119,8 @@ def writerows(self, rows: Iterable[Iterable[Any]]) -> None:
 
 
 def write_header(
-    file: Union[Path, str, TextIOBase],
-    header: Dict[str, Any],
+    file: Path | str | TextIOBase,
+    header: dict[str, Any],
     comment: str = "",
     **kwargs: Any,
 ) -> None:
@@ -137,7 +149,7 @@ def write_header(
 
 
 def write_data(
-    filename: Union[Path, str], data: Any, comment: str = "", **kwargs: Any
+    filename: Path | str, data: Any, comment: str = "", **kwargs: Any
 ) -> None:
     """Writes the tabular data to the chosen file, adding it after the header.
 
@@ -160,7 +172,7 @@ def write_data(
 
 @register_writer
 def write_numpy(
-    filename: Union[Path, str], data: Any, comment: str = "", **kwargs: Any
+    filename: Path | str, data: Any, comment: str = "", **kwargs: Any
 ) -> bool:
     """Writes the numpy array to the chosen file, adding it after the header.
 
@@ -193,7 +205,7 @@ def write_numpy(
 
 @register_writer
 def write_pandas(
-    filename: Union[Path, str], data: Any, comment: str = "", **kwargs: Any
+    filename: Path | str, data: Any, comment: str = "", **kwargs: Any
 ) -> bool:
     """Writes the pandas dataframe to the chosen file, adding it after the header.
 
@@ -224,7 +236,7 @@ def write_pandas(
 
 
 def write_csv(
-    filename: Union[Path, str], data: Any, comment: str = "", **kwargs: Any
+    filename: Path | str, data: Any, comment: str = "", **kwargs: Any
 ) -> bool:
     """Writes the tabular to the chosen file, adding it after the header.
 
@@ -239,7 +251,6 @@ def write_csv(
     Returns:
         True if the writer worked, False otherwise.
     """
-
     with open(filename, "a", newline="") as f:
         writer = csv.writer(f, **kwargs)
         for row in data:
diff --git a/poetry.lock b/poetry.lock
index ba0b263..4b7bdb1 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -19,52 +19,6 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
 tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
 tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
 
-[[package]]
-name = "black"
-version = "23.12.1"
-description = "The uncompromising code formatter."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"},
-    {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"},
-    {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"},
-    {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"},
-    {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"},
-    {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"},
-    {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"},
-    {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"},
-    {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"},
-    {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"},
-    {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"},
-    {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"},
-    {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"},
-    {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"},
-    {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"},
-    {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"},
-    {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"},
-    {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"},
-    {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"},
-    {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"},
-    {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"},
-    {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"},
-]
-
-[package.dependencies]
-click = ">=8.0.0"
-mypy-extensions = ">=0.4.3"
-packaging = ">=22.0"
-pathspec = ">=0.9.0"
-platformdirs = ">=2"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
-
-[package.extras]
-colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
-jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-uvloop = ["uvloop (>=0.15.2)"]
-
 [[package]]
 name = "bump2version"
 version = "1.0.1"
@@ -350,22 +304,6 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.
 testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
 typing = ["typing-extensions (>=4.12.2)"]
 
-[[package]]
-name = "flake8"
-version = "4.0.1"
-description = "the modular source code checker: pep8 pyflakes and co"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
-    {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
-]
-
-[package.dependencies]
-mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.8.0,<2.9.0"
-pyflakes = ">=2.4.0,<2.5.0"
-
 [[package]]
 name = "ghp-import"
 version = "2.1.0"
@@ -459,20 +397,6 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
-[[package]]
-name = "isort"
-version = "5.13.2"
-description = "A Python utility / library to sort Python imports."
-optional = false
-python-versions = ">=3.8.0"
-files = [
-    {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
-    {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
-]
-
-[package.extras]
-colors = ["colorama (>=0.4.6)"]
-
 [[package]]
 name = "jinja2"
 version = "3.1.4"
@@ -577,17 +501,6 @@ files = [
     {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
 ]
 
-[[package]]
-name = "mccabe"
-version = "0.6.1"
-description = "McCabe checker, plugin for flake8"
-optional = false
-python-versions = "*"
-files = [
-    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
-    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
-]
-
 [[package]]
 name = "mergedeep"
 version = "1.3.4"
@@ -1047,28 +960,6 @@ nodeenv = ">=0.11.1"
 pyyaml = ">=5.1"
 virtualenv = ">=20.10.0"
 
-[[package]]
-name = "pycodestyle"
-version = "2.8.0"
-description = "Python style guide checker"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-files = [
-    {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
-    {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
-]
-
-[[package]]
-name = "pyflakes"
-version = "2.4.0"
-description = "passive checker of Python programs"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
-    {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
-    {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
-]
-
 [[package]]
 name = "pygments"
 version = "2.18.0"
@@ -1300,6 +1191,33 @@ urllib3 = ">=1.21.1,<3"
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
 use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
+[[package]]
+name = "ruff"
+version = "0.6.8"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
+    {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
+    {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
+    {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
+    {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
+    {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
+    {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
+    {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
+    {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
+    {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
+    {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
+    {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
+    {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
+    {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
+    {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
+    {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
+    {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
+    {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
+]
+
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -1456,4 +1374,4 @@ type = ["pytest-mypy"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "27623a749bb883c01bebf8bec0b913c9254cd4279808f3ee6afddbc1e095c02f"
+content-hash = "b0446d787719bdd9d5e2eb5c7589cee89dda137be65b03d969a9c8640313bcfe"
diff --git a/pyproject.toml b/pyproject.toml
index 7d0ff4f..9925993 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,15 +35,13 @@ pytest = "^7.2"
 pytest-cov = "^3.0.0"
 pytest-mypy = "^0.9.1"
 pytest-mock = "^3.7.0"
-isort = "^5.10.1"
 pre-commit = "^2.18.1"
-black = "^23.3.0"
 numpy = "^1.24.2"
 pandas = "^2.0.0"
-flake8 = "^4.0.1"
 types-PyYAML = "^6.0.7"
 bump2version = "^1.0.1"
 coverage = "^7.1.0"
+ruff = "^0.6.8"
 
 [tool.poetry.group.docs]
 optional = true
@@ -56,6 +54,18 @@ mkdocs-gen-files = "^0.4.0"
 mkdocs-literate-nav = "^0.5.0"
 mkdocs-section-index = "^0.3.4"
 
+[tool.ruff]
+target-version = "py39"
+lint.select = [
+    "D",   # pydocstyle
+    "E",   # pycodestyle
+    "F",   # Pyflakes
+    "I",   # isort
+    "UP",  # pyupgrade
+    "RUF", # ruff
+]
+lint.pydocstyle.convention = "google"
+
 [build-system]
 requires = ["poetry-core>=1.0.0"]
 build-backend = "poetry.core.masonry.api"


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..6a4f508 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,1 @@
+"""Tests for the csvy package."""
diff --git a/tests/conftest.py b/tests/conftest.py
index 9124356..8562954 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,8 +1,11 @@
+"""Fixtures for the test suite."""
+
 import pytest
 
 
 @pytest.fixture
 def data_path():
+    """A data path fixture."""
     from pathlib import Path
 
     return Path(__file__).parent / "data.csv"
@@ -10,6 +13,7 @@ def data_path():
 
 @pytest.fixture
 def data_comment_path():
+    """A data comment path fixture."""
     from pathlib import Path
 
     return Path(__file__).parent / "data_comment.csv"
@@ -17,6 +21,7 @@ def data_comment_path():
 
 @pytest.fixture
 def array_data_path():
+    """A data array path fixture."""
     from pathlib import Path
 
     return Path(__file__).parent / "array_data.csv"
diff --git a/tests/test_read.py b/tests/test_read.py
index 32bf32d..ed95ca7 100644
--- a/tests/test_read.py
+++ b/tests/test_read.py
@@ -1,9 +1,12 @@
+"""Tests for the csvy reader functions."""
+
 from unittest.mock import patch
 
 import pytest
 
 
 def test_get_comment():
+    """Test the get_comment function."""
     from csvy.readers import get_comment
 
     assert "" == get_comment("--- Something else")
@@ -15,6 +18,7 @@ def test_get_comment():
 
 
 def test_get_header(data_path, data_comment_path):
+    """Test the read_header function."""
     from csvy.readers import read_header
 
     header, nlines, comment = read_header(data_path)
@@ -34,6 +38,7 @@ def test_get_header(data_path, data_comment_path):
 
 @patch("csvy.readers.read_header")
 def test_read_metadata(read_header_mock, data_path):
+    """Test the read_metadata function."""
     from csvy import read_metadata
 
     read_header_mock.return_value = ("a", "b")
@@ -47,6 +52,7 @@ def test_read_metadata(read_header_mock, data_path):
 
 
 def test_read_to_array(array_data_path):
+    """Test the read_to_array function."""
     import numpy as np
 
     from csvy.readers import read_to_array
@@ -66,6 +72,7 @@ def test_read_to_array(array_data_path):
 
 
 def test_read_to_dataframe(data_path):
+    """Test the read_to_dataframe function."""
     import pandas as pd
 
     from csvy.readers import read_to_dataframe
@@ -85,6 +92,7 @@ def test_read_to_dataframe(data_path):
 
 
 def test_read_to_list(array_data_path):
+    """Test the read_to_list function."""
     from csvy.readers import read_to_list
 
     data, header = read_to_list(array_data_path, csv_options={"delimiter": ","})
diff --git a/tests/test_write.py b/tests/test_write.py
index 8e1c18f..365104a 100644
--- a/tests/test_write.py
+++ b/tests/test_write.py
@@ -1,14 +1,19 @@
+"""Tests for the csvy writer functions."""
+
 from unittest.mock import MagicMock, patch
 
 import pytest
 
 
 class MockCSVWriter:
+    """A mock CSV writer."""
+
     writerow = MagicMock()
     writerows = MagicMock()
 
 
 def test_save_header(tmpdir, mocker):
+    """Test the write_header function."""
     import yaml
 
     from csvy.writers import write_header
@@ -47,6 +52,7 @@ def test_save_header(tmpdir, mocker):
 
 @patch("numpy.savetxt")
 def test_write_numpy(mock_save, tmpdir):
+    """Test the write_numpy function."""
     import numpy as np
 
     from csvy.writers import write_numpy
@@ -63,6 +69,7 @@ def test_write_numpy(mock_save, tmpdir):
 
 @patch("pandas.DataFrame.to_csv")
 def test_write_pandas(mock_save, tmpdir):
+    """Test the save_header function."""
     import pandas as pd
 
     from csvy.writers import write_pandas
@@ -79,6 +86,7 @@ def test_write_pandas(mock_save, tmpdir):
 
 @patch("csv.writer")
 def test_write_csv(mock_save, tmpdir):
+    """Test the write_csv function."""
     from csvy.writers import write_csv
 
     class Writer:
@@ -105,6 +113,7 @@ class Writer:
     ),
 )
 def test_writer(mock_write_header, mock_csv_writer, csv_options, yaml_options, tmpdir):
+    """Test the Writer class."""
     from csvy.writers import Writer
 
     mock_csv_writer.return_value = MockCSVWriter
@@ -126,6 +135,7 @@ def test_writer(mock_write_header, mock_csv_writer, csv_options, yaml_options, t
 @patch("csv.writer")
 @patch("csvy.writers.write_header")
 def test_writer_writerow(mock_write_header, mock_csv_writer, tmpdir):
+    """Test Writer's writerow method."""
     from csvy.writers import Writer
 
     filename = tmpdir / "some_file.csv"
@@ -139,6 +149,7 @@ def test_writer_writerow(mock_write_header, mock_csv_writer, tmpdir):
 @patch("csv.writer")
 @patch("csvy.writers.write_header")
 def test_writer_writerows(mock_write_header, mock_csv_writer, tmpdir):
+    """Test Writer's writerows method."""
     from csvy.writers import Writer
 
     filename = tmpdir / "some_file.csv"
@@ -150,6 +161,7 @@ def test_writer_writerows(mock_write_header, mock_csv_writer, tmpdir):
 
 
 def test_writer_close(tmpdir):
+    """Test Writer's file closure."""
     from csvy.writers import Writer
 
     filename = tmpdir / "some_file.csv"
@@ -160,6 +172,7 @@ def test_writer_close(tmpdir):
 
 
 def test_writer_context(tmpdir):
+    """Test Writer's context manager."""
     from csvy.writers import Writer
 
     filename = tmpdir / "some_file.csv"
@@ -177,6 +190,7 @@ def test_writer_context(tmpdir):
 @patch("csvy.writers.write_header")
 @patch("csvy.writers.write_data")
 def test_write(mock_write_data, mock_write_header):
+    """Test the write function."""
     from csvy.writers import write
 
     filename = "here.csv"
@@ -201,6 +215,7 @@ def test_write(mock_write_data, mock_write_header):
 
 @patch("csvy.writers.write_csv")
 def test_write_data(mock_write_csv):
+    """Test the write_data function."""
     from csvy.writers import KNOWN_WRITERS, write_data
 
     filename = "here.csv"