From e8fc6099e2b48e6f3c7397dc20900fcb560013d5 Mon Sep 17 00:00:00 2001 From: Littie28 Date: Thu, 22 Jan 2026 13:18:42 +0100 Subject: [PATCH 1/4] Fix internal usage of deprecated APIs triggering user warnings Added _derivatives and _error_components for internal use. Public APIs retain deprecation warnings as intended. Fixes #352 --- CHANGES.rst | 6 ++++ tests/test_uncertainties.py | 4 +-- uncertainties/core.py | 57 +++++++++++++++++++++--------------- uncertainties/unumpy/core.py | 6 ++-- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c4870485..7956dc84 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Change Log =================== +3.2.4a 2026-January-22 +----------------------- + +Adds: +- Add new internal _derivatives and _error_components properties for internal use without deprecation warning + 3.2.4 2026-January-9 ----------------------- diff --git a/tests/test_uncertainties.py b/tests/test_uncertainties.py index a4cc3276..e90d6bdf 100644 --- a/tests/test_uncertainties.py +++ b/tests/test_uncertainties.py @@ -504,7 +504,7 @@ def test_basic_access_to_data(): # Details on the sources of error: a = ufloat(-1, 0.001) y = 2 * x + 3 * x + 2 + a - error_sources = y.error_components() + error_sources = y._error_components() assert len(error_sources) == 2 # 'a' and 'x' assert error_sources[x] == 0.05 assert error_sources[a] == 0.001 @@ -514,7 +514,7 @@ def test_basic_access_to_data(): # Modification of the standard deviation of variables: x.std_dev = 1 - assert y.error_components()[x] == 5 # New error contribution! + assert y._error_components()[x] == 5 # New error contribution! # Calculated values with uncertainties should not have a settable # standard deviation: diff --git a/uncertainties/core.py b/uncertainties/core.py index 1c7ff51e..ff013de8 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -430,10 +430,8 @@ def nominal_value(self): ############################################################ - # Making derivatives a property gives the user a clean syntax, - # which is consistent with derivatives becoming a dictionary. @property - def derivatives(self): + def _derivatives(self): """ Return a mapping from each Variable object on which the function (self) depends to the value of the derivative with respect to @@ -445,13 +443,6 @@ def derivatives(self): This mapping is cached, for subsequent calls. """ - warn( - f"{self.__class__.__name__}.derivatives() is deprecated. It will " - f"be removed in a future release.", - FutureWarning, - stacklevel=2, - ) - if not self._linear_part.expanded(): self._linear_part.expand() # Attempts to get the contribution of a variable that the @@ -460,20 +451,27 @@ def derivatives(self): return self._linear_part.linear_combo + # Making derivatives a property gives the user a clean syntax, + # which is consistent with derivatives becoming a dictionary. + @property + def derivatives(self): + """ + Public wrapper for _derivatives. + """ + warn( + f"{self.__class__.__name__}.derivatives() is deprecated. It will " + f"be removed in a future release.", + FutureWarning, + stacklevel=2, + ) + return self._derivatives + ######################################## # Uncertainties handling: - def error_components(self): """ - Individual components of the standard deviation of the affine - function (in absolute value), returned as a dictionary with - Variable objects as keys. The returned variables are the - independent variables that the affine function depends on. - - This method assumes that the derivatives contained in the - object take scalar values (and are not a tuple, like what - math.frexp() returns, for instance). + Publich wrapper of _error_compents with FutureWarning. """ warn( f"{self.__class__.__name__}.error_components() is currently an " @@ -485,10 +483,23 @@ def error_components(self): stacklevel=2, ) + return self._error_components() + + def _error_components(self): + """ + Individual components of the standard deviation of the affine + function (in absolute value), returned as a dictionary with + Variable objects as keys. The returned variables are the + independent variables that the affine function depends on. + + This method assumes that the derivatives contained in the + object take scalar values (and are not a tuple, like what + math.frexp() returns, for instance). + """ # Calculation of the variance: error_components = {} - for variable, derivative in self.derivatives.items(): + for variable, derivative in self._derivatives.items(): # print "TYPE", type(variable), type(derivative) # Individual standard error due to variable: @@ -523,7 +534,7 @@ def std_dev(self): # std_dev value (in fact, many intermediate AffineScalarFunc do # not need to have their std_dev calculated: only the final # AffineScalarFunc returned to the user does). - return float(sqrt(sum(delta**2 for delta in self.error_components().values()))) + return float(sqrt(sum(delta**2 for delta in self._error_components().values()))) # Abbreviation (for formulas, etc.): s = std_dev @@ -920,12 +931,12 @@ def covariance_matrix(nums_with_uncert): covariance_matrix = [] for i1, expr1 in enumerate(nums_with_uncert, 1): - derivatives1 = expr1.derivatives # Optimization + derivatives1 = expr1._derivatives # Optimization vars1 = set(derivatives1) # !! Python 2.7+: viewkeys() would work coefs_expr1 = [] for expr2 in nums_with_uncert[:i1]: - derivatives2 = expr2.derivatives # Optimization + derivatives2 = expr2._derivatives # Optimization coefs_expr1.append( sum( ( diff --git a/uncertainties/unumpy/core.py b/uncertainties/unumpy/core.py index 714f1729..cb6f897e 100644 --- a/uncertainties/unumpy/core.py +++ b/uncertainties/unumpy/core.py @@ -133,7 +133,7 @@ def derivative(u, var): """ if isinstance(u, uncert_core.AffineScalarFunc): try: - return u.derivatives[var] + return u._derivatives[var] except KeyError: return 0.0 else: @@ -195,7 +195,7 @@ def wrapped_func(arr, *args, **kwargs): # working with a large number of arrays? # # !! set() is only needed for Python 2 compatibility: - variables |= set(element.derivatives.keys()) + variables |= set(element._derivatives.keys()) # If the matrix has no variables, then the function value can be # directly returned: @@ -415,7 +415,7 @@ def wrapped_func(array_like, *args, **kwargs): # floats, etc. might be present if isinstance(element, uncert_core.AffineScalarFunc): # !!! set() is only needed for Python 2 compatibility: - variables |= set(element.derivatives.keys()) + variables |= set(element._derivatives.keys()) array_nominal = nominal_values(array_version) # Function value, then derivatives at array_nominal (the From bc6f53a9821e3c7fd718300187da36232ee1fa6d Mon Sep 17 00:00:00 2001 From: Littie28 Date: Thu, 22 Jan 2026 15:24:43 +0100 Subject: [PATCH 2/4] Addresses feedback - updated changelog according to feedback - moved historic docstring to public version of derivatives and error_components --- CHANGES.rst | 2 +- uncertainties/core.py | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7956dc84..2cd407b3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ Change Log =================== -3.2.4a 2026-January-22 +Unreleased ----------------------- Adds: diff --git a/uncertainties/core.py b/uncertainties/core.py index ff013de8..89a19314 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -433,15 +433,7 @@ def nominal_value(self): @property def _derivatives(self): """ - Return a mapping from each Variable object on which the function - (self) depends to the value of the derivative with respect to - that variable. - - This mapping should not be modified. - - Derivative values are always floats. - - This mapping is cached, for subsequent calls. + Private wrapper for derivatives without raising a FutureWarning. """ if not self._linear_part.expanded(): self._linear_part.expand() @@ -456,7 +448,15 @@ def _derivatives(self): @property def derivatives(self): """ - Public wrapper for _derivatives. + Return a mapping from each Variable object on which the function + (self) depends to the value of the derivative with respect to + that variable. + + This mapping should not be modified. + + Derivative values are always floats. + + This mapping is cached, for subsequent calls. """ warn( f"{self.__class__.__name__}.derivatives() is deprecated. It will " @@ -471,7 +471,14 @@ def derivatives(self): # Uncertainties handling: def error_components(self): """ - Publich wrapper of _error_compents with FutureWarning. + Individual components of the standard deviation of the affine + function (in absolute value), returned as a dictionary with + Variable objects as keys. The returned variables are the + independent variables that the affine function depends on. + + This method assumes that the derivatives contained in the + object take scalar values (and are not a tuple, like what + math.frexp() returns, for instance). """ warn( f"{self.__class__.__name__}.error_components() is currently an " @@ -487,14 +494,7 @@ def error_components(self): def _error_components(self): """ - Individual components of the standard deviation of the affine - function (in absolute value), returned as a dictionary with - Variable objects as keys. The returned variables are the - independent variables that the affine function depends on. - - This method assumes that the derivatives contained in the - object take scalar values (and are not a tuple, like what - math.frexp() returns, for instance). + Private wrapper for error_compents without FutureWarning. """ # Calculation of the variance: error_components = {} From ca8bf35f4d3c6d1346f8e470d863e814469bc178 Mon Sep 17 00:00:00 2001 From: Littie28 Date: Thu, 22 Jan 2026 16:23:10 +0100 Subject: [PATCH 3/4] Use private API for test suite - changed all `derivatives` and `error_components` references to private versions `_derivatives` and `_error_components` - reduces warnings raised during test execution from 809 to 352 --- doc/user_guide.rst | 12 ++++++------ tests/test_power.py | 8 ++++---- tests/test_umath.py | 6 +++--- tests/test_uncertainties.py | 26 +++++++++++++------------- tests/test_unumpy.py | 8 ++++---- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/doc/user_guide.rst b/doc/user_guide.rst index 0c555a23..ef839a2c 100644 --- a/doc/user_guide.rst +++ b/doc/user_guide.rst @@ -322,7 +322,7 @@ At these points the ``x`` derivative would be complex so a NaN value is used: >>> x = ufloat(0, 0.2) >>> y=1.5 ->>> print((x**y).error_components()) +>>> print((x**y)._error_components()) {0.0+/-0.2: nan} The ``y`` derivative is real anywhere ``x**y`` is real as long as ``x>=0``. @@ -330,7 +330,7 @@ For ``x < 0`` the ``y`` derivative is always complex valued so a NaN value is us >>> x = -2 >>> y = ufloat(1, 0.2) ->>> print((x**y).error_components()) +>>> print((x**y)._error_components()) {1.0+/-0.2: nan} Automatic correlations @@ -384,7 +384,7 @@ when the variables are **tagged**: >>> sum_value = u+2*v >>> sum_value 21.0+/-0.223606797749979 ->>> for (var, error) in sum_value.error_components().items(): +>>> for (var, error) in sum_value._error_components().items(): ... print("{}: {}".format(var.tag, error)) ... v variable: 0.2 @@ -405,7 +405,7 @@ the total uncertainty of :data:`result` can simply be obtained as: >>> result = x**y / z >>> syst_error = math.sqrt(sum( # Error from *all* systematic errors ... error**2 -... for (var, error) in result.error_components().items() +... for (var, error) in result._error_components().items() ... if var.tag == "systematic")) >>> print(format(syst_error, ".3f")) 577.984 @@ -649,9 +649,9 @@ variables: >>> u = ufloat(1, 0.1) >>> v = ufloat(10, 0.1) >>> sum_value = u+2*v ->>> sum_value.derivatives[u] +>>> sum_value._derivatives[u] 1.0 ->>> sum_value.derivatives[v] +>>> sum_value._derivatives[v] 2.0 These values are obtained with a :ref:`fast differentiation algorithm diff --git a/tests/test_power.py b/tests/test_power.py index 82d00e96..0ae9d599 100644 --- a/tests/test_power.py +++ b/tests/test_power.py @@ -70,14 +70,14 @@ def test_pow_deriv_0(x, y, x_deriv_expected, y_deriv_expected): ) def test_power_derivatives(first_ufloat, second_ufloat, first_der, second_der): result = pow(first_ufloat, second_ufloat) - first_der_result = result.derivatives[first_ufloat] - second_der_result = result.derivatives[second_ufloat] + first_der_result = result._derivatives[first_ufloat] + second_der_result = result._derivatives[second_ufloat] assert nan_close(first_der_result, first_der) assert nan_close(second_der_result, second_der) result = umath_pow(first_ufloat, second_ufloat) - first_der_result = result.derivatives[first_ufloat] - second_der_result = result.derivatives[second_ufloat] + first_der_result = result._derivatives[first_ufloat] + second_der_result = result._derivatives[second_ufloat] assert nan_close(first_der_result, first_der) assert nan_close(second_der_result, second_der) diff --git a/tests/test_umath.py b/tests/test_umath.py index 95c423b2..1ba6cf29 100644 --- a/tests/test_umath.py +++ b/tests/test_umath.py @@ -44,7 +44,7 @@ def test_umath_function_derivatives(func_name, ufloat_tuples): result = func(*ufloat_arg_list) for arg_num, arg in enumerate(ufloat_arg_list): - ufloat_deriv_value = result.derivatives[arg] + ufloat_deriv_value = result._derivatives[arg] numerical_deriv_func = partial_derivative(func, arg_num) numerical_deriv_value = numerical_deriv_func(*float_arg_list) assert math.isclose( @@ -278,8 +278,8 @@ def test_hypot(): # Derivatives that cannot be calculated simply return NaN, with no # exception being raised, normally: result = umath_core.hypot(x, y) - assert isnan(result.derivatives[x]) - assert isnan(result.derivatives[y]) + assert isnan(result._derivatives[x]) + assert isnan(result._derivatives[y]) @pytest.mark.parametrize("function_name", umath_core.deprecated_functions) diff --git a/tests/test_uncertainties.py b/tests/test_uncertainties.py index e90d6bdf..4997a2e5 100644 --- a/tests/test_uncertainties.py +++ b/tests/test_uncertainties.py @@ -172,7 +172,7 @@ def test_ufloat_method_derivativs(func_name, ufloat_tuples): result = bound_func(*ufloat_arg_list[1:]) for arg_num, arg in enumerate(ufloat_arg_list): - ufloat_deriv_value = result.derivatives[arg] + ufloat_deriv_value = result._derivatives[arg] numerical_deriv_func = partial_derivative(unbound_func, arg_num) numerical_deriv_value = numerical_deriv_func(*float_arg_list) assert math.isclose( @@ -200,7 +200,7 @@ def test_copy(): y = copy.copy(x) assert x != y assert not (x == y) - assert y in y.derivatives.keys() # y must not copy the dependence on x + assert y in y._derivatives.keys() # y must not copy the dependence on x z = copy.deepcopy(x) assert x != z @@ -208,13 +208,13 @@ def test_copy(): # Copy tests on expressions: t = x + 2 * z # t depends on x: - assert x in t.derivatives + assert x in t._derivatives # The relationship between the copy of an expression and the # original variables should be preserved: t_copy = copy.copy(t) # Shallow copy: the variables on which t depends are not copied: - assert x in t_copy.derivatives + assert x in t_copy._derivatives assert uncert_core.covariance_matrix([t, z]) == uncert_core.covariance_matrix( [t_copy, z] ) @@ -223,7 +223,7 @@ def test_copy(): # variables should be broken, since the deep copy created new, # independent variables: t_deepcopy = copy.deepcopy(t) - assert x not in t_deepcopy.derivatives + assert x not in t_deepcopy._derivatives assert uncert_core.covariance_matrix([t, z]) != uncert_core.covariance_matrix( [t_deepcopy, z] ) @@ -238,7 +238,7 @@ def test_copy(): gc.collect() - assert y in list(y.derivatives.keys()) + assert y in list(y._derivatives.keys()) ## Classes for the pickling tests (put at the module level, so that @@ -510,7 +510,7 @@ def test_basic_access_to_data(): assert error_sources[a] == 0.001 # Derivative values should be available: - assert y.derivatives[x] == 5 + assert y._derivatives[x] == 5 # Modification of the standard deviation of variables: x.std_dev = 1 @@ -975,8 +975,8 @@ def f(x, y, *args, **kwargs): # to try to confuse the code: assert ( - f_wrapped2(x, y, z, t=t).derivatives[y] - == f_auto_unc(x, y, z, t=t).derivatives[y] + f_wrapped2(x, y, z, t=t)._derivatives[y] + == f_auto_unc(x, y, z, t=t)._derivatives[y] ) # Derivatives supplied through the keyword-parameter dictionary of @@ -992,12 +992,12 @@ def f(x, y, *args, **kwargs): # The derivatives should be exactly the same, because they are # obtained with the exact same analytic formula: assert ( - f_wrapped3(x, y, z, t=t).derivatives[z] - == f_auto_unc(x, y, z, t=t).derivatives[z] + f_wrapped3(x, y, z, t=t)._derivatives[z] + == f_auto_unc(x, y, z, t=t)._derivatives[z] ) assert ( - f_wrapped3(x, y, z, t=t).derivatives[t] - == f_auto_unc(x, y, z, t=t).derivatives[t] + f_wrapped3(x, y, z, t=t)._derivatives[t] + == f_auto_unc(x, y, z, t=t)._derivatives[t] ) ######################################## diff --git a/tests/test_unumpy.py b/tests/test_unumpy.py index 701137ff..118c7d72 100644 --- a/tests/test_unumpy.py +++ b/tests/test_unumpy.py @@ -96,11 +96,11 @@ def derivatives_close(x, y): """ # x and y must depend on the same variables: - if set(x.derivatives) != set(y.derivatives): + if set(x._derivatives) != set(y._derivatives): return False # Not the same variables return all( - nan_close(x.derivatives[var], y.derivatives[var]) for var in x.derivatives + nan_close(x._derivatives[var], y._derivatives[var]) for var in x._derivatives ) @@ -162,8 +162,8 @@ def test_inverse(): # There are correlations if both the next two derivatives are # not zero: - assert m_inverse[0, 0].derivatives[x] - assert m_inverse[0, 1].derivatives[x] + assert m_inverse[0, 0]._derivatives[x] + assert m_inverse[0, 1]._derivatives[x] # Correlations between m and m_inverse should create a perfect # inversion: From 5cfb2cb0fc44f4dc2f24eb673430b483de25521d Mon Sep 17 00:00:00 2001 From: Littie28 Date: Thu, 22 Jan 2026 17:00:06 +0100 Subject: [PATCH 4/4] Addresses feedback on docstrings - updated docstrings according to PR discussion --- uncertainties/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uncertainties/core.py b/uncertainties/core.py index 89a19314..5a141edf 100644 --- a/uncertainties/core.py +++ b/uncertainties/core.py @@ -433,7 +433,7 @@ def nominal_value(self): @property def _derivatives(self): """ - Private wrapper for derivatives without raising a FutureWarning. + Private version of `derivatives` for internal use. """ if not self._linear_part.expanded(): self._linear_part.expand() @@ -494,7 +494,7 @@ def error_components(self): def _error_components(self): """ - Private wrapper for error_compents without FutureWarning. + Private version of `error_components` for internal use. """ # Calculation of the variance: error_components = {}