Repository: pyproj4/pyproj
Issue: Keyword arguments to Proj() are silently ignored when projparams are specified

I came across this when trying to initialize a `Proj()` and include `geoidgrids`:

```
proj = pyproj.Proj('EPSG:3162', geoidgrids='CGG2013an83.gtx')
proj.crs.is_vertical
>>> False
```

This is confusing.  Why is it not taking my geoidgrids?  Did I get the file name/path wrong?

Turns out the positional argument `projparams` is mutually exclusive with any passed `**kwargs`:

https://github.com/pyproj4/pyproj/blob/632c39818e77f8226bbec0db2a3fbe7fc02f7d33/pyproj/proj.py#L143

Yet there is no indication that this is not a valid way to initialize it.

In my opinion it should throw an exception, and it could perhaps be made clearer in the documentation as well.  It's a minor user experience thing, but still something I had to spend time troubleshooting.  Happy to attempt a PR if others agree.

[(stackexchange thread that spawned this)](https://gis.stackexchange.com/q/352277/64255)

Changed files:
pyproj/crs/crs.py, test/test_proj.py, pyproj/proj.py, test/crs/test_crs.py

diff --git a/pyproj/crs/crs.py b/pyproj/crs/crs.py
index 3ecec51f..0ba2c865 100644
--- a/pyproj/crs/crs.py
+++ b/pyproj/crs/crs.py
@@ -37,9 +37,9 @@ from pyproj.exceptions import CRSError
 from pyproj.geod import Geod
 
 
-def _prepare_from_dict(projparams: dict) -> str:
+def _prepare_from_dict(projparams: dict, allow_json: bool = True) -> str:
     # check if it is a PROJ JSON dict
-    if "proj" not in projparams and "init" not in projparams:
+    if "proj" not in projparams and "init" not in projparams and allow_json:
         return json.dumps(projparams)
     # convert a dict to a proj string.
     pjargs = []
@@ -277,20 +277,25 @@ class CRS(_CRS):
         >>> crs.is_geographic
         False
         """
-        if isinstance(projparams, str):
-            projstring = _prepare_from_string(projparams)
-        elif isinstance(projparams, dict):
-            projstring = _prepare_from_dict(projparams)
-        elif kwargs:
-            projstring = _prepare_from_dict(kwargs)
-        elif isinstance(projparams, int):
-            projstring = _prepare_from_epsg(projparams)
-        elif isinstance(projparams, (list, tuple)) and len(projparams) == 2:
-            projstring = _prepare_from_authority(*projparams)
-        elif hasattr(projparams, "to_wkt"):
-            projstring = projparams.to_wkt()  # type: ignore
-        else:
-            raise CRSError("Invalid CRS input: {!r}".format(projparams))
+        projstring = ""
+
+        if projparams:
+            if isinstance(projparams, str):
+                projstring = _prepare_from_string(projparams)
+            elif isinstance(projparams, dict):
+                projstring = _prepare_from_dict(projparams)
+            elif isinstance(projparams, int):
+                projstring = _prepare_from_epsg(projparams)
+            elif isinstance(projparams, (list, tuple)) and len(projparams) == 2:
+                projstring = _prepare_from_authority(*projparams)
+            elif hasattr(projparams, "to_wkt"):
+                projstring = projparams.to_wkt()  # type: ignore
+            else:
+                raise CRSError("Invalid CRS input: {!r}".format(projparams))
+
+        if kwargs:
+            projkwargs = _prepare_from_dict(kwargs, allow_json=False)
+            projstring = _prepare_from_string(" ".join((projstring, projkwargs)))
 
         super().__init__(projstring)
 
@@ -411,7 +416,7 @@ class CRS(_CRS):
         return self.srs
 
     @staticmethod
-    def from_user_input(value: Any) -> "CRS":
+    def from_user_input(value: Any, **kwargs) -> "CRS":
         """
         Initialize a CRS class instance with:
           - PROJ string
@@ -436,7 +441,7 @@ class CRS(_CRS):
         """
         if isinstance(value, CRS):
             return value
-        return CRS(value)
+        return CRS(value, **kwargs)
 
     def get_geod(self) -> Optional[Geod]:
         """
diff --git a/pyproj/proj.py b/pyproj/proj.py
index 3d818c0b..8c2f0373 100644
--- a/pyproj/proj.py
+++ b/pyproj/proj.py
@@ -95,7 +95,7 @@ class Proj(_Proj):
         Parameters
         ----------
         projparams: int, str, dict, pyproj.CRS
-            A PROJ or WKT string, PROJ dict, EPSG integer, or a pyproj.CRS instnace.
+            A PROJ or WKT string, PROJ dict, EPSG integer, or a pyproj.CRS instance.
         preserve_units: bool
             If false, will ensure +units=m.
         **kwargs:
@@ -143,7 +143,7 @@ class Proj(_Proj):
         >>> '{:.3f} {:.3f}'.format(x2, y2)
         '116.366 39.867'
         """
-        self.crs = CRS.from_user_input(projparams if projparams is not None else kwargs)
+        self.crs = CRS.from_user_input(projparams, **kwargs)
         # make sure units are meters if preserve_units is False.
         if not preserve_units and "foot" in self.crs.axis_info[0].unit_name:
             # ignore export to PROJ string deprecation warning
diff --git a/test/crs/test_crs.py b/test/crs/test_crs.py
index 732c972e..a055abd6 100644
--- a/test/crs/test_crs.py
+++ b/test/crs/test_crs.py
@@ -77,6 +77,12 @@ def test_from_string():
         )
 
 
+def test_initialize_projparams_with_kwargs():
+    crs_mixed_args = CRS("+proj=utm +zone=10", ellps="WGS84")
+    crs_positional = CRS("+proj=utm +zone=10 +ellps=WGS84")
+    assert crs_mixed_args.is_exact_same(crs_positional)
+
+
 def test_bare_parameters():
     """ Make sure that bare parameters (e.g., no_defs) are handled properly,
     even if they come in with key=True.  This covers interaction with pyproj,
diff --git a/test/test_proj.py b/test/test_proj.py
index ec0634d6..6b84bc6a 100644
--- a/test/test_proj.py
+++ b/test/test_proj.py
@@ -398,6 +398,12 @@ def test_initialize_proj_crs_no_plus():
     assert proj.crs.srs == "proj=lonlat type=crs"
 
 
+def test_initialize_projparams_with_kwargs():
+    proj_mixed_args = Proj("+proj=utm +zone=10", ellps="WGS84")
+    proj_positional = Proj("+proj=utm +zone=10 +ellps=WGS84")
+    assert proj_mixed_args.is_exact_same(proj_positional)
+
+
 def test_equals_different_type():
     assert Proj("epsg:4326") != ""