From 94e01a12c18dc76de17cf57fcf9c2aa3ee810bd8 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 16 Aug 2021 09:55:18 -0600 Subject: [PATCH 1/2] add _optional_import and _DeferredImportError to pvlib.tools --- pvlib/tools.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pvlib/tools.py b/pvlib/tools.py index eef80a3b37..b61f609d34 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd import pytz +import importlib def cosd(angle): @@ -344,3 +345,39 @@ def _golden_sect_DataFrame(params, VL, VH, func): raise Exception("EXCEPTION:iterations exceeded maximum (50)") return func(df, 'V1'), df['V1'] + + +def _optional_import(module_name, message): + """ + Import a module, deferring import errors. + + If the module cannot be imported, don't raise an error, but instead return + a dummy object that raises an error when the module actually gets used + for something. + + Parameters + ---------- + module_name: str + Name of the module to import, e.g. 'pandas' + message: str + Deferred error message, e.g. 'pandas must be installed for read_csv' + """ + try: + return importlib.import_module(module_name) + except ImportError: + return _DeferredImportError(message) + + +class _DeferredImportError: + """ + Defer import errors until an imported package actually gets used. + + Useful for importing optional dependencies at the top of a file + instead of hiding them inside the functions that use them. + """ + + def __init__(self, message): + self.message = message + + def __getattr__(self, attrname): + raise ImportError(self.message) From 5da50445d9d10f96d5bc95cc094e2194f8b50977 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 16 Aug 2021 09:55:46 -0600 Subject: [PATCH 2/2] use new import machinery in solarposition.py and ecmwf_macc.py --- pvlib/iotools/ecmwf_macc.py | 26 ++++++++------------------ pvlib/solarposition.py | 15 ++------------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/pvlib/iotools/ecmwf_macc.py b/pvlib/iotools/ecmwf_macc.py index fb42454ee3..6875549147 100644 --- a/pvlib/iotools/ecmwf_macc.py +++ b/pvlib/iotools/ecmwf_macc.py @@ -4,24 +4,14 @@ import threading import pandas as pd +from pvlib.tools import _optional_import + +netCDF4 = _optional_import('netCDF4', ( + 'Reading ECMWF data requires netCDF4 to be installed.')) +ecmwfapi = _optional_import('ecmwfapi', ( + 'To download data from ECMWF requires the API client.\nSee https:/' + '/confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets')) -try: - import netCDF4 -except ImportError: - class netCDF4: - @staticmethod - def Dataset(*a, **kw): - raise ImportError( - 'Reading ECMWF data requires netCDF4 to be installed.') - -try: - from ecmwfapi import ECMWFDataServer -except ImportError: - def ECMWFDataServer(*a, **kw): - raise ImportError( - 'To download data from ECMWF requires the API client.\nSee https:/' - '/confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets' - ) #: map of ECMWF MACC parameter keynames and codes used in API PARAMS = { @@ -164,7 +154,7 @@ def get_ecmwf_macc(filename, params, start, end, lookup_params=True, startdate = start.strftime('%Y-%m-%d') enddate = end.strftime('%Y-%m-%d') if not server: - server = ECMWFDataServer() + server = ecmwfapi.ECMWFDataServer() t = threading.Thread(target=target, daemon=True, args=(server, startdate, enddate, params, filename)) t.start() diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index 4047187533..810c5a0cc2 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -24,7 +24,8 @@ import warnings from pvlib import atmosphere -from pvlib.tools import datetime_to_djd, djd_to_datetime +from pvlib.tools import datetime_to_djd, djd_to_datetime, _optional_import +ephem = _optional_import('ephem', 'PyEphem must be installed') NS_PER_HR = 1.e9 * 3600. # nanoseconds per hour @@ -487,7 +488,6 @@ def _ephem_to_timezone(date, tzinfo): def _ephem_setup(latitude, longitude, altitude, pressure, temperature, horizon): - import ephem # initialize a PyEphem observer obs = ephem.Observer() obs.lat = str(latitude) @@ -544,11 +544,6 @@ def sun_rise_set_transit_ephem(times, latitude, longitude, pyephem """ - try: - import ephem - except ImportError: - raise ImportError('PyEphem must be installed') - # times must be localized if times.tz: tzinfo = times.tz @@ -630,10 +625,6 @@ def pyephem(time, latitude, longitude, altitude=0, pressure=101325, """ # Written by Will Holmgren (@wholmgren), University of Arizona, 2014 - try: - import ephem - except ImportError: - raise ImportError('PyEphem must be installed') # if localized, convert to UTC. otherwise, assume UTC. try: @@ -943,8 +934,6 @@ def pyephem_earthsun_distance(time): pd.Series. Earth-sun distance in AU. """ - import ephem - sun = ephem.Sun() earthsun = [] for thetime in time: