Skip to content

Commit 6937607

Browse files
author
Jon Duckworth
authored
Merge pull request #454 from stac-utils/fix/rde/avoid-deepcopy
Avoid calling deepcopy in from_dict methods when unnecessary
2 parents 2739661 + 68df9bd commit 6937607

File tree

8 files changed

+71
-10
lines changed

8 files changed

+71
-10
lines changed

pystac/catalog.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,7 @@ def from_dict(
906906
href: Optional[str] = None,
907907
root: Optional["Catalog"] = None,
908908
migrate: bool = False,
909+
preserve_dict: bool = True,
909910
) -> "Catalog":
910911
if migrate:
911912
info = identify_stac_object(d)
@@ -916,7 +917,8 @@ def from_dict(
916917

917918
catalog_type = CatalogType.determine_type(d)
918919

919-
d = deepcopy(d)
920+
if preserve_dict:
921+
d = deepcopy(d)
920922

921923
id = d.pop("id")
922924
description = d.pop("description")

pystac/collection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ def from_dict(
587587
href: Optional[str] = None,
588588
root: Optional[Catalog] = None,
589589
migrate: bool = False,
590+
preserve_dict: bool = True,
590591
) -> "Collection":
591592
if migrate:
592593
info = identify_stac_object(d)
@@ -597,7 +598,9 @@ def from_dict(
597598

598599
catalog_type = CatalogType.determine_type(d)
599600

600-
d = deepcopy(d)
601+
if preserve_dict:
602+
d = deepcopy(d)
603+
601604
id = d.pop("id")
602605
description = d.pop("description")
603606
license = d.pop("license")

pystac/item.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ def from_dict(
915915
href: Optional[str] = None,
916916
root: Optional[Catalog] = None,
917917
migrate: bool = False,
918+
preserve_dict: bool = True,
918919
) -> "Item":
919920
if migrate:
920921
info = identify_stac_object(d)
@@ -925,7 +926,9 @@ def from_dict(
925926
f"{d} does not represent a {cls.__name__} instance"
926927
)
927928

928-
d = deepcopy(d)
929+
if preserve_dict:
930+
d = deepcopy(d)
931+
929932
id = d.pop("id")
930933
geometry = d.pop("geometry")
931934
properties = d.pop("properties")

pystac/stac_io.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def stac_object_from_dict(
9999
d: Dict[str, Any],
100100
href: Optional[str] = None,
101101
root: Optional["Catalog_Type"] = None,
102+
preserve_dict: bool = True,
102103
) -> "STACObject_Type":
103104
if identify_stac_object_type(d) == pystac.STACObjectType.ITEM:
104105
collection_cache = None
@@ -114,15 +115,21 @@ def stac_object_from_dict(
114115
d = migrate_to_latest(d, info)
115116

116117
if info.object_type == pystac.STACObjectType.CATALOG:
117-
result = pystac.Catalog.from_dict(d, href=href, root=root, migrate=False)
118+
result = pystac.Catalog.from_dict(
119+
d, href=href, root=root, migrate=False, preserve_dict=preserve_dict
120+
)
118121
result._stac_io = self
119122
return result
120123

121124
if info.object_type == pystac.STACObjectType.COLLECTION:
122-
return pystac.Collection.from_dict(d, href=href, root=root, migrate=False)
125+
return pystac.Collection.from_dict(
126+
d, href=href, root=root, migrate=False, preserve_dict=preserve_dict
127+
)
123128

124129
if info.object_type == pystac.STACObjectType.ITEM:
125-
return pystac.Item.from_dict(d, href=href, root=root, migrate=False)
130+
return pystac.Item.from_dict(
131+
d, href=href, root=root, migrate=False, preserve_dict=preserve_dict
132+
)
126133

127134
raise ValueError(f"Unknown STAC object type {info.object_type}")
128135

@@ -164,7 +171,7 @@ def read_stac_object(
164171
"""
165172
d = self.read_json(source)
166173
href = source if isinstance(source, str) else source.get_absolute_href()
167-
return self.stac_object_from_dict(d, href=href, root=root)
174+
return self.stac_object_from_dict(d, href=href, root=root, preserve_dict=False)
168175

169176
def save_json(
170177
self, dest: Union[str, "Link_Type"], json_dict: Dict[str, Any]

pystac/stac_object.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def from_file(
473473
href = make_absolute_href(href)
474474

475475
d = stac_io.read_json(href)
476-
o = cls.from_dict(d, href=href, migrate=True)
476+
o = cls.from_dict(d, href=href, migrate=True, preserve_dict=False)
477477

478478
# Set the self HREF, if it's not already set to something else.
479479
if o.get_self_href() is None:
@@ -495,6 +495,7 @@ def from_dict(
495495
href: Optional[str] = None,
496496
root: Optional["Catalog_Type"] = None,
497497
migrate: bool = False,
498+
preserve_dict: bool = True,
498499
) -> "STACObject":
499500
"""Parses this STACObject from the passed in dictionary.
500501
@@ -507,6 +508,11 @@ def from_dict(
507508
previously resolved instances of the STAC object.
508509
migrate: Use True if this dict represents JSON from an older STAC object,
509510
so that migrations are run against it.
511+
preserve_dict: If False, the dict parameter ``d`` may be modified
512+
during this method call. Otherwise the dict is not mutated.
513+
Defaults to True, which results results in a deepcopy of the
514+
parameter. Set to False when possible to avoid the performance
515+
hit of a deepcopy.
510516
511517
Returns:
512518
STACObject: The STACObject parsed from this dict.

tests/test_catalog.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from copy import deepcopy
12
import os
23
import json
34
import tempfile
@@ -85,6 +86,19 @@ def test_create_and_read(self) -> None:
8586

8687
self.assertEqual(len(list(items)), 8)
8788

89+
def test_from_dict_preserves_dict(self) -> None:
90+
catalog_dict = TestCases.test_case_1().to_dict()
91+
param_dict = deepcopy(catalog_dict)
92+
93+
# test that the parameter is preserved
94+
_ = Catalog.from_dict(param_dict)
95+
self.assertEqual(param_dict, catalog_dict)
96+
97+
# assert that the parameter is not preserved with
98+
# non-default parameter
99+
_ = Catalog.from_dict(param_dict, preserve_dict=False)
100+
self.assertNotEqual(param_dict, catalog_dict)
101+
88102
def test_read_remote(self) -> None:
89103
# TODO: Move this URL to the main stac-spec repo once the example JSON is fixed.
90104
catalog_url = (

tests/test_collection.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from copy import deepcopy
12
import unittest
23
import os
34
import json
@@ -182,6 +183,21 @@ def test_assets(self) -> None:
182183
collection = pystac.Collection.from_dict(data)
183184
collection.validate()
184185

186+
def test_to_dict_preserves_dict(self) -> None:
187+
path = TestCases.get_path("data-files/collections/with-assets.json")
188+
with open(path) as f:
189+
collection_dict = json.load(f)
190+
param_dict = deepcopy(collection_dict)
191+
192+
# test that the parameter is preserved
193+
_ = Collection.from_dict(param_dict)
194+
self.assertEqual(param_dict, collection_dict)
195+
196+
# assert that the parameter is not preserved with
197+
# non-default parameter
198+
_ = Collection.from_dict(param_dict, preserve_dict=False)
199+
self.assertNotEqual(param_dict, collection_dict)
200+
185201
def test_schema_summary(self) -> None:
186202
collection = pystac.Collection.from_file(
187203
TestCases.get_path(

tests/test_item.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from copy import deepcopy
12
import os
23
from datetime import datetime
34
import json
@@ -25,9 +26,10 @@ def test_to_from_dict(self) -> None:
2526
self.maxDiff = None
2627

2728
item_dict = self.get_example_item_dict()
29+
param_dict = deepcopy(item_dict)
2830

29-
assert_to_from_dict(self, Item, item_dict)
30-
item = Item.from_dict(item_dict)
31+
assert_to_from_dict(self, Item, param_dict)
32+
item = Item.from_dict(param_dict)
3133
self.assertEqual(item.id, "CS3-20160503_132131_05")
3234

3335
# test asset creation additional field(s)
@@ -37,6 +39,14 @@ def test_to_from_dict(self) -> None:
3739
)
3840
self.assertEqual(len(item.assets["thumbnail"].properties), 0)
3941

42+
# test that the parameter is preserved
43+
self.assertEqual(param_dict, item_dict)
44+
45+
# assert that the parameter is not preserved with
46+
# non-default parameter
47+
_ = Item.from_dict(param_dict, preserve_dict=False)
48+
self.assertNotEqual(param_dict, item_dict)
49+
4050
def test_set_self_href_does_not_break_asset_hrefs(self) -> None:
4151
cat = TestCases.test_case_2()
4252
for item in cat.get_all_items():

0 commit comments

Comments
 (0)