Skip to content

Commit a363d9c

Browse files
authored
Merge pull request #1268 from tethysplatform/component-app-bugfixes
Fixes two issues: #1266 and another
2 parents d59edee + 8c3d975 commit a363d9c

4 files changed

Lines changed: 48 additions & 34 deletions

File tree

tests/unit_tests/test_tethys_components/test_utils.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import pytest
2-
from unittest import mock
3-
from tethys_components import utils
42
from pathlib import Path
3+
from unittest import mock
54
from urllib.parse import urlencode, urljoin
65

6+
from tethys_components import utils
7+
78
THIS_DIR = Path(__file__).parent
89
TEST_APP_DIR = (
910
THIS_DIR.parents[1] / "apps" / "tethysapp-test_app" / "tethysapp" / "test_app"
1011
)
1112

1213
MOCK_APP = mock.MagicMock()
1314
MOCK_USER = mock.MagicMock()
15+
mock_pyproj = mock.MagicMock()
1416

1517

1618
@mock.patch("tethys_components.utils.inspect")
@@ -384,36 +386,32 @@ def test_fetch():
384386
assert data == test_content
385387

386388

389+
@mock.patch("tethys_components.utils.pyproj", new=mock_pyproj)
387390
def test_transform_coordinate():
388391
coordinate = [0, 0]
389392
src_proj = "EPSG:3857"
390393
target_proj = "EPSG:4326"
391394

392-
with mock.patch("builtins.__import__") as mock_import:
393-
result = utils.transform_coordinate(coordinate, src_proj, target_proj)
395+
result = utils.transform_coordinate(coordinate, src_proj, target_proj)
394396

395397
assert (
396-
mock_import.return_value.Transformer.from_crs.return_value.transform.return_value
397-
== result
398-
)
399-
mock_import.return_value.CRS.assert_has_calls(
400-
[mock.call(src_proj), mock.call(target_proj)]
398+
mock_pyproj.Transformer.from_crs.return_value.transform.return_value == result
401399
)
400+
mock_pyproj.CRS.assert_has_calls([mock.call(src_proj), mock.call(target_proj)])
402401

403402

403+
@mock.patch("tethys_components.utils.pyproj", new=mock_pyproj)
404404
def test_transform_coordinate_custom_projections():
405405
coordinate = [0, 0]
406406
src_proj = {"definition": "test src proj"}
407407
target_proj = {"definition": "test src proj"}
408408

409-
with mock.patch("builtins.__import__") as mock_import:
410-
result = utils.transform_coordinate(coordinate, src_proj, target_proj)
409+
result = utils.transform_coordinate(coordinate, src_proj, target_proj)
411410

