Skip to content

Commit caa30e6

Browse files
committed
New load() method, docs, tests (closes #29)
1 parent 4cf6e87 commit caa30e6

File tree

5 files changed

+131
-66
lines changed

5 files changed

+131
-66
lines changed

pycaching/cache.py

+98-55
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class Cache(object):
109109
"print_page": "seek/cdpf.aspx",
110110
}
111111

112-
def __init__(self, geocaching, wp, **kwargs):
112+
def __init__(self, geocaching, wp=None, **kwargs):
113113
"""Create a cache instance.
114114
115115
:param .Geocaching geocaching: Reference to :class:`.Geocaching` instance, used for loading
@@ -125,8 +125,8 @@ def __init__(self, geocaching, wp, **kwargs):
125125

126126
known_kwargs = {"name", "type", "location", "original_location", "state", "found", "size",
127127
"difficulty", "terrain", "author", "hidden", "attributes", "summary",
128-
"description", "hint", "favorites", "pm_only", "url", "waypoints", "_logbook_token",
129-
"_trackable_page_url", "guid"}
128+
"description", "hint", "favorites", "pm_only", "waypoints", "guid",
129+
"_logbook_token", "_trackable_page_url"}
130130

131131
for name in known_kwargs:
132132
if name in kwargs:
@@ -181,12 +181,13 @@ def wp(self, wp):
181181
self._wp = wp
182182

183183
@property
184+
@lazy_loaded
184185
def guid(self):
185186
"""The cache GUID. An identifier used at some places on geoaching.com
186187
187188
:type: :class:`str`
188189
"""
189-
return getattr(self, "_guid", None)
190+
return self._guid
190191

191192
@guid.setter
192193
def guid(self, guid):
@@ -532,32 +533,47 @@ def _trackable_page_url(self, trackable_page_url):
532533
self.__trackable_page_url = trackable_page_url
533534

534535
def load(self):
535-
"""Load all possible cache details.
536+
"""Load cache details.
537+
538+
.. note::
539+
This method is called automatically when you access a property which isn't yet filled in
540+
(so-called "lazy loading"). You don't have to call it explicitly.
536541
537-
Use full cache details page. Therefore all possible properties are filled in, but the
542+
This method picks the loading method based on the available cache ID:
543+
544+
+ If a GUID is known, it uses :meth:`.Cache.load_print`.
545+
+ Else if a WP is known, it uses :meth:`.Cache.load_normal`.
546+
+ Else, it throws a :class:`.LoadError`.
547+
548+
For details on different loading methods, please see their documentation. Feel free not to
549+
call this method, but please use directly that one, which better suits your needs.
550+
551+
:raise .PMOnlyException: If cache is PM only and current user is basic member.
552+
:raise .LoadError: If cache cannot be loaded.
553+
"""
554+
if hasattr(self, "_guid"):
555+
return self.load_print()
556+
elif hasattr(self, "_wp"):
557+
return self.load_normal()
558+
else:
559+
raise errors.LoadError("Cache lacks info for loading")
560+
561+
def load_normal(self):
562+
"""Load all cache details.
563+
564+
It uses a full cache details page. Therefore all possible properties are filled in, but the
538565
loading is a bit slow.
539566
540567
If you want to load basic details about a PM only cache, the :class:`.PMOnlyException` is
541-
still thrown, but avaliable details are filled in. If you know, that the cache you are
568+
thrown, but all available details are filled in. If you know, that the cache you are
542569
loading is PM only, please consider using :meth:`load_quick` as it will load the same
543-
details, but quicker.
544-
545-
.. note::
546-
This method is called automatically when you access a property which isn't yet filled in
547-
(so-called "lazy loading"). You don't have to call it explicitly.
570+
details, but faster.
548571
549572
:raise .PMOnlyException: If cache is PM only and current user is basic member.
550573
:raise .LoadError: If cache loading fails (probably because of not existing cache).
551574
"""
552575
try:
553-
# pick url based on what info we have right now
554-
if hasattr(self, "url"):
555-
root = self.geocaching._request(self.url)
556-
elif hasattr(self, "_wp"):
557-
root = self.geocaching._request(self._urls["cache_details"],
558-
params={"wp": self._wp})
559-
else:
560-
raise errors.LoadError("Cache lacks info for loading")
576+
root = self.geocaching._request(self._urls["cache_details"], params={"wp": self._wp})
561577
except errors.Error as e:
562578
# probably 404 during cache loading - cache not exists
563579
raise errors.LoadError("Error in loading cache") from e
@@ -567,7 +583,7 @@ def load(self):
567583

