Skip to content

Commit c275ca7

Browse files
committed
Allow resolved self links
When getting a STAC object's self href, there was a cast that assumed that the object's self link's target was a string, which was not always the case (the self link's target could be a string or another STAC object). This commit removes that cast by splitting a link's target into two separate "private" attributes, `_target_href` and `_target_object`. Fixes #499.
1 parent e4059c8 commit c275ca7

File tree

3 files changed

+62
-21
lines changed

3 files changed

+62
-21
lines changed

pystac/link.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from copy import copy
2-
from typing import Any, Dict, Optional, TYPE_CHECKING, Union, cast
2+
from typing import Any, Dict, Optional, TYPE_CHECKING, Union
33

44
import pystac
55
from pystac.utils import make_absolute_href, make_relative_href, is_absolute_href
@@ -52,11 +52,6 @@ class Link:
5252
"""The relation of the link (e.g. 'child', 'item'). Registered rel Types are
5353
preferred. See :class:`~pystac.RelType` for common media types."""
5454

55-
target: Union[str, "STACObject_Type"]
56-
"""The target of the link. If the link is unresolved, or the link is to something
57-
that is not a STACObject, the target is an HREF. If resolved, the target is a
58-
STACObject."""
59-
6055
media_type: Optional[str]
6156
"""Optional description of the media type. Registered Media Types are preferred.
6257
See :class:`~pystac.MediaType` for common media types."""
@@ -82,7 +77,12 @@ def __init__(
8277
extra_fields: Optional[Dict[str, Any]] = None,
8378
) -> None:
8479
self.rel = rel
85-
self.target = target
80+
if isinstance(target, str):
81+
self._target_href: Optional[str] = target
82+
self._target_object = None
83+
else:
84+
self._target_href = None
85+
self._target_object = target
8686
self.media_type = media_type
8787
self.title = title
8888
self.extra_fields = extra_fields or {}
@@ -119,10 +119,10 @@ def get_href(self) -> Optional[str]:
119119
In all other cases, this method will return an absolute HREF.
120120
"""
121121
# get the self href
122-
if self.is_resolved():
123-
href = cast(pystac.STACObject, self.target).get_self_href()
122+
if self._target_object:
123+
href = self._target_object.get_self_href()
124124
else:
125-
href = cast(Optional[str], self.target)
125+
href = self._target_href
126126

127127
if href and is_absolute_href(href) and self.owner and self.owner.get_root():
128128
root = self.owner.get_root()
@@ -158,16 +158,42 @@ def get_absolute_href(self) -> Optional[str]:
158158
from this link; however, if the link is relative, has no owner,
159159
and has an unresolved target, this will return a relative HREF.
160160
"""
161-
if self.is_resolved():
162-
href = cast(pystac.STACObject, self.target).get_self_href()
161+
if self._target_object:
162+
href = self._target_object.get_self_href()
163163
else:
164-
href = cast(Optional[str], self.target)
164+
href = self._target_href
165165

166166
if href is not None and self.owner is not None:
167167
href = make_absolute_href(href, self.owner.get_self_href())
168168

169169
return href
170170

171+
@property
172+
def target(self) -> Union[str, "STACObject_Type"]:
173+
"""The target of the link. If the link is unresolved, or the link is to something
174+
that is not a STACObject, the target is an HREF. If resolved, the target is a
175+
STACObject."""
176+
if self._target_object:
177+
return self._target_object
178+
elif self._target_href:
179+
return self._target_href
180+
else:
181+
raise ValueError("No target defined for link.")
182+
183+
@target.setter
184+
def target(self, target: Union[str, "STACObject_Type"]) -> None:
185+
"""Sets this link's target to a string or a STAC object."""
186+
if isinstance(target, str):
187+
self._target_href = target
188+
self._target_object = None
189+
else:
190+
self._target_href = None
191+
self._target_object = target
192+
193+
def get_target_str(self) -> Optional[str]:
194+
"""Returns this link's target as a string."""
195+
return self._target_href
196+
171197
def __repr__(self) -> str:
172198
return "<Link rel={} target={}>".format(self.rel, self.target)
173199

@@ -180,8 +206,10 @@ def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link":
180206
If provided, the root's resolved object cache is used to search for
181207
previously resolved instances of the STAC object.
182208
"""
183-
if isinstance(self.target, str):
184-
target_href = self.target
209+
if self._target_object:
210+
pass
211+
elif self._target_href:
212+
target_href = self._target_href
185213

186214
# If it's a relative link, base it off the parent.
187215
if not is_absolute_href(target_href):
@@ -221,17 +249,17 @@ def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link":
221249
if root is not None:
222250
obj = root._resolved_objects.get_or_cache(obj)
223251
obj.set_root(root)
252+
self._target_object = obj
224253
else:
225-
obj = self.target
226-
227-
self.target = obj
254+
raise ValueError("Cannot resolve STAC object without a target")
228255

229256
if (
230257
self.owner
231258
and self.rel in [pystac.RelType.CHILD, pystac.RelType.ITEM]
232259
and isinstance(self.owner, pystac.Catalog)
233260
):
234-
self.target.set_parent(self.owner)
261+
assert self._target_object
262+
self._target_object.set_parent(self.owner)
235263

236264
return self
237265

@@ -241,7 +269,7 @@ def is_resolved(self) -> bool:
241269
Returns:
242270
bool: True if this link is resolved.
243271
"""
244-
return not isinstance(self.target, str)
272+
return self._target_object is not None
245273

246274
def to_dict(self) -> Dict[str, Any]:
247275
"""Generate a dictionary representing the JSON of this serialized Link.

pystac/stac_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def get_self_href(self) -> Optional[str]:
159159
"""
160160
self_link = self.get_single_link(pystac.RelType.SELF)
161161
if self_link:
162-
return cast(str, self_link.target)
162+
return self_link.get_target_str()
163163
else:
164164
return None
165165

tests/test_link.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import datetime
2+
import os.path
23
import unittest
4+
from tempfile import TemporaryDirectory
35
from typing import Any, Dict, List
46

57
import pystac
@@ -82,6 +84,17 @@ def test_resolve_stac_object_no_root_and_target_is_item(self) -> None:
8284
link = pystac.Link("my rel", target=self.item)
8385
link.resolve_stac_object()
8486

87+
def test_resolved_self_href(self) -> None:
88+
catalog = pystac.Catalog(id="test", description="test desc")
89+
with TemporaryDirectory() as temporary_directory:
90+
catalog.normalize_and_save(temporary_directory)
91+
path = os.path.join(temporary_directory, "catalog.json")
92+
catalog = pystac.Catalog.from_file(path)
93+
link = catalog.get_single_link(pystac.RelType.SELF)
94+
assert link
95+
link.resolve_stac_object()
96+
self.assertEqual(link.get_absolute_href(), path)
97+
8598

8699
class StaticLinkTest(unittest.TestCase):
87100
def setUp(self) -> None:

0 commit comments

Comments
 (0)