Repository: lmfit/uncertainties
Pull request #301: Fix doctests
Author: login: jagerber48
Created: 2025-04-12T05:58:44Z | Merged: 2025-04-14T20:30:37Z

- [ ] Closes #281 
- [ ] Executed `pre-commit run --all-files` with no errors
- [ ] The change is fully covered by automated unit tests
- [ ] Documented in docs/ as appropriate
- [ ] Added an entry to the CHANGES file

It will be valuable to have all doctests passing. Especially for the release of version `4.0.0`. Right now I see 111/206 doctests failing. I will slowly work through them but, especially on this PR, help from others would be appreciated.

Linked issue #281: Get doctests to pass
During #279 I discovered that there are doctests in the documentation, however, many are failing. On my tests run 111/204 doctests failed. We should get these doctests to pass. It would also be a good idea to run doctests to run in the CI.
Linked issue comments: user: login: andrewgsavage
created_at: 2025-01-24T12:34:19Z
body: this would prevent issues like https://github.com/lmfit/uncertainties/pull/286 yea
author_association: CONTRIBUTOR
user: login: newville
created_at: 2025-01-25T20:19:33Z
body: @jagerber48 @andrewgsavage Sorry to be dense, but I don't understand.  Are there doctests in the documentation?  Where are these?  I don't see a single `doctest` or `testcode` directive in the rst files. 
If there are such tests, how do you run them?  I have no idea.

I've always found code in docstrings unreliable for testing, as they may be incomplete snippets.    Do we want to run these and count "passing" and "failing"?    I could be persuaded, especially if this was included in the CI, but it might be real effort...
author_association: MEMBER

Diff:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 58201322..6c10ad5a 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -28,11 +28,10 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        python -m pip install .[arrays,test]
+        python -m pip install .[all]
     - name: Test source code and docs
       run: |
-        cd tests
-        python -m pytest --cov=uncertainties --cov=. --cov-report=xml --cov-report=term
+        python -m pytest tests/ doc/ uncertainties/ --cov=uncertainties --cov-report=xml --cov-report=term --doctest-modules --doctest-glob="*.rst"
     - name: Upload coverage reports to Codecov
       uses: codecov/codecov-action@v4.6.0
       with:
diff --git a/doc/formatting.rst b/doc/formatting.rst
index 27270ba1..b0dae52d 100644
--- a/doc/formatting.rst
+++ b/doc/formatting.rst
@@ -16,6 +16,8 @@ Printing
 
 Numbers with uncertainties can be printed conveniently:
 
+>>> from uncertainties import ufloat
+>>> x = ufloat(0.2, 0.01)
 >>> print(x)
 0.200+/-0.010
 
@@ -160,7 +162,7 @@ integer:
 >>> print(ufloat(3.1415, 0))
 3.1415+/-0
 >>> print(ufloat(3.1415e10, 0))
-(3.1415+/-0)e+10
+31415000000.0+/-0
 >>> print(ufloat(3.1415, 0.0005))
 3.1415+/-0.0005
 >>> print('{:.2f}'.format(ufloat(3.14, 0.001)))
diff --git a/doc/numpy_guide.rst b/doc/numpy_guide.rst
index f350765f..c4e56c63 100644
--- a/doc/numpy_guide.rst
+++ b/doc/numpy_guide.rst
@@ -15,10 +15,12 @@ Arrays of uncertainties Variables
 It is possible to put uncertainties Variable  in NumPy_ arrays and
 matrices:
 
->>> arr = numpy.array([ufloat(1, 0.01), ufloat(2, 0.1)])
->>> 2*arr
+>>> import numpy as np
+>>> from uncertainties import ufloat
+>>> arr = np.array([ufloat(1, 0.01), ufloat(2, 0.1)])
+>>> print(2*arr)
 [2.0+/-0.02 4.0+/-0.2]
->>> print arr.sum()
+>>> print(str(arr.sum()))
 3.00+/-0.10
 
 Many common operations on NumPy arrays can be performed transparently
@@ -72,7 +74,7 @@ uncertainties:
 NumPy arrays of numbers with uncertainties can also be built directly
 through NumPy, thanks to NumPy's support of arrays of arbitrary objects:
 
->>> arr = numpy.array([ufloat(1, 0.1), ufloat(2, 0.002)])
+>>> arr = np.array([ufloat(1, 0.1), ufloat(2, 0.002)])
 
 .. index::
    single: matrices; creation and manipulation
@@ -101,6 +103,8 @@ the inverse and the pseudo-inverse of a matrix can be calculated in
 the usual way: if :data:`mat` is a :class:`unumpy.matrix`,
 
 >>> print(mat.I)
+[[0.19999999999999996+/-0.012004265908417718]
+ [0.3999999999999999+/-0.01600179989876138]]
 
 does calculate the inverse or pseudo-inverse of :data:`mat` with
 uncertainties.
@@ -118,9 +122,9 @@ directly accessed (through functions that work on pure float arrays
 too):
 
 >>> unumpy.nominal_values(arr)
