# Source code for dipy.io.gradients

    import io
    from pathlib import Path
    import re
    import warnings

    import numpy as np

    def read_bvals_bvecs(fbvals, fbvecs):
        """Read b-values and b-vectors from disk.

        Parameters
        ----------
        fbvals : str or Path
           Full path to file with b-values. None to not read bvals.
        fbvecs : str or Path
           Full path of file with b-vectors. None to not read bvecs.

        Returns
        -------
        bvals : array, (N,) or None
        bvecs : array, (N, 3) or None

        Notes
        -----
        Files can be either '.bvals'/'.bvecs' or '.txt' or '.npy' (containing
        arrays stored with the appropriate values).

        """
        # Loop over the provided inputs, reading each one in turn and adding them
        # to this list:
        vals = []
        for this_fname in [fbvals, fbvecs]:
            # If the input was None or empty string, we don't read anything and
            # move on:
            if this_fname is None or not this_fname:
                vals.append(None)
                continue

            if not isinstance(this_fname, (str, Path)):
                raise ValueError("String with full path to file is required")

            ext = Path(this_fname).suffix
            if ext in [
                ".bvals",
                ".bval",
                ".bvecs",
                ".bvec",
                ".txt",
                ".eddy_rotated_bvecs",
                "",
            ]:
                with open(this_fname, "r") as f:
                    content = f.read()

                munged_content = io.StringIO(re.sub(r"(\t|,)", " ", content))
                vals.append(np.squeeze(np.loadtxt(munged_content)))
            elif ext == ".npy":
                vals.append(np.squeeze(np.load(this_fname)))
            else:
                e_s = f"File type {ext} is not recognized"
                raise ValueError(e_s)

        # Once out of the loop, unpack them:
        bvals, bvecs = vals[0], vals[1]

        # If bvecs is None, you can just return now w/o making more checks:
        if bvecs is None:
            return bvals, bvecs

        if 3 not in bvecs.shape:
            raise OSError("bvec file should have three rows")
        if bvecs.ndim != 2:
            bvecs = bvecs[None, ...]
            bvals = bvals[None, ...]
            msg = "Detected only 1 direction on your bvec file. For diffusion "
            msg += "dataset, it is recommended to have at least 3 directions."
            msg += "You may have problems during the reconstruction step."
            warnings.warn(msg, stacklevel=2)
        if bvecs.shape[1] != 3:
            bvecs = bvecs.T

        # If bvals is None, you don't need to check that they have the same shape:
        if bvals is None:
            return bvals, bvecs

        if len(bvals.shape) > 1:
            raise OSError("bval file should have one row")

        if bvals.shape[0] != bvecs.shape[0]:
            raise OSError("b-values and b-vectors shapes do not correspond")

        return bvals, bvecs