Repository: GenericMappingTools/pygmt

Issue:
ModuleNotFoundError exception when IPython in not installed
```
python ~/bad.py (this is the script posted above)
Traceback (most recent call last):
  File "/Users/pwessel/bad.py", line 2, in <module>
    import pygmt
  File "/Users/pwessel/GMTdev/pygmt/pygmt/__init__.py", line 28, in <module>
    from pygmt.figure import Figure, set_display
  File "/Users/pwessel/GMTdev/pygmt/pygmt/figure.py", line 9, in <module>
    import IPython
ModuleNotFoundError: No module named 'IPython'
```

_Originally posted by @PaulWessel in https://github.com/GenericMappingTools/pygmt/issues/732#issuecomment-827182631_

Discussion:
weiji14: > A test is needed but currently I have no idea how to test it.

Maybe follow the style of 329e4802e060c8f7bdabd84bb0465404208e534f to only install IPython on the Python3.9/NumPy1.20 tests, but not on the Python 3.7/NumPy1.17 tests?
seisman: > > A test is needed but currently I have no idea how to test it.
> 
> Maybe follow the style of [329e480](https://github.com/GenericMappingTools/pygmt/commit/329e4802e060c8f7bdabd84bb0465404208e534f) to only install IPython on the Python3.9/NumPy1.20 tests, but not on the Python 3.7/NumPy1.17 tests?

Good suggestion. Done in db3641051ca5e4a13c1baedce9b07a7ca096f589 using the `${{ matrix.optional-packages }}` variable.
weiji14: Ok, when IPython isn't installed, there are a few failures on the Python 3.7/NumPy 1.17 tests:

- `test_figure_show` at https://github.com/GenericMappingTools/pygmt/pull/1247/checks?check_run_id=2452429190#step:11:477
- `pygmt.src.inset.inset` doctest https://github.com/GenericMappingTools/pygmt/pull/1247/checks?check_run_id=2452429190#step:11:465
- `test_pygmtscraper` https://github.com/GenericMappingTools/pygmt/pull/1247/checks?check_run_id=2452429190#step:11:600

We can either do 1) Use something like [pytest.importorskip](https://docs.pytest.org/en/stable/skipping.html#skipping-on-a-missing-import-dependency) or 2) Catch the `pygmt.exceptions.GMTError` for these tests
seisman: > Ok, when IPython isn't installed, there are a few failures on the Python 3.7/NumPy 1.17 tests:
> 
> * `test_figure_show` at [#1247 (checks)](https://github.com/GenericMappingTools/pygmt/pull/1247/checks?check_run_id=2452429190#step:11:477)
> * `pygmt.src.inset.inset` doctest [#1247 (checks)](https://github.com/GenericMappingTools/pygmt/pull/1247/checks?check_run_id=2452429190#step:11:465)
> * `test_pygmtscraper` [#1247 (checks)](https://github.com/GenericMappingTools/pygmt/pull/1247/checks?check_run_id=2452429190#step:11:600)
> 
> We can either do 1) Use something like [pytest.importorskip](https://docs.pytest.org/en/stable/skipping.html#skipping-on-a-missing-import-dependency) or 2) Catch the `pygmt.exceptions.GMTError` for these tests

Neither is good for the `inset` test, which is added in the docstring as an example.
seisman: As mentioned in https://github.com/GenericMappingTools/pygmt/pull/1247#issuecomment-830981685, I'm not happy with either way. Currently, I prefer to only commit the fix (79f329b1328ddc4dd977bfbf66ca435777ae573e) and don't try to work on the test/coverage, so that people can still use PyGMT even IPython is not installed (Fix #1241).

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6332d8f7..94fad782 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -219,7 +219,7 @@ General guidelines for pull requests (PRs):
   do.
 * Each pull request should consist of a **small** and logical collection of changes.
 * Larger changes should be broken down into smaller components and integrated
-  separately.
+  separately. For example, break the wrapping of aliases into multiple pull requests.
 * Bug fixes should be submitted in separate PRs.
 * Use underscores for all Python (*.py) files as per [PEP8](https://www.python.org/dev/peps/pep-0008/),
   not hyphens. Directory names should also use underscores instead of hyphens.
@@ -303,6 +303,12 @@ warn us and you can make a new commit with the formatted code.
 Even better, you can just write `/format` in the first line of any comment in a
 Pull Request to lint the code automatically.
 
+When wrapping a new alias, use an underscore to separate words bridged by vowels
+(aeiou), such as `no_skip` and `z_only`. Do not use an underscore to separate
+words bridged only by consonants, such as `distcalc`, and `crossprofile`. This
+convention is not applied by the code checking tools, but the PyGMT maintainers
+will comment on any pull requests as needed.
+
 We also use [flake8](http://flake8.pycqa.org/en/latest/) and
 [pylint](https://www.pylint.org/) to check the quality of the code and quickly catch
 common errors.
diff --git a/examples/gallery/symbols/points_categorical.py b/examples/gallery/symbols/points_categorical.py
index 9a2a3029..3abf829e 100644
--- a/examples/gallery/symbols/points_categorical.py
+++ b/examples/gallery/symbols/points_categorical.py
@@ -53,7 +53,7 @@ fig.plot(
     x=df.bill_length_mm,
     y=df.bill_depth_mm,
     # Vary each symbol size according to another feature (body mass, scaled by 7.5*10e-5)
-    sizes=df.body_mass_g * 7.5e-5,
+    size=df.body_mass_g * 7.5e-5,
     # Points colored by categorical number code
     color=df.species.cat.codes.astype(int),
     # Use colormap created by makecpt
diff --git a/examples/gallery/symbols/scatter.py b/examples/gallery/symbols/scatter.py
index 1631c306..2009b494 100644
--- a/examples/gallery/symbols/scatter.py
+++ b/examples/gallery/symbols/scatter.py
@@ -23,13 +23,13 @@ fig.basemap(
 )
 for color in ["gray73", "darkorange", "slateblue"]:
     x, y = np.random.rand(2, n)  # random X and Y data in [0,1]
-    sizes = np.random.rand(n) * 0.5  # random size [0,0.5], in cm
+    size = np.random.rand(n) * 0.5  # random size [0,0.5], in cm
     # plot data points as circles (style="c"), with different sizes
     fig.plot(
         x=x,
         y=y,
         style="c",
-        sizes=sizes,
+        size=size,
         color=color,
         # Set the legend label,
         # and set the symbol size to be 0.25 cm (+S0.25c) in legend
diff --git a/examples/projections/cyl/cyl_oblique_mercator_1.py b/examples/projections/cyl/cyl_oblique_mercator_1.py
new file mode 100644
index 00000000..70bf818f
--- /dev/null
+++ b/examples/projections/cyl/cyl_oblique_mercator_1.py
@@ -0,0 +1,32 @@
+"""
+Oblique Mercator
+================
+
+Oblique configurations of the cylinder give rise to the oblique Mercator
+projection. It is particularly useful when mapping regions of large lateral
+extent in an oblique direction. Both parallels and meridians are complex
+curves. The projection was developed in the early 1900s by several workers.
+
+**oa**\|\ **oA**\ *lon0/lat0/azimuth/scale*\[**+v**] or
+**Oa**\|\ **OA**\ *lon0/lat0/azimuth/width*\[**+v**]
+
+The projection is set with **o** or **O**. The pole is set in the 
+northern hemisphere with **a** or the southern hemisphere 
+with **A**. The central meridian is set by *lon0/lat0*. The oblique equator 
+is set by *azimuth*. Align the y-axis
+with the optional **+v**. The figure size is set with *scale* or *width*.
+"""
+import pygmt
+
+fig = pygmt.Figure()
+# Using the origin and azimuth
+fig.coast(
+    projection="Oa-120/25/-30/6c+v",
+    # Set bottom left and top right coordinates of the figure with "+r"
+    region="-122/35/-107/22+r",
+    frame="afg",
+    land="gray",
+    shorelines="1/thin",
+    water="lightblue",
+)
+fig.show()
diff --git a/examples/projections/cyl/cyl_oblique_mercator_2.py b/examples/projections/cyl/cyl_oblique_mercator_2.py
new file mode 100644
index 00000000..afe88986
--- /dev/null
+++ b/examples/projections/cyl/cyl_oblique_mercator_2.py
@@ -0,0 +1,32 @@
+"""
+Oblique Mercator
+================
+
+Oblique configurations of the cylinder give rise to the oblique Mercator
+projection. It is particularly useful when mapping regions of large lateral
+extent in an oblique direction. Both parallels and meridians are complex
+curves. The projection was developed in the early 1900s by several workers.
+
+**ob**\|\ **oB**\ *lon0/lat0/lon1/lat1/scale*\ [**+v**] or
+**Ob**\|\ **OB**\ *lon0/lat0/lon1/lat1/width*\ [**+v**]
+
+The projection is set with **o** or **O**. The pole is set in the 
+northern hemisphere with **b** or the southern hemisphere 
+with **B**. The central meridian is set by *lon0/lat0*. The oblique 
+equator is set by *lon1/lat1*. Align the y-axis
+with the optional **+v**. The figure size is set with *scale* or *width*.
+"""
+import pygmt
+
+fig = pygmt.Figure()
+# Using the origin and two points
+fig.coast(
+    projection="Ob130/35/25/35/6c",
+    # Set bottom left and top right coordinates of the figure with "+r"
+    region="130/35/145/40+r",
+    frame="afg",
+    land="gray",
+    shorelines="1/thin",
+    water="lightblue",
+)
+fig.show()
diff --git a/examples/projections/cyl/cyl_oblique_mercator_3.py b/examples/projections/cyl/cyl_oblique_mercator_3.py
new file mode 100644
index 00000000..399dd33d
--- /dev/null
+++ b/examples/projections/cyl/cyl_oblique_mercator_3.py
@@ -0,0 +1,31 @@
+"""
+Oblique Mercator
+================
+
+Oblique configurations of the cylinder give rise to the oblique Mercator
+projection. It is particularly useful when mapping regions of large lateral
+extent in an oblique direction. Both parallels and meridians are complex
+curves. The projection was developed in the early 1900s by several workers.
+
+**oc**\|\ **oC**\ *lon0/lat0/lonp/latp/scale*\ [**+v**] or
+**Oc**\|\ **OC**\ *lon0/lat0/lonp/latp/width*\ [**+v**]
+
+The projection is set with **o** or **O**. The central meridian is set
+by *lon0/lat0*. The projection pole is set by *lonp/latp* in option three.
+Align the y-axis with the optional **+v**. The figure size is set
+with *scale* or *width*.
+"""
+import pygmt
+
+fig = pygmt.Figure()
+# Using the origin projection pole
+fig.coast(
+    projection="Oc280/25.5/22/69/12c",
+    # Set bottom left and top right coordinates of the figure with "+r"
+    region="270/20/305/25+r",
+    frame="afg",
+    land="gray",
+    shorelines="1/thin",
+    water="lightblue",
+)
+fig.show()
diff --git a/examples/projections/table/README.txt b/examples/projections/table/README.txt
index e0a821ed..b5a84f7d 100644
--- a/examples/projections/table/README.txt
+++ b/examples/projections/table/README.txt
@@ -53,11 +53,11 @@ The below table shows the projection codes for the 31 GMT projections.
    * - **N**\ [|lon0|/]\ *width*
      - :doc:`Robinson </projections/misc/misc_robinson>`
    * - **Oa**\ |lon0|/|lat0|/\ *azim*/*width*\ [**+v**]
-     - Oblique Mercator, 1: origin and azim
+     - :doc:`Oblique Mercator, 1: origin and azim </projections/cyl/cyl_oblique_mercator_1>`
    * - **Ob**\ |lon0|/|lat0|/|lon1|/|lat1|/\ *width*\ [**+v**]
-     - Oblique Mercator, 2: two points
+     - :doc:`Oblique Mercator, 2: two points </projections/cyl/cyl_oblique_mercator_2>`
    * - **Oc**\ |lon0|/|lat0|/|lonp|/|latp|/\ *width*\ [**+v**]
-     - Oblique Mercator, 3: origin and pole
+     - :doc:`Oblique Mercator, 3: origin and pole </projections/cyl/cyl_oblique_mercator_3>`
    * - **P**\ *width*\ [**+a**]\ [**+f**\ [**e**\|\ **p**\|\ *radius*]]\
        [**+r**\ *offset*][**+t**\ *origin*][**+z**\ [**p**\|\ *radius*]]
      - :doc:`Polar </projections/nongeo/polar>` [azimuthal]
diff --git a/examples/tutorials/plot.py b/examples/tutorials/plot.py
index 31d6d540..cead5c55 100644
--- a/examples/tutorials/plot.py
+++ b/examples/tutorials/plot.py
@@ -45,7 +45,7 @@ fig.show()
 # parameter controls the outline of the symbols and the ``color`` parameter controls the fill.
 #
 # We can map the size of the circles to the earthquake magnitude by passing an array to
-# the ``sizes`` parameter. Because the magnitude is on a logarithmic scale, it helps to
+# the ``size`` parameter. Because the magnitude is on a logarithmic scale, it helps to
 # show the differences by scaling the values using a power law.
 
 fig = pygmt.Figure()
@@ -54,7 +54,7 @@ fig.coast(land="black", water="skyblue")
 fig.plot(
     x=data.longitude,
     y=data.latitude,
-    sizes=0.02 * (2 ** data.magnitude),
+    size=0.02 * (2 ** data.magnitude),
     style="cc",
     color="white",
     pen="black",
@@ -63,7 +63,7 @@ fig.show()
 
 ########################################################################################
 # Notice that we didn't include the size in the ``style`` parameter this time, just the
-# symbol ``c`` (circles) and the unit ``c`` (centimeter). So in this case, the sizes
+# symbol ``c`` (circles) and the unit ``c`` (centimeter). So in this case, the size
 # will be interpreted as being in centimeters.
 #
 # We can also map the colors of the markers to the depths by passing an array to the
@@ -82,7 +82,7 @@ pygmt.makecpt(cmap="viridis", series=[data.depth_km.min(), data.depth_km.max()])
 fig.plot(
     x=data.longitude,
     y=data.latitude,
-    sizes=0.02 * 2 ** data.magnitude,
+    size=0.02 * 2 ** data.magnitude,
     color=data.depth_km,
     cmap=True,
     style="cc",
diff --git a/pygmt/figure.py b/pygmt/figure.py
index d7c7193a..e2b9089c 100644
--- a/pygmt/figure.py
+++ b/pygmt/figure.py
@@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory
 
 try:
     import IPython
-except KeyError:
+except ModuleNotFoundError:
     IPython = None  # pylint: disable=invalid-name
 
 
diff --git a/pygmt/src/meca.py b/pygmt/src/meca.py
index e73f95b3..cde18474 100644
--- a/pygmt/src/meca.py
+++ b/pygmt/src/meca.py
@@ -98,8 +98,8 @@ def data_format_code(convention, component="full"):
 @use_alias(
     R="region",
     J="projection",
+    A="offset",
     B="frame",
-    C="offset",
     N="no_clip",
     V="verbose",
     X="xshift",
@@ -334,8 +334,8 @@ def meca(
                 if arg is None:
                     spec.append(0)
                 else:
-                    if "C" not in kwargs:
-                        kwargs["C"] = True
+                    if "A" not in kwargs:
+                        kwargs["A"] = True
                     spec.append(arg)
 
         # or assemble the 2D array for the case of lists as values
@@ -389,8 +389,8 @@ def meca(
                     if arg is None:
                         row.append(0)
                     else:
-                        if "C" not in kwargs:
-                            kwargs["C"] = True
+                        if "A" not in kwargs:
+                            kwargs["A"] = True
                         row.append(arg[index])
                 spec_array.append(row)
             spec = spec_array
@@ -435,8 +435,8 @@ def meca(
                     if arg is None:
                         row.append(0)
                     else:
-                        if "C" not in kwargs:
-                            kwargs["C"] = True
+                        if "A" not in kwargs:
+                            kwargs["A"] = True
                         row.append(arg[index])
                 spec_array.append(row)
             spec = spec_array
diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py
index f3be49b5..60ff1540 100644
--- a/pygmt/src/plot.py
+++ b/pygmt/src/plot.py
@@ -6,6 +6,7 @@ from pygmt.exceptions import GMTInvalidInput
 from pygmt.helpers import (
     build_arg_string,
     data_kind,
+    deprecate_parameter,
     fmt_docstring,
     is_nonstr_iter,
     kwargs_to_strings,
@@ -43,7 +44,8 @@ from pygmt.helpers import (
     t="transparency",
 )
 @kwargs_to_strings(R="sequence", c="sequence_comma", i="sequence_comma", p="sequence")
-def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs):
+@deprecate_parameter("sizes", "size", "v0.4.0", remove_version="v0.6.0")
+def plot(self, x=None, y=None, data=None, size=None, direction=None, **kwargs):
     r"""
     Plot lines, polygons, and symbols in 2-D.
 
@@ -78,8 +80,8 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs):
         Either a data file name or a 2d numpy array with the tabular data.
         Use parameter ``columns`` to choose which columns are x, y, color,
         and size, respectively.
-    sizes : 1d array
-        The sizes of the data points in units specified using ``style``.
+    size : 1d array
+        The size of the data points in units specified using ``style``.
         Only valid if using ``x``/``y``.
     direction : list of two 1d arrays
         If plotting vectors (using ``style='V'`` or ``style='v'``), then
@@ -215,12 +217,12 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs):
             )
         extra_arrays.append(kwargs["G"])
         del kwargs["G"]
-    if sizes is not None:
+    if size is not None:
         if kind != "vectors":
             raise GMTInvalidInput(
-                "Can't use arrays for sizes if data is matrix or file."
+                "Can't use arrays for 'size' if data is a matrix or file."
             )
-        extra_arrays.append(sizes)
+        extra_arrays.append(size)
 
     for flag in ["I", "t"]:
         if flag in kwargs and is_nonstr_iter(kwargs[flag]):


Test patch:
diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml
index ac100f8f..dbacd83b 100644
--- a/.github/workflows/ci_tests.yaml
+++ b/.github/workflows/ci_tests.yaml
@@ -141,7 +141,7 @@ jobs:
 
       # Upload coverage to Codecov
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v1.4.1
+        uses: codecov/codecov-action@v1.5.0
         with:
           file: ./coverage.xml # optional
           env_vars: OS,PYTHON,NUMPY
diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py
index 7f50fc75..2fff587d 100644
--- a/pygmt/tests/test_plot.py
+++ b/pygmt/tests/test_plot.py
@@ -93,7 +93,7 @@ def test_plot_fail_no_data(data):
 
 def test_plot_fail_color_size_intensity(data):
     """
-    Should raise an exception if array color, sizes and intensity are used with
+    Should raise an exception if array color, size and intensity are used with
     matrix.
     """
     fig = Figure()
@@ -101,7 +101,7 @@ def test_plot_fail_color_size_intensity(data):
     with pytest.raises(GMTInvalidInput):
         fig.plot(style="c0.2c", color=data[:, 2], **kwargs)
     with pytest.raises(GMTInvalidInput):
-        fig.plot(style="cc", sizes=data[:, 2], color="red", **kwargs)
+        fig.plot(style="cc", size=data[:, 2], color="red", **kwargs)
     with pytest.raises(GMTInvalidInput):
         fig.plot(style="c0.2c", color="red", intensity=data[:, 2], **kwargs)
 
@@ -152,7 +152,7 @@ def test_plot_sizes(data, region):
     fig.plot(
         x=data[:, 0],
         y=data[:, 1],
-        sizes=0.5 * data[:, 2],
+        size=0.5 * data[:, 2],
         region=region,
         projection="X10c",
         style="cc",
@@ -172,7 +172,7 @@ def test_plot_colors_sizes(data, region):
         x=data[:, 0],
         y=data[:, 1],
         color=data[:, 2],
-        sizes=0.5 * data[:, 2],
+        size=0.5 * data[:, 2],
         region=region,
         projection="X10c",
         style="cc",
@@ -193,7 +193,7 @@ def test_plot_colors_sizes_proj(data, region):
         x=data[:, 0],
         y=data[:, 1],
         color=data[:, 2],
-        sizes=0.5 * data[:, 2],
+        size=0.5 * data[:, 2],
         style="cc",
         cmap="copper",
     )
@@ -288,7 +288,7 @@ def test_plot_sizes_colors_transparencies():
         frame=True,
         style="cc",
         color=color,
-        sizes=size,
+        size=size,
         cmap="gray",
         transparency=transparency,
     )
@@ -446,3 +446,27 @@ def test_plot_datetime():
     y = [8.5, 9.5]
     fig.plot(x, y, style="i0.2c", pen="1p")
     return fig
+
+
+@pytest.mark.mpl_image_compare(filename="test_plot_sizes.png")
+def test_plot_deprecate_sizes_to_size(data, region):
+    """
+    Make sure that the old parameter "sizes" is supported and it reports an
+    warning.
+
+    Modified from the test_plot_sizes() test.
+    """
+    fig = Figure()
+    with pytest.warns(expected_warning=FutureWarning) as record:
+        fig.plot(
+            x=data[:, 0],
+            y=data[:, 1],
+            sizes=0.5 * data[:, 2],
+            region=region,
+            projection="X10c",
+            style="cc",
+            color="blue",
+            frame="af",
+        )
+        assert len(record) == 1  # check that only one warning was raised
+    return fig