diff --git a/ci/requirements/py36-hypothesis.yml b/ci/requirements/py36-hypothesis.yml index 57f4a236845..380a7760890 100644 --- a/ci/requirements/py36-hypothesis.yml +++ b/ci/requirements/py36-hypothesis.yml @@ -27,3 +27,4 @@ dependencies: - zarr - pydap - lxml + - entrypoints diff --git a/ci/requirements/py36.yml b/ci/requirements/py36.yml index 60259d59965..86cd12080c0 100644 --- a/ci/requirements/py36.yml +++ b/ci/requirements/py36.yml @@ -34,5 +34,6 @@ dependencies: - iris>=1.10 - pydap - lxml + - entrypoints - pip: - mypy==0.711 diff --git a/ci/requirements/py37-windows.yml b/ci/requirements/py37-windows.yml index 51936279134..c166dde4cbb 100644 --- a/ci/requirements/py37-windows.yml +++ b/ci/requirements/py37-windows.yml @@ -24,3 +24,4 @@ dependencies: - rasterio - boto3 - zarr + - entrypoints diff --git a/ci/requirements/py37.yml b/ci/requirements/py37.yml index f1b6d46fd95..b6d00122354 100644 --- a/ci/requirements/py37.yml +++ b/ci/requirements/py37.yml @@ -30,6 +30,7 @@ dependencies: - cfgrib>=0.9.2 - lxml - pydap + - entrypoints - pip: - mypy==0.650 - numbagg diff --git a/doc/environment.yml b/doc/environment.yml index b2f89bd9f96..7776403baef 100644 --- a/doc/environment.yml +++ b/doc/environment.yml @@ -24,4 +24,5 @@ dependencies: - pillow=5.4.1 - sphinx_rtd_theme=0.4.2 - mock=2.0.0 + - entrypoints=0.3 - pip diff --git a/setup.py b/setup.py index 977ad2e1bd8..5afc95c9038 100644 --- a/setup.py +++ b/setup.py @@ -88,6 +88,17 @@ - SciPy2015 talk: https://www.youtube.com/watch?v=X0pAhJgySxk """ # noqa +ENTRY_POINTS = { + 'xarray.backends': [ + 'netcdf4 = xarray.backends:NetCDF4DataStore.open', + 'scipy = xarray.backends:ScipyDataStore', + 'pydap = xarray.backends:PydapDataStore.open', + 'h5netcdf = xarray.backends:H5NetCDFStore', + 'pynio = xarray.backends:NioDataStore', + 'pseudonetcdf = xarray.backends:PseudoNetCDFDataStore.open', + 'cfgrib = xarray.backends:CfGribDataStore', + ] +} setup(name=DISTNAME, version=versioneer.get_version(), @@ -104,4 +115,5 @@ tests_require=TESTS_REQUIRE, url=URL, packages=find_packages(), - package_data={'xarray': ['py.typed', 'tests/data/*']}) + package_data={'xarray': ['py.typed', 'tests/data/*']}, + entry_points=ENTRY_POINTS) diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 21d91a886af..4013ecaa20b 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -129,6 +129,17 @@ def _get_default_engine(path, allow_remote=False): return engine +def _get_backend_cls(engine): + import entrypoints + try: + return entrypoints.get_single('xarray.backends', engine).load() + except entrypoints.NoSuchEntryPoint: + all_entrypoints = entrypoints.get_group_named('xarray.backends') + raise ValueError('unrecognized engine for open_dataset: {}\n' + 'must be one of: {}' + .format(engine, list(all_entrypoints.keys()))) + + def _normalize_path(path): if is_remote_uri(path): return path @@ -352,12 +363,6 @@ def open_dataset(filename_or_obj, group=None, decode_cf=True, -------- open_mfdataset """ - engines = [None, 'netcdf4', 'scipy', 'pydap', 'h5netcdf', 'pynio', - 'cfgrib', 'pseudonetcdf'] - if engine not in engines: - raise ValueError('unrecognized engine for open_dataset: {}\n' - 'must be one of: {}' - .format(engine, engines)) if autoclose is not None: warnings.warn( @@ -382,6 +387,7 @@ def open_dataset(filename_or_obj, group=None, decode_cf=True, if backend_kwargs is None: backend_kwargs = {} + extra_kwargs = {} def maybe_decode_store(store, lock=False): ds = conventions.decode_cf( @@ -417,44 +423,28 @@ def maybe_decode_store(store, lock=False): if isinstance(filename_or_obj, AbstractDataStore): store = filename_or_obj + else: + if isinstance(filename_or_obj, str): + filename_or_obj = _normalize_path(filename_or_obj) - elif isinstance(filename_or_obj, str): - filename_or_obj = _normalize_path(filename_or_obj) - - if engine is None: - engine = _get_default_engine(filename_or_obj, - allow_remote=True) - if engine == 'netcdf4': - store = backends.NetCDF4DataStore.open( - filename_or_obj, group=group, lock=lock, **backend_kwargs) - elif engine == 'scipy': - store = backends.ScipyDataStore(filename_or_obj, **backend_kwargs) - elif engine == 'pydap': - store = backends.PydapDataStore.open( - filename_or_obj, **backend_kwargs) - elif engine == 'h5netcdf': - store = backends.H5NetCDFStore( - filename_or_obj, group=group, lock=lock, **backend_kwargs) - elif engine == 'pynio': - store = backends.NioDataStore( - filename_or_obj, lock=lock, **backend_kwargs) - elif engine == 'pseudonetcdf': - store = backends.PseudoNetCDFDataStore.open( - filename_or_obj, lock=lock, **backend_kwargs) - elif engine == 'cfgrib': - store = backends.CfGribDataStore( - filename_or_obj, lock=lock, **backend_kwargs) + if engine is None: + engine = _get_default_engine(filename_or_obj, + allow_remote=True) - else: - if engine not in [None, 'scipy', 'h5netcdf']: - raise ValueError("can only read bytes or file-like objects " - "with engine='scipy' or 'h5netcdf'") - engine = _get_engine_from_magic_number(filename_or_obj) - if engine == 'scipy': - store = backends.ScipyDataStore(filename_or_obj, **backend_kwargs) - elif engine == 'h5netcdf': - store = backends.H5NetCDFStore(filename_or_obj, group=group, - lock=lock, **backend_kwargs) + else: + if engine not in [None, 'scipy', 'h5netcdf']: + raise ValueError("can only read bytes or file-like objects " + "with engine='scipy' or 'h5netcdf'") + engine = _get_engine_from_magic_number(filename_or_obj) + + if engine in ['netcdf4', 'h5netcdf']: + extra_kwargs['group'] = group + extra_kwargs['lock'] = lock + elif engine in ['pynio', 'pseudonetcdf', 'cfgrib']: + extra_kwargs['lock'] = lock + + opener = _get_backend_cls(engine) + store = opener(filename_or_obj, **backend_kwargs, **extra_kwargs) with close_on_error(store): ds = maybe_decode_store(store) diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py index 539703962c7..3035e7b890e 100644 --- a/xarray/tests/test_backends.py +++ b/xarray/tests/test_backends.py @@ -2019,7 +2019,7 @@ def test_engine(self): open_dataset(tmp_file, engine='foobar') netcdf_bytes = data.to_netcdf() - with raises_regex(ValueError, 'unrecognized engine'): + with raises_regex(ValueError, 'can only read bytes or file-like'): open_dataset(BytesIO(netcdf_bytes), engine='foobar') def test_cross_engine_read_write_netcdf3(self):