-array([ 1.,  2.])
+array([1., 2.])
 >>> unumpy.std_devs(mat)
-matrix([[ 0.1  ,  0.002]])
+matrix([[0.1  , 0.002]])
 
 
 .. index:: mathematical operation; on an array of numbers
@@ -133,6 +137,8 @@ generalize those from :mod:`uncertainties.umath` so that they work on
 NumPy arrays of numbers with uncertainties instead of just scalars:
 
 >>> print(unumpy.cos(arr))  # Cosine of each array element
+[0.5403023058681398+/-0.08414709848078966
+ -0.4161468365471424+/-0.0018185948536513636]
 
 NumPy's function names are used, and not those from the :mod:`math`
 module (for instance, :func:`unumpy.arccos` is defined, like in NumPy,
@@ -156,15 +162,15 @@ Since NaN±1 is *not* (the scalar) NaN, functions like
 with a NaN nominal value:
 
 >>> nan = float("nan")
->>> arr = numpy.array([nan, uncertainties.ufloat(nan, 1), uncertainties.ufloat(1, nan), 2])
->>> arr
-array([nan, nan+/-1.0, 1.0+/-nan, 2], dtype=object)
->>> arr[~unumpy.isnan(arr)].mean()
+>>> arr = np.array([nan, ufloat(nan, 1), ufloat(1, nan), 2])
+>>> print(arr)
+[nan nan+/-1.0 1.0+/-nan 2]
+>>> print(arr[~unumpy.isnan(arr)].mean())
 1.5+/-nan
 
 or equivalently, by using masked arrays:
 
->>> masked_arr = numpy.ma.array(arr, mask=unumpy.isnan(arr))
+>>> masked_arr = np.ma.array(arr, mask=unumpy.isnan(arr))
 >>> masked_arr.mean()
 1.5+/-nan
 
@@ -180,39 +186,65 @@ values.
 Storing arrays in text format
 =============================
 
-Arrays of numbers with uncertainties can be directly :ref:`pickled
-<pickling>`, saved to file and read from a file. Pickling has the
-advantage of preserving correlations between errors.
-
-Storing arrays in **text format** loses correlations between errors but has the
-advantage of being both computer- and human-readable. This can be done through
-NumPy's :func:`savetxt` and :func:`loadtxt`.
-
-Writing the array to file can be done by asking NumPy to use the
-*representation* of numbers with uncertainties (instead of the default float
-conversion):
-
->>> numpy.savetxt('arr.txt', arr, fmt='%r')
-
-This produces a file `arr.txt` that contains a text representation of
-the array::
-
-  1.0+/-0.01
-  2.0+/-0.002
-
-The file can then be read back by instructing NumPy with :meth:`numpy.loadtxt`,
-but for object arrays, this requires a converter function for each column
-separately.  We can use func:`uncertainties.ufloat_fromstr`, but
-:meth:`numpy.loadtxt` passes bytes to converters, they must first be converted
-into a string.  In addition the number of maximum number of columns must be
-known.  An example of using all of this to unpack the data saved with
-:meth:`numpy.savetxt` would be:
+Number with uncertainties can easy be cast to strings and back. This means that arrays
+of numbers with uncertainties can also be cast to string representations and back.
+There are many ways to convert an array of numbers with uncertainties to a string
+representation for storage and then convert it back to a python array of numbers with
+uncertainties.
+Here is one example set of functions to perform this operation.
 
+>>> import json
 >>> from uncertainties import ufloat_fromstr
->>> max_cols = 1
->>> converters = {col: lambda dat: ufloat_fromstr(dat.decode("utf-8"))
-....                              for col in range(max_cols)}
->>> arr = numpy.loadtxt('arr.txt', converters=converters, dtype=object)
+>>> def serialize_unumpy_array(u_arr):
+...     string_u_arr = np.vectorize(repr)(u_arr)
+...     return json.dumps(string_u_arr.tolist(), indent=4)
+>>>
+>>> def deserialize_unumpy_arr(serialized_u_arr):
+...     string_u_arr = np.array(json.loads(serialized_u_arr))
+...     return np.vectorize(ufloat_fromstr)(string_u_arr)
+
+We can use the first function to serialize an array
+
+>>> u_arr = np.array([
+...     [ufloat(1, 0.1), ufloat(2, 0.2)],
+...     [ufloat(3, 0.3), ufloat(4, 0.4)],
+... ])
+>>> print(u_arr)
+[[1.0+/-0.1 2.0+/-0.2]
+ [3.0+/-0.3 4.0+/-0.4]]
+>>> serialized_u_arr = serialize_unumpy_array(u_arr)
+>>> print(serialized_u_arr)
+[
+    [
+        "1.0+/-0.1",
+        "2.0+/-0.2"
+    ],
+    [
+        "3.0+/-0.3",
+        "4.0+/-0.4"
+    ]
+]
+
+This can then of course be stored in a ``.json`` file using ``json.dump``.
+We can then deserialize
+
+>>> u_arr_2 = deserialize_unumpy_arr(serialized_u_arr)
+>>> print(u_arr_2)
+[[1.0+/-0.1 2.0+/-0.2]
+ [3.0+/-0.3 4.0+/-0.4]]
+
+Note that the process of serializing and deserializing the array of numbers with
+uncertainties has result in all correlations between numbers within one array, and also
+between numbers from the original array and its deserialized copy
+
+>>> print(u_arr[0, 0] - u_arr_2[0, 0])
+0.00+/-0.14
+>>> print(u_arr[0, 0] == u_arr_2[0, 0])
+False
+
+A future release of :mod:`uncertainties` may provide functionality for
+serializing/deserializing number with uncertainties in such a way that correlations can
+be preserved.
 
 .. index:: linear algebra; additional functions, ulinalg
 
@@ -226,10 +258,10 @@ It currently offers generalizations of two functions from
 :mod:`numpy.linalg` that work on arrays (or matrices) that contain
 numbers with uncertainties, the **matrix inverse and pseudo-inverse**:
 
->>> unumpy.ulinalg.inv([[ufloat(2, 0.1)]])
-array([[0.5+/-0.025]], dtype=object)
->>> unumpy.ulinalg.pinv(mat)
-matrix([[0.2+/-0.0012419339757],
-        [0.4+/-0.00161789987329]], dtype=object)
+>>> print(unumpy.ulinalg.inv([[ufloat(2, 0.1)]]))
+[[0.5+/-0.025]]
+>>> print(unumpy.ulinalg.pinv(mat))
+[[0.19999999999999996+/-0.012004265908417718]
+ [0.3999999999999999+/-0.01600179989876138]]
 
 .. _NumPy: http://numpy.scipy.org/
diff --git a/doc/tech_guide.rst b/doc/tech_guide.rst
index f3dc5284..98ae4b93 100644
--- a/doc/tech_guide.rst
+++ b/doc/tech_guide.rst
@@ -64,6 +64,7 @@ If multiple variables are pickled together (including when pickling
 :doc:`NumPy arrays <numpy_guide>`), their correlations are preserved:
 
 >>> import pickle
+>>> from uncertainties import ufloat
 >>> x = ufloat(2, 0.1)
 >>> y = 2*x
 >>> p = pickle.dumps([x, y])  # Pickling to a string
@@ -243,6 +244,7 @@ derivative.
 
 As a consequence, it is possible for uncertainties to be ``nan``:
 
+>>> from uncertainties import umath
 >>> umath.sqrt(ufloat(0, 1))
 0.0+/-nan
 
@@ -348,7 +350,7 @@ yield correct uncertainties.  For example:
 >>> poly
 46.0+/-0.4
 >>> poly - x*x
-42+/-0
+42.0+/-0
 
 Even though ``x*x`` has a non-zero uncertainty, the result has a zero
 uncertainty, because it is equal to :data:`a`.
@@ -357,16 +359,16 @@ If the variable :data:`a` above is modified, the value of :data:`poly`
 is not modified, as is usual in Python:
 
 >>> a = 123
->>> print(poly)
-46.0+/-0.4  # Still equal to x**2 + 42, not x**2 + 123
+>>> print(poly)  # Still equal to x**2 + 42, not x**2 + 123
+46.0+/-0.4
 
 Random variables can, on the other hand, have their uncertainty
 updated on the fly, because quantities with uncertainties (like
 :data:`poly`) keep track of them:
 
 >>> x.std_dev = 0
->>> print(poly)
-46+/-0  # Zero uncertainty, now
+>>> print(poly)  # Zero uncertainty, now
+46.0+/-0
 
 As usual, Python keeps track of objects as long as they are used.
 Thus, redefining the value of :data:`x` does not change the fact that
@@ -374,8 +376,8 @@ Thus, redefining the value of :data:`x` does not change the fact that
 in :data:`x`:
 
 >>> x = 10000
->>> print(poly)
-46+/-0  # Unchanged
+>>> print(poly)  # Unchanged
+46.0+/-0
 
 These mechanisms make quantities with uncertainties behave mostly like
 regular numbers, while providing a fully transparent way of handling
@@ -408,7 +410,7 @@ a :class:`Variable` object:
 
 >>> x = ufloat(1, 0.1)
 >>> type(x)
-<class 'uncertainties.Variable'>
+<class 'uncertainties.core.Variable'>
 
 :class:`Variable` objects can be used as if they were regular Python
 numbers (the summation, etc. of these objects is defined).
@@ -419,7 +421,7 @@ represent mathematical functions and not simple variables; these
 objects store all the variables they depend on:
 
 >>> type(umath.sin(x))
-<class 'uncertainties.AffineScalarFunc'>
+<class 'uncertainties.core.AffineScalarFunc'>
 
 
 .. _automatic differentiation: http://en.wikipedia.org/wiki/Automatic_differentiation
diff --git a/doc/user_guide.rst b/doc/user_guide.rst
index 5ba5d465..9226bd9a 100644
--- a/doc/user_guide.rst
+++ b/doc/user_guide.rst
@@ -78,7 +78,7 @@ as with other Python numbers and variables.
 >>> t = ufloat(0.2, 0.01)
 >>> double = 2.0*t
 >>> print(double)
-0.4+/-0.02
+0.400+/-0.020
 >>> square = t**2
 >>> print(square)
 0.040+/-0.004
@@ -89,7 +89,7 @@ When adding two Variables, the uncertainty in the result is the quadrature sum
 >>> x = ufloat(20, 4)
 >>> y = ufloat(12, 3)
 >>> print(x+y)
-32.0+/-5.0
+32+/-5
 
 We can check that error propagation when adding two independent variables
 (using the abbreviation `.s` for the standard error):
@@ -103,7 +103,7 @@ Multiplying two Variables will properly propagate those
 uncertainties too:
 
 >>> print(x*y)
-240.0+/-76.83749084919418
+(2.4+/-0.8)e+02
 >>> (x*y).s == (x*y).n *  sqrt((x.s/x.n)**2 + (y.s/y.n)**2 )
 True
 
@@ -111,9 +111,9 @@ But note that adding a Variable to itself does not add its uncertainties in
 quadrature, but are simply scaled:
 
 >>> print(x+x)
-40.0+/-8.0
+40+/-8
 >>> print(3*x + 10)
-70.0+/-12.0
+70+/-12
 
 
 It is important to understand that calculations done with Variable know about
@@ -279,6 +279,7 @@ the number of variables involved, and whatever the complexity of the
 calculation. For example, when :data:`x` is the number with
 uncertainty defined above,
 
+>>> x = ufloat(0.2, 0.01)
 >>> square = x**2
 >>> print(square)
 0.040+/-0.004
@@ -324,8 +325,8 @@ when the variables are **tagged**:
 >>> for (var, error) in sum_value.error_components().items():
 ...     print("{}: {}".format(var.tag, error))
 ...
-u variable: 0.1
 v variable: 0.2
+u variable: 0.1
 
 The variance (i.e. squared uncertainty) of the result
 (:data:`sum_value`) is the quadratic sum of these independent
@@ -335,10 +336,17 @@ The tags *do not have to be distinct*. For instance, *multiple* random
 variables can be tagged as ``"systematic"``, and their contribution to
 the total uncertainty of :data:`result` can simply be obtained as:
 
+>>> import math
+>>> x = ufloat(132, 0.02, "statistical")
+>>> y = ufloat(2.1, 0.05, "systematic")
+>>> z = ufloat(12, 0.1, "systematic")
+>>> result = x**y / z
 >>> syst_error = math.sqrt(sum(  # Error from *all* systematic errors
 ...     error**2
 ...     for (var, error) in result.error_components().items()
 ...     if var.tag == "systematic"))
+>>> print(format(syst_error, ".3f"))
+577.984
 
 The remaining contribution to the uncertainty is:
 
@@ -362,8 +370,9 @@ Covariance matrix
 The covariance matrix between various variables or calculated
 quantities can be simply obtained:
 
+>>> from uncertainties import covariance_matrix
 >>> sum_value = u+2*v
->>> cov_matrix = uncertainties.covariance_matrix([u, v, sum_value])
+>>> cov_matrix = covariance_matrix([u, v, sum_value])
 
 has value
 
@@ -388,11 +397,12 @@ Correlation matrix
 If the NumPy_ package is available, the correlation matrix can be
 obtained as well:
 
->>> corr_matrix = uncertainties.correlation_matrix([u, v, sum_value])
->>> corr_matrix
-array([[ 1.        ,  0.        ,  0.4472136 ],
-       [ 0.        ,  1.        ,  0.89442719],
-       [ 0.4472136 ,  0.89442719,  1.        ]])
+>>> from uncertainties import correlation_matrix
+>>> corr_matrix = correlation_matrix([u, v, sum_value])
+>>> print(corr_matrix)
+[[1.         0.         0.4472136 ]
+ [0.         1.         0.89442719]
+ [0.4472136  0.89442719 1.        ]]
 
 .. index:: correlations; correlated variables
 
@@ -407,17 +417,18 @@ Use of a covariance matrix
 
 Correlated variables can be obtained through the *covariance* matrix:
 
->>> (u2, v2, sum2) = uncertainties.correlated_values([1, 10, 21], cov_matrix)
+>>> from uncertainties import correlated_values
+>>> (u2, v2, sum2) = correlated_values([1, 10, 21], cov_matrix)
 
 creates three new variables with the listed nominal values, and the given
 covariance matrix:
 
->>> sum_value
-21.0+/-0.223606797749979
->>> sum2
-21.0+/-0.223606797749979
->>> sum2 - (u2+2*v2)
-0.0+/-3.83371856862256e-09
+>>> print(sum_value)
+21.00+/-0.22
+>>> print(sum2)
+21.00+/-0.22
+>>> print(format(sum2 - (u2+2*v2), ".6f"))
+0.000000+/-0.000000
 
 The theoretical value of the last expression is exactly zero, like for
 ``sum - (u+2*v)``, but numerical errors yield a small uncertainty
@@ -426,7 +437,11 @@ correlations should in fact cancel the uncertainty on :data:`sum2`).
 
 The covariance matrix is the desired one:
 