568584
cache_details = root.find(id="ctl00_divContentMain") if self.pm_only else root.find(id="cacheDetails")
569585

570-
# details also avaliable for basic members for PM only caches -----------------------------
586+
# details also available for basic members for PM only caches -----------------------------
571587

572588
if self.pm_only:
573589
self.wp = cache_details.find("li", "li__gccode").text.strip()
@@ -587,6 +603,7 @@ def load(self):
587603
self.size = Size.from_string(details[8])
588604

589605
self.favorites = int(details[11])
606+
590607
else:
591608
# parse from <title> - get first word
592609
try:
@@ -668,27 +685,41 @@ def load(self):
668685
else:
669686
self._trackable_page_url = None
670687

671-
# Additional Waypoints
688+
# additional Waypoints
672689
self.waypoints = Waypoint.from_html(root, "ctl00_ContentBody_Waypoints")
673690

674-
logging.debug("Cache loaded: {}".format(self))
691+
logging.debug("Cache loaded (normal): {}".format(self))
675692

676693
def load_quick(self):
677694
"""Load basic cache details.
678695
679-
Use information from geocaching map tooltips. Therefore loading is very quick, but
680-
the only loaded properties are: `name`, `type`, `state`, `size`, `difficulty`, `terrain`,
681-
`hidden`, `author`, `favorites` and `pm_only`.
696+
Uses a data from geocaching map tooltips. Therefore loading is very quick, but the only
697+
loaded properties are:
698+
699+
+ `name`
700+
+ `type`
701+
+ `state`
702+
+ `size`
703+
+ `difficulty`
704+
+ `terrain`
705+
+ `hidden`
706+
+ `author`
707+
+ `favorites`
708+
+ `pm_only`
682709
683710
:raise .LoadError: If cache loading fails (probably because of not existing cache).
684711
"""
685-
res = self.geocaching._request(self._urls["tiles_server"],
686-
params={"i": self.wp},
687-
expect="json")
712+
try:
713+
res = self.geocaching._request(self._urls["tiles_server"],
714+
params={"i": self._wp},
715+
expect="json")
716+
717+
if res["status"] == "failed" or len(res["data"]) != 1:
718+
msg = res["msg"] if "msg" in res else "Unknown error (probably not existing cache)"
719+
raise errors.LoadError(msg)
688720

689-
if res["status"] == "failed" or len(res["data"]) != 1:
690-
msg = res["msg"] if "msg" in res else "Unknown error (probably not existing cache)"
691-
raise errors.LoadError("Cache {} cannot be loaded: {}".format(self, msg))
721+
except errors.Error as e:
722+
raise errors.LoadError("Error in loading cache") from e
692723

693724
data = res["data"][0]
694725

@@ -705,37 +736,49 @@ def load_quick(self):
705736
self.pm_only = data["subrOnly"]
706737
self.guid = res["data"][0]["g"]
707738

708-
logging.debug("Cache loaded: {}".format(self))
739+
logging.debug("Cache loaded (quick): {}".format(self))
709740

710-
def load_by_guid(self):
711-
"""Load cache details using the GUID to request and parse the caches
712-
'print-page'. Loading as many properties as possible except the
713-
following ones, since they are not present on the 'print-page':
741+
def load_print(self):
742+
"""Load most of the cache details.
714743
715-
+ original_location
716-
+ state
717-
+ found
718-
+ pm_only
744+
Uses a cache print page. This is significantly faster, but requires a GUID to be set. If
745+
the GUID is missing, it calls :meth:`.Cache.load_quick` to get it by a WP, which is both
746+
still faster than :meth:`.Cache.load_normal`!
719747
720-
:raise .PMOnlyException: If the PM only warning is shown on the page
748+
However, not all properties are presented on the print page, so the following ones are not
749+
loaded:
750+
751+
+ `original_location`
752+
+ `state`
753+
+ `found`
754+
+ `pm_only`
755+
756+
Also, in comparison to :meth:`.Cache.load_normal` – if the cache is PM-only, this method
757+
doesn't load anything.
758+
759+
:raise .PMOnlyException: If the cache is PM only.
760+
:raise .LoadError: If cache loading fails (probably because of not existing cache).
721761
"""
722-
# If GUID has not yet been set, load it using the "tiles_server"
723-
# utilizing `load_quick()`
724-
if not self.guid:
762+
# if GUID has not yet been set, load it using the `load_quick()`
763+
# the getattr() will prevent lazy-loading of GUID
764+
if not getattr(self, "_guid", None):
725765
self.load_quick()
726766

727-
res = self.geocaching._request(self._urls["print_page"],
728-
params={"guid": self.guid})
767+
try:
768+
res = self.geocaching._request(self._urls["print_page"], params={"guid": self._guid})
769+
except errors.Error as e:
770+
raise errors.LoadError("Error in loading cache") from e
771+
729772
if res.find("p", "Warning") is not None:
730773
raise errors.PMOnlyException()
774+
775+
self.wp = res.find(id="Header").find_all("h1")[-1].text
776+
731777
content = res.find(id="Content")
732778

733779
self.name = content.find("h2").text
734780

735-
self.wp = res.find(id="Header").find_all("h1")[-1].text
736-
737-
self.location = Point.from_string(
738-
content.find("p", "LatLong Meta").text)
781+
self.location = Point.from_string(content.find("p", "LatLong Meta").text)
739782

740783
type_img = os.path.basename(content.find("img").get("src"))
741784
self.type = Type.from_filename(os.path.splitext(type_img)[0])
@@ -764,11 +807,9 @@ def load_by_guid(self):
764807
in attributes_raw if not appendix.startswith("blank")
765808
}
766809

767-
self.summary = content.find(
768-
"h2", text="Short Description").find_next("div").text
810+
self.summary = content.find("h2", text="Short Description").find_next("div").text
769811

770-
self.description = content.find(
771-
"h2", text="Long Description").find_next("div").text
812+
self.description = content.find("h2", text="Long Description").find_next("div").text
772813

773814
self.hint = content.find(id="uxEncryptedHint").text
774815

@@ -777,6 +818,8 @@ def load_by_guid(self):
777818

778819
self.waypoints = Waypoint.from_html(content, "Waypoints")
779820

821+
logging.debug("Cache loaded (print): {}".format(self))
822+
780823
def _logbook_get_page(self, page=0, per_page=25):
781824
"""Load one page from logbook.
782825

test/test_cache.py

+29-11
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def test_log_page_url(self):
171171

172172

173173
class TestMethods(unittest.TestCase):
174+
_multiprocess_shared_ = True
174175

175176
@classmethod
176177
def setUpClass(cls):
@@ -179,13 +180,31 @@ def setUpClass(cls):
179180
cls.c = Cache(cls.gc, "GC1PAR2")
180181
cls.c.load()
181182

182-
def test_load(self):
183+
@mock.patch("pycaching.Cache.load_normal")
184+
@mock.patch("pycaching.Cache.load_print")
185+
def test_load(self, mock_load_print, mock_load_normal):
186+
with self.subTest("use load_print for GUID"):
187+
cache = Cache(self.gc, guid="5f45114d-1d79-4fdb-93ae-8f49f1d27188")
188+
cache.load()
189+
self.assertTrue(mock_load_print.called)
190+
191+
with self.subTest("use load_normal for WP"):
192+
cache = Cache(self.gc, wp="GC4808G")
193+
cache.load()
194+
self.assertTrue(mock_load_normal.called)
195+
196+
with self.subTest("fail for no loading info"):
197+
with self.assertRaises(LoadError):
198+
cache = Cache(self.gc)
199+
cache.load()
200+
201+
def test_load_normal(self):
183202
with self.subTest("normal (with explicit call of load())"):
184203
cache = Cache(self.gc, "GC4808G")
185-
cache.load()
204+
cache.load_normal()
186205
self.assertEqual("Nekonecne ticho", cache.name)
187206

188-
with self.subTest("normal"):
207+
with self.subTest("normal (lazy-loaded)"):
189208
cache = Cache(self.gc, "GC4808G")
190209
self.assertEqual("Nekonecne ticho", cache.name)
191210

@@ -196,12 +215,12 @@ def test_load(self):
196215
with self.subTest("PM only"):
197216
with self.assertRaises(PMOnlyException):
198217
cache = Cache(self.gc, "GC3AHDM")
199-
cache.load()
218+
cache.load_normal()
200219

201220
with self.subTest("fail"):
202221
with self.assertRaises(LoadError):
203222
cache = Cache(self.gc, "GC123456")
204-
cache.load()
223+
cache.load_normal()
205224

206225
def test_load_quick(self):
207226
with self.subTest("normal"):
@@ -216,12 +235,11 @@ def test_load_quick(self):
216235
cache = Cache(self.gc, "GC123456")
217236
cache.load_quick()
218237

219-
@mock.patch("pycaching.Cache.load")
220-
@mock.patch("pycaching.Cache.load_quick")
221-
def test_load_by_guid(self, mock_load_quick, mock_load):
238+
@mock.patch.object(Cache, "load_quick")
239+
def test_load_print(self, mock_load_quick):
222240
with self.subTest("normal"):
223241
cache = Cache(self.gc, "GC2WXPN", guid="5f45114d-1d79-4fdb-93ae-8f49f1d27188")
224-
cache.load_by_guid()
242+
cache.load_print()
225243
self.assertEqual(cache.name, "Der Schatz vom Luftschloss")
226244
self.assertEqual(cache.location, Point("N 49° 57.895' E 008° 12.988'"))
227245
self.assertEqual(cache.type, Type.mystery)
@@ -248,12 +266,12 @@ def test_load_by_guid(self, mock_load_quick, mock_load):
248266
with self.subTest("PM-only"):
249267
cache = Cache(self.gc, "GC6MKEF", guid="53d34c4d-12b5-4771-86d3-89318f71efb1")
250268
with self.assertRaises(PMOnlyException):
251-
cache.load_by_guid()
269+
cache.load_print()
252270

253271
with self.subTest("calls load_quick if no guid"):
254272
cache = Cache(self.gc, "GC2WXPN")
255273
with self.assertRaises(Exception):
256-
cache.load_by_guid() # Raises error since we mocked load_quick()
274+
cache.load_print() # raises error since we mocked load_quick()
257275
self.assertTrue(mock_load_quick.called)
258276

259277
def test_load_trackables(self):

test/test_geo.py

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def test_diagonal(self):
179179

180180

181181
class TestTile(unittest.TestCase):
182+
_multiprocess_shared_ = True
182183

183184
# see
184185
# http://gis.stackexchange.com/questions/8650/how-to-measure-the-accuracy-of-latitude-and-longitude

test/test_geocaching.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919

2020
class TestMethods(unittest.TestCase):
21+
_multiprocess_shared_ = True
2122

2223
@classmethod
2324
def setUpClass(cls):
@@ -242,6 +243,7 @@ def test_load_credentials(self):
242243

243244

244245
class TestShortcuts(unittest.TestCase):
246+
_multiprocess_shared_ = True
245247

246248
@classmethod
247249
def setUpClass(cls):

test/test_trackable.py

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def test_log_page_url(self):
5050

5151

5252
class TestMethods(unittest.TestCase):
53+
_multiprocess_shared_ = True
5354

5455
@classmethod
5556
def setUpClass(cls):

0 commit comments

Comments
 (0)