diff --git a/docs/source/conf.py b/docs/source/conf.py index 99dc93f6..39187884 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -75,7 +75,7 @@ napoleon_custom_sections = [ ("Managed Parameters", "Attributes"), ("Usable Metadata", "Attributes"), - ("General Metadata", "Attributes"), + ("General Metadata", "params_style"), ("Metadata", "Attributes"), ("Properties", "Attributes"), ("Operator Attributes", "Attributes"), @@ -334,4 +334,7 @@ # Example configuration for intersphinx: refer to the Python standard library. -# intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable/", None), +} diff --git a/news/documentation-metadata.rst b/news/documentation-metadata.rst new file mode 100644 index 00000000..3a124fc7 --- /dev/null +++ b/news/documentation-metadata.rst @@ -0,0 +1,23 @@ +**Added:** + +* No News Added: rebuild the documentation with proper metadata handling + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/fix-qmax-update.rst b/news/fix-qmax-update.rst new file mode 100644 index 00000000..958a4dbe --- /dev/null +++ b/news/fix-qmax-update.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fixed load new parsed data with updated `Qmax` attribute + +**Security:** + +* diff --git a/news/fix-test-package.rst b/news/fix-test-package.rst new file mode 100644 index 00000000..a569932c --- /dev/null +++ b/news/fix-test-package.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added `diffpy.structure` back to the requirements and run the test + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/news/iteratepars-behavior.rst b/news/iteratepars-behavior.rst new file mode 100644 index 00000000..556a91c6 --- /dev/null +++ b/news/iteratepars-behavior.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* Changed `iterPars` method to match all equal-type atoms to have same ADPs + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/requirements/conda.txt b/requirements/conda.txt index 8bc0acb8..2e2402dd 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -3,3 +3,4 @@ numpy scipy bg-mpl-stylesheets diffpy.utils +diffpy.structure diff --git a/requirements/pip.txt b/requirements/pip.txt index 4deb9dbf..6a05210c 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -3,3 +3,4 @@ numpy scipy bg-mpl-stylesheets diffpy.utils +diffpy.structure diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index 22cbdcc6..bcf7fcd3 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -15,3 +15,6 @@ # See LICENSE.rst for license information. # ############################################################################## +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) diff --git a/src/diffpy/srfit/fitbase/fitrecipe.py b/src/diffpy/srfit/fitbase/fitrecipe.py index d4fdf279..3a10f1d5 100644 --- a/src/diffpy/srfit/fitbase/fitrecipe.py +++ b/src/diffpy/srfit/fitbase/fitrecipe.py @@ -489,16 +489,17 @@ def residual(self, p=[]): ---------- p : list or numpy.ndarray The list of current variable values, provided in the same order - as the '_parameters' list. If p is an empty iterable (default), - then it is assumed that the parameters have already been - updated in some other way, and the explicit update within this - function is skipped. + as the ``_parameters`` list. If ``p`` is an empty iterable + (default), then it is assumed that the parameters have already + been updated in some other way, and the explicit update within + this function is skipped. - Return - ------ - chiv : numpy.ndarray - The array of residuals to be optimized. The array is such that - dot(chiv, chiv) = chi^2 + restraints. + Notes + ----- + The residual is by default the weighted concatenation of each + :class:`FitContribution` residual, plus the value of each restraint. + The returned array ``chiv`` satisfies + ``dot(chiv, chiv) = chi^2 + restraints``. """ # Prepare, if necessary @@ -541,15 +542,17 @@ def scalar_residual(self, p=[]): ---------- p : list or numpy.ndarray The list of current variable values, provided in the same order - as the '_parameters' list. If p is an empty iterable (default), - then it is assumed that the parameters have already been - updated in some other way, and the explicit update within this - function is skipped. + as the ``_parameters`` list. If ``p`` is an empty iterable + (default), then it is assumed that the parameters have already + been updated in some other way, and the explicit update within + this function is skipped. + Notes + ----- The residual is by default the weighted concatenation of each - FitContribution's residual, plus the value of each restraint. The array - returned, denoted chiv, is such that - dot(chiv, chiv) = chi^2 + restraints. + :class:`FitContribution` residual, plus the value of each restraint. + The returned array, denoted ``chiv``, is such that + ``dot(chiv, chiv) = chi^2 + restraints``. """ chiv = self.residual(p) return dot(chiv, chiv) diff --git a/src/diffpy/srfit/fitbase/profile.py b/src/diffpy/srfit/fitbase/profile.py index d66e61f7..18c8331f 100644 --- a/src/diffpy/srfit/fitbase/profile.py +++ b/src/diffpy/srfit/fitbase/profile.py @@ -186,11 +186,16 @@ def set_observed_profile(self, xobs, yobs, dyobs=None): Numpy array of the observed signal. dyobs Numpy array of the uncertainty in the observed signal. If - dyobs is None (default), it will be set to 1 at each - observed xobs. + `dyobs` is None (default), it will be set to 1 at each + observed `xobs`. - Raises ValueError if len(yobs) != len(xobs) - Raises ValueError if dyobs != None and len(dyobs) != len(xobs) + + Raises + ----------- + ValueError + if len(yobs) != len(xobs) + ValueError + if dyobs != None and len(dyobs) != len(xobs) """ if len(yobs) != len(xobs): raise ValueError("xobs and yobs are different lengths") @@ -247,8 +252,8 @@ def set_calculation_range(self, xmin=None, xmax=None, dx=None): The sample spacing in the independent variable. When different from the data, resample the ``x`` as anchored at ``xmin``. - Note that xmin is always inclusive (unless clipped). xmax is inclusive - if it is within the bounds of the observed data. + Note that ``xmin`` is always inclusive (unless clipped). + ``xmax`` is inclusive if it is within the bounds of the observed data. Raises ------ diff --git a/src/diffpy/srfit/fitbase/profilegenerator.py b/src/diffpy/srfit/fitbase/profilegenerator.py index e0206084..8adc661f 100644 --- a/src/diffpy/srfit/fitbase/profilegenerator.py +++ b/src/diffpy/srfit/fitbase/profilegenerator.py @@ -163,15 +163,18 @@ def set_profile(self, profile): will store the calculated signal. """ if self.profile is not None: - self.profile.removeObserver(self._flush) + self.profile.removeObserver(self._on_profile_update) self.profile = profile - self.profile.addObserver(self._flush) - self._flush(other=(self,)) + self.profile.addObserver(self._on_profile_update) + self._on_profile_update(other=(self,)) + return - # Merge the profiles metadata with our own - self.meta.update(self.profile.meta) - self._process_metadata() + def _on_profile_update(self, other=()): + if self.profile is not None: + self.meta.update(self.profile.meta) + self.processMetaData() + self._flush(other=other) return def _process_metadata(self): diff --git a/src/diffpy/srfit/fitbase/profileparser.py b/src/diffpy/srfit/fitbase/profileparser.py index 7713abc5..78dc3c27 100644 --- a/src/diffpy/srfit/fitbase/profileparser.py +++ b/src/diffpy/srfit/fitbase/profileparser.py @@ -153,7 +153,9 @@ def parseString(self, patstring): patstring A string containing the pattern - Raises ParseError if the string cannot be parsed + Raises + ---------- + ParseError if the string cannot be parsed """ raise NotImplementedError() @@ -170,8 +172,12 @@ def parseFile(self, filename): filename The name of the file to parse - Raises IOError if the file cannot be read - Raises ParseError if the file cannot be parsed + Raises + ---------- + IOError + if the file cannot be read + ParseError + if the file cannot be parsed """ infile = open(filename, "r") self._banks = [] @@ -339,7 +345,10 @@ def select_bank(self, index): index index of bank (integer, starting at 0). - Raises IndexError if requesting a bank that does not exist + Raises + ---------- + IndexError + if requesting a bank that does not exist """ if index is None: index = self._meta.get("bank", 0) @@ -384,6 +393,8 @@ def get_data(self, index=None): index of bank (integer, starting at 0, default None). If index is None then the currently selected bank is used. + Returns + ---------- This returns (x, y, dx, dy) tuple for the bank. dx is 0 if it cannot be determined from the data format. """ diff --git a/src/diffpy/srfit/fitbase/recipeorganizer.py b/src/diffpy/srfit/fitbase/recipeorganizer.py index 20d16c49..2079f813 100644 --- a/src/diffpy/srfit/fitbase/recipeorganizer.py +++ b/src/diffpy/srfit/fitbase/recipeorganizer.py @@ -240,48 +240,70 @@ def _iter_managed(self): """Get iterator over managed objects.""" return chain(*(d.values() for d in self.__managed)) - def iterate_over_parameters(self, pattern="", recurse=True): + def iterPars(self, pattern="", recurse=True, fullnames=False): """Iterate over the Parameters contained in this object. Parameters ---------- - pattern : str, optional - The regular expression pattern to match parameter - names against. Only parameters with names matching - this pattern will be returned. Default is an empty - string, which matches all parameter names. - recurse : bool, optional - The flag indicating whether to recurse into managed - objects when iterating over parameters. If True - (default), the method will also iterate over - parameters in managed sub-objects. If False, only - top-level parameters will be iterated over. - - Example - ------- - - .. - for param in recipe.iterate_over_parameters(pattern="scale_"): - # print the name and value of parameters containing "scale_" - print(f"{param.name}={param.value}") + pattern : str + Iterate over parameters with names matching this regular expression + (all parameters by default). + + When `fullnames` is True, the regular expression is matched against + dotted parameter names relative to this object, e.g. ``Ni0.Biso``. + recurse : bool + Recurse into managed objects when True (default). + fullnames : bool + Match against hierarchical dotted names relative to this object + when True. Match only leaf parameter names when False (default). """ regexp = re.compile(pattern) - for parameter in list(self._parameters.values()): - if regexp.search(parameter.name): - yield parameter + if not fullnames: + for par in list(self._parameters.values()): + if regexp.search(par.name): + yield par + if not recurse: + return + managed = self.__managed[:] + managed.remove(self._parameters) + for m in managed: + for obj in m.values(): + if hasattr(obj, "iterPars"): + for par in obj.iterPars(pattern=pattern, recurse=True): + yield par + return + for par in self._iterpars_fullnames( + regexp, recurse=recurse, prefix="" + ): + yield par + + def _iterpars_fullnames(self, regexp, recurse=True, prefix=""): + """Internal helper for iterPars(fullnames=True).""" + for par in list(self._parameters.values()): + name = f"{prefix}{par.name}" + if regexp.search(name): + yield par + if not recurse: return - # Iterate over objects within the managed dictionaries. + managed = self.__managed[:] managed.remove(self._parameters) for m in managed: for obj in m.values(): - if hasattr(obj, "iterate_over_parameters"): - for parameter in obj.iterate_over_parameters( - pattern=pattern + if hasattr(obj, "_iterpars_fullnames"): + childprefix = f"{prefix}{obj.name}." + for par in obj._iterpars_fullnames( + regexp, + recurse=True, + prefix=childprefix, ): - yield parameter - return + yield par + elif hasattr(obj, "iterPars"): + for par in obj.iterPars( + pattern=regexp.pattern, recurse=True + ): + yield par @deprecated(iterPars_deprecation_msg) def iterPars(self, pattern="", recurse=True): diff --git a/src/diffpy/srfit/fitbase/simplerecipe.py b/src/diffpy/srfit/fitbase/simplerecipe.py index 5f27889b..56891eee 100644 --- a/src/diffpy/srfit/fitbase/simplerecipe.py +++ b/src/diffpy/srfit/fitbase/simplerecipe.py @@ -195,9 +195,12 @@ def set_observed_profile(self, xobs, yobs, dyobs=None): dyobs is None (default), it will be set to 1 at each observed xobs. - - Raises ValueError if len(yobs) != len(xobs) - Raises ValueError if dyobs != None and len(dyobs) != len(xobs) + Raises + ---------- + ValueError + if len(yobs) != len(xobs) + ValueError + if dyobs != None and len(dyobs) != len(xobs) """ return self.profile.set_observed_profile(xobs, yobs, dyobs) @@ -222,20 +225,20 @@ def set_calculation_range(self, xmin=None, xmax=None, dx=None): Parameters ---------- - xmin : float or "obs", optional + xmin : float or `obs`, optional The minimum value of the independent variable. Keep the current minimum when not specified. If specified as "obs" reset to the minimum observed value. - xmax : float or "obs", optional + xmax : float or `obs`, optional The maximum value of the independent variable. Keep the current maximum when not specified. If specified as "obs" reset to the maximum observed value. - dx : float or "obs", optional + dx : float or `obs`, optional The sample spacing in the independent variable. When different from the data, resample the ``x`` as anchored at ``xmin``. - Note that xmin is always inclusive (unless clipped). xmax is inclusive - if it is within the bounds of the observed data. + Note that ``xmin`` is always inclusive (unless clipped). + ``xmax`` is inclusive if it is within the bounds of the observed data. Raises ------ diff --git a/src/diffpy/srfit/pdf/pdfcontribution.py b/src/diffpy/srfit/pdf/pdfcontribution.py index 73e666ea..1b22c1fd 100644 --- a/src/diffpy/srfit/pdf/pdfcontribution.py +++ b/src/diffpy/srfit/pdf/pdfcontribution.py @@ -130,20 +130,20 @@ def setCalculationRange(self, xmin=None, xmax=None, dx=None): Parameters ---------- - xmin : float or "obs", optional + xmin : float or `obs`, optional The minimum value of the independent variable. Keep the current minimum when not specified. If specified as "obs" reset to the minimum observed value. - xmax : float or "obs", optional + xmax : float or `obs`, optional The maximum value of the independent variable. Keep the current maximum when not specified. If specified as "obs" reset to the maximum observed value. - dx : float or "obs", optional + dx : float or `obs`, optional The sample spacing in the independent variable. When different from the data, resample the ``x`` as anchored at ``xmin``. - Note that xmin is always inclusive (unless clipped). xmax is inclusive - if it is within the bounds of the observed data. + Note that ``xmin`` is always inclusive (unless clipped). + ``xmax`` is inclusive if it is within the bounds of the observed data. Raises ------ diff --git a/src/diffpy/srfit/pdf/pdfparser.py b/src/diffpy/srfit/pdf/pdfparser.py index bfb10a15..18bd4d32 100644 --- a/src/diffpy/srfit/pdf/pdfparser.py +++ b/src/diffpy/srfit/pdf/pdfparser.py @@ -137,7 +137,10 @@ def parseString(self, patstring): patstring A string containing the pattern - Raises ParseError if the string cannot be parsed + Raises + ---------- + ParseError + if the string cannot be parsed """ # useful regex patterns: diff --git a/src/diffpy/srfit/sas/sasparser.py b/src/diffpy/srfit/sas/sasparser.py index 5503a417..6484b2e4 100644 --- a/src/diffpy/srfit/sas/sasparser.py +++ b/src/diffpy/srfit/sas/sasparser.py @@ -99,8 +99,12 @@ def parseFile(self, filename): filename The name of the file to parse - Raises IOError if the file cannot be read - Raises ParseError if the file cannot be parsed + Raises + ---------- + IOError + if the file cannot be read + ParseError + if the file cannot be parsed """ import sasdata.dataloader.loader as sas_dataloader @@ -142,7 +146,10 @@ def parseString(self, patstring): patstring A string containing the pattern - Raises ParseError if the string cannot be parsed + Raises + ---------- + ParseError + if the string cannot be parsed """ # This calls on parseFile, as that is how the sas data loader works. import tempfile diff --git a/src/diffpy/srfit/sas/sasprofile.py b/src/diffpy/srfit/sas/sasprofile.py index 2ac6ab3b..a0761fb8 100644 --- a/src/diffpy/srfit/sas/sasprofile.py +++ b/src/diffpy/srfit/sas/sasprofile.py @@ -100,16 +100,20 @@ def setObservedProfile(self, xobs, yobs, dyobs=None): Parameters ---------- xobs - Numpy array of the independent variable + Numpy array of the independent variable. yobs Numpy array of the observed signal. dyobs Numpy array of the uncertainty in the observed signal. If - dyobs is None (default), it will be set to 1 at each - observed xobs. + ``dyobs`` is ``None`` (default), it will be set to 1 at each + observed ``xobs``. - Raises ValueError if len(yobs) != len(xobs) - Raises ValueError if dyobs != None and len(dyobs) != len(xobs) + Raises + ------ + ValueError + If ``len(yobs) != len(xobs)``. + ValueError + If ``dyobs is not None`` and ``len(dyobs) != len(xobs)``. """ Profile.setObservedProfile(self, xobs, yobs, dyobs) # Copy the arrays to the _datainfo attribute. diff --git a/tests/conftest.py b/tests/conftest.py index 23b92f6a..de6535d8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -73,11 +73,6 @@ def sas_available(): return has_sas() -@pytest.fixture(scope="session") -def diffpy_structure_available(): - return has_diffpy_structure() - - @pytest.fixture(scope="session") def diffpy_srreal_available(): return has_diffpy_srreal() diff --git a/tests/test_diffpyparset.py b/tests/test_diffpyparset.py index 2e523d35..a6e519b3 100644 --- a/tests/test_diffpyparset.py +++ b/tests/test_diffpyparset.py @@ -18,15 +18,12 @@ import unittest import numpy as np -import pytest from diffpy.srfit.structure.diffpyparset import DiffpyStructureParSet -def testDiffpyStructureParSet(diffpy_structure_available): +def testDiffpyStructureParSet(): """Test the structure conversion.""" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") from diffpy.structure import Atom, Lattice, Structure a1 = Atom("Cu", xyz=np.array([0.0, 0.1, 0.2]), Uisoequiv=0.003) @@ -103,10 +100,8 @@ def _testLattice(): return -def test___repr__(diffpy_structure_available): +def test___repr__(): """Test representation of DiffpyStructureParSet objects.""" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") from diffpy.structure import Atom, Lattice, Structure lat = Lattice(3, 3, 2, 90, 90, 90) @@ -119,10 +114,8 @@ def test___repr__(diffpy_structure_available): return -def test_pickling(diffpy_structure_available): +def test_pickling(): """Test pickling of DiffpyStructureParSet.""" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") from diffpy.structure import Atom, Structure stru = Structure([Atom("C", [0, 0.2, 0.5])]) diff --git a/tests/test_pdf.py b/tests/test_pdf.py index 908d5eb1..ee229c30 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -23,7 +23,8 @@ import pytest from diffpy.srfit.exceptions import SrFitError -from diffpy.srfit.fitbase import ProfileParser +from diffpy.srfit.fitbase.parameter import Parameter +from diffpy.srfit.fitbase.recipeorganizer import RecipeContainer from diffpy.srfit.pdf import PDFContribution, PDFGenerator, PDFParser # ---------------------------------------------------------------------------- @@ -141,11 +142,7 @@ def testParser2(datafile): return -def testGenerator( - diffpy_srreal_available, diffpy_structure_available, datafile -): - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") +def testGenerator(diffpy_srreal_available, datafile): if not diffpy_srreal_available: pytest.skip("diffpy.srreal package not available") @@ -201,7 +198,7 @@ def testGenerator( return -def test_setQmin(diffpy_structure_available, diffpy_srreal_available): +def test_setQmin(diffpy_srreal_available): """Verify qmin is propagated to the calculator object.""" if not diffpy_srreal_available: pytest.skip("diffpy.srreal package not available") @@ -215,10 +212,8 @@ def test_setQmin(diffpy_structure_available, diffpy_srreal_available): return -def test_setQmax(diffpy_structure_available, diffpy_srreal_available): +def test_setQmax(diffpy_srreal_available): """Check PDFContribution.setQmax()""" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") from diffpy.structure import Structure if not diffpy_srreal_available: @@ -234,10 +229,8 @@ def test_setQmax(diffpy_structure_available, diffpy_srreal_available): return -def test_getQmax(diffpy_structure_available, diffpy_srreal_available): +def test_getQmax(diffpy_srreal_available): """Check PDFContribution.getQmax()""" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") from diffpy.structure import Structure if not diffpy_srreal_available: @@ -261,12 +254,8 @@ def test_getQmax(diffpy_structure_available, diffpy_srreal_available): return -def test_savetxt( - diffpy_structure_available, diffpy_srreal_available, datafile -): +def test_savetxt(diffpy_srreal_available, datafile): "check PDFContribution.savetxt()" - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") from diffpy.structure import Structure if not diffpy_srreal_available: @@ -287,12 +276,8 @@ def test_savetxt( return -def test_pickling( - diffpy_structure_available, diffpy_srreal_available, datafile -): +def test_pickling(diffpy_srreal_available, datafile): "validate PDFContribution.residual() after pickling." - if not diffpy_structure_available: - pytest.skip("diffpy.structure package not available") from diffpy.structure import loadStructure if not diffpy_srreal_available: @@ -321,3 +306,75 @@ def test_pickling( if __name__ == "__main__": unittest.main() + + +def _make_iterpars_tree(): + """Build a small hierarchy for iterPars tests.""" + root = RecipeContainer("root") + root._containers = {} + root._manage(root._containers) + + root_biso = Parameter("Biso", 10) + root._add_object(root_biso, root._parameters) + + ni0 = RecipeContainer("Ni0") + ni0_biso = Parameter("Biso", 20) + ni0_uiso = Parameter("Uiso", 30) + ni0._add_object(ni0_biso, ni0._parameters) + ni0._add_object(ni0_uiso, ni0._parameters) + + ni1 = RecipeContainer("Ni1") + ni1_biso = Parameter("Biso", 40) + ni1._add_object(ni1_biso, ni1._parameters) + + o0 = RecipeContainer("O0") + o0_biso = Parameter("Biso", 50) + o0._add_object(o0_biso, o0._parameters) + + root._add_object(ni0, root._containers) + root._add_object(ni1, root._containers) + root._add_object(o0, root._containers) + + return { + "root": root, + "root_biso": root_biso, + "ni0": ni0, + "ni0_biso": ni0_biso, + "ni0_uiso": ni0_uiso, + "ni1": ni1, + "ni1_biso": ni1_biso, + "o0": o0, + "o0_biso": o0_biso, + } + + +@pytest.mark.parametrize( + ("pattern", "kwargs", "expected_values"), + [ + (r"^Biso$", {}, [10, 20, 40, 50]), + (r"^Ni\d+\.Biso$", {}, []), + (r"^Ni\d+\.Biso$", {"fullnames": True}, [20, 40]), + (r"^Ni0\.Uiso$", {"fullnames": True}, [30]), + (r"^O0\.Biso$", {"fullnames": True}, [50]), + (r"^Ni\d+\.Biso$", {"fullnames": True, "recurse": False}, []), + (r"^Biso$", {"fullnames": True, "recurse": False}, [10]), + ], +) +def test_iterpars_fullname_matching(pattern, kwargs, expected_values): + """Verify leaf-name and fullname matching in iterPars.""" + objs = _make_iterpars_tree() + root = objs["root"] + + values = [par.value for par in root.iterPars(pattern, **kwargs)] + + assert values == expected_values + + +def test_iterpars_fullnames_are_relative_to_called_container(): + """Verify fullname matching is relative to the called container.""" + objs = _make_iterpars_tree() + ni0 = objs["ni0"] + + assert list(ni0.iterPars(r"^Biso$", fullnames=True)) == [objs["ni0_biso"]] + assert list(ni0.iterPars(r"^Uiso$", fullnames=True)) == [objs["ni0_uiso"]] + assert list(ni0.iterPars(r"^Ni0\.Biso$", fullnames=True)) == []