->>> uncertainties.covariance_matrix([u2, v2, sum2])
+>>> import numpy as np
+>>> print(np.array_str(np.array(covariance_matrix([u2, v2, sum2])), suppress_small=True))
+[[0.01 0.   0.01]
+ [0.   0.01 0.02]
+ [0.01 0.02 0.05]]
 
 reproduces the original covariance matrix :data:`cov_matrix` (up to
 rounding errors).
@@ -445,8 +460,11 @@ Alternatively, correlated values can be defined through:
 
 Example:
 
->>> (u3, v3, sum3) = uncertainties.correlated_values_norm(
-...     [(1, 0.1), (10, 0.1), (21, 0.22360679774997899)], corr_matrix)
+>>> from uncertainties import correlated_values_norm
+>>> (u3, v3, sum3) = correlated_values_norm(
+...     [(1, 0.1), (10, 0.1), (21, 0.22360679774997899)],
+...     corr_matrix,
+... )
 >>> print(u3)
 1.00+/-0.10
 
@@ -475,7 +493,15 @@ It is thus possible to take a function :func:`f` *that returns a
 single float*, and to automatically generalize it so that it also
 works with numbers with uncertainties:
 
->>> wrapped_f = uncertainties.wrap(f)
+>>> from scipy.special import jv
+>>> from uncertainties import wrap as u_wrap
+>>> x = ufloat(2, 0.01)
+>>> jv(0, x)
+Traceback (most recent call last):
+ ...
+TypeError: ufunc 'jv' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
+>>> print(u_wrap(jv)(0, x))
+0.224+/-0.006
 
 The new function :func:`wrapped_f` (optionally) *accepts a number
 with uncertainty* in place of any float *argument* of :func:`f` (note
@@ -525,21 +551,24 @@ access the **nominal value and uncertainty of all numbers in a uniform
 manner**.  This is what the :func:`nominal_value` and
 :func:`std_dev` functions do:
 
->>> print(uncertainties.nominal_value(x))
+>>> from uncertainties import nominal_value, std_dev
+>>> x = ufloat(0.2, 0.01)
+>>> print(nominal_value(x))
 0.2
->>> print(uncertainties.std_dev(x))
+>>> print(std_dev(x))
 0.01
->>> uncertainties.nominal_value(3)
+>>> print(nominal_value(3))
 3
->>> uncertainties.std_dev(3)
+>>> print(std_dev(3))
 0.0
 
 Finally, a utility method is provided that directly yields the
 `standard score <http://en.wikipedia.org/wiki/Standard_score>`_
 (number of standard deviations) between a number and a result with
-uncertainty: with :data:`x` equal to 0.20±0.01,
+uncertainty:
 
->>> x.std_score(0.17)
+>>> x = ufloat(0.20, 0.01)
+>>> print(x.std_score(0.17))
 -3.0
 
 .. index:: derivatives
diff --git a/pyproject.toml b/pyproject.toml
index ebc28ceb..e552acb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,7 +62,7 @@ Changelog = "https://github.com/lmfit/uncertainties/blob/master/CHANGES.rst"
 
 [project.optional-dependencies]
 arrays = ["numpy"]
-test = ["pytest", "pytest_codspeed", "pytest_cov"]
+test = ["pytest", "pytest_codspeed", "pytest_cov", "scipy"]
 doc = ["sphinx", "sphinx-copybutton", "python-docs-theme"]
 all = ["uncertainties[doc,test,arrays]"]
 
diff --git a/tests/cases/generate_ufloat_method_cases.py b/tests/cases/generate_ufloat_method_cases.py
index 2012a07b..85808496 100644
--- a/tests/cases/generate_ufloat_method_cases.py
+++ b/tests/cases/generate_ufloat_method_cases.py
@@ -50,5 +50,6 @@
 
 ufloat_method_cases_json_path = Path(Path.cwd(), "ufloat_method_cases.json")
 
-with open(ufloat_method_cases_json_path, "w") as f:
-    json.dump(ufloat_method_cases_dict, f, indent=4)
+if __name__ == "__main__":
+    with open(ufloat_method_cases_json_path, "w") as f:
+        json.dump(ufloat_method_cases_dict, f, indent=4)
diff --git a/tests/cases/generate_umath_function_cases.py b/tests/cases/generate_umath_function_cases.py
index a83aeb30..db50edc2 100644
--- a/tests/cases/generate_umath_function_cases.py
+++ b/tests/cases/generate_umath_function_cases.py
@@ -103,5 +103,6 @@
 
 umath_function_cases_json_path = Path(Path.cwd(), "umath_function_cases.json")
 
-with open(umath_function_cases_json_path, "w") as f:
-    json.dump(umath_function_cases_dict, f, indent=4)
+if __name__ == "__main__":
+    with open(umath_function_cases_json_path, "w") as f:
+        json.dump(umath_function_cases_dict, f, indent=4)
diff --git a/tests/test_umath.py b/tests/test_umath.py
index 98b03eff..531c51f4 100644
--- a/tests/test_umath.py
+++ b/tests/test_umath.py
@@ -16,7 +16,7 @@
 # Unit tests
 
 umath_function_cases_json_path = Path(
-    Path.cwd(),
+    Path(__file__).parent,
     "cases",
     "umath_function_cases.json",
 )
diff --git a/tests/test_uncertainties.py b/tests/test_uncertainties.py
index 13e62769..df5a40fe 100644
--- a/tests/test_uncertainties.py
+++ b/tests/test_uncertainties.py
@@ -155,7 +155,7 @@ def test_ufloat_fromstr():
 ###############################################################################
 
 ufloat_method_cases_json_path = Path(
-    Path.cwd(),
+    Path(__file__).parent,
     "cases",
     "ufloat_method_cases.json",
 )
diff --git a/uncertainties/core.py b/uncertainties/core.py
index 390d3f58..07b6d9b4 100644
--- a/uncertainties/core.py
+++ b/uncertainties/core.py
@@ -969,6 +969,7 @@ def ufloat_fromstr(representation, tag=None):
     Examples:
     -----------
 
+    >>> from uncertainties import ufloat_fromstr
     >>> x = ufloat_fromstr("12.58+/-0.23")  # = ufloat(12.58, 0.23)
     >>> x = ufloat_fromstr("12.58 ± 0.23")  # = ufloat(12.58, 0.23)
     >>> x = ufloat_fromstr("3.85e5 +/- 2.3e4")  # = ufloat(3.8e5, 2.3e4)
@@ -1006,6 +1007,7 @@ def ufloat(nominal_value, std_dev=None, tag=None):
 
     Examples
     ----------
+    >>> from uncertainties import ufloat
     >>> a = ufloat(5, 0.2)
     >>> b = ufloat(1000, 30, tag='kilo')
 
diff --git a/uncertainties/unumpy/core.py b/uncertainties/unumpy/core.py
index 763f57d9..ddfbf5ef 100644
--- a/uncertainties/unumpy/core.py
+++ b/uncertainties/unumpy/core.py
@@ -500,8 +500,7 @@ def inv_with_derivatives(arr, input_type, derivatives):
 
 
 inv = func_with_deriv_to_uncert_func(inv_with_derivatives)
-inv.__doc__ = (
-    """\
+inv.__doc__ = """
     Version of numpy.linalg.inv that works with array-like objects
     that contain numbers with uncertainties.
 
@@ -509,12 +508,7 @@ def inv_with_derivatives(arr, input_type, derivatives):
     matrix for the array of nominal values.
 
     Analytical formulas are used.
-
-    Original documentation:
-    %s
     """
-    % numpy.linalg.inv.__doc__
-)
 
 ########## Matrix pseudo-inverse
 
@@ -598,11 +592,7 @@ def pinv(array_like, rcond=pinv_default):
     matrix for the array of nominal values.
 
     Analytical formulas are used.
-
-    Original documentation:
-    %s
     """
-    % numpy.linalg.pinv.__doc__
 )(pinv)
 
 ########## Matrix class


Reviews:
user: login: newville
body: @jagerber48 Thanks!  adding `scipy` as a test dependency seems fine to me.   I could have been convinced to give up doctests. But it is impressive that they pass(!), and with this change probably not too hard to keep them working... so I'm in favor of not dropping them.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-12T17:30:30Z
user: login: jagerber48
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T21:28:37Z
user: login: jagerber48
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T21:30:44Z
user: login: jagerber48
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T21:31:32Z
user: login: jagerber48
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T21:31:39Z
user: login: newville
body: @jagerber48 Thanks!  This all looks very nice to me.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-14T17:16:56Z

Review comments:
diff_hunk: @@ -28,11 +28,10 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        python -m pip install .[arrays,test]
+        python -m pip install .[all]
     - name: Test source code and docs
       run: |
-        cd tests
-        python -m pytest --cov=uncertainties --cov=. --cov-report=xml --cov-report=term
+        python -m pytest tests/ doc/ uncertainties/ --cov=uncertainties --cov-report=xml --cov-report=term --doctest-modules --doctest-glob="*.rst"
path: .github/workflows/python-package.yml
user: login: jagerber48
body: The directories immediately following `pytest` are what are tested. The directory after `--cov` is the directory whose coverage is reported. `--doctest-modules` runs the tests in docstrings in `.py` files. `--doctest-glob="*.rst"` lets the `.rst` files in `doc/` directory be detected for doctest testing.
created_at: 2025-04-13T21:28:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,39 +186,65 @@ values.
 Storing arrays in text format
 =============================
 
-Arrays of numbers with uncertainties can be directly :ref:`pickled
-<pickling>`, saved to file and read from a file. Pickling has the
-advantage of preserving correlations between errors.
-
-Storing arrays in **text format** loses correlations between errors but has the
-advantage of being both computer- and human-readable. This can be done through
-NumPy's :func:`savetxt` and :func:`loadtxt`.
-
-Writing the array to file can be done by asking NumPy to use the
-*representation* of numbers with uncertainties (instead of the default float
-conversion):
-
->>> numpy.savetxt('arr.txt', arr, fmt='%r')
-
-This produces a file `arr.txt` that contains a text representation of
-the array::
-
-  1.0+/-0.01
-  2.0+/-0.002
-
-The file can then be read back by instructing NumPy with :meth:`numpy.loadtxt`,
-but for object arrays, this requires a converter function for each column
-separately.  We can use func:`uncertainties.ufloat_fromstr`, but
-:meth:`numpy.loadtxt` passes bytes to converters, they must first be converted
-into a string.  In addition the number of maximum number of columns must be
-known.  An example of using all of this to unpack the data saved with
-:meth:`numpy.savetxt` would be:
+Number with uncertainties can easy be cast to strings and back. This means that arrays
path: doc/numpy_guide.rst
user: login: jagerber48
body: This entire section was changed. The old test was failing in a python/numpy-version dependent way on the `np.loadtxt` call. I couldn't figure out what was happening. This old version of the section ended up being more a tutorial on `np.savetxt` and `np.loadtxt` so I just wrote a different serialization/deserialization example. I think the emphasis on pickling should be removed from the documentation. Pickling may change some in `4.0.0` anyways.
created_at: 2025-04-13T21:30:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -500,21 +500,15 @@ def inv_with_derivatives(arr, input_type, derivatives):
 
 
 inv = func_with_deriv_to_uncert_func(inv_with_derivatives)
-inv.__doc__ = (
-    """\
+inv.__doc__ = """
     Version of numpy.linalg.inv that works with array-like objects
     that contain numbers with uncertainties.
 
     The result is a unumpy.matrix if numpy.linalg.pinv would return a
     matrix for the array of nominal values.
 
     Analytical formulas are used.
-
-    Original documentation:
-    %s
     """
-    % numpy.linalg.inv.__doc__
path: uncertainties/unumpy/core.py
user: login: jagerber48
body: The numpy docstring was failing the doctest.. it's also strange to be inserting 3rd party docstrings like this
created_at: 2025-04-13T21:31:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -598,11 +592,7 @@ def pinv(array_like, rcond=pinv_default):
     matrix for the array of nominal values.
 
     Analytical formulas are used.
-
-    Original documentation:
-    %s
     """
-    % numpy.linalg.pinv.__doc__
path: uncertainties/unumpy/core.py
user: login: jagerber48
body: The numpy docstring was failing the doctest.. it's also strange to be inserting 3rd party docstrings like this
created_at: 2025-04-13T21:31:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,11 +28,10 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        python -m pip install .[arrays,test]
+        python -m pip install .[all]
     - name: Test source code and docs
       run: |
-        cd tests
-        python -m pytest --cov=uncertainties --cov=. --cov-report=xml --cov-report=term
+        python -m pytest tests/ doc/ uncertainties/ --cov=uncertainties --cov-report=xml --cov-report=term --doctest-modules --doctest-glob="*.rst"
path: .github/workflows/python-package.yml
user: login: newville
body: @jagerber48 OK, that sounds good to me.
created_at: 2025-04-14T17:15:04Z
author_association: MEMBER
diff_hunk: @@ -598,11 +592,7 @@ def pinv(array_like, rcond=pinv_default):
     matrix for the array of nominal values.
 
     Analytical formulas are used.
-
-    Original documentation:
-    %s
     """
-    % numpy.linalg.pinv.__doc__
path: uncertainties/unumpy/core.py
user: login: newville
body: agreed
created_at: 2025-04-14T17:16:10Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-12T05:59:35Z
body: ## [Codecov](https://app.codecov.io/gh/lmfit/uncertainties/pull/301?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.44%. Comparing base [(`cf7556c`)](https://app.codecov.io/gh/lmfit/uncertainties/commit/cf7556caac3a24d5777f7f0bca502c803c9a9a19?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) to head [(`b570060`)](https://app.codecov.io/gh/lmfit/uncertainties/commit/b57006003351fa8419de86076c65101e0e31f266?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit).
> Report is 16 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master     #301       +/-   ##
===========================================
- Coverage   96.66%   86.44%   -10.23%     
===========================================
  Files          18       18               
  Lines        1888     1888               
===========================================
- Hits         1825     1632      -193     
- Misses         63      256      +193     
```

| [Flag](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | Coverage Δ | |
|---|---|---|
| [macos-latest-3.10](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [macos-latest-3.11](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [macos-latest-3.12](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [macos-latest-3.8](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.39%)` | :arrow_up: |
| [macos-latest-3.9](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.39%)` | :arrow_up: |
| [no-numpy](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `78.54% <0.00%> (+3.91%)` | :arrow_up: |
| [ubuntu-latest-3.10](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [ubuntu-latest-3.11](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [ubuntu-latest-3.12](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [ubuntu-latest-3.8](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.39%)` | :arrow_up: |
| [ubuntu-latest-3.9](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.39%)` | :arrow_up: |
| [windows-latest-3.10](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [windows-latest-3.11](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [windows-latest-3.12](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.38%)` | :arrow_up: |
| [windows-latest-3.8](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.39%)` | :arrow_up: |
| [windows-latest-3.9](https://app.codecov.io/gh/lmfit/uncertainties/pull/301/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit) | `95.56% <100.00%> (+0.39%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/lmfit/uncertainties/pull/301?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lmfit).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: codspeed-hq[bot]
created_at: 2025-04-12T06:02:57Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/lmfit/uncertainties/branches/jagerber48%3Afix_doctests)

### Merging #301 will **not alter performance**

<sub>Comparing <code>jagerber48:fix_doctests</code> (b570060) with <code>master</code> (cf7556c)</sub>



### Summary

`✅ 5` untouched benchmarks  




author_association: NONE
user: login: jagerber48
created_at: 2025-04-12T12:25:03Z
body: I added `scipy` as a dependency for running tests so that the `uncertainties.wrap` function could be practically tested in the documentation. This is a bit heavy-weight, but it was the easiest way I could think of to get a function that doesn't have a simple analytical form to be tested. It fits the documentation exactly "This is for instance
useful when calling external functions (which are out of the user's
control), including functions written in C or Fortran."
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-12T19:43:35Z
body: I'm going to continue to work on this PR until all the doctests are passing. Will probably be at least a few days or weeks depending on my flexibility
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-13T12:10:10Z
body: Many of the test results for arrays are nasty since they're shown with arbitrary precision. I need to figure out way to truncate the precision that also doesn't "get in the way" of the documentation code.
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-13T12:10:32Z
body: TODO: run the doctests in CI
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-13T13:10:21Z
body: Ugh, the tests of OS/python version dependent rounding differences. There's also a pair of tests that save and load a file that has OS-dependent differences. Need to deal with these.
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-13T21:34:41Z
body: Ok, doctests and docstrings are now being tested in CI and they are all passing. This went faster than I expected.

The one remaining thing I would do is make sure the docs still "look ok". For example, making it so that not too many decimal places are shown in arrays. Basically to get the docs to this point I just worked as fast as possible just making the tests pass by copy and pasting their current results. 

Others can weigh in on if that is worth doing. Other than that, this PR is ready for review. 

- There was a weirdness with injecting `numpy` docs into the `unumpy` docs
- I rewrote the section on saving/loading `numpy` arrays of `UFloat`s
- I change the CI configuration a little bit.
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-14T02:08:03Z
body: Yeah I'm going to leave it at this unless others have opinions. I think arrays of `ufloat` need a dedicated method for "nice" printing and I don't want to work on that just to get the docs looking pristine with pristine doctests. we can work on that as we improve the `unumpy` interface.

This PR is ready for review
author_association: CONTRIBUTOR
user: login: newville
created_at: 2025-04-14T17:18:15Z
body: @jagerber48 Thanks for doing this.   
Github is suggesting a "Rebase and merge".   Is that acceptable?
author_association: MEMBER
user: login: jagerber48
created_at: 2025-04-14T17:26:53Z
body: > @jagerber48 Thanks for doing this. Github is suggesting a "Rebase and merge". Is that acceptable?

Now that (I think) I understand how this repo is configured I feel like I would prefer "squash and merge" to keep the 33 commits off of the `master` branch. But I think my real preference would be to do just do a normal merge. I think there was some buried conversation a while ago where we made the decision to disable regular merging in this repo and opted for rebase-merge or squash-merge. I'll dig that conversation up.

In the meantime: weak preference for "squash + merge", "rebase + merge" is fine.

author_association: CONTRIBUTOR
user: login: newville
created_at: 2025-04-14T17:38:55Z
body: @jagerber48 Oh, you are right: "allow merge requests" is not checked as a valid merge method.  I'm sorry, but I do not recall that conversation

I would slightly prefer "regular merge" here too, though "squash and merge" would be OK. 
Anyway, happy to merge with whatever method is best.

author_association: MEMBER
user: login: wshanks
created_at: 2025-04-14T17:53:57Z
body: My preference is for squash and merge because it keeps the history clean. I don't feel strongly and I am not committing as much you all so I defer to your preferences though. If a PR has unrelated changes that should be kept as separate commits, I think rebase and merge is better than a plain merge if it works. If there are merge conflicts in the middle of the branch that get resolved later, the rebase option doesn't work though.
author_association: CONTRIBUTOR
user: login: jagerber48
created_at: 2025-04-14T20:05:15Z
body: Let's just squash merge this and the other PRs to get them in. We can discuss the merge strategy elsewhere. I'll merge this one and some other approved PRs tomorrow if other folks haven't by then.
author_association: CONTRIBUTOR