Repository: pvlib/pvlib-python

Issue:
remove **kwargs from PVSystem, ModelChain, Location
These objects accept arbitrary kwargs so that users can be lazy about splatting dictionaries into the object constructors. I guess this is nice in some situations. But it also leads to bugs when users mistype a parameter name because python doesn't raise an exception. I ran into this when working on #1022 and #1027. 

I propose that we remove the kwargs without deprecation in 0.8.


Discussion:

Patch:
diff --git a/pvlib/location.py b/pvlib/location.py
--- a/pvlib/location.py
+++ b/pvlib/location.py
@@ -5,11 +5,13 @@
 # Will Holmgren, University of Arizona, 2014-2016.
 
 import datetime
+import warnings
 
 import pandas as pd
 import pytz
 
 from pvlib import solarposition, clearsky, atmosphere, irradiance
+from pvlib._deprecation import pvlibDeprecationWarning
 
 
 class Location:
@@ -48,10 +50,6 @@ class Location:
     name : None or string, default None.
         Sets the name attribute of the Location object.
 
-    **kwargs
-        Arbitrary keyword arguments.
-        Included for compatibility, but not used.
-
     See also
     --------
     pvlib.pvsystem.PVSystem
@@ -82,6 +80,12 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=0,
 
         self.name = name
 