412411
assert (
413-
mock_import.return_value.Transformer.from_crs.return_value.transform.return_value
414-
== result
412+
mock_pyproj.Transformer.from_crs.return_value.transform.return_value == result
415413
)
416-
mock_import.return_value.CRS.assert_has_calls(
414+
mock_pyproj.CRS.assert_has_calls(
417415
[mock.call(src_proj["definition"]), mock.call(target_proj["definition"])]
418416
)
419417

@@ -423,16 +421,17 @@ def test_transform_coordinate_invalid_src_proj():
423421
src_proj = 1234
424422
target_proj = {"definition": "test src proj"}
425423

426-
with mock.patch("builtins.__import__"), pytest.raises(ValueError):
424+
with pytest.raises(ValueError):
427425
utils.transform_coordinate(coordinate, src_proj, target_proj)
428426

429427

428+
@mock.patch("tethys_components.utils.pyproj", new=mock_pyproj)
430429
def test_transform_coordinate_invalid_target_proj():
431430
coordinate = [0, 0]
432431
src_proj = {"definition": "test src proj"}
433432
target_proj = 1234
434433

435-
with mock.patch("builtins.__import__"), pytest.raises(ValueError):
434+
with pytest.raises(ValueError):
436435
utils.transform_coordinate(coordinate, src_proj, target_proj)
437436

438437

@@ -585,6 +584,7 @@ def test_get_legend_url_basic_with_single_layer_in_layers():
585584
assert "LAYER=layer1" in url
586585

587586

587+
@mock.patch("tethys_components.utils.pyproj", new=mock_pyproj)
588588
def test_get_legend_url_with_resolution_and_scale():
589589
vdom = {
590590
"tagName": "ImageWMSSource",
@@ -599,10 +599,9 @@ def test_get_legend_url_with_resolution_and_scale():
599599
with mock.patch("builtins.__import__") as mock_import:
600600
mock_import.return_value.urljoin = urljoin
601601
mock_import.return_value.urlencode = urlencode
602-
mock_crs = mock_import.return_value.CRS
603602
mock_axis = mock.MagicMock()
604603
mock_axis.unit_conversion_factor = 2
605-
mock_crs.return_value.axis_info = [mock_axis]
604+
mock_pyproj.CRS.return_value.axis_info = [mock_axis]
606605

607606
# Call with resolution so SCALE is computed
608607
result = utils._get_legend_url_(vdom, resolution=100)
@@ -635,6 +634,7 @@ def test_get_feature_info_url_not_implemented_for_diff_projections():
635634
utils._get_feature_info_url_(vdom, [0, 0], 1, "EPSG:3857", "EPSG:4326")
636635

637636

637+
@mock.patch("tethys_components.utils.pyproj", new=mock_pyproj)
638638
def test_get_feature_info_url_success():
639639
vdom = {
640640
"tagName": "ImageWMSSource",
@@ -650,12 +650,11 @@ def test_get_feature_info_url_success():
650650
with mock.patch("builtins.__import__") as mock_import:
651651
mock_import.return_value.urljoin = urljoin
652652
mock_import.return_value.urlencode = urlencode
653-
mock_crs = mock_import.return_value.CRS
654653
mock_axis1 = mock.MagicMock()
655654
mock_axis1.direction = "north"
656655
mock_axis2 = mock.MagicMock()
657656
mock_axis2.direction = "east"
658-
mock_crs.return_value.axis_info = [mock_axis1, mock_axis2]
657+
mock_pyproj.CRS.return_value.axis_info = [mock_axis1, mock_axis2]
659658

660659
feature_url = utils._get_feature_info_url_(
661660
vdom,

tethys_apps/static/tethys_apps/js/ol-mods/source/Vector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function VectorSource (...props) {
2424

2525
if (props.features || props.options.features) {
2626
features = props.features || props.options.features;
27-
if (Array.isArray(features) && features.length > 0 && features[0] instanceof Feature) {
27+
if (Array.isArray(features) && (features.length == 0 || features[0] instanceof Feature)) {
2828
'pass';
2929
} else {
3030
if (!format) {

tethys_components/library.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -728,18 +728,18 @@ def __getattr__(self, attr):
728728

729729

730730
class DynamicPackageManager:
731-
from reactpy import web
732-
733731
def __init__(
734732
self,
735733
library: ComponentLibrary = None,
736734
package: Package = None,
737735
component: str = "",
738736
):
737+
from reactpy import web
739738

740739
self.library = library
741740
self.package = package
742741
self.component = component
742+
self.web = web
743743

744744
def __getattr__(self, attr):
745745
component = f"{self.component}.{attr}" if self.component else attr

tethys_components/utils.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from tethys_components import layouts
66
from typing import Any
77
from pathlib import Path
8+
from tethys_portal.optional_dependencies import optional_import
89
from tethys_apps.harvester import SingletonHarvester
910
from tethys_apps.base.paths import (
1011
_get_user_workspace,
@@ -14,6 +15,11 @@
1415
)
1516
from concurrent.futures import ThreadPoolExecutor
1617

18+
pyproj = optional_import(
19+
"pyproj",
20+
error_message="The `pyproj` package is required for the utility function you are accessing.",
21+
)
22+
1723

1824
class DotNotationDict(dict):
1925
"""Wrapper for event args provided by ReactPy as dicts to allow attribute access"""
@@ -319,27 +325,38 @@ def background_execute(
319325

320326

321327
def transform_coordinate(coordinate, src_proj, target_proj):
322-
from pyproj import Transformer, CRS
328+
"""
329+
Transforms a coordinate from a source projection to a target projection using pyproj.
323330
331+
Args:
332+
coordinate (list): A list representing the coordinate to be transformed, in the format [x, y].
333+
src_proj (str or dict): The source projection, either as a string (e.g. "EPSG:4326") or a dictionary with a "definition" key containing the projection string.
334+
target_proj (str or dict): The target projection, either as a string (e.g. "EPSG:3857") or a dictionary with a "definition" key containing the projection string.
335+
Returns:
336+
A list representing the transformed coordinate in the target projection, in the format [x, y].
337+
Raises:
338+
ValueError: If src_proj or target_proj are not in the expected formats.
339+
tethys_portal.optional_dependencies.MissingOptionalDependency: If the pyproj library is not installed.
340+
"""
324341
if isinstance(src_proj, dict):
325-
source_crs = CRS(src_proj["definition"])
342+
source_crs = pyproj.CRS(src_proj["definition"])
326343
elif isinstance(src_proj, str):
327-
source_crs = CRS(src_proj)
344+
source_crs = pyproj.CRS(src_proj)
328345
else:
329346
raise ValueError(
330347
"src_proj must be a string or dictionary with a definition key"
331348
)
332349

333350
if isinstance(target_proj, dict):
334-
target_crs = CRS(target_proj["definition"])
351+
target_crs = pyproj.CRS(target_proj["definition"])
335352
elif isinstance(target_proj, str):
336-
target_crs = CRS(target_proj)
353+
target_crs = pyproj.CRS(target_proj)
337354
else:
338355
raise ValueError(
339356
"target_proj must be a string or dictionary with a definition key"
340357
)
341358

342-
transformer = Transformer.from_crs(source_crs, target_crs)
359+
transformer = pyproj.Transformer.from_crs(source_crs, target_crs)
343360
return transformer.transform(coordinate[0], coordinate[1])
344361

345362

@@ -349,7 +366,7 @@ class Props(dict):
349366
They are converted back to ReactPy propery dictionaries when accessed.
350367
351368
Example:
352-
Instead of lib.html.div({"backgroundColor": "red", "fontSize": "12px"}, "Hello"), you can use lib.html.div(Props(background_color="red, font_size="12px"), "Hello")
369+
Instead of lib.html.div({"backgroundColor": "red", "fontSize": "12px"}, "Hello"), you can use lib.html.div(Props(background_color="red", font_size="12px"), "Hello")
353370
"""
354371

355372
def _snake_to_camel(self, snake):
@@ -443,7 +460,6 @@ def _get_legend_url_(vdom_element, resolution=None, params=None):
443460
)
444461

445462
from urllib.parse import urlencode, urljoin
446-
from pyproj import CRS
447463

448464
if not params:
449465
params = {}
@@ -468,7 +484,7 @@ def _get_legend_url_(vdom_element, resolution=None, params=None):
468484

469485
if resolution:
470486
mpu = (
471-
CRS(
487+
pyproj.CRS(
472488
source_params["projection"]
473489
if "projection" in source_params
474490
else "EPSG:3857"
@@ -493,7 +509,6 @@ def _get_feature_info_url_(
493509
)
494510

495511
from urllib.parse import urlencode, urljoin
496-
from pyproj import CRS
497512

498513
if not params:
499514
params = {}
@@ -516,7 +531,7 @@ def _get_feature_info_url_(
516531
x = round(math.floor((map_coordinate[0] - extent[0]) / map_resolution), DECIMALS)
517532
y = round(math.floor((extent[3] - map_coordinate[1]) / map_resolution), DECIMALS)
518533

519-
axisOrientation = "".join([a.direction[0] for a in CRS(map_proj).axis_info])
534+
axisOrientation = "".join([a.direction[0] for a in pyproj.CRS(map_proj).axis_info])
520535
bbox = (
521536
[extent[1], extent[0], extent[3], extent[2]]
522537
if axisOrientation == "ne"

0 commit comments

Comments
 (0)