diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dab83d7..707a383 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,7 +43,7 @@ jobs: channels: conda-forge mamba-version: '*' channel-priority: strict - activate-environment: test_env_xmovie # Defined in ci/environment*.yml + activate-environment: xmovie-test # Defined in ci/environment*.yml auto-update-conda: false python-version: ${{ matrix.python-version }} environment-file: ci/environment.yml @@ -86,9 +86,9 @@ jobs: key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('ci/environment-core-deps.yml') }} - uses: conda-incubator/setup-miniconda@v2 with: - activate-environment: test_env_xmovie # Defined in ci/environment*.yml + activate-environment: xmovie-test-core # Defined in ci/environment*.yml auto-update-conda: false - python-version: 3.9 + python-version: '3.9' environment-file: ci/environment-core-deps.yml use-only-tar-bz2: true # IMPORTANT: This needs to be set for caching to work properly! - name: Set up conda environment diff --git a/.gitignore b/.gitignore index 4f9bec2..0866e21 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ var/ .installed.cfg *.egg +# Virtual environment +venv*/ + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7e487b5..a3aec93 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,5 +1,9 @@ version: 2 +python: + install: + - requirements: docs/requirements-rtd.txt + build: os: "ubuntu-20.04" tools: diff --git a/ci/environment-core-deps.yml b/ci/environment-core-deps.yml index 0b03118..69b857b 100644 --- a/ci/environment-core-deps.yml +++ b/ci/environment-core-deps.yml @@ -1,17 +1,20 @@ -name: test_env_xmovie +# Conda environment for testing xmovie in CI +name: xmovie-test-core channels: - conda-forge + - nodefaults dependencies: - - xarray - - dask - - scipy - - numpy - - pytest - - future + - python + # + # xmovie core - matplotlib - - cartopy - - pykdtree + - numpy + - xarray + # + - ffmpeg + # + # Tests - opencv - - pip: - - codecov - - pytest-cov + - pillow + - pytest + - pytest-cov diff --git a/ci/environment.yml b/ci/environment.yml index 42afbe6..ef18d48 100644 --- a/ci/environment.yml +++ b/ci/environment.yml @@ -1,19 +1,28 @@ -name: test_env_xmovie +# Conda environment for testing xmovie in CI +name: xmovie-test channels: - conda-forge + - nodefaults dependencies: - - xarray - - dask - - scipy - - numpy - - pytest - - future + - python + # + # xmovie core - matplotlib - - cartopy - - pykdtree + - numpy + - xarray + # - ffmpeg + # + # Extras + - cartopy + # - cftime + - dask-core + # - nc-time-axis + # - scipy - tqdm + # + # Test - opencv - - pip: - - codecov - - pytest-cov + - pillow + - pytest + - pytest-cov diff --git a/docs/conf.py b/docs/conf.py index 1ee735d..6908315 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,7 +8,6 @@ import xmovie - # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, diff --git a/ci/environment-docs.yml b/docs/environment-docs-dev.yml similarity index 65% rename from ci/environment-docs.yml rename to docs/environment-docs-dev.yml index 6cbabf9..fa7b9ef 100644 --- a/ci/environment-docs.yml +++ b/docs/environment-docs-dev.yml @@ -1,29 +1,35 @@ +# Conda environment for docs development, +# including running the example notebooks name: xmovie-docs channels: - conda-forge - nodefaults dependencies: - python=3.8 - # Core - - xarray + - pip + # + # xmovie core - matplotlib - - cartopy - - ffmpeg + - numpy + - xarray + # # Optional - - tqdm + - cartopy - dask-core - # Examples - - pooch - - jupyterlab + - tqdm + # + # Example notebooks - ipywidgets + - jupyterlab + - pooch + # # Docs + - nbsphinx - sphinx>=4.0 - sphinx-autobuild - sphinx-book-theme - - nbsphinx - sphinx-copybutton # - - pip - pip: - sphinx-prompt - - '-e ../' # xmovie + - '-e ../' # xmovie itself diff --git a/docs/examples/quickstart.ipynb b/docs/examples/quickstart.ipynb index d017026..f77e7e8 100644 --- a/docs/examples/quickstart.ipynb +++ b/docs/examples/quickstart.ipynb @@ -249,15 +249,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![movie_fast.gif](movie_fast.gif)\n", - "![movie_slow.gif](movie_slow.gif)" + "![fast GIF](movie_fast.gif)\n", + "![slow GIF](movie_slow.gif)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "![](movie_combo.gif)" + "![combo GIF](movie_combo.gif)" ] }, { @@ -297,7 +297,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![](lon_movie.gif)" + "![lon dim GIF](lon_movie.gif)" ] }, { @@ -355,7 +355,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![movie_rotating.gif](movie_rotating.gif)" + "![rotating GIF](movie_rotating.gif)" ] }, { @@ -395,7 +395,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![](movie_rotating_dark.gif)" + "![rotating dark GIF](movie_rotating_dark.gif)" ] }, { @@ -440,8 +440,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![](movie_cont.gif)\n", - "![](movie_contf.gif)" + "![rotating contour GIF](movie_cont.gif)\n", + "![rotating contourf GIF](movie_contf.gif)" ] }, { @@ -509,7 +509,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![](movie_rasm.gif)" + "![RASM GIF](movie_rasm.gif)" ] }, { @@ -666,7 +666,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "![](movie_custom.gif)" + "![custom plotfunc movie](movie_custom.gif)" ] } ], diff --git a/docs/index.rst b/docs/index.rst index a6a9848..13cdb89 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,11 +55,18 @@ The main aims of this module are: Installation ------------ +.. important:: + + `ffmpeg`_ is required in order to create the movies from the saved PNG frames. + See `their installation instructions `_. + ``ffmpeg``\'s location must be in the OS search path (``PATH``) for movie creation to work. + .. note:: For now, ``dask(-core)`` and ``cartopy`` are included with ``xmovie``, but they may be optional dependencies in the future. + Conda ~~~~~ @@ -69,6 +76,15 @@ The easiest way to install ``xmovie`` is via ``conda``: conda install -c conda-forge xmovie + +.. note:: + + conda-forge includes a recipe for ffmpeg. To install both: + + .. prompt:: bash + + conda install -c conda-forge xmovie ffmpeg + Pip ~~~ @@ -103,3 +119,5 @@ If you want to install the latest version from GitHub, simply run .. _xarray: https://xarray.pydata.org .. _Matplotlib: https://matplotlib.org .. _Dask: https://dask.org +.. _ffmpeg: https://ffmpeg.org/ +.. _ffmpeg-download: https://ffmpeg.org/download.html diff --git a/docs/requirements-rtd.txt b/docs/requirements-rtd.txt new file mode 100644 index 0000000..8b8477a --- /dev/null +++ b/docs/requirements-rtd.txt @@ -0,0 +1,9 @@ +# pip requirements to build the docs on RTD +# (example notebook deps are not needed since we do not run them in the docs build) +../ +ipython +nbsphinx +sphinx ~= 4.0 +sphinx-book-theme +sphinx-copybutton +sphinx-prompt diff --git a/docs/whats-new.rst b/docs/whats-new.rst index e19b031..eeff869 100644 --- a/docs/whats-new.rst +++ b/docs/whats-new.rst @@ -1,16 +1,28 @@ What's New ========== -v0.3.1 + +v0.y.z (unreleased) ------------------- +Packaging +~~~~~~~~~ +- ``cartopy`` (used in the :func:`~xmovie.rotating_globe` preset) + and ``dask`` (used for parallel frame saving) + are now optional extras instead of package requirements + (:pull:`73`). + By `zmoon `_. + +v0.3.1 (2022/3/21) +------------------ + Bug fixes ~~~~~~~~~~~~~ -- Fixed a bug that prevented parallel frame saving to work with `xarray.Datasets` (:pull:`83`). - By `Tomas Chor `. +- Fixed a bug that prevented parallel frame saving to work with :class:`xarray.Dataset`\s (:pull:`83`). + By `Tomas Chor `_. v0.3.0 (2022/3/17) -------------------- +------------------ Documentation ~~~~~~~~~~~~~ diff --git a/setup.cfg b/setup.cfg index eba7d5e..0c65499 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,51 @@ +[metadata] +name = xmovie +description = Simply create beautiful movies from xarray objects +author = xmovie developers +author_email = jbusecke@princeton.edu +url = https://github.com/jbusecke/xmovie +license = MIT +license_file = LICENSE +classifiers = + Development Status :: 4 - Beta + Topic :: Scientific/Engineering + Intended Audience :: Science/Research + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + License :: OSI Approved :: MIT License + +[options] +install_requires = + matplotlib + numpy + xarray +setup_requires = + setuptools_scm +python_requires = >=3.7 +include_package_data = True +zip_safe = False +packages = find: + +[options.extras_require] +maps = + cartopy +parallel = + dask +cftime = + cftime + nc-time-axis +progress = tqdm +all = + %(maps)s + %(parallel)s + %(cftime)s + %(progress)s + + [sdist] formats = gztar @@ -27,48 +75,3 @@ default_section = THIRDPARTY known_first_party = xmovie skip = docs/conf.py - - -[metadata] -name = xmovie -description = Simply create beautiful movies from xarray objects -author = xmovie developers -url=https://github.com/jbusecke/xmovie -license = MIT -license_file = LICENSE - -## These need to be filled in by the author! -# For details see: https://pypi.org/classifiers/ - -classifiers = - Development Status :: 4 - Beta - Topic :: Scientific/Engineering - Intended Audience :: Science/Research - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - # Dont change this one - License :: OSI Approved :: MIT License - -## Add your email here -author_email = jbusecke@princeton.edu - - -### make sure to fill in your dependencies! -[options] -install_requires = - numpy - xarray - dask - cartopy -setup_requires= - setuptools_scm -python_requires = >=3.7 -################ Up until here - -include_package_data = True -zip_safe = False -packages = find: diff --git a/xmovie/_util.py b/xmovie/_util.py new file mode 100644 index 0000000..0546d4d --- /dev/null +++ b/xmovie/_util.py @@ -0,0 +1,34 @@ +import importlib +from functools import wraps + + +def requires(*modules): + """Function decorator to check for required modules before running. + + Parameters + ---------- + *modules : iterable of str + Required modules to check for. + """ + + def inner(fn): + @wraps(fn) + def wrapped(*args, **kwargs): + failed = [] + for module in modules: + try: + importlib.import_module(module) + except ImportError: + failed.append(module) + + if failed: + raise RuntimeError( + f"Required modules failed to import: {', '.join(f'{m!r}' for m in modules)}. " + "Please install the relevant packages if you wish to proceed." + ) + + return fn(*args, **kwargs) + + return wrapped + + return inner diff --git a/xmovie/core.py b/xmovie/core.py index 03a1282..8c480a1 100644 --- a/xmovie/core.py +++ b/xmovie/core.py @@ -1,6 +1,3 @@ -import matplotlib as mpl - -mpl.use("Agg") import gc import glob import os @@ -9,28 +6,37 @@ import warnings from subprocess import PIPE, STDOUT, Popen +import matplotlib as mpl import matplotlib.pyplot as plt import xarray as xr +from ._util import requires from .presets import basic try: from tqdm.auto import tqdm - - tqdm_avail = True -except Exception: +except ImportError: warnings.warn( - "Optional dependency `tqdm` not found. This will make progressbars a lot nicer. \ - Install with `conda install -c conda-forge tqdm`" + "Optional dependency `tqdm` not found. " + "This will make progressbars a lot nicer. " + "Install with `conda install -c conda-forge tqdm`" ) tqdm_avail = False +else: + tqdm_avail = True + +try: + import dask.array as dsa +except ImportError: + dask_array_avail = False +else: + dask_array_avail = True -# import xarray as xr -# import dask.bag as db -import dask.array as dsa -# is it a good idea to set these here? -# Needs to be dependent on dpi and videosize +# TODO: maybe should only change when necessary, using context manager +mpl.use("Agg") + +# TODO: should be dependent on dpi and videosize; don't change globally plt.rcParams.update({"font.size": 14}) @@ -63,7 +69,7 @@ def _parse_plot_defaults(da, kwargs): # if any value is dask.array compute them here. for k in ["vmin", "vmax"]: - if isinstance(kwargs[k], dsa.Array): + if dask_array_avail and isinstance(kwargs[k], dsa.Array): kwargs[k] = kwargs[k].compute() return kwargs @@ -368,12 +374,13 @@ def save_frames_serial(self, odir, progress=False): if tqdm_avail and progress: frame_range = tqdm(frame_range) elif ~tqdm_avail and progress: - warnings.warn("Cant show progess bar at this point. Install tqdm") + warnings.warn("Can't show progess bar at this point. Install tqdm.") for timestep in frame_range: fig, ax, pp = self.render_single_frame(timestep) save_single_frame(fig, timestep, odir=odir, frame_pattern=self.frame_pattern, dpi=self.dpi) + @requires("dask.array") def save_frames_parallel(self, odir, parallel_compute_kwargs=dict()): """ Saves all frames in parallel using dask.map_blocks. diff --git a/xmovie/presets.py b/xmovie/presets.py index e508cee..764f4f6 100644 --- a/xmovie/presets.py +++ b/xmovie/presets.py @@ -1,13 +1,11 @@ import warnings -import cartopy.crs as ccrs -import cartopy.feature as cfeature import matplotlib.pyplot as plt import matplotlib.ticker as mticker import numpy as np -import shapely.geometry as sgeom import xarray as xr -from cartopy.mpl import geoaxes + +from ._util import requires def _check_input(da, fieldname): @@ -69,11 +67,14 @@ def _base_plot(ax, base_data, timestamp, framedim, plotmethod=None, **kwargs): # projections utilities and hacks +@requires("cartopy", "shapely.geometry") def _smooth_boundary_NearsidePerspective(projection): + import cartopy.crs as ccrs + import shapely.geometry as sgeom + # workaround for a smoother outer boundary # (https://github.com/SciTools/cartopy/issues/613) # Re-implement the cartopy code to figure out the boundary. - # This is just really a guess.... WGS84_SEMIMAJOR_AXIS = 6378137.0 # because I cannot import it above...this should be fixed upstream @@ -90,6 +91,9 @@ def _smooth_boundary_NearsidePerspective(projection): # def _smooth_boundary_globe(projection): +# import cartopy.crs as ccrs +# import shapely.geometry as sgeom +# # # workaround for a smoother outer boundary # # (https://github.com/SciTools/cartopy/issues/613) # @@ -134,10 +138,13 @@ def _style_dict(style=None): def _set_style(fig, ax, pp, style): "Sets the colorscheme for figure, axis and plot object (`pp`) according to style" - # check if ax is 'normal' or cartopy projection - is_geoax = False - if isinstance(ax, geoaxes.GeoAxesSubplot): - is_geoax = True + # Check if ax is 'normal' or cartopy projection + try: + from cartopy.mpl import geoaxes + except ImportError: # pragma: no cover + is_geoax = False + else: + is_geoax = isinstance(ax, geoaxes.GeoAxesSubplot) # parse styles style_dict = _style_dict(style) @@ -186,9 +193,13 @@ def _set_style(fig, ax, pp, style): plt.setp(plt.getp(cb.ax.axes, "yticklabels"), color=fgcolor) +@requires("cartopy") def _add_land(ax, style): + import cartopy.feature as cfeature + from cartopy.mpl import geoaxes + if not isinstance(ax, geoaxes.GeoAxesSubplot): - raise ValueError("Cannot add land on non-cartopy axes. Got ($s)" % type(ax)) + raise TypeError("Cannot add land on non-cartopy axes. Got %s." % type(ax)) style_dict = _style_dict(style) feature = cfeature.NaturalEarthFeature( name="land", category="physical", scale="50m", facecolor=style_dict["landcolor"] @@ -196,9 +207,13 @@ def _add_land(ax, style): ax.add_feature(feature) +@requires("cartopy") def _add_coast(ax, style): + import cartopy.feature as cfeature + from cartopy.mpl import geoaxes + if not isinstance(ax, geoaxes.GeoAxesSubplot): - raise ValueError("Cannot add land on non-cartopy axes. Got ($s)" % type(ax)) + raise TypeError("Cannot add land on non-cartopy axes. Got %s." % type(ax)) style_dict = _style_dict(style) feature = cfeature.NaturalEarthFeature( name="coastline", @@ -225,6 +240,7 @@ def basic( return ax, pp +@requires("cartopy") def rotating_globe( da, fig, @@ -284,6 +300,7 @@ def rotating_globe( **kwargs Passed on to the xarray plotting method. """ + import cartopy.crs as ccrs # rotate lon_rotations times throughout movie and start at lon_start lon = np.linspace(0, 360 * lon_rotations, len(da[framedim])) + lon_start @@ -337,6 +354,7 @@ def rotating_globe( return ax, pp +@requires("cartopy") def rotating_globe_dark(da, fig, timestamp, **kwargs): warnings.warn( "This preset will be deprecated in the future. \ diff --git a/xmovie/test/__init__.py b/xmovie/test/__init__.py new file mode 100644 index 0000000..789886c --- /dev/null +++ b/xmovie/test/__init__.py @@ -0,0 +1,24 @@ +import importlib + +import pytest +from packaging.version import Version + + +def _importorskip(modname, minversion=None): + # https://github.com/pydata/xarray/blob/95bb9ae4233c16639682a532c14b26a3ea2728f3/xarray/tests/__init__.py#L43-L53 + try: + mod = importlib.import_module(modname) + has = True + if minversion is not None: + if Version(mod.__version__) < Version(minversion): + raise ImportError("Minimum version not satisfied") + except ImportError: + has = False + func = pytest.mark.skipif(not has, reason=f"requires {modname}") + return has, func + + +has_cartopy, requires_cartopy = _importorskip("cartopy") +has_dask, requires_dask = _importorskip("dask") +has_dask_array, requires_dask_array = _importorskip("dask.array") +has_tqdm, requires_tqdm = _importorskip("tqdm") diff --git a/xmovie/test/test_core.py b/xmovie/test/test_core.py index ec89d76..f67e0d8 100644 --- a/xmovie/test/test_core.py +++ b/xmovie/test/test_core.py @@ -1,7 +1,6 @@ import os import cv2 -import dask.array as dsa import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np @@ -22,6 +21,8 @@ ) from xmovie.presets import basic, rotating_globe +from . import has_cartopy, has_dask_array, requires_dask_array + def test_parse_plot_defaults(): # create dummy array @@ -33,17 +34,25 @@ def test_parse_plot_defaults(): # deactivated while bug persists.. # assert d["cbar_kwargs"] == dict(extend="neither") assert d["extend"] == "neither" - da = xr.DataArray(np.arange(20), dims=["x"]).chunk({"x": 1}) - d = _parse_plot_defaults(da, {}) - assert isinstance(da.data, dsa.Array) - assert not isinstance(d["vmin"], dsa.Array) - assert not isinstance(d["vmax"], dsa.Array) + with pytest.raises(RuntimeError): _parse_plot_defaults(5, {}) + for var in ["vmin", "vmax", "test"]: expected = _parse_plot_defaults(da, {var: "input"})[var] assert expected == "input" + if not has_dask_array: + pytest.skip("`dask.array` required") + else: + import dask.array as dsa + + da = xr.DataArray(np.arange(20), dims=["x"]).chunk({"x": 1}) + d = _parse_plot_defaults(da, {}) + assert isinstance(da.data, dsa.Array) + assert not isinstance(d["vmin"], dsa.Array) + assert not isinstance(d["vmax"], dsa.Array) + def dummy_plotfunc(da, fig, timestep, framedim, **kwargs): # a very simple plotfunc, which might be passed by the user @@ -228,6 +237,8 @@ def test_Movie(plotfunc, framedim, frame_pattern, dpi, pixelheight, pixelwidth): pixelheight=pixelheight, dpi=dpi, ) + if plotfunc is rotating_globe and not has_cartopy: + pytest.skip("`rotating_globe` requires `cartopy`") # if not time, hide it to test changing default if framedim != "time": @@ -311,6 +322,9 @@ def test_movie_save_frames(tmpdir, frame_pattern): ], ) def test_movie_save(tmpdir, parallel, filename, gif_palette, framerate, gif_framerate, ffmpeg_options): + if not has_dask_array: + pytest.skip("Parallel save requires `dask.array`") + print(gif_palette) # Need more tests for progress, verbose, overwriting path = tmpdir.join(filename) @@ -344,6 +358,7 @@ def test_movie_save(tmpdir, parallel, filename, gif_palette, framerate, gif_fram mov.save(path.strpath, overwrite_existing=False) +@requires_dask_array def test_movie_save_parallel_no_dask(tmpdir): path = tmpdir.join("movie.mp4") da = test_dataarray() @@ -357,6 +372,7 @@ def test_movie_save_parallel_no_dask(tmpdir): assert "Input data needs to be a dask array to save in parallel" in str(excinfo.value) +@requires_dask_array def test_movie_save_parallel_wrong_chunk(tmpdir): path = tmpdir.join("movie.mp4") da = test_dataarray().chunk({"time": 2}) @@ -383,12 +399,13 @@ def plotfunc(ds, fig, tt, framedim="time", test1=None, **kwargs): mov.save_frames_serial(tmpdir) -def test_plotfunc_kwargs_xfail(tmpdir): - pytest.xfail( - "if **kwargs is not in the function signature \ - and the input is checked, this should error out." +@pytest.mark.xfail( + reason=( + "if **kwargs is not in the function signature " + "and the input is checked, this should error out." ) - +) +def test_plotfunc_kwargs_xfail(tmpdir): def plotfunc(ds, fig, tt, test1=None): if test1 is None: raise RuntimeError("test1 cannot be None") diff --git a/xmovie/test/test_presets.py b/xmovie/test/test_presets.py index de89ba9..3f32d40 100644 --- a/xmovie/test/test_presets.py +++ b/xmovie/test/test_presets.py @@ -1,4 +1,3 @@ -import cartopy.crs as ccrs import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np @@ -6,11 +5,17 @@ import xarray as xr from xmovie.presets import ( + _add_coast, + _add_land, _check_input, _core_plot, _smooth_boundary_NearsidePerspective, + rotating_globe, + rotating_globe_dark, ) +from . import has_cartopy, requires_cartopy + def test_check_input(): # this should be done with a more sophisticated example @@ -36,17 +41,20 @@ def test_check_input(): ) def test_core_plot(plotmethod, expected_type, filled): da = xr.DataArray(np.random.rand(4, 6)) - fig, ax = plt.subplots() + _, ax = plt.subplots() pp = _core_plot(ax, da, plotmethod=plotmethod) assert isinstance(pp, expected_type) if filled is not None: assert pp.filled == filled +@requires_cartopy @pytest.mark.parametrize("lon", [-700, -300, 1, 300, 700]) @pytest.mark.parametrize("lat", [-200, -90, 0, 90, 180]) @pytest.mark.parametrize("sat_height", [35785831, 45785831]) def test_smooth_boundary_NearsidePerspective(lon, lat, sat_height): + import cartopy.crs as ccrs + lon = -100 lat = -40 sat_height = 35785831 @@ -56,5 +64,21 @@ def test_smooth_boundary_NearsidePerspective(lon, lat, sat_height): # modify the projection with smooth boundary pr_mod = _smooth_boundary_NearsidePerspective(pr) + assert type(pr) is type(pr_mod) and isinstance(pr_mod, ccrs.Projection) assert pr.proj4_params == pr_mod.proj4_params assert pr.globe == pr_mod.globe + + +@pytest.mark.skipif(has_cartopy, reason="no req check error if cartopy is installed") +@pytest.mark.parametrize("fn", [_add_coast, _add_land, rotating_globe, rotating_globe_dark]) +def test_cartopy_req_check(fn): + with pytest.raises(RuntimeError, match="Required modules failed to import: 'cartopy'"): + fn() + + +@requires_cartopy +@pytest.mark.parametrize("fn", [_add_coast, _add_land]) +def test_raise_no_geoax(fn): + _, ax = plt.subplots() + with pytest.raises(TypeError, match="^Cannot add land on non-cartopy axes."): + fn(ax, "standard")