+        if kwargs:
+            warnings.warn(
+                'Arbitrary Location kwargs are deprecated and will be '
+                'removed in v0.9', pvlibDeprecationWarning
+            )
+
     def __repr__(self):
         attrs = ['name', 'latitude', 'longitude', 'altitude', 'tz']
         return ('Location: \n  ' + '\n  '.join(
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -333,10 +333,6 @@ class ModelChain:
 
     name: None or str, default None
         Name of ModelChain instance.
-
-    **kwargs
-        Arbitrary keyword arguments. Included for compatibility, but not
-        used.
     """
 
     def __init__(self, system, location,
@@ -372,6 +368,12 @@ def __init__(self, system, location,
         self.times = None
         self.solar_position = None
 
+        if kwargs:
+            warnings.warn(
+                'Arbitrary ModelChain kwargs are deprecated and will be '
+                'removed in v0.9', pvlibDeprecationWarning
+            )
+
     @classmethod
     def with_pvwatts(cls, system, location,
                      orientation_strategy=None,
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -79,9 +79,6 @@ class PVSystem:
     :py:class:`~pvlib.modelchain.ModelChain`
     objects.
 
-    See the :py:class:`LocalizedPVSystem` class for an object model that
-    describes an installed PV system.
-
     The class supports basic system topologies consisting of:
 
         * `N` total modules arranged in series
@@ -164,7 +161,6 @@ class PVSystem:
     --------
     pvlib.location.Location
     pvlib.tracking.SingleAxisTracker
-    pvlib.pvsystem.LocalizedPVSystem
     """
 
     def __init__(self,
@@ -220,6 +216,12 @@ def __init__(self,
 
         self.name = name
 
+        if kwargs:
+            warnings.warn(
+                'Arbitrary PVSystem kwargs are deprecated and will be '
+                'removed in v0.9', pvlibDeprecationWarning
+            )
+
     def __repr__(self):
         attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module',
                  'inverter', 'albedo', 'racking_model', 'module_type',
@@ -819,9 +821,12 @@ def pvwatts_ac(self, pdc):
         return inverter.pvwatts(pdc, self.inverter_parameters['pdc0'],
                                 **kwargs)
 
+    @deprecated('0.8', alternative='PVSystem, Location, and ModelChain',
+                name='PVSystem.localize', removal='0.9')
     def localize(self, location=None, latitude=None, longitude=None,
                  **kwargs):
-        """Creates a LocalizedPVSystem object using this object
+        """
+        Creates a LocalizedPVSystem object using this object
         and location data. Must supply either location object or
         latitude, longitude, and any location kwargs
 
@@ -843,6 +848,8 @@ def localize(self, location=None, latitude=None, longitude=None,
         return LocalizedPVSystem(pvsystem=self, location=location)
 
 
+@deprecated('0.8', alternative='PVSystem, Location, and ModelChain',
+            name='LocalizedPVSystem', removal='0.9')
 class LocalizedPVSystem(PVSystem, Location):
     """
     The LocalizedPVSystem class defines a standard set of installed PV
diff --git a/pvlib/tracking.py b/pvlib/tracking.py
--- a/pvlib/tracking.py
+++ b/pvlib/tracking.py
@@ -6,6 +6,7 @@
 from pvlib.pvsystem import PVSystem
 from pvlib.location import Location
 from pvlib import irradiance, atmosphere
+from pvlib._deprecation import deprecated
 
 
 class SingleAxisTracker(PVSystem):
@@ -55,6 +56,9 @@ class SingleAxisTracker(PVSystem):
         :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate
         `cross_axis_tilt`. [degrees]
 
+    **kwargs
+        Passed to :py:class:`~pvlib.pvsystem.PVSystem`.
+
     See also
     --------
     pvlib.tracking.singleaxis
@@ -112,6 +116,9 @@ def singleaxis(self, apparent_zenith, apparent_azimuth):
 
         return tracking_data
 
+    @deprecated('0.8',
+                alternative='SingleAxisTracker, Location, and ModelChain',
+                name='SingleAxisTracker.localize', removal='0.9')
     def localize(self, location=None, latitude=None, longitude=None,
                  **kwargs):
         """
@@ -232,6 +239,8 @@ def get_irradiance(self, surface_tilt, surface_azimuth,
                                                **kwargs)
 
 
+@deprecated('0.8', alternative='SingleAxisTracker, Location, and ModelChain',
+            name='LocalizedSingleAxisTracker', removal='0.9')
 class LocalizedSingleAxisTracker(SingleAxisTracker, Location):
     """
     The :py:class:`~pvlib.tracking.LocalizedSingleAxisTracker` class defines a


Test patch:
diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py
--- a/pvlib/tests/test_location.py
+++ b/pvlib/tests/test_location.py
@@ -16,7 +16,8 @@
 from pvlib.solarposition import declination_spencer71
 from pvlib.solarposition import equation_of_time_spencer71
 from test_solarposition import expected_solpos, golden, golden_mst
-from conftest import requires_ephem, requires_tables
+from pvlib._deprecation import pvlibDeprecationWarning
+from conftest import requires_ephem, requires_tables, fail_on_pvlib_version
 
 
 def test_location_required():
@@ -323,3 +324,10 @@ def test_get_sun_rise_set_transit_valueerror(golden):
                              tz='MST')
     with pytest.raises(ValueError):
         golden.get_sun_rise_set_transit(times, method='eyeball')
+
+
+@fail_on_pvlib_version('0.9')
+def test_deprecated_09():
+    match = "Arbitrary Location kwargs"
+    with pytest.warns(pvlibDeprecationWarning, match=match):
+        Location(32.2, -111, arbitrary_kwarg='value')
diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py
--- a/pvlib/tests/test_modelchain.py
+++ b/pvlib/tests/test_modelchain.py
@@ -776,6 +776,13 @@ def test_deprecated_09(sapm_dc_snl_ac_system, cec_dc_adr_ac_system,
                    aoi_model='no_loss', spectral_model='no_loss')
 
 
+@fail_on_pvlib_version('0.9')
+def test_ModelChain_kwargs_deprecated_09(sapm_dc_snl_ac_system, location):
+    match = "Arbitrary ModelChain kwargs"
+    with pytest.warns(pvlibDeprecationWarning, match=match):
+        ModelChain(sapm_dc_snl_ac_system, location, arbitrary_kwarg='value')
+
+
 def test_basic_chain_required(sam_data, cec_inverter_parameters,
                               sapm_temperature_cs5p_220m):
     times = pd.date_range(start='20160101 1200-0700',
diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py
--- a/pvlib/tests/test_pvsystem.py
+++ b/pvlib/tests/test_pvsystem.py
@@ -1064,10 +1064,12 @@ def test_PVSystem_get_irradiance():
     assert_frame_equal(irradiance, expected, check_less_precise=2)
 
 
+@fail_on_pvlib_version('0.9')
 def test_PVSystem_localize_with_location():
     system = pvsystem.PVSystem(module='blah', inverter='blarg')
     location = Location(latitude=32, longitude=-111)
-    localized_system = system.localize(location=location)
+    with pytest.warns(pvlibDeprecationWarning):
+        localized_system = system.localize(location=location)
 
     assert localized_system.module == 'blah'
     assert localized_system.inverter == 'blarg'
@@ -1075,9 +1077,11 @@ def test_PVSystem_localize_with_location():
     assert localized_system.longitude == -111
 
 
+@fail_on_pvlib_version('0.9')
 def test_PVSystem_localize_with_latlon():
     system = pvsystem.PVSystem(module='blah', inverter='blarg')
-    localized_system = system.localize(latitude=32, longitude=-111)
+    with pytest.warns(pvlibDeprecationWarning):
+        localized_system = system.localize(latitude=32, longitude=-111)
 
     assert localized_system.module == 'blah'
     assert localized_system.inverter == 'blarg'
@@ -1103,11 +1107,13 @@ def test_PVSystem___repr__():
     assert system.__repr__() == expected
 
 
+@fail_on_pvlib_version('0.9')
 def test_PVSystem_localize___repr__():
     system = pvsystem.PVSystem(
         module='blah', inverter='blarg', name='pv ftw',
         temperature_model_parameters={'a': -3.56})
-    localized_system = system.localize(latitude=32, longitude=-111)
+    with pytest.warns(pvlibDeprecationWarning):
+        localized_system = system.localize(latitude=32, longitude=-111)
     # apparently name is not preserved when creating a system using localize
     expected = """LocalizedPVSystem:
   name: None
@@ -1131,12 +1137,13 @@ def test_PVSystem_localize___repr__():
 # when they are attached to LocalizedPVSystem, but
 # that's probably not necessary at this point.
 
-
+@fail_on_pvlib_version('0.9')
 def test_LocalizedPVSystem_creation():
-    localized_system = pvsystem.LocalizedPVSystem(latitude=32,
-                                                  longitude=-111,
-                                                  module='blah',
-                                                  inverter='blarg')
+    with pytest.warns(pvlibDeprecationWarning):
+        localized_system = pvsystem.LocalizedPVSystem(latitude=32,
+                                                      longitude=-111,
+                                                      module='blah',
+                                                      inverter='blarg')
 
     assert localized_system.module == 'blah'
     assert localized_system.inverter == 'blarg'
@@ -1144,10 +1151,12 @@ def test_LocalizedPVSystem_creation():
     assert localized_system.longitude == -111
 
 
+@fail_on_pvlib_version('0.9')
 def test_LocalizedPVSystem___repr__():
-    localized_system = pvsystem.LocalizedPVSystem(
-        latitude=32, longitude=-111, module='blah', inverter='blarg',
-        name='my name', temperature_model_parameters={'a': -3.56})
+    with pytest.warns(pvlibDeprecationWarning):
+        localized_system = pvsystem.LocalizedPVSystem(
+            latitude=32, longitude=-111, module='blah', inverter='blarg',
+            name='my name', temperature_model_parameters={'a': -3.56})
 
     expected = """LocalizedPVSystem:
   name: my name
@@ -1311,3 +1320,6 @@ def test_deprecated_09(cec_inverter_parameters, adr_inverter_parameters):
     system = pvsystem.PVSystem()
     with pytest.warns(pvlibDeprecationWarning, match=match):
         system.sapm_celltemp(1, 2, 3)
+    match = "Arbitrary PVSystem kwargs"
+    with pytest.warns(pvlibDeprecationWarning, match=match):
+        system = pvsystem.PVSystem(arbitrary_kwarg='value')