Merge ~cjwatson/launchpadlib:fakelaunchpad-better-resource-creation into launchpadlib:main

Proposed by Colin Watson
Status: Merged
Merged at revision: a16d50865b379eb15a0f0a9e1ba2a327dc3eaab3
Proposed branch: ~cjwatson/launchpadlib:fakelaunchpad-better-resource-creation
Merge into: launchpadlib:main
Diff against target: 303 lines (+136/-38)
4 files modified
NEWS.rst (+2/-0)
src/launchpadlib/testing/launchpad.py (+59/-28)
src/launchpadlib/testing/testing-wadl.xml (+8/-0)
src/launchpadlib/testing/tests/test_launchpad.py (+67/-10)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+435261@code.launchpad.net

Commit message

Allow sample data with links to other entries/collections

Description of the change

`FakeLaunchpad` allowed setting sample data with plain attributes of entries, but it wasn't quite smart enough to support sample entries that link to other entries or collections, which is often necessary when testing `launchpadlib` clients in practice. It can handle this now.

I had to do a bit of flailing around with `wadllib` to figure out how all this works, but I've been able to use this for real testing of an `lpcraft` branch with some moderately complex structure in its sample data, so I'm reasonably sure I've got it right.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/NEWS.rst b/NEWS.rst
2index b3475eb..29fc779 100644
3--- a/NEWS.rst
4+++ b/NEWS.rst
5@@ -7,6 +7,8 @@ NEWS for launchpadlib
6 - Move the ``keyring`` dependency to a new ``keyring`` extra.
7 - Support setting fake methods that return None on instances of
8 ``launchpadlib.testing.launchpad.FakeLaunchpad``.
9+- Allow setting ``FakeLaunchpad`` sample data with attributes that are links
10+ to other entries or collections.
11
12 1.10.18 (2022-10-28)
13 ====================
14diff --git a/src/launchpadlib/testing/launchpad.py b/src/launchpadlib/testing/launchpad.py
15index a079da1..3b68798 100644
16--- a/src/launchpadlib/testing/launchpad.py
17+++ b/src/launchpadlib/testing/launchpad.py
18@@ -76,8 +76,6 @@ import sys
19 if sys.version_info[0] >= 3:
20 basestring = str
21
22-JSON_MEDIA_TYPE = "application/json"
23-
24
25 class IntegrityError(Exception):
26 """Raised when bad sample data is used with a L{FakeLaunchpad} instance."""
27@@ -185,6 +183,11 @@ def strip_suffix(string, suffix):
28 return string
29
30
31+def wadl_tag(tag_name):
32+ """Scope a tag name with the WADL namespace."""
33+ return "{http://research.sun.com/wadl/2006/10}" + tag_name
34+
35+
36 class FakeResource(object):
37 """
38 Represents valid sample data on L{FakeLaunchpad} instances.
39@@ -283,16 +286,17 @@ class FakeResource(object):
40 this resource or if C{values} isn't a valid object for the C{name}
41 attribute.
42 """
43- root_resource = self._application.get_resource_by_path("")
44+ xml_id = self._find_representation_id(self._resource_type, "get")
45+ representation = self._application.representation_definitions[xml_id]
46+ params = {
47+ child.name: child
48+ for child in representation.params(representation)
49+ }
50 is_link = False
51- param = root_resource.get_parameter(
52- name + "_collection_link", JSON_MEDIA_TYPE
53- )
54+ param = params.get(name + "_collection_link")
55 if param is None:
56 is_link = True
57- param = root_resource.get_parameter(
58- name + "_link", JSON_MEDIA_TYPE
59- )
60+ param = params.get(name + "_link")
61 if param is None:
62 raise IntegrityError("%s isn't a valid property." % (name,))
63 resource_type = self._get_resource_type(param)
64@@ -319,7 +323,7 @@ class FakeResource(object):
65 @return: The resource type for the parameter, or None if one isn't
66 available.
67 """
68- [link] = list(param.tag)
69+ link = param.tag.find(wadl_tag("link"))
70 name = link.get("resource_type")
71 return self._application.get_resource_type(name)
72
73@@ -411,21 +415,34 @@ class FakeResource(object):
74 name or if C{value}'s type is not valid for the attribute.
75 """
76 representation = self._application.representation_definitions[xml_id]
77- parameters = {child.get("name"): child for child in representation.tag}
78- if name not in parameters:
79- raise IntegrityError("%s not found" % name)
80- parameter = parameters[name]
81- data_type = parameter.get("type")
82- if data_type is None:
83- if not isinstance(value, basestring):
84- raise IntegrityError(
85- "%s is not a str or unicode for %s" % (value, name)
86- )
87- elif data_type == "xsd:dateTime":
88- if not isinstance(value, datetime):
89- raise IntegrityError(
90- "%s is not a datetime for %s" % (value, name)
91- )
92+ params = {
93+ child.name: child
94+ for child in representation.params(representation)
95+ }
96+ if name + "_collection_link" in params:
97+ resource_type = self._get_resource_type(
98+ params[name + "_collection_link"]
99+ )
100+ child_name, child_resource_type = self._check_collection_type(
101+ resource_type, value
102+ )
103+ elif name + "_link" in params:
104+ resource_type = self._get_resource_type(params[name + "_link"])
105+ self._check_resource_type(resource_type, value)
106+ else:
107+ param = params.get(name)
108+ if param is None:
109+ raise IntegrityError("%s not found" % name)
110+ if param.type is None:
111+ if not isinstance(value, basestring):
112+ raise IntegrityError(
113+ "%s is not a str or unicode for %s" % (value, name)
114+ )
115+ elif param.type == "xsd:dateTime":
116+ if not isinstance(value, datetime):
117+ raise IntegrityError(
118+ "%s is not a datetime for %s" % (value, name)
119+ )
120
121 def _get_method(self, resource_type, name):
122 """Get the C{name} method on C{resource_type}.
123@@ -486,9 +503,23 @@ class FakeResource(object):
124 if xml_id not in self._application.resource_types:
125 xml_id += "-resource"
126 result_resource_type = self._application.resource_types[xml_id]
127- self._check_resource_type(result_resource_type, result)
128- # XXX: Should this wrap in collection?
129- return FakeResource(self._application, result_resource_type, result)
130+ if xml_id.endswith("-page-resource"):
131+ name, child_resource_type = self._check_collection_type(
132+ result_resource_type, result
133+ )
134+ return FakeCollection(
135+ self._application,
136+ result_resource_type,
137+ result,
138+ name,
139+ child_resource_type,
140+ )
141+ else:
142+ self._check_resource_type(result_resource_type, result)
143+ resource = FakeEntry(self._application, result_resource_type)
144+ for child_name, child_value in result.items():
145+ setattr(resource, child_name, child_value)
146+ return resource
147
148 def _get_child_resource_type(self, resource_type):
149 """Get the name and resource type for the entries in a collection.
150diff --git a/src/launchpadlib/testing/testing-wadl.xml b/src/launchpadlib/testing/testing-wadl.xml
151index cc33f88..650711d 100644
152--- a/src/launchpadlib/testing/testing-wadl.xml
153+++ b/src/launchpadlib/testing/testing-wadl.xml
154@@ -187,6 +187,10 @@
155 <wadl:param style="plain" required="true"
156 path="$['name']" name="name">
157 </wadl:param>
158+ <wadl:param style="plain" required="true" name="linked_bugs_collection_link" path="$['linked_bugs_collection_link']">
159+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">The bugs linked to this branch.</wadl:doc>
160+ <wadl:link resource_type="https://api.example.com/testing/#bug-page-resource"/>
161+ </wadl:param>
162 </wadl:representation>
163
164 <wadl:resource_type id="branches">
165@@ -341,6 +345,10 @@
166 <wadl:param style="plain" required="true"
167 path="$['title']" name="title">
168 </wadl:param>
169+ <wadl:param style="plain" required="true" name="owner_link" path="$['owner_link']">
170+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">The owner's IPerson</wadl:doc>
171+ <wadl:link resource_type="https://api.example.com/testing/#person"/>
172+ </wadl:param>
173 </wadl:representation>
174
175 </wadl:application>
176diff --git a/src/launchpadlib/testing/tests/test_launchpad.py b/src/launchpadlib/testing/tests/test_launchpad.py
177index 956fc06..cad2f10 100644
178--- a/src/launchpadlib/testing/tests/test_launchpad.py
179+++ b/src/launchpadlib/testing/tests/test_launchpad.py
180@@ -42,14 +42,23 @@ class FakeResourceTest(ResourcedTestCase):
181
182 resources = [("launchpad", FakeLaunchpadResource())]
183
184- def test_repr(self):
185- """A custom C{__repr__} is provided for L{FakeResource}s."""
186+ def test_repr_entry(self):
187+ """A custom C{__repr__} is provided for L{FakeEntry}s."""
188+ bug = dict()
189+ self.launchpad.bugs = dict(entries=[bug])
190+ [bug] = list(self.launchpad.bugs)
191+ self.assertEqual(
192+ "<FakeEntry bug object at %s>" % hex(id(bug)), repr(bug)
193+ )
194+
195+ def test_repr_collection(self):
196+ """A custom C{__repr__} is provided for L{FakeCollection}s."""
197 branches = dict(total_size="test-branch")
198 self.launchpad.me = dict(getBranches=lambda statuses: branches)
199 branches = self.launchpad.me.getBranches([])
200 obj_id = hex(id(branches))
201 self.assertEqual(
202- "<FakeResource branch-page-resource object at %s>" % obj_id,
203+ "<FakeCollection branch-page-resource object at %s>" % obj_id,
204 repr(branches),
205 )
206
207@@ -72,9 +81,7 @@ class FakeResourceTest(ResourcedTestCase):
208 bug = dict(id="1", title="Bug #1")
209 self.launchpad.bugs = dict(entries=[bug])
210 [bug] = list(self.launchpad.bugs)
211- self.assertEqual(
212- "<FakeResource bug 1 at %s>" % hex(id(bug)), repr(bug)
213- )
214+ self.assertEqual("<FakeEntry bug 1 at %s>" % hex(id(bug)), repr(bug))
215
216
217 class FakeLaunchpadTest(ResourcedTestCase):
218@@ -254,16 +261,52 @@ class FakeLaunchpadTest(ResourcedTestCase):
219 self.launchpad.branches = dict(getByUniqueName=lambda name: None)
220 self.assertIsNone(self.launchpad.branches.getByUniqueName("foo"))
221
222- def test_collection_property(self):
223+ def test_entry_property(self):
224+ """
225+ Attributes that represent links to other objects are set using a
226+ dict representing the object.
227+ """
228+ bug = dict(owner=dict(name="test-person"))
229+ self.launchpad.bugs = dict(entries=[bug])
230+ bug = self.launchpad.bugs[0]
231+ self.assertEqual("test-person", bug.owner.name)
232+
233+ def test_invalid_entry_property(self):
234+ """
235+ Sample data for linked entries is validated.
236+ """
237+ bug = dict(owner=dict(foo="bar"))
238+ self.assertRaises(
239+ IntegrityError,
240+ setattr,
241+ self.launchpad,
242+ "bugs",
243+ dict(entries=[bug]),
244+ )
245+
246+ def test_top_level_collection_property(self):
247 """
248- Sample collections can be set on L{FakeLaunchpad} instances. They are
249- validated the same way other sample data is validated.
250+ Sample top-level collections can be set on L{FakeLaunchpad}
251+ instances. They are validated the same way other sample data is
252+ validated.
253 """
254 branch = dict(name="foo")
255 self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
256 branch = self.launchpad.branches.getByUniqueName("foo")
257 self.assertEqual("foo", branch.name)
258
259+ def test_collection_property(self):
260+ """
261+ Attributes that represent links to collections of other objects are
262+ set using a dict representing the collection.
263+ """
264+ bug = dict(id="1")
265+ branch = dict(linked_bugs=dict(entries=[bug]))
266+ self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
267+ branch = self.launchpad.branches.getByUniqueName("foo")
268+ [bug] = list(branch.linked_bugs)
269+ self.assertEqual("1", bug.id)
270+
271 def test_iterate_collection(self):
272 """
273 Data for a sample collection set on a L{FakeLaunchpad} instance can be
274@@ -277,7 +320,7 @@ class FakeLaunchpadTest(ResourcedTestCase):
275 self.assertEqual("1", bug.id)
276 self.assertEqual("Bug #1", bug.title)
277
278- def test_collection_with_invalid_entries(self):
279+ def test_top_level_collection_with_invalid_entries(self):
280 """
281 Sample data for each entry in a collection is validated when it's set
282 on a L{FakeLaunchpad} instance.
283@@ -291,6 +334,20 @@ class FakeLaunchpadTest(ResourcedTestCase):
284 dict(entries=[bug]),
285 )
286
287+ def test_collection_with_invalid_entries(self):
288+ """
289+ Sample data for each entry in a collection is validated when it's set
290+ on an attribute representing a link to a collection of objects.
291+ """
292+ bug = dict(foo="bar")
293+ branch = dict(linked_bugs=dict(entries=[bug]))
294+ self.launchpad.branches = dict(getByUniqueName=lambda name: branch)
295+ self.assertRaises(
296+ IntegrityError,
297+ self.launchpad.branches.getByUniqueName,
298+ "foo",
299+ )
300+
301 def test_slice_collection(self):
302 """
303 Data for a sample collection set on a L{FakeLaunchpad} instance can be

Subscribers

People subscribed via source and target branches