Skip to content

Commit

Permalink
Preserve Collection assets on clone (#834)
Browse files Browse the repository at this point in the history
* Allow assets to be passed to Item init

* Allow assets to be passed to Collection init

* Preserve Collection assets on clone

* Add CHANGELOG entry for #834

* Match clone tests for Item and Collection
  • Loading branch information
duckontheweb authored Jun 29, 2022
1 parent 0c6074a commit b7fbd6e
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768))
- Add Grid Extension support ([#799](https://github.com/stac-utils/pystac/pull/799))
- Rich HTML representations for Jupyter Notebook display ([#743](https://github.com/stac-utils/pystac/pull/743))
- Add `assets` argument to `Item` and `Collection` init methods to allow adding Assets during object initialization ([#834](https://github.com/stac-utils/pystac/pull/834))

### Removed

Expand All @@ -24,6 +25,7 @@
- "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775))
- Add a `variables` argument, to accompany `dimensions`, for the `apply` method of stac objects extended with datacube ([#782](https://github.com/stac-utils/pystac/pull/782))
- Deepcopy collection properties on clone. Implement `clone` method for `Summaries` ([#794](https://github.com/stac-utils/pystac/pull/794))
- Collection assets are now preserved when using `Collection.clone` ([#834](https://github.com/stac-utils/pystac/pull/834))
- Docstrings for `StacIO.read_text` and `StacIO.write_text` now match the type annotations for the `source` argument. ([#835](https://github.com/stac-utils/pystac/pull/835))

## [v1.4.0]
Expand Down
18 changes: 13 additions & 5 deletions pystac/collection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from html import escape
from copy import deepcopy
from datetime import datetime

from pystac.errors import STACTypeError
from pystac.html.jinja_env import get_jinja_env
from typing import (
Expand Down Expand Up @@ -446,6 +447,9 @@ class Collection(Catalog):
either a set of values or statistics such as a range.
extra_fields : Extra fields that are part of the top-level
JSON properties of the Collection.
assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All
:class:`~pystac.Asset` values in the dictionary will have their
:attr:`~pystac.Asset.owner` attribute set to the created Collection.
"""

assets: Dict[str, Asset]
Expand Down Expand Up @@ -504,6 +508,7 @@ def __init__(
keywords: Optional[List[str]] = None,
providers: Optional[List["Provider_Type"]] = None,
summaries: Optional[Summaries] = None,
assets: Optional[Dict[str, Asset]] = None,
):
super().__init__(
id,
Expand All @@ -523,6 +528,9 @@ def __init__(
self.summaries = summaries or Summaries.empty()

self.assets = {}
if assets is not None:
for k, asset in assets.items():
self.add_asset(k, asset)

def __repr__(self) -> str:
return "<Collection id={}>".format(self.id)
Expand Down Expand Up @@ -579,6 +587,7 @@ def clone(self) -> "Collection":
keywords=self.keywords.copy() if self.keywords is not None else None,
providers=deepcopy(self.providers),
summaries=self.summaries.clone(),
assets={k: asset.clone() for k, asset in self.assets.items()},
)

clone._resolved_objects.cache(clone)
Expand Down Expand Up @@ -631,7 +640,9 @@ def from_dict(
if summaries is not None:
summaries = Summaries(summaries)

assets: Optional[Dict[str, Any]] = d.get("assets", None)
assets: Optional[Dict[str, Any]] = {
k: Asset.from_dict(v) for k, v in d.get("assets", {}).items()
}
links = d.pop("links")

d.pop("stac_version")
Expand All @@ -649,6 +660,7 @@ def from_dict(
summaries=summaries,
href=href,
catalog_type=catalog_type,
assets=assets,
)

for link in links:
Expand All @@ -659,10 +671,6 @@ def from_dict(
if link["rel"] != pystac.RelType.SELF or href is None:
collection.add_link(Link.from_dict(link))

if assets is not None:
for asset_key, asset_dict in assets.items():
collection.add_asset(asset_key, Asset.from_dict(asset_dict))

if root:
collection.set_root(root)

Expand Down
19 changes: 11 additions & 8 deletions pystac/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class Item(STACObject):
belongs to.
extra_fields : Extra fields that are part of the top-level JSON
properties of the Item.
assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All
:class:`~pystac.Asset` values in the dictionary will have their
:attr:`~pystac.Asset.owner` attribute set to the created Item.
"""

assets: Dict[str, Asset]
Expand Down Expand Up @@ -107,6 +110,7 @@ def __init__(
href: Optional[str] = None,
collection: Optional[Union[str, Collection]] = None,
extra_fields: Optional[Dict[str, Any]] = None,
assets: Optional[Dict[str, Asset]] = None,
):
super().__init__(stac_extensions or [])

Expand Down Expand Up @@ -144,6 +148,11 @@ def __init__(
else:
self.collection_id = collection

self.assets = {}
if assets is not None:
for k, asset in assets.items():
self.add_asset(k, asset)

def __repr__(self) -> str:
return "<Item id={}>".format(self.id)

Expand Down Expand Up @@ -359,13 +368,11 @@ def clone(self) -> "Item":
properties=deepcopy(self.properties),
stac_extensions=deepcopy(self.stac_extensions),
collection=self.collection_id,
assets={k: asset.clone() for k, asset in self.assets.items()},
)
for link in self.links:
clone.add_link(link.clone())

for k, asset in self.assets.items():
clone.add_asset(k, asset.clone())

return clone

def _object_links(self) -> List[Union[str, pystac.RelType]]:
Expand Down Expand Up @@ -420,6 +427,7 @@ def from_dict(
stac_extensions=stac_extensions,
collection=collection_id,
extra_fields=d,
assets={k: Asset.from_dict(v) for k, v in assets.items()},
)

has_self_link = False
Expand All @@ -430,11 +438,6 @@ def from_dict(
if not has_self_link and href is not None:
item.add_link(Link.self_href(href))

for k, v in assets.items():
asset = Asset.from_dict(v)
asset.set_owner(item)
item.assets[k] = asset

if root:
item.set_root(root)

Expand Down
19 changes: 19 additions & 0 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,25 @@ def test_from_invalid_dict_raises_exception(self) -> None:
with self.assertRaises(pystac.STACTypeError):
_ = pystac.Collection.from_dict(catalog_dict)

def test_clone_preserves_assets(self) -> None:
path = TestCases.get_path("data-files/collections/with-assets.json")
original_collection = Collection.from_file(path)
assert len(original_collection.assets) > 0
assert all(
asset.owner is original_collection
for asset in original_collection.assets.values()
)

cloned_collection = original_collection.clone()

for key in original_collection.assets:
with self.subTest(f"Preserves {key} asset"):
self.assertIn(key, cloned_collection.assets)
cloned_asset = cloned_collection.assets.get(key)
if cloned_asset is not None:
with self.subTest(f"Sets owner for {key}"):
self.assertIs(cloned_asset.owner, cloned_collection)


class ExtentTest(unittest.TestCase):
def setUp(self) -> None:
Expand Down
22 changes: 15 additions & 7 deletions tests/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,23 @@ def test_0_9_item_with_no_extensions_does_not_read_collection_data(self) -> None
)
self.assertFalse(did_merge)

def test_clone_sets_asset_owner(self) -> None:
def test_clone_preserves_assets(self) -> None:
cat = TestCases.test_case_2()
item = next(iter(cat.get_all_items()))
original_asset = list(item.assets.values())[0]
assert original_asset.owner is item
original_item = next(iter(cat.get_all_items()))
assert len(original_item.assets) > 0
assert all(
asset.owner is original_item for asset in original_item.assets.values()
)

cloned_item = original_item.clone()

clone = item.clone()
clone_asset = list(clone.assets.values())[0]
self.assertIs(clone_asset.owner, clone)
for key in original_item.assets:
with self.subTest(f"Preserves {key} asset"):
self.assertIn(key, cloned_item.assets)
cloned_asset = cloned_item.assets.get(key)
if cloned_asset is not None:
with self.subTest(f"Sets owner for {key}"):
self.assertIs(cloned_asset.owner, cloned_item)

def test_make_asset_href_relative_is_noop_on_relative_hrefs(self) -> None:
cat = TestCases.test_case_2()
Expand Down

0 comments on commit b7fbd6e

Please sign in to comment.