Repository: scikit-hep/pyhf

Issue:
Make `model.config.par_names()` a property?
### Summary

Currently [`model.config.par_names()`](https://github.com/scikit-hep/pyhf/blob/767ed59d06274f810e47944f1fe4c95d94968543/src/pyhf/pdf.py#L304-L329) takes no arguments and returns a list of parameter names. The behavior slightly differs from `model.config.channels` and `model.config.samples`, which are properties. For consistency, it may be useful to turn `par_names` into a property as well.

### Additional Information

`model.config.par_names()` used to take an argument to set formatting, which required it to be a function. Since that has been dropped in the meantime, this opens up the possibility of using `@property`.

I don't know whether both `par_names` and `par_names()` can be supported simultaneously, which would allow to not break existing workflows with `par_names()`, but it may be better to only have a single API regardless.

### Code of Conduct

- [X] I agree to follow the Code of Conduct


Discussion:
Similarly to this, `suggested_init`, `suggested_bounds` and `suggested_fixed` currently take no arguments either. One other consideration is that maybe some of these parts of the API are meant to eventually support kwargs, in which case switching to a property would not be useful.

Resolving patch:
diff --git a/src/pyhf/optimize/mixins.py b/src/pyhf/optimize/mixins.py
--- a/src/pyhf/optimize/mixins.py
+++ b/src/pyhf/optimize/mixins.py
@@ -181,7 +181,7 @@ def minimize(
 
         # handle non-pyhf ModelConfigs
         try:
-            par_names = pdf.config.par_names()
+            par_names = pdf.config.par_names
         except AttributeError:
             par_names = None
 
diff --git a/src/pyhf/pdf.py b/src/pyhf/pdf.py
--- a/src/pyhf/pdf.py
+++ b/src/pyhf/pdf.py
@@ -358,6 +358,7 @@ def par_slice(self, name):
         """
         return self.par_map[name]['slice']
 
+    @property
     def par_names(self):
         """
         The names of the parameters in the model including binned-parameter indexing.
@@ -370,8 +371,10 @@ def par_names(self):
             >>> model = pyhf.simplemodels.uncorrelated_background(
             ...     signal=[12.0, 11.0], bkg=[50.0, 52.0], bkg_uncertainty=[3.0, 7.0]
             ... )
-            >>> model.config.par_names()
+            >>> model.config.par_names
             ['mu', 'uncorr_bkguncrt[0]', 'uncorr_bkguncrt[1]']
+
+        .. versionchanged:: 0.7.0 Changed from method to property attribute.
         """
         _names = []
         for name in self.par_order:


Test patch:
diff --git a/tests/test_optim.py b/tests/test_optim.py
--- a/tests/test_optim.py
+++ b/tests/test_optim.py
@@ -1,3 +1,4 @@
+from unittest.mock import patch, PropertyMock
 import pyhf
 from pyhf.optimize.mixins import OptimizerMixin
 from pyhf.optimize.common import _get_tensor_shim, _make_stitch_pars
@@ -576,7 +577,10 @@ def test_minuit_param_names(mocker):
     assert 'minuit' in result
     assert result.minuit.parameters == ('mu', 'uncorr_bkguncrt[0]')
 
-    pdf.config.par_names = mocker.Mock(return_value=None)
-    _, result = pyhf.infer.mle.fit(data, pdf, return_result_obj=True)
-    assert 'minuit' in result
-    assert result.minuit.parameters == ('x0', 'x1')
+    with patch(
+        "pyhf.pdf._ModelConfig.par_names", new_callable=PropertyMock
+    ) as mock_par_names:
+        mock_par_names.return_value = None
+        _, result = pyhf.infer.mle.fit(data, pdf, return_result_obj=True)
+        assert "minuit" in result
+        assert result.minuit.parameters == ("x0", "x1")
diff --git a/tests/test_pdf.py b/tests/test_pdf.py
--- a/tests/test_pdf.py
+++ b/tests/test_pdf.py
@@ -957,7 +957,7 @@ def test_par_names_scalar_nonscalar():
 
     model = pyhf.Model(spec, poi_name="scalar")
     assert model.config.par_order == ["scalar", "nonscalar"]
-    assert model.config.par_names() == [
+    assert model.config.par_names == [
         'scalar',
         'nonscalar[0]',
     ]
@@ -1159,7 +1159,7 @@ def test_pdf_clipping(backend):
     model = ws.model()
     data = tensorlib.astensor([100.0, 100.0, 10.0, 0.0, 0.0])
 
-    for par_name in model.config.par_names():
+    for par_name in model.config.par_names:
         if "np" in par_name:
             par_values.append(-0.6)  # np_1 / np_2
         else:
diff --git a/tests/test_simplemodels.py b/tests/test_simplemodels.py
--- a/tests/test_simplemodels.py
+++ b/tests/test_simplemodels.py
@@ -18,7 +18,7 @@ def test_correlated_background(backend):
     assert model.config.channels == ["single_channel"]
     assert model.config.samples == ["background", "signal"]
     assert model.config.par_order == ["correlated_bkg_uncertainty", "mu"]
-    assert model.config.par_names() == ['correlated_bkg_uncertainty', "mu"]
+    assert model.config.par_names == ["correlated_bkg_uncertainty", "mu"]
     assert model.config.suggested_init() == [0.0, 1.0]
 
 
@@ -29,7 +29,7 @@ def test_uncorrelated_background(backend):
     assert model.config.channels == ["singlechannel"]
     assert model.config.samples == ["background", "signal"]
     assert model.config.par_order == ["mu", "uncorr_bkguncrt"]
-    assert model.config.par_names() == [
+    assert model.config.par_names == [
         'mu',
         'uncorr_bkguncrt[0]',
         'uncorr_bkguncrt[1]',
@@ -52,7 +52,7 @@ def test_correlated_background_default_backend(default_backend):
     assert model.config.channels == ["single_channel"]
     assert model.config.samples == ["background", "signal"]
     assert model.config.par_order == ["correlated_bkg_uncertainty", "mu"]
-    assert model.config.par_names() == ['correlated_bkg_uncertainty', "mu"]
+    assert model.config.par_names == ["correlated_bkg_uncertainty", "mu"]
     assert model.config.suggested_init() == [0.0, 1.0]
 
 
@@ -68,7 +68,7 @@ def test_uncorrelated_background_default_backend(default_backend):
     assert model.config.channels == ["singlechannel"]
     assert model.config.samples == ["background", "signal"]
     assert model.config.par_order == ["mu", "uncorr_bkguncrt"]
-    assert model.config.par_names() == [
+    assert model.config.par_names == [
         'mu',
         'uncorr_bkguncrt[0]',
         'uncorr_bkguncrt[1]',