Repository: eWaterCycle/era5cli

Issue:
Add support for ERA5 back extention and ERA5-Land
ERA5 now has a preliminary back extension (1950-1978).

As it has a seprate entry in the CDS, we need to teach era5cli how to download this and other era5 variants such as ERA5-Land

See here for an overview: https://confluence.ecmwf.int/display/CKB/The+family+of+ERA5+datasets

Discussion:

Patch:
diff --git a/era5cli/cli.py b/era5cli/cli.py
index a231604..4b3d65f 100644
--- a/era5cli/cli.py
+++ b/era5cli/cli.py
@@ -139,7 +139,22 @@ def _build_parser():
                              downloads data from the preliminary back
                              extension. Note that when "--prelimbe" is used,
                              "--startyear" and "--endyear" should be set
-                             between 1950 and 1978.
+                             between 1950 and 1978. --prelimbe is incompatible
+                             with --land.
+
+                             ''')
+    )
+
+    common.add_argument(
+        "--land", action="store_true", default=False,
+        help=textwrap.dedent('''\
+                             Download data from ERA5-Land.
+                             Providing the "--land" argument
+                             downloads data from the ERA5-Land dataset.
+                             Note that the ERA5-Land dataset starts in
+                             1981.
+                             --land is incompatible with the use of
+                             --prelimbe and --ensemble.
 
                              ''')
     )
@@ -261,6 +276,8 @@ def _build_parser():
                              "2Dvars" for all available single level or 2D
                              variables \n
                              "3Dvars" for all available 3D variables \n
+                             "land" for all available variables in
+                             ERA5-land \n
                              Enter variable name (e.g. "total_precipitation")
                              or pressure level (e.g. "825") to show if the
                              variable or level is available and in which list.
@@ -300,6 +317,10 @@ def _construct_year_list(args):
             assert 1950 <= year <= 1978, (
                 'year should be between 1950 and 1978'
             )
+        elif args.land:
+            assert 1981 <= year <= datetime.now().year, (
+                'for ERA5-Land, year should be between 1981 and present'
+            )
         else:
             assert 1979 <= year <= datetime.now().year, (
                 'year should be between 1979 and present'
@@ -335,7 +356,7 @@ def _set_period_args(args):
             assert args.ensemble, (
                 "Statistics can only be computed over an ensemble, "
                 "add --ensemble or remove --statistics."
-                )
+            )
         days = args.days
         hours = args.hours
     else:
@@ -372,6 +393,7 @@ def _execute(args):
             threads=args.threads,
             merge=args.merge,
             prelimbe=args.prelimbe,
+            land=args.land
         )
         era5.fetch(dryrun=args.dryrun)
         return True
diff --git a/era5cli/fetch.py b/era5cli/fetch.py
index d67b1e6..4cd216e 100644
--- a/era5cli/fetch.py
+++ b/era5cli/fetch.py
@@ -59,13 +59,15 @@ class Fetch:
             be written to stdout.
         prelimbe: bool
             Whether to download the preliminary back extension (1950-1978).
+        land: bool
+            Whether to download ERA5-Land data.
     """
 
     def __init__(self, years: list, months: list, days: list,
                  hours: list, variables: list, outputformat: str,
                  outputprefix: str, period: str, ensemble: bool,
                  statistics=None, synoptic=None, pressurelevels=None,
-                 merge=False, threads=None, prelimbe=False):
+                 merge=False, threads=None, prelimbe=False, land=False):
         """Initialization of Fetch class."""
         self.months = era5cli.utils._zpad_months(months)
         """list(str): List of zero-padded strings of months
@@ -112,6 +114,9 @@ class Fetch:
         self.prelimbe = prelimbe
         """bool: Whether to select from the ERA5 preliminary back
         extension which supports years from 1950 to 1978"""
+        self.land = land
+        """bool: Whether to download from the ERA5-Land
+        dataset."""
 
     def fetch(self, dryrun=False):
         """Split calls and fetch results.
@@ -146,23 +151,19 @@ class Fetch:
 
     def _define_outputfilename(self, var, years):
         """Define output filename."""
-        start_year = years[0]
-        end_year = years[-1]
-        if not end_year or (end_year == start_year):
-            fname = ("{}_{}_{}_{}".format(
-                self.outputprefix, var,
-                start_year, self.period))
-        else:
-            fname = ("{}_{}_{}-{}_{}".format(
-                self.outputprefix, var,
-                start_year, end_year, self.period))
+        start, end = years[0], years[-1]
+
+        prefix = (f"{self.outputprefix}-land" if self.land
+                  else self.outputprefix)
+        yearblock = f"{start}-{end}" if not start == end else f"{start}"
+        fname = f"{prefix}_{var}_{yearblock}_{self.period}"
         if self.ensemble:
             fname += "_ensemble"
         if self.statistics:
             fname += "_statistics"
         if self.synoptic:
             fname += "_synoptic"
-        fname += ".{}".format(self.ext)
+        fname += f".{self.ext}"
         return fname
 
     def _split_variable(self):
@@ -193,81 +194,128 @@ class Fetch:
 
     def _product_type(self):
         """Construct the product type name from the options."""
-        producttype = ""
-
-        if self.ensemble:
-            producttype += "ensemble_members"
-        elif not self.ensemble:
-            producttype += "reanalysis"
-
-        if self.period == "monthly" and not self.prelimbe:
-            producttype = "monthly_averaged_" + producttype
-            if self.synoptic:
-                producttype += "_by_hour_of_day"
-        elif self.period == "monthly" and self.prelimbe:
-            if self.ensemble:
-                producttype = "members-"
-            elif not self.ensemble:
-                producttype = "reanalysis-"
-            if self.synoptic:
-                producttype += "synoptic-monthly-means"
-            elif not self.synoptic:
-                producttype += "monthly-means-of-daily-means"
-        elif self.period == "hourly" and self.ensemble and self.statistics:
-            producttype = [
+        if self.period == 'hourly' and self.ensemble and self.statistics:
+            # The only configuration to return a list
+            return [
                 "ensemble_members",
                 "ensemble_mean",
                 "ensemble_spread",
             ]
 
+        if self.land and self.period == "hourly":
+            # The only configuration to return None
+            return None
+
+        # Default flow
+        if self.ensemble:
+            producttype = "ensemble_members"
+        else:
+            producttype = "reanalysis"
+
+        if self.period == "hourly":
+            return producttype
+
+        producttype = "monthly_averaged_" + producttype
+        if self.synoptic:
+            producttype += "_by_hour_of_day"
+
+        if not self.prelimbe:
+            return producttype
+
+        # Prelimbe has deviating product types for monthly data
+        if self.ensemble:
+            producttype = "members-"
+        else:
+            producttype = "reanalysis-"
+        if self.synoptic:
+            producttype += "synoptic-monthly-means"
+        else:
+            producttype += "monthly-means-of-daily-means"
         return producttype
 
-    def _build_request(self, variable, years):
-        """Build the download request for the retrieve method of cdsapi."""
-        name = "reanalysis-era5-"
-        request = {'variable': variable,
-                   'year': years,
-                   'product_type': self._product_type(),
-                   'month': self.months,
-                   'time': self.hours,
-                   'format': self.outputformat}
+    def _check_levels(self):
+        """Retrieve pressure level info for request"""
+        if not self.pressure_levels:
+            raise ValueError(
+                "Requested 3D variable(s), but no pressure levels specified."
+                "Aborting."
+            )
+        if not all(level in ref.PLEVELS for level in self.pressure_levels):
+            raise ValueError(
+                f"Invalid pressure levels. Allowed values are: {ref.PLEVELS}"
+            )
 
-        # variable is pressure level variable
-        if variable in ref.PLVARS:
-            try:
-                if all(level in ref.PLEVELS for level in self.pressure_levels):
-                    name += "pressure-levels"
-                    request["pressure_level"] = self.pressure_levels
-                else:
-                    raise ValueError(
-                        "Invalid pressure levels. Allowed values are: {}"
-                        .format(ref.PLEVELS))
-            except TypeError:
+    def _check_variable(self, variable):
+        """Check variable available and compatible with other inputs."""
+        # if land then the variable must be in era5 land
+        if self.land:
+            if variable not in ref.ERA5_LAND_VARS:
                 raise ValueError(
-                    "Invalid pressure levels. Allowed values are: {}"
-                    .format(ref.PLEVELS))
-        # variable is single level variable
+                    f"Variable {variable} is not available in ERA5-Land.\n"
+                    f"Choose from {ref.ERA5_LAND_VARS}"
+                )
+        elif variable in ref.PLVARS+ref.SLVARS:
+            if self.period == "monthly":
+                if variable in ref.MISSING_MONTHLY_VARS:
+                    header = ("There is no monthly data available for the "
+                              "following variables:\n")
+                    raise ValueError(era5cli.utils._print_multicolumn(
+                        header,
+                        ref.MISSING_MONTHLY_VARS))
+        else:
+            raise ValueError(
+                "Invalid variable name: {}".format(variable)
+            )
+
+    def _build_name(self, variable):
+        """Build up name of dataset to use"""
+
+        name = "reanalysis-era5"
+
+        if self.land:
+            name += "-land"
+        elif variable in ref.PLVARS:
+            name += "-pressure-levels"
         elif variable in ref.SLVARS:
-            name += "single-levels"
-        # variable is unknown
+            name += "-single-levels"
         else:
-            raise ValueError('Invalid variable name: {}'.format(variable))
+            raise ValueError(
+                "Invalid variable name: {}".format(variable)
+            )
 
         if self.period == "monthly":
             name += "-monthly-means"
-            if variable in ref.MISSING_MONTHLY_VARS:
-                header = ("There is no monthly data available for the "
-                          "following variables:\n")
-                raise ValueError(era5cli.utils._print_multicolumn(
-                    header,
-                    ref.MISSING_MONTHLY_VARS))
-        elif self.period == "hourly":
-            # Add day list to request if applicable
-            if self.days:
-                request["day"] = self.days
 
         if self.prelimbe:
+            if self.land:
+                raise ValueError(
+                    "Back extension not (yet) available for ERA5-Land."
+                )
             name += "-preliminary-back-extension"
+        return name
+
+    def _build_request(self, variable, years):
+        """Build the download request for the retrieve method of cdsapi."""
+        self._check_variable(variable)
+
+        name = self._build_name(variable)
+
+        request = {'variable': variable,
+                   'year': years,
+                   'month': self.months,
+                   'time': self.hours,
+                   'format': self.outputformat}
+
+        if "pressure-levels" in name:
+            self._check_levels()
+            request["pressure_level"] = self.pressure_levels
+
+        product_type = self._product_type()
+        if product_type is not None:
+            request["product_type"] = product_type
+
+        if self.period == "hourly":
+            request["day"] = self.days
 
         return(name, request)
 
@@ -287,7 +335,7 @@ class Fetch:
                 "It can take some time before downloading starts, ",
                 "please do not kill this process in the meantime.",
                 os.linesep
-                )
+            )
             connection = cdsapi.Client()
             print("".join(queueing_message))  # print queueing message
             connection.retrieve(name, request, outputfile)
diff --git a/era5cli/info.py b/era5cli/info.py
index 7ff2dde..f17a72a 100644
--- a/era5cli/info.py
+++ b/era5cli/info.py
@@ -11,8 +11,8 @@ class Info:
     ----------
         infoname: str
             Name of information that needs to be printed. Supported are
-            'levels', '2Dvars', '3Dvars' and any variable or pressure level
-            defined in era5cli.inputref
+            'levels', '2Dvars', '3Dvars', 'ERA5land', and any variable or
+            pressure level defined in era5cli.inputref
 
     Raises
     ------
@@ -33,10 +33,11 @@ class Info:
             """list: List with information to be printed."""
             self.infotype = "list"
         except KeyError:
+            self.infotype = []
             for valname, vallist in ref.REFDICT.items():
                 if self.infoname in vallist:
-                    self.infotype = valname
-        if self.infotype is None:
+                    self.infotype.append(valname)
+        if len(self.infotype) == 0:
             raise ValueError('Unknown value for reference argument.')
 
     def list(self):
@@ -53,13 +54,15 @@ class Info:
 
         Print in which list the given variable occurs.
         """
-        print("{} is in the list: {}".format(self.infoname, self.infotype))
+        lists = ', '.join(self.infotype)
+        print("{} is in the list(s): {}".format(self.infoname, lists))
 
     def _define_table_header(self):
         """Define table header."""
         hdict = {
             'levels': 'pressure levels',
             '2Dvars': '2D variables',
-            '3Dvars': '3D variables'
+            '3Dvars': '3D variables',
+            'land': 'ERA5-land variables'
         }
         self.header = "Available {}:".format(hdict[self.infoname])
diff --git a/era5cli/inputref.py b/era5cli/inputref.py
index 411d55b..278ef01 100644
--- a/era5cli/inputref.py
+++ b/era5cli/inputref.py
@@ -308,9 +308,65 @@ SLVARS = [
     'zero_degree_level',
 ]
 
+
+ERA5_LAND_VARS = [
+    '10m_u_component_of_wind',
+    '10m_v_component_of_wind',
+    '2m_dewpoint_temperature',
+    '2m_temperature',
+    'evaporation_from_bare_soil',
+    'evaporation_from_open_water_surfaces_excluding_oceans',
+    'evaporation_from_the_top_of_canopy',
+    'evaporation_from_vegetation_transpiration',
+    'forecast_albedo',
+    'lake_bottom_temperature',
+    'lake_ice_depth',
+    'lake_ice_temperature',
+    'lake_mix_layer_depth',
+    'lake_mix_layer_temperature',
+    'lake_shape_factor',
+    'lake_total_layer_temperature',
+    'leaf_area_index_high_vegetation',
+    'leaf_area_index_low_vegetation',
+    'potential_evaporation',
+    'runoff',
+    'skin_reservoir_content',
+    'skin_temperature',
+    'snow_albedo',
+    'snow_cover',
+    'snow_density',
+    'snow_depth',
+    'snow_depth_water_equivalent',
+    'snow_evaporation',
+    'snowfall',
+    'snowmelt',
+    'soil_temperature_level_1',
+    'soil_temperature_level_2',
+    'soil_temperature_level_3',
+    'soil_temperature_level_4',
+    'sub_surface_runoff',
+    'surface_latent_heat_flux',
+    'surface_net_solar_radiation',
+    'surface_net_thermal_radiation',
+    'surface_pressure',
+    'surface_runoff',
+    'surface_sensible_heat_flux',
+    'surface_solar_radiation_downwards',
+    'surface_thermal_radiation_downwards',
+    'temperature_of_snow_layer',
+    'total_evaporation',
+    'total_precipitation',
+    'volumetric_soil_water_layer_1',
+    'volumetric_soil_water_layer_2',
+    'volumetric_soil_water_layer_3',
+    'volumetric_soil_water_layer_4'
+]
+
+
 REFDICT = {"levels": PLEVELS,
            "2Dvars": SLVARS,
            "3Dvars": PLVARS,
+           "land": ERA5_LAND_VARS
            }
 
 MISSING_MONTHLY_VARS = [


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 13c1cda..7ec3242 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -11,7 +11,7 @@ def test_parse_args():
     """Test argument parser of cli."""
     argv = ['hourly', '--startyear', '2008',
             '--variables', 'total_precipitation', '--statistics',
-            '--endyear', '2008', '--ensemble']
+            '--endyear', '2008', '--ensemble', '--land']
     args = cli._parse_args(argv)
     assert args.command == 'hourly'
     assert args.days == list(range(1, 32))
@@ -27,6 +27,7 @@ def test_parse_args():
     assert args.statistics
     assert not args.threads
     assert args.variables == ['total_precipitation']
+    assert args.land
 
 
 def test_period_args():
@@ -102,6 +103,14 @@ def test_main_fetch(fetch):
     args = cli._parse_args(argv)
     cli._execute(args)
 
+    # no land available for back extension
+    argv = ['monthly', '--startyear', '1980', '--endyear', '1980',
+            '--variables', 'total_precipitation', '--synoptic',
+            '--ensemble', '--land']
+    args = cli._parse_args(argv)
+    with pytest.raises(AssertionError):
+        cli._execute(args)
+
 
 @mock.patch("era5cli.info.Info", autospec=True)
 def test_main_info(info):
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
index cff9ff8..ef18f26 100644
--- a/tests/test_fetch.py
+++ b/tests/test_fetch.py
@@ -10,7 +10,7 @@ def initialize(outputformat='netcdf', merge=False, statistics=None,
                threads=2, period='hourly', variables=['total_precipitation'],
                years=[2008, 2009], months=list(range(1, 13)),
                days=list(range(1, 32)), hours=list(range(0, 24)),
-               prelimbe=False):
+               prelimbe=False, land=False):
     """Initializer of the class."""
     era5 = fetch.Fetch(years=years,
                        months=months,
@@ -26,7 +26,8 @@ def initialize(outputformat='netcdf', merge=False, statistics=None,
                        pressurelevels=pressurelevels,
                        merge=merge,
                        threads=threads,
-                       prelimbe=prelimbe)
+                       prelimbe=prelimbe,
+                       land=land)
     return era5
 
 
@@ -75,6 +76,7 @@ def test_init():
     assert not era5.merge
     assert era5.threads == 2
     assert not era5.prelimbe
+    assert not era5.land
 
     # initializing hourly variable with days=None should result in ValueError
     with pytest.raises(TypeError):
@@ -123,14 +125,14 @@ def test_fetch_nodryrun(cds, era5cli_utils_append_history):
                       threads=None, pressurelevels=[1, 2, 9],
                       variables=['temperature'])
     with pytest.raises(ValueError):
-        assert era5.fetch() is None
+        assert era5.fetch()
 
     # invalid variable name should raise ValueError
     era5 = initialize(outputformat='grib', merge=True,
                       threads=None,
                       variables=['unknown'])
     with pytest.raises(ValueError):
-        assert era5.fetch() is None
+        assert era5.fetch()
 
     # check check against monthly unavailable data raise ValueError
     era5 = initialize(outputformat='grib', merge=True,
@@ -193,6 +195,12 @@ def test_define_outputfilename():
     fn = 'era5_total_precipitation_2008-2009_hourly_ensemble_synoptic.grb'
     assert fname == fn
 
+    era5 = initialize(land=True, ensemble=False)
+    era5._extension()
+    fname = era5._define_outputfilename('total_precipitation', [2008])
+    fn = 'era5-land_total_precipitation_2008_hourly.nc'
+    assert fname == fn
+
 
 def test_number_outputfiles(capsys):
     """Test function for the number of outputs."""
@@ -201,79 +209,209 @@ def test_number_outputfiles(capsys):
                       years=[2007, 2008, 2009], merge=False)
     era5.fetch(dryrun=True)
     captured = capsys.readouterr()
-    outputlenght = len(captured.out.split('\n')) - 1
+    outputlength = len(captured.out.split('\n')) - 1
     if not era5.merge:
         # No. of outputs is 2*3 = 6 if merge = False
-        assert outputlenght == len(era5.years) * len(era5.variables)
+        assert outputlength == len(era5.years) * len(era5.variables)
     else:
         # No. of outputs is 2*1 = 2 if merge = True
-        assert outputlenght == len(era5.variables)
+        assert outputlength == len(era5.variables)
 
     # one variable and three years
     era5 = initialize(variables=['total_precipitation'],
                       years=[2007, 2008, 2009], merge=False)
     era5.fetch(dryrun=True)
     captured = capsys.readouterr()
-    outputlenght = len(captured.out.split('\n')) - 1
+    outputlength = len(captured.out.split('\n')) - 1
     if not era5.merge:
         # No. of outputs is 1*3 = 3 if merge = False
-        assert outputlenght == len(era5.years) * len(era5.variables)
+        assert outputlength == len(era5.years) * len(era5.variables)
     else:
         # No. of outputs is 1 if merge = True
-        assert outputlenght == len(era5.variables)
+        assert outputlength == len(era5.variables)
 
     # two variables and one year
     era5 = initialize(variables=['total_precipitation', 'runoff'],
                       years=[2007], merge=False)
     era5.fetch(dryrun=True)
     captured = capsys.readouterr()
-    outputlenght = len(captured.out.split('\n')) - 1
+    outputlength = len(captured.out.split('\n')) - 1
     if not era5.merge:
         # No. of outputs is 2*1 = 2 if merge = False
-        assert outputlenght == len(era5.years) * len(era5.variables)
+        assert outputlength == len(era5.years) * len(era5.variables)
     else:
         # No. of outputs is 2 if merge = True
-        assert outputlenght == len(era5.variables)
+        assert outputlength == len(era5.variables)
     del era5, captured
 
 
 def test_product_type():
     """Test _product_type function of Fetch class."""
+    # Default hourly data
     era5 = initialize()
     producttype = era5._product_type()
     assert producttype == 'ensemble_members'
 
-    era5.ensemble = False
+    era5.statistics = True
+    producttype = era5._product_type()
+    assert producttype == [
+        'ensemble_members', 'ensemble_mean', 'ensemble_spread'
+    ]
+
+    era5.ensemble = False  # statistics will be ignored
     producttype = era5._product_type()
     assert producttype == 'reanalysis'
 
+    # Default monthly data
     era5.period = 'monthly'
     producttype = era5._product_type()
     assert producttype == 'monthly_averaged_reanalysis'
 
+    era5.synoptic = True
+    producttype = era5._product_type()
+    assert producttype == 'monthly_averaged_reanalysis_by_hour_of_day'
+
+    era5.ensemble = True
+    producttype = era5._product_type()
+    assert producttype == 'monthly_averaged_ensemble_members_by_hour_of_day'
+
+    era5.synoptic = False
+    producttype = era5._product_type()
+    assert producttype == 'monthly_averaged_ensemble_members'
+
+    # Preliminary back extension monthly data have different names
     era5.prelimbe = True
     producttype = era5._product_type()
-    assert producttype == 'reanalysis-monthly-means-of-daily-means'
+    assert producttype == 'members-monthly-means-of-daily-means'
 
-    era5.prelimbe = False
     era5.synoptic = True
     producttype = era5._product_type()
-    assert producttype == 'monthly_averaged_reanalysis_by_hour_of_day'
+    assert producttype == 'members-synoptic-monthly-means'
 
-    era5.prelimbe = True
+    era5.ensemble = False
     producttype = era5._product_type()
     assert producttype == 'reanalysis-synoptic-monthly-means'
 
-    era5.ensemble = True
+    era5.synoptic = False
     producttype = era5._product_type()
-    assert producttype == 'members-synoptic-monthly-means'
+    assert producttype == 'reanalysis-monthly-means-of-daily-means'
 
+    # ERA5 land has more limited options
+    era5.land = True
     era5.prelimbe = False
-    era5.ensemble = False
-    era5.statistics = True
+    producttype = era5._product_type()
+    assert producttype == 'monthly_averaged_reanalysis'
+
+    era5.synoptic = True
     producttype = era5._product_type()
     assert producttype == 'monthly_averaged_reanalysis_by_hour_of_day'
 
+    era5.period = 'hourly'
+    producttype = era5._product_type()
+    assert producttype is None
+
+
+def test_check_levels():
+    """Test _check_levels function of Fetch class"""
+    era5 = initialize()
+    era5.variables = "temperature"
+
+    # No levels should raise
+    with pytest.raises(ValueError):
+        era5._check_levels()
+
+    # Valid levels should pass
+    era5.pressure_levels = [1000, 950]
+    era5._check_levels()
+
+    # Invalid levels should raise
+    era5.pressure_levels = [777]
+    with pytest.raises(ValueError):
+        era5._check_levels()
+
+
+def test_check_variable():
+    """Test _check_variable function of Fetch class."""
+    era5 = initialize()
+
+    land_only_variable = "snow_cover"
+    slev_only_variable = "vertical_integral_of_mass_tendency"
+    missing_monthly_var = "altimeter_wave_height"
+
+    # Invalid variables should raise
+    with pytest.raises(ValueError):
+        era5._check_variable("invalid_precipitation")
+
+    # Valid variable with invalid dataset option should raise
+    with pytest.raises(ValueError):
+        era5._check_variable(land_only_variable)
+
+    # But with the right dataset option it should pass
+    era5.land = True
+    era5._check_variable(land_only_variable)
+
+    # And variables incompatible with ERA5 land should fail
+    with pytest.raises(ValueError):
+        era5._check_variable(slev_only_variable)
+
+    # Missing monthly vars should pass if period is hourly
+    era5.period = 'hourly'
+    era5.land = False
+    era5._check_variable(missing_monthly_var)
+
+    # Missing monthly vars should fail if period is monthly
+    era5.period = 'monthly'
+    with pytest.raises(ValueError):
+        era5._check_variable(missing_monthly_var)
+
+
+def test_build_name():
+    """Test _build_name function of Fetch class."""
+    era5 = initialize()
+
+    name = era5._build_name('total_precipitation')
+    assert name == "reanalysis-era5-single-levels"
+
+    name = era5._build_name('temperature')
+    assert name == "reanalysis-era5-pressure-levels"
+
+    era5.period = "monthly"
+    name = era5._build_name('temperature')
+    assert name == "reanalysis-era5-pressure-levels-monthly-means"
+
+    name = era5._build_name('total_precipitation')
+    assert name == "reanalysis-era5-single-levels-monthly-means"
+
+    # Test names for back extension
+    era5.prelimbe = True
+    name = era5._build_name('temperature')
+    assert name == ("reanalysis-era5-pressure-levels-monthly-means"
+                    "-preliminary-back-extension")
+
+    name = era5._build_name('total_precipitation')
+    assert name == ("reanalysis-era5-single-levels-monthly-means"
+                    "-preliminary-back-extension")
+
+    era5.period = "hourly"
+    name = era5._build_name('temperature')
+    assert name == "reanalysis-era5-pressure-levels-preliminary-back-extension"
+
+    name = era5._build_name('total_precipitation')
+    assert name == "reanalysis-era5-single-levels-preliminary-back-extension"
+
+    # Tests for era5 land
+    era5.prelimbe = False
+    with pytest.raises(ValueError):
+        era5._build_name('snow_cover')
+
+    era5.land = True
+    name = era5._build_name('snow_cover')
+    assert name == "reanalysis-era5-land"
+
+    era5.period = "monthly"
+    name = era5._build_name('snow_cover')
+    assert name == "reanalysis-era5-land-monthly-means"
+
 
 def test_build_request():
     """Test _build_request function of Fetch class."""
@@ -337,7 +475,48 @@ def test_build_request():
            'format': 'netcdf'}
     assert request == req
 
+    # land
+    era5 = initialize(period='monthly',
+                      variables=['snow_cover'],
+                      hours=[0],
+                      land=True,
+                      ensemble=False)
+
+    (name, request) = era5._build_request('snow_cover', [2008])
+    assert name == (
+        "reanalysis-era5-land-monthly-means"
+    )
+    req = {'variable': 'snow_cover', 'year': [2008],
+           'product_type': 'monthly_averaged_reanalysis',
+           'month': ['01', '02', '03', '04', '05', '06',
+                     '07', '08', '09', '10', '11', '12'],
+           'time': ['00:00'],
+           'format': 'netcdf'}
+    assert request == req
+
     # requesting 3d variable with pressurelevels=None should give a ValueError
     era5 = initialize(variables=['temperature'], pressurelevels=None)
     with pytest.raises(ValueError):
         assert era5._build_request('temperature', [2008])
+
+
+def test_incompatible_options():
+    """Test that invalid combinations of arguments don't silently pass."""
+    era5 = initialize(land=True, prelimbe=True)
+    with pytest.raises(ValueError):
+        era5._build_request('total_precipitation', [2008])
+
+    era5 = initialize(land=False)
+    with pytest.raises(ValueError):
+        era5._build_request('snow_cover', [2008])
+
+
+@pytest.mark.xfail(reason='https://github.com/eWaterCycle/era5cli/issues/68')
+def test_more_incompatible_options():
+    era5 = initialize(land=True, ensemble=True)
+    with pytest.raises(ValueError):
+        era5._build_request('total_precipitation', [2008])
+
+    era5 = initialize(statistics=True, ensemble=False)
+    with pytest.raises(ValueError):
+        era5._build_request('total_precipitation', [2008])
diff --git a/tests/test_info.py b/tests/test_info.py
index 1fe80df..31b4552 100644
--- a/tests/test_info.py
+++ b/tests/test_info.py
@@ -4,16 +4,18 @@ import pytest
 from era5cli import info
 
 
-@pytest.mark.parametrize(
-    'arg',
-    ['levels', '2Dvars', '3Dvars', 'total_precipitation', 'temperature'])
+INFO_PARAMS = ['levels', '2Dvars', '3Dvars', 'land',
+               'total_precipitation', 'temperature']
+
+
+@pytest.mark.parametrize('arg', INFO_PARAMS)
 def test_init(arg):
     """Test init function of Info class."""
     era5info = info.Info(arg)
     assert isinstance(era5info.infolist, list)
 
 
-@pytest.mark.parametrize('arg', ['levels', '2Dvars', '3Dvars'])
+@pytest.mark.parametrize('arg', ['levels', '2Dvars', '3Dvars', 'land'])
 def test_define_table_header(arg):
     """Test _define_table_header function of Info class."""
     era5info = info.Info(arg)
@@ -21,7 +23,7 @@ def test_define_table_header(arg):
     assert isinstance(era5info.header, str)
 
 
-@pytest.mark.parametrize('arg', ['levels', '2Dvars', '3Dvars'])
+@pytest.mark.parametrize('arg', ['levels', '2Dvars', '3Dvars', 'land'])
 def test_list(arg):
     """Test list function of Info class."""
     era5info = info.Info(arg)
@@ -34,11 +36,13 @@ def test_vars(capsys):
     era5info = info.Info('total_precipitation')
     era5info.vars()
     captured = capsys.readouterr()
-    assert captured.out.split(' ')[-1].strip() == '2Dvars'
+    assert '2Dvars' in captured.out
+    assert 'land' in captured.out
+    assert '3Dvars' not in captured.out
     del era5info, captured
 
     era5info = info.Info(850)
     era5info.vars()
     captured = capsys.readouterr()
-    assert captured.out.split(' ')[-1].strip() == 'levels'
+    assert 'levels' in captured.out
     del era5info, captured