diff --git a/docs/history.rst b/docs/history.rst index af517a908..ba8b77b9c 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -8,6 +8,7 @@ Change Log * Refactor Proj to inherit from Transformer (issue #624) * ENH: Support obects with '__array__' method (pandas.Series, xarray.DataArray, dask.array.Array) (issue #573) * ENH: Added :func:`pyproj.datadir.get_user_data_dir` (pull #636) +* ENH: Add network methods to Transformer (issue #629) 2.6.1 ~~~~~ diff --git a/docs/transformation_grids.rst b/docs/transformation_grids.rst index 6e9ab63c8..3b9b9d201 100644 --- a/docs/transformation_grids.rst +++ b/docs/transformation_grids.rst @@ -56,6 +56,9 @@ Available methods for download include: - Enabling `PROJ network `__ capabilities. + .. note:: You can use the `network` kwarg when initializing + :class:`pyproj.Proj ` or :class:`pyproj.Transformer ` + - Use `conda `__ with the `conda-forge `__ channel: .. code-block:: bash diff --git a/pyproj/_datadir.pxd b/pyproj/_datadir.pxd index 4e511948d..c978025e4 100644 --- a/pyproj/_datadir.pxd +++ b/pyproj/_datadir.pxd @@ -2,7 +2,8 @@ include "proj.pxi" cdef void pyproj_context_initialize( PJ_CONTEXT* context, - bint free_context_on_error) except * + bint free_context_on_error, + network=*) except * cdef class ContextManager: cdef PJ_CONTEXT *context diff --git a/pyproj/_datadir.pyx b/pyproj/_datadir.pyx index 7f673eab9..52b56c1e0 100644 --- a/pyproj/_datadir.pyx +++ b/pyproj/_datadir.pyx @@ -1,5 +1,5 @@ import os - +import warnings from libc.stdlib cimport malloc, free from pyproj.compat import cstrencode, pystrdecode @@ -33,6 +33,7 @@ cdef void set_context_data_dir(PJ_CONTEXT* context) except *: cdef void pyproj_context_initialize( PJ_CONTEXT* context, bint free_context_on_error, + network=None, ) except *: """ Setup the context for pyproj @@ -40,6 +41,10 @@ cdef void pyproj_context_initialize( proj_log_func(context, NULL, pyproj_log_function) proj_context_use_proj4_init_rules(context, 1) proj_context_set_autoclose_database(context, 1) + if network is not None: + enabled = proj_context_set_enable_network(context, bool(network)) + if network and not enabled: + warnings.warn("PROJ network cannot be enabled.") try: set_context_data_dir(context) except DataDirError: diff --git a/pyproj/_transformer.pyi b/pyproj/_transformer.pyi index 38732565d..e5701b514 100644 --- a/pyproj/_transformer.pyi +++ b/pyproj/_transformer.pyi @@ -36,6 +36,7 @@ class _TransformerGroup: skip_equivalent: bool = False, always_xy: bool = False, area_of_interest: Optional[AreaOfInterest] = None, + network: Optional[bool] = None, ) -> None: ... class _Transformer(Base): @@ -60,6 +61,8 @@ class _Transformer(Base): def area_of_use(self) -> AreaOfUse: ... @property def operations(self) -> Union[Tuple[CoordinateOperation], None]: ... + @property + def is_network_enabled(self) -> bool: ... @staticmethod def from_crs( crs_from: _CRS, @@ -67,9 +70,12 @@ class _Transformer(Base): skip_equivalent: bool = False, always_xy: bool = False, area_of_interest: Optional[AreaOfInterest] = None, + network: Optional[bool] = None, ) -> "_Transformer": ... @staticmethod - def from_pipeline(proj_pipeline: str) -> "_Transformer": ... + def from_pipeline( + proj_pipeline: str, network: Optional[bool] = None + ) -> "_Transformer": ... def _transform( self, inx: Any, diff --git a/pyproj/_transformer.pyx b/pyproj/_transformer.pyx index 3978b3511..613cf624c 100644 --- a/pyproj/_transformer.pyx +++ b/pyproj/_transformer.pyx @@ -132,6 +132,7 @@ cdef class _TransformerGroup: skip_equivalent=False, always_xy=False, area_of_interest=None, + network=None, ): """ From PROJ docs: @@ -143,7 +144,7 @@ cdef class _TransformerGroup: with unknown accuracy are sorted last, whatever their area. """ self.context = proj_context_create() - pyproj_context_initialize(self.context, False) + pyproj_context_initialize(self.context, False, network=network) cdef PJ_OPERATION_FACTORY_CONTEXT* operation_factory_context = NULL cdef PJ_OBJ_LIST * pj_operations = NULL cdef PJ* pj_transform = NULL @@ -194,7 +195,7 @@ cdef class _TransformerGroup: num_operations = proj_list_get_count(pj_operations) for iii in range(num_operations): context = proj_context_create() - pyproj_context_initialize(context, True) + pyproj_context_initialize(context, True, network=network) pj_transform = proj_list_get( context, pj_operations, @@ -326,6 +327,16 @@ cdef class _Transformer(Base): self._operations = _get_concatenated_operations(self.context, self.projobj) return self._operations + @property + def is_network_enabled(self): + """ + .. versionadded:: 3.0.0 + + bool: + If the network is enabled. + """ + return proj_context_is_network_enabled(self.context) == 1 + @staticmethod def from_crs( _CRS crs_from, @@ -333,6 +344,7 @@ cdef class _Transformer(Base): skip_equivalent=False, always_xy=False, area_of_interest=None, + network=None, ): """ Create a transformer from CRS objects @@ -363,7 +375,7 @@ cdef class _Transformer(Base): north_lat_degree, ) transformer.context = proj_context_create() - pyproj_context_initialize(transformer.context, False) + pyproj_context_initialize(transformer.context, False, network=network) transformer.projobj = proj_create_crs_to_crs( transformer.context, cstrencode(crs_from.srs), @@ -413,13 +425,13 @@ cdef class _Transformer(Base): return transformer @staticmethod - def from_pipeline(const char *proj_pipeline): + def from_pipeline(const char *proj_pipeline, network=None): """ Create Transformer from a PROJ pipeline string. """ cdef _Transformer transformer = _Transformer() transformer.context = proj_context_create() - pyproj_context_initialize(transformer.context, False) + pyproj_context_initialize(transformer.context, False, network=network) # initialize projection transformer.projobj = proj_create( transformer.context, diff --git a/pyproj/proj.pxi b/pyproj/proj.pxi index c72be5912..51b2094be 100644 --- a/pyproj/proj.pxi +++ b/pyproj/proj.pxi @@ -437,6 +437,7 @@ cdef extern from "proj.h": double dy_dphi PJ_FACTORS proj_factors(PJ *P, PJ_COORD lp) nogil - # neworking related const char *proj_context_get_user_writable_directory(PJ_CONTEXT *ctx, int create) + int proj_context_set_enable_network(PJ_CONTEXT* ctx, int enabled) + int proj_context_is_network_enabled(PJ_CONTEXT* ctx) diff --git a/pyproj/proj.py b/pyproj/proj.py index 2a166b61d..499783e67 100644 --- a/pyproj/proj.py +++ b/pyproj/proj.py @@ -45,7 +45,11 @@ class Proj(Transformer): """ def __init__( - self, projparams: Any = None, preserve_units: bool = True, **kwargs + self, + projparams: Any = None, + preserve_units: bool = True, + network=None, + **kwargs, ) -> None: """ A Proj class instance is initialized with proj map projection @@ -55,12 +59,19 @@ def __init__( https://proj.org/operations/projections/index.html for examples of key/value pairs defining different map projections. + .. versionadded:: 3.0.0 network + Parameters ---------- projparams: int, str, dict, pyproj.CRS A PROJ or WKT string, PROJ dict, EPSG integer, or a pyproj.CRS instance. preserve_units: bool If false, will ensure +units=m. + network: bool, optional + Default is None, which uses the system defaults for networking. + If True, it will force the use of network for grids regardless of + any other network setting. If False, it will force disable use of + network for grids regardless of any other network setting. **kwargs: PROJ projection parameters. @@ -131,7 +142,9 @@ def __init__( projstring = self.crs.to_proj4() or self.crs.srs self.srs = re.sub(r"\s\+?type=crs", "", projstring).strip() - super().__init__(_Transformer.from_pipeline(cstrencode(self.srs))) + super().__init__( + _Transformer.from_pipeline(cstrencode(self.srs), network=network) + ) def __call__( self, diff --git a/pyproj/transformer.py b/pyproj/transformer.py index 959cf0811..32f2819e0 100644 --- a/pyproj/transformer.py +++ b/pyproj/transformer.py @@ -51,6 +51,7 @@ def __init__( skip_equivalent: bool = False, always_xy: bool = False, area_of_interest: Optional[AreaOfInterest] = None, + network: Optional[bool] = None, ) -> None: """Get all possible transformations from a :obj:`pyproj.crs.CRS` or input used to create one. @@ -73,6 +74,11 @@ def __init__( area_of_interest: :class:`pyproj.transformer.AreaOfInterest`, optional The area of interest to help order the transformations based on the best operation for the area. + network: bool, optional + Default is None, which uses the system defaults for networking. + If True, it will force the use of network for grids regardless of + any other network setting. If False, it will force disable use of + network for grids regardless of any other network setting. """ super().__init__( @@ -81,6 +87,7 @@ def __init__( skip_equivalent=skip_equivalent, always_xy=always_xy, area_of_interest=area_of_interest, + network=network, ) for iii, transformer in enumerate(self._transformers): self._transformers[iii] = Transformer(transformer) @@ -222,6 +229,16 @@ def operations(self) -> Optional[Tuple[CoordinateOperation]]: """ return self._transformer.operations + @property + def is_network_enabled(self) -> bool: + """ + .. versionadded:: 3.0.0 + + bool: + If the network is enabled. + """ + return self._transformer.is_network_enabled + @staticmethod def from_proj( proj_from: Any, @@ -229,12 +246,14 @@ def from_proj( skip_equivalent: bool = False, always_xy: bool = False, area_of_interest: Optional[AreaOfInterest] = None, + network: Optional[bool] = None, ) -> "Transformer": """Make a Transformer from a :obj:`pyproj.proj.Proj` or input used to create one. .. versionadded:: 2.1.2 skip_equivalent .. versionadded:: 2.2.0 always_xy .. versionadded:: 2.3.0 area_of_interest + .. versionadded:: 3.0.0 network Parameters ---------- @@ -252,6 +271,11 @@ def from_proj( Default is false. area_of_interest: :class:`pyproj.transformer.AreaOfInterest`, optional The area of interest to help select the transformation. + network: bool, optional + Default is None, which uses the system defaults for networking. + If True, it will force the use of network for grids regardless of + any other network setting. If False, it will force disable use of + network for grids regardless of any other network setting. Returns ------- @@ -271,6 +295,7 @@ def from_proj( skip_equivalent=skip_equivalent, always_xy=always_xy, area_of_interest=area_of_interest, + network=network, ) @staticmethod @@ -280,12 +305,14 @@ def from_crs( skip_equivalent: bool = False, always_xy: bool = False, area_of_interest: Optional[AreaOfInterest] = None, + network: Optional[bool] = None, ) -> "Transformer": """Make a Transformer from a :obj:`pyproj.crs.CRS` or input used to create one. .. versionadded:: 2.1.2 skip_equivalent .. versionadded:: 2.2.0 always_xy .. versionadded:: 2.3.0 area_of_interest + .. versionadded:: 3.0.0 network Parameters ---------- @@ -303,6 +330,11 @@ def from_crs( Default is false. area_of_interest: :class:`pyproj.transformer.AreaOfInterest`, optional The area of interest to help select the transformation. + network: bool, optional + Default is None, which uses the system defaults for networking. + If True, it will force the use of network for grids regardless of + any other network setting. If False, it will force disable use of + network for grids regardless of any other network setting. Returns ------- @@ -316,26 +348,38 @@ def from_crs( skip_equivalent=skip_equivalent, always_xy=always_xy, area_of_interest=area_of_interest, + network=network, ) ) @staticmethod - def from_pipeline(proj_pipeline: str) -> "Transformer": + def from_pipeline( + proj_pipeline: str, network: Optional[bool] = None + ) -> "Transformer": """Make a Transformer from a PROJ pipeline string. https://proj.org/operations/pipeline.html + .. versionadded:: 3.0.0 network + Parameters ---------- proj_pipeline: str Projection pipeline string. + network: bool, optional + Default is None, which uses the system defaults for networking. + If True, it will force the use of network for grids regardless of + any other network setting. If False, it will force disable use of + network for grids regardless of any other network setting. Returns ------- Transformer """ - return Transformer(_Transformer.from_pipeline(cstrencode(proj_pipeline))) + return Transformer( + _Transformer.from_pipeline(cstrencode(proj_pipeline), network=network) + ) def transform( self, diff --git a/test/test_proj.py b/test/test_proj.py index 506e15fdf..a69664078 100644 --- a/test/test_proj.py +++ b/test/test_proj.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- import math +import os import sys import unittest import numpy as np import pytest +from mock import patch from numpy.testing import assert_almost_equal from pyproj import Geod, Proj, pj_ellps, pj_list, transform @@ -533,3 +535,20 @@ def test_proj_radians_warning(): proj = Proj("epsg:4326") with pytest.warns(UserWarning, match="radian"): proj(1, 2, radians=True) + + +@patch.dict("os.environ", {"PROJ_NETWORK": "ON"}, clear=True) +def test_network__disable(): + transformer = Proj(3857, network=False) + assert transformer.is_network_enabled is False + + +@patch.dict("os.environ", {"PROJ_NETWORK": "OFF"}, clear=True) +def test_network__enable(): + transformer = Proj(3857, network=True) + assert transformer.is_network_enabled is True + + +def test_network__default(): + transformer = Proj(3857) + assert transformer.is_network_enabled == (os.environ.get("PROJ_NETWORK") == "ON") diff --git a/test/test_transformer.py b/test/test_transformer.py index e6104db37..de552d639 100644 --- a/test/test_transformer.py +++ b/test/test_transformer.py @@ -1,5 +1,10 @@ +import os +from functools import partial +from pathlib import Path + import numpy as np import pytest +from mock import patch from numpy.testing import assert_almost_equal import pyproj @@ -579,6 +584,11 @@ def test_transformer_group(): def test_transformer_group__unavailable(): trans_group = TransformerGroup(4326, 2964) + for transformer in trans_group.transformers: + assert transformer.is_network_enabled == ( + os.environ.get("PROJ_NETWORK") == "ON" + ) + if not grids_available("us_noaa_alaska.tif"): assert len(trans_group.unavailable_operations) == 2 assert ( @@ -750,3 +760,80 @@ def test_pipeline_radian_transform_warning(): trans = Transformer.from_pipeline("+proj=pipeline +ellps=GRS80 +step +proj=cart") with pytest.warns(UserWarning, match="radian"): trans.transform(0.1, 0.1, 0, radians=True) + + +@pytest.mark.parametrize( + "transformer", + [ + partial( + Transformer.from_pipeline, "+proj=pipeline +ellps=GRS80 +step +proj=cart" + ), + partial(Transformer.from_crs, 4326, 3857), + partial(Transformer.from_proj, 4326, 3857), + ], +) +@patch.dict("os.environ", {"PROJ_NETWORK": "ON"}, clear=True) +def test_network__disable(transformer): + trans = transformer(network=False) + assert trans.is_network_enabled is False + + +@pytest.mark.parametrize( + "transformer", + [ + partial( + Transformer.from_pipeline, "+proj=pipeline +ellps=GRS80 +step +proj=cart" + ), + partial(Transformer.from_crs, 4326, 3857), + partial(Transformer.from_proj, 4326, 3857), + ], +) +@patch.dict("os.environ", {"PROJ_NETWORK": "OFF"}, clear=True) +def test_network__enable(transformer): + trans = transformer(network=True) + assert trans.is_network_enabled is True + + +@pytest.mark.parametrize( + "transformer", + [ + partial( + Transformer.from_pipeline, "+proj=pipeline +ellps=GRS80 +step +proj=cart" + ), + partial(Transformer.from_crs, 4326, 3857), + partial(Transformer.from_proj, 4326, 3857), + ], +) +def test_network__default(transformer): + trans = transformer() + assert trans.is_network_enabled == (os.environ.get("PROJ_NETWORK") == "ON") + + +@patch.dict("os.environ", {"PROJ_NETWORK": "OFF"}, clear=True) +def test_transformer_group__network_enabled(): + trans_group = TransformerGroup(4326, 2964, network=True) + assert len(trans_group.unavailable_operations) == 0 + assert len(trans_group.transformers) == 10 + assert trans_group.best_available + for transformer in trans_group.transformers: + assert transformer.is_network_enabled is True + + +@patch.dict("os.environ", {"PROJ_NETWORK": "ON"}, clear=True) +def test_transformer_group__network_disabled(): + trans_group = TransformerGroup(4326, 2964, network=False) + for transformer in trans_group.transformers: + assert transformer.is_network_enabled is False + + if not Path(pyproj.datadir.get_data_dir(), "us_noaa_alaska.tif").exists(): + assert len(trans_group.unavailable_operations) == 2 + assert ( + trans_group.unavailable_operations[0].name + == "Inverse of NAD27 to WGS 84 (85) + Alaska Albers" + ) + assert len(trans_group.transformers) == 8 + assert not trans_group.best_available + else: + assert len(trans_group.unavailable_operations) == 0 + assert len(trans_group.transformers) == 10 + assert trans_group.best_available