Merge lp:~exarkun/divmod.org/explained-inventory-1193494 into lp:divmod.org
- explained-inventory-1193494
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 2710 | ||||
Proposed branch: | lp:~exarkun/divmod.org/explained-inventory-1193494 | ||||
Merge into: | lp:divmod.org | ||||
Diff against target: |
843 lines (+493/-50) 13 files modified
Imaginary/ExampleGame/examplegame/test/test_furniture.py (+4/-7) Imaginary/ExampleGame/examplegame/test/test_glass.py (+1/-1) Imaginary/ExampleGame/examplegame/test/test_japanese.py (+4/-3) Imaginary/ExampleGame/examplegame/test/test_mice.py (+6/-5) Imaginary/imaginary/language.py (+79/-2) Imaginary/imaginary/objects.py (+73/-3) Imaginary/imaginary/test/commandutils.py (+25/-10) Imaginary/imaginary/test/test_actions.py (+7/-7) Imaginary/imaginary/test/test_container.py (+170/-6) Imaginary/imaginary/test/test_garments.py (+4/-3) Imaginary/imaginary/test/test_illumination.py (+1/-1) Imaginary/imaginary/test/test_language.py (+116/-0) Imaginary/imaginary/world.py (+3/-2) |
||||
To merge this branch: | bzr merge lp:~exarkun/divmod.org/explained-inventory-1193494 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jean-Paul Calderone | Approve | ||
Review via email: mp+172932@code.launchpad.net |
Commit message
Description of the change
This introduces a new API, imaginary.
The branch uses this functionality to vary the way the contents of things are described. Locations describe their contents as "Here, you see {contents}." (clunky because lack of verb conjugation machinery). Generic containers describe their contents as "{subject:pronoun} contains {contents}.". Players describe their contents as "{subject:pronoun} is carrying {contents}."
Likely some follow-up work is needed to make it terribly convenient to create containers that describe their contents properly.
Jonathan Jacobs (jjacobs) wrote : | # |
Jean-Paul Calderone (exarkun) wrote : | # |
Jonathan, thank you very much for your input!
Jeremy Thurgood (jerith) wrote : | # |
I'm not sufficiently familiar with the codebase to do a thorough review, but I did notice a few small things:
* ExpressContents
* ConceptTemplate
* ExpressContents
* You could probably move ExpressContents
- 2708. By Jean-Paul Calderone
-
Add some docstrings
- 2709. By Jean-Paul Calderone
-
Refactor some test code to remove duplicate container population.
- 2710. By Jean-Paul Calderone
-
A note about a nice future direction
- 2711. By Jean-Paul Calderone
-
Squash template errors into some readable text. Oops, try not to screw up!
- 2712. By Jean-Paul Calderone
-
Add a test for another case of expansion - happily, passing.
- 2713. By Jean-Paul Calderone
-
Be a little more consistent, both in the implementation and the tests, for ExpressContents
.concepts in the empty case.
Jean-Paul Calderone (exarkun) wrote : | # |
Thanks again for the reviews jerith and jjacobs. I've addressed all the points you raised.
glyph expressed his approval for this branch on IRC as well, so I'm convinced it's time to merge these changes.
Preview Diff
1 | === modified file 'Imaginary/ExampleGame/examplegame/test/test_furniture.py' |
2 | --- Imaginary/ExampleGame/examplegame/test/test_furniture.py 2009-08-17 02:40:03 +0000 |
3 | +++ Imaginary/ExampleGame/examplegame/test/test_furniture.py 2013-07-24 22:23:27 +0000 |
4 | @@ -5,9 +5,9 @@ |
5 | |
6 | from twisted.trial.unittest import TestCase |
7 | |
8 | -from imaginary.test.commandutils import CommandTestCaseMixin, E |
9 | +from imaginary.test.commandutils import CommandTestCaseMixin, E, createLocation |
10 | |
11 | -from imaginary.objects import Thing, Container, Exit |
12 | +from imaginary.objects import Thing, Exit |
13 | from examplegame.furniture import Chair |
14 | |
15 | class SitAndStandTests(CommandTestCaseMixin, TestCase): |
16 | @@ -79,8 +79,7 @@ |
17 | first. |
18 | """ |
19 | self.test_sitDown() |
20 | - otherRoom = Thing(store=self.store, name=u'elsewhere') |
21 | - Container.createFor(otherRoom, capacity=1000) |
22 | + otherRoom = createLocation(self.store, u"elsewhere", None).thing |
23 | Exit.link(self.location, otherRoom, u'north') |
24 | self.assertCommandOutput( |
25 | "go north", |
26 | @@ -102,6 +101,4 @@ |
27 | # at. |
28 | [E("[ Test Location ]"), |
29 | "Location for testing.", |
30 | - "Observer Player and a chair"]) |
31 | - |
32 | - |
33 | + "Here, you see Observer Player and a chair."]) |
34 | |
35 | === modified file 'Imaginary/ExampleGame/examplegame/test/test_glass.py' |
36 | --- Imaginary/ExampleGame/examplegame/test/test_glass.py 2009-08-17 02:40:03 +0000 |
37 | +++ Imaginary/ExampleGame/examplegame/test/test_glass.py 2013-07-24 22:23:27 +0000 |
38 | @@ -50,7 +50,7 @@ |
39 | "look at box", |
40 | [E("[ box ]"), |
41 | "The system under test.", |
42 | - "a ball"]) |
43 | + "It contains a ball."]) |
44 | |
45 | |
46 | def test_take(self): |
47 | |
48 | === modified file 'Imaginary/ExampleGame/examplegame/test/test_japanese.py' |
49 | --- Imaginary/ExampleGame/examplegame/test/test_japanese.py 2009-06-29 04:03:17 +0000 |
50 | +++ Imaginary/ExampleGame/examplegame/test/test_japanese.py 2013-07-24 22:23:27 +0000 |
51 | @@ -414,8 +414,9 @@ |
52 | """ |
53 | clock = task.Clock() |
54 | |
55 | - closet = objects.Thing(store=self.store, name=u"Closet") |
56 | - closetContainer = objects.Container.createFor(closet, capacity=500) |
57 | + closetContainer = commandutils.createLocation( |
58 | + self.store, u"Closet", None) |
59 | + closet = closetContainer.thing |
60 | |
61 | mouse = mice.createHiraganaMouse( |
62 | store=self.store, |
63 | @@ -432,7 +433,7 @@ |
64 | "north", |
65 | [commandutils.E("[ Closet ]"), |
66 | commandutils.E("( south )"), |
67 | - commandutils.E(self.mouseName)], |
68 | + commandutils.E(u"Here, you see " + self.mouseName + u".")], |
69 | ["Test Player leaves north."]) |
70 | |
71 | clock.advance(mousehood.challengeInterval) |
72 | |
73 | === modified file 'Imaginary/ExampleGame/examplegame/test/test_mice.py' |
74 | --- Imaginary/ExampleGame/examplegame/test/test_mice.py 2009-06-29 04:03:17 +0000 |
75 | +++ Imaginary/ExampleGame/examplegame/test/test_mice.py 2013-07-24 22:23:27 +0000 |
76 | @@ -14,8 +14,9 @@ |
77 | def setUp(self): |
78 | self.store = store.Store() |
79 | |
80 | - self.location = objects.Thing(store=self.store, name=u"Place") |
81 | - self.locationContainer = objects.Container.createFor(self.location, capacity=1000) |
82 | + self.locationContainer = commandutils.createLocation( |
83 | + self.store, u"Place", None) |
84 | + self.location = self.locationContainer.thing |
85 | |
86 | self.alice = objects.Thing(store=self.store, name=u"Alice") |
87 | self.actor = objects.Actor.createFor(self.alice) |
88 | @@ -136,8 +137,8 @@ |
89 | intelligence = iimaginary.IActor(mouse).getIntelligence() |
90 | intelligence._callLater = clock.callLater |
91 | |
92 | - elsewhere = objects.Thing(store=self.store, name=u"Mouse Hole") |
93 | - objects.Container.createFor(elsewhere, capacity=1000) |
94 | + elsewhere = commandutils.createLocation( |
95 | + self.store, u"Mouse Hole", None).thing |
96 | |
97 | objects.Exit.link(self.location, elsewhere, u"south") |
98 | |
99 | @@ -147,7 +148,7 @@ |
100 | "south", |
101 | [commandutils.E("[ Mouse Hole ]"), |
102 | commandutils.E("( north )"), |
103 | - commandutils.E("a squeaker")], |
104 | + commandutils.E("Here, you see a squeaker.")], |
105 | ['Test Player leaves south.']) |
106 | |
107 | clock.advance(0) |
108 | |
109 | === modified file 'Imaginary/imaginary/language.py' |
110 | --- Imaginary/imaginary/language.py 2009-08-17 02:40:03 +0000 |
111 | +++ Imaginary/imaginary/language.py 2013-07-24 22:23:27 +0000 |
112 | @@ -6,8 +6,9 @@ |
113 | |
114 | """ |
115 | import types |
116 | +from string import Formatter |
117 | |
118 | -from zope.interface import implements |
119 | +from zope.interface import implements, implementer |
120 | |
121 | from twisted.python.components import registerAdapter |
122 | |
123 | @@ -115,8 +116,9 @@ |
124 | def flattenWithoutColors(vt102): |
125 | return T.flatten(vt102, useColors=False) |
126 | |
127 | + |
128 | +@implementer(iimaginary.IConcept) |
129 | class BaseExpress(object): |
130 | - implements(iimaginary.IConcept) |
131 | |
132 | def __init__(self, original): |
133 | self.original = original |
134 | @@ -310,3 +312,78 @@ |
135 | yield u'and ' |
136 | yield desc[-1] |
137 | |
138 | + |
139 | + |
140 | +class ConceptTemplate(object): |
141 | + """ |
142 | + A L{ConceptTemplate} wraps a text template which may intersperse literal |
143 | + strings with markers for substitution. |
144 | + |
145 | + Substitution markers follow U{the syntax for str.format<http://docs.python.org/2/library/string.html#format-string-syntax>}. |
146 | + |
147 | + Values for field names are supplied to the L{expand} method. |
148 | + """ |
149 | + def __init__(self, templateText): |
150 | + """ |
151 | + @param templateText: The text of the template. For example, |
152 | + C{u"Hello, {target:name}."}. |
153 | + @type templateText: L{unicode} |
154 | + """ |
155 | + self.templateText = templateText |
156 | + |
157 | + |
158 | + def expand(self, values): |
159 | + """ |
160 | + Generate concepts based on the template. |
161 | + |
162 | + @param values: A L{dict} mapping substitution markers to application |
163 | + objects from which to take values for those substitutions. For |
164 | + example, a key might be C{u"target"}. The associated value will be |
165 | + sustituted each place C{u"{target}"} appears in the template |
166 | + string. Or, the value's name will be substituted each place |
167 | + C{u"{target:name}"} appears in the template string. |
168 | + @type values: L{dict} mapping L{unicode} to L{object} |
169 | + |
170 | + @return: An iterator the combined elements of which represent the |
171 | + result of expansion of the template. The elements are adaptable to |
172 | + L{IConcept}. |
173 | + """ |
174 | + parts = Formatter().parse(self.templateText) |
175 | + for (literalText, fieldName, formatSpec, conversion) in parts: |
176 | + if literalText: |
177 | + yield ExpressString(literalText) |
178 | + if fieldName: |
179 | + try: |
180 | + target = values[fieldName.lower()] |
181 | + except KeyError: |
182 | + extra = u"" |
183 | + if formatSpec: |
184 | + extra = u" '%s'" % (formatSpec,) |
185 | + yield u"<missing target '%s' for%s expansion>" % ( |
186 | + fieldName, extra) |
187 | + else: |
188 | + if formatSpec: |
189 | + # A nice enhancement would be to delegate this logic to target |
190 | + try: |
191 | + expander = getattr(self, '_expand_' + formatSpec.upper()) |
192 | + except AttributeError: |
193 | + yield u"<'%s' unsupported by target '%s'>" % ( |
194 | + formatSpec, fieldName) |
195 | + else: |
196 | + yield expander(target) |
197 | + else: |
198 | + yield target |
199 | + |
200 | + |
201 | + def _expand_NAME(self, target): |
202 | + """ |
203 | + Get the name of a L{Thing}. |
204 | + """ |
205 | + return target.name |
206 | + |
207 | + |
208 | + def _expand_PRONOUN(self, target): |
209 | + """ |
210 | + Get the personal pronoun of a L{Thing}. |
211 | + """ |
212 | + return Noun(target).heShe() |
213 | |
214 | === modified file 'Imaginary/imaginary/objects.py' |
215 | --- Imaginary/imaginary/objects.py 2011-09-16 20:39:43 +0000 |
216 | +++ Imaginary/imaginary/objects.py 2013-07-24 22:23:27 +0000 |
217 | @@ -708,9 +708,7 @@ |
218 | @return: an L{ExpressSurroundings} with an iterable of all visible |
219 | contents of this container. |
220 | """ |
221 | - return ExpressSurroundings( |
222 | - self.thing.idea.obtain( |
223 | - _ContainedBy(CanSee(ProviderOf(iimaginary.IThing)), self))) |
224 | + return ExpressContents(self) |
225 | |
226 | |
227 | |
228 | @@ -838,6 +836,14 @@ |
229 | """ |
230 | A generic L{_Enhancement} that implements containment. |
231 | """ |
232 | + contentsTemplate = attributes.text( |
233 | + doc=""" |
234 | + Define how the contents of this container are presented to observers. |
235 | + Certain substrings will be given special treatment. |
236 | + |
237 | + @see: L{imaginary.language.ConceptTemplate} |
238 | + """, |
239 | + allowNone=True, default=None) |
240 | |
241 | capacity = attributes.integer( |
242 | doc=""" |
243 | @@ -856,6 +862,70 @@ |
244 | |
245 | |
246 | |
247 | +class ExpressContents(language.Sentence): |
248 | + """ |
249 | + A concept representing the things contained by another thing - excluding |
250 | + the observer of the concept. |
251 | + """ |
252 | + _CONDITION = CanSee(ProviderOf(iimaginary.IThing)) |
253 | + |
254 | + def _contentConcepts(self, observer): |
255 | + """ |
256 | + Get concepts for the contents of the thing wrapped by this concept. |
257 | + |
258 | + @param observer: The L{objects.Thing} which will observe these |
259 | + concepts. |
260 | + |
261 | + @return: A L{list} of the contents of C{self.original}, excluding |
262 | + C{observer}. |
263 | + """ |
264 | + container = self.original |
265 | + idea = container.thing.idea |
266 | + return [ |
267 | + concept |
268 | + for concept |
269 | + in idea.obtain(_ContainedBy(self._CONDITION, container)) |
270 | + if concept is not observer] |
271 | + |
272 | + |
273 | + @property |
274 | + def template(self): |
275 | + """ |
276 | + This is the template string which is used to construct the overall |
277 | + concept, indicating what the container is and what its contents are. |
278 | + """ |
279 | + template = self.original.contentsTemplate |
280 | + if template is None: |
281 | + template = u"{subject:pronoun} contains {contents}." |
282 | + return template |
283 | + |
284 | + |
285 | + def expand(self, template, observer): |
286 | + """ |
287 | + Expand the given template using the wrapped container's L{Thing} as the |
288 | + subject. |
289 | + |
290 | + C{u"contents"} is also available for substitution with the contents of |
291 | + the container. |
292 | + |
293 | + @return: An iterator of concepts derived from the given template. |
294 | + """ |
295 | + return language.ConceptTemplate(template).expand(dict( |
296 | + subject=self.original.thing, |
297 | + contents=language.ItemizedList(self._contentConcepts(observer)))) |
298 | + |
299 | + |
300 | + def concepts(self, observer): |
301 | + """ |
302 | + Return a L{list} of L{IConcept} providers which express the contents of |
303 | + the wrapped container. |
304 | + """ |
305 | + if self._contentConcepts(observer): |
306 | + return list(self.expand(self.template, observer)) |
307 | + return [] |
308 | + |
309 | + |
310 | + |
311 | class ExpressCondition(language.BaseExpress): |
312 | implements(iimaginary.IConcept) |
313 | |
314 | |
315 | === modified file 'Imaginary/imaginary/test/commandutils.py' |
316 | --- Imaginary/imaginary/test/commandutils.py 2009-08-17 02:40:03 +0000 |
317 | +++ Imaginary/imaginary/test/commandutils.py 2013-07-24 22:23:27 +0000 |
318 | @@ -38,21 +38,14 @@ |
319 | @ivar observer: The L{Thing} representing the observer who sees the main |
320 | player's actions. |
321 | """ |
322 | - |
323 | def setUp(self): |
324 | """ |
325 | Set up a store with a location, a player and an observer. |
326 | """ |
327 | self.store = store.Store() |
328 | - |
329 | - self.location = objects.Thing( |
330 | - store=self.store, |
331 | - name=u"Test Location", |
332 | - description=u"Location for testing.", |
333 | - proper=True) |
334 | - |
335 | - locContainer = objects.Container.createFor(self.location, capacity=1000) |
336 | - |
337 | + locContainer = createLocation( |
338 | + self.store, u"Test Location", u"Location for testing.") |
339 | + self.location = locContainer.thing |
340 | self.world = ImaginaryWorld(store=self.store, origin=self.location) |
341 | self.player = self.world.create( |
342 | u"Test Player", gender=language.Gender.FEMALE) |
343 | @@ -216,6 +209,28 @@ |
344 | |
345 | |
346 | |
347 | +def createLocation(store, name, description): |
348 | + """ |
349 | + Create a new L{Thing} and create an L{objects.Container} for it. |
350 | + |
351 | + @param name: The name given to the created L{Thing}. |
352 | + @type name: L{unicode} |
353 | + |
354 | + @param description: The description given to the created L{Thing}. |
355 | + @type description: L{unicode} |
356 | + |
357 | + @return: The containment enhancement of the created L{Thing}. |
358 | + @rtype: L{objects.Container}. |
359 | + """ |
360 | + location = objects.Thing( |
361 | + store=store, name=name, description=description, proper=True) |
362 | + |
363 | + return objects.Container.createFor( |
364 | + location, capacity=1000, |
365 | + contentsTemplate=u"Here, you see {contents}.") |
366 | + |
367 | + |
368 | + |
369 | def createPlayer(store, name): |
370 | """ |
371 | Create a mock player with a mock intelligence with the given |
372 | |
373 | === modified file 'Imaginary/imaginary/test/test_actions.py' |
374 | --- Imaginary/imaginary/test/test_actions.py 2013-07-21 13:47:12 +0000 |
375 | +++ Imaginary/imaginary/test/test_actions.py 2013-07-24 22:23:27 +0000 |
376 | @@ -207,13 +207,13 @@ |
377 | "look", |
378 | [E("[ Test Location ]"), |
379 | "Location for testing.", |
380 | - "Observer Player"]) |
381 | + "Here, you see Observer Player."]) |
382 | |
383 | self._test( |
384 | "look here", |
385 | [E("[ Test Location ]"), |
386 | "Location for testing.", |
387 | - "Observer Player"]) |
388 | + "Here, you see Observer Player."]) |
389 | |
390 | objects.Exit.link(self.location, self.location, u"north") |
391 | self._test( |
392 | @@ -221,7 +221,7 @@ |
393 | [E("[ Test Location ]"), |
394 | E("( north south )"), |
395 | "Location for testing.", |
396 | - "Observer Player"]) |
397 | + "Here, you see Observer Player."]) |
398 | |
399 | self._test( |
400 | "look me", |
401 | @@ -300,7 +300,7 @@ |
402 | "search here", |
403 | [E("[ Test Location ]"), |
404 | "Location for testing.", |
405 | - "Observer Player", |
406 | + "Here, you see Observer Player.", |
407 | ""]) |
408 | |
409 | self._test( |
410 | @@ -512,7 +512,7 @@ |
411 | [E("[ Test Location ]"), |
412 | E("( west )"), |
413 | "Location for testing.", |
414 | - "Observer Player"], |
415 | + "Here, you see Observer Player."], |
416 | ["Test Player arrives from the west."]) |
417 | |
418 | |
419 | @@ -531,7 +531,7 @@ |
420 | "north", |
421 | [E("[ Test Location ]"), |
422 | "Location for testing.", |
423 | - "Observer Player"], |
424 | + "Here, you see Observer Player."], |
425 | ["Test Player arrives from the south."]) |
426 | self.player.moveTo(secretRoom) |
427 | myExit.name = u'elsewhere' |
428 | @@ -539,7 +539,7 @@ |
429 | "go elsewhere", |
430 | [E("[ Test Location ]"), |
431 | "Location for testing.", |
432 | - "Observer Player"], |
433 | + "Here, you see Observer Player."], |
434 | ["Test Player arrives."]) |
435 | |
436 | |
437 | |
438 | === modified file 'Imaginary/imaginary/test/test_container.py' |
439 | --- Imaginary/imaginary/test/test_container.py 2009-08-17 02:40:03 +0000 |
440 | +++ Imaginary/imaginary/test/test_container.py 2013-07-24 22:23:27 +0000 |
441 | @@ -1,11 +1,15 @@ |
442 | # -*- test-case-name: imaginary.test -*- |
443 | |
444 | +from zope.interface.verify import verifyObject |
445 | + |
446 | from twisted.trial import unittest |
447 | |
448 | from axiom import store |
449 | |
450 | -from imaginary import eimaginary, objects |
451 | -from imaginary.test.commandutils import CommandTestCaseMixin, E |
452 | +from imaginary import iimaginary, eimaginary, objects |
453 | +from imaginary.test.commandutils import ( |
454 | + CommandTestCaseMixin, E, createLocation, flatten) |
455 | +from imaginary.language import ExpressList |
456 | |
457 | class ContainerTestCase(unittest.TestCase): |
458 | def setUp(self): |
459 | @@ -69,6 +73,165 @@ |
460 | |
461 | |
462 | |
463 | +class ExpressContentsTests(unittest.TestCase): |
464 | + """ |
465 | + Tests for L{ExpressContents}. |
466 | + """ |
467 | + def setUp(self): |
468 | + self.store = store.Store() |
469 | + self.box = objects.Thing(store=self.store, name=u"box") |
470 | + self.container = objects.Container.createFor(self.box, capacity=123) |
471 | + self.concept = objects.ExpressContents(self.container) |
472 | + self.observer = objects.Thing(store=self.store, name=u"observer") |
473 | + |
474 | + |
475 | + def addContents(self, names): |
476 | + """ |
477 | + Add a new L{Thing} to C{self.container} for each element of C{names}. |
478 | + |
479 | + @param names: An iterable of L{unicode} giving the names of the things |
480 | + to create and add. |
481 | + """ |
482 | + things = [] |
483 | + for name in names: |
484 | + thing = objects.Thing(store=self.store, name=name, proper=True) |
485 | + thing.moveTo(self.container) |
486 | + things.append(thing) |
487 | + return things |
488 | + |
489 | + |
490 | + def test_interface(self): |
491 | + """ |
492 | + An instance of L{ExpressContents} provides L{IConcept}. |
493 | + """ |
494 | + self.assertTrue(verifyObject(iimaginary.IConcept, self.concept)) |
495 | + |
496 | + |
497 | + def test_contentConceptsEmpty(self): |
498 | + """ |
499 | + L{ExpressContents._contentConcepts} returns an empty L{list} if the |
500 | + L{Container} the L{ExpressContents} instance is initialized with has no |
501 | + contents. |
502 | + """ |
503 | + contents = self.concept._contentConcepts(self.observer) |
504 | + self.assertEqual([], contents) |
505 | + |
506 | + |
507 | + def test_contentConcepts(self): |
508 | + """ |
509 | + L{ExpressContents._contentConcepts} returns a L{list} of L{IConcept} |
510 | + providers representing the things contained by the L{Container} the |
511 | + L{ExpressContents} instance is initialized with. |
512 | + """ |
513 | + [something] = self.addContents([u"something"]) |
514 | + |
515 | + contents = self.concept._contentConcepts(self.observer) |
516 | + self.assertEqual([something], contents) |
517 | + |
518 | + |
519 | + def test_contentConceptsExcludesObserver(self): |
520 | + """ |
521 | + The L{list} returned by L{ExpressContents._contentConcepts} does not |
522 | + include the observer, even if the observer is contained by the |
523 | + L{Container} the L{ExpressContents} instance is initialized with. |
524 | + """ |
525 | + [something] = self.addContents([u"something"]) |
526 | + self.observer.moveTo(self.container) |
527 | + |
528 | + concepts = self.concept._contentConcepts(self.observer) |
529 | + self.assertEqual([something], concepts) |
530 | + |
531 | + |
532 | + def test_contentConceptsExcludesUnseen(self): |
533 | + """ |
534 | + If the L{Container} used to initialize L{ExpressContents} cannot be |
535 | + seen by the observer passed to L{ExpressContents._contentConcepts}, it |
536 | + is not included in the returned L{list}. |
537 | + """ |
538 | + objects.LocationLighting.createFor(self.box, candelas=0) |
539 | + [something] = self.addContents([u"something"]) |
540 | + |
541 | + concepts = self.concept._contentConcepts(self.observer) |
542 | + self.assertEqual([], concepts) |
543 | + |
544 | + |
545 | + def test_template(self): |
546 | + """ |
547 | + L{ExpressContents.template} evaluates to the value of the |
548 | + C{contentsTemplate} attribute of the L{Container} used to initialize |
549 | + the L{ExpressContents} instance. |
550 | + """ |
551 | + template = u"{pronoun} is carrying {contents}." |
552 | + self.container.contentsTemplate = template |
553 | + self.assertEqual(template, self.concept.template) |
554 | + |
555 | + |
556 | + def test_defaultTemplate(self): |
557 | + """ |
558 | + If the wrapped L{Container}'s C{contentsTemplate} is C{None}, |
559 | + L{ExpressContents.template} evaluates to a string giving a simple, |
560 | + generic English-language template. |
561 | + """ |
562 | + self.container.contentsTemplate = None |
563 | + self.assertEqual( |
564 | + u"{subject:pronoun} contains {contents}.", self.concept.template) |
565 | + |
566 | + |
567 | + def test_expandSubject(self): |
568 | + """ |
569 | + L{ExpressContents.expand} expands a concept template string using |
570 | + the wrapped L{Container}'s L{Thing} as I{subject}. |
571 | + """ |
572 | + self.assertEqual( |
573 | + [self.box.name], |
574 | + list(self.concept.expand(u"{subject:name}", self.observer))) |
575 | + |
576 | + |
577 | + def conceptAsText(self, concept, observer): |
578 | + """ |
579 | + Express C{concept} to C{observer} and flatten the result into a |
580 | + L{unicode} string. |
581 | + |
582 | + @return: The text result expressing the concept. |
583 | + """ |
584 | + return flatten( |
585 | + ExpressList(concept.concepts(observer)).plaintext(observer)) |
586 | + |
587 | + |
588 | + def test_expandContents(self): |
589 | + """ |
590 | + L{ExpressContents.expand} expands a concept template string using the |
591 | + contents of the L{Thing} as I{contents}. |
592 | + """ |
593 | + self.addContents([u"something", u"something else"]) |
594 | + |
595 | + contents = self.concept.expand(u"{contents}", self.observer) |
596 | + self.assertEqual( |
597 | + u"something and something else", |
598 | + self.conceptAsText(ExpressList(contents), self.observer)) |
599 | + |
600 | + |
601 | + def test_concepts(self): |
602 | + """ |
603 | + L{ExpressContents.concepts} returns a L{list} expressing the contents |
604 | + of the wrapped container to the given observer. |
605 | + """ |
606 | + self.addContents([u"red fish", u"blue fish"]) |
607 | + self.assertEqual( |
608 | + u"it contains red fish and blue fish.", |
609 | + self.conceptAsText(self.concept, self.observer)) |
610 | + |
611 | + |
612 | + def test_emptyConcepts(self): |
613 | + """ |
614 | + If the wrapped container is empty, L{ExpressContents.concepts} returns |
615 | + an empty list. |
616 | + """ |
617 | + self.assertEqual( |
618 | + u"", self.conceptAsText(self.concept, self.observer)) |
619 | + |
620 | + |
621 | + |
622 | class IngressAndEgressTestCase(CommandTestCaseMixin, unittest.TestCase): |
623 | """ |
624 | I should be able to enter and exit containers that are sufficiently big. |
625 | @@ -79,8 +242,9 @@ |
626 | Create a container, C{self.box} that is large enough to stand in. |
627 | """ |
628 | CommandTestCaseMixin.setUp(self) |
629 | - self.box = objects.Thing(store=self.store, name=u'box') |
630 | - self.container = objects.Container.createFor(self.box, capacity=1000) |
631 | + self.container = createLocation(self.store, u"box", None) |
632 | + self.box = self.container.thing |
633 | + self.box.proper = False |
634 | self.box.moveTo(self.location) |
635 | |
636 | |
637 | @@ -92,7 +256,7 @@ |
638 | 'enter box', |
639 | [E('[ Test Location ]'), |
640 | 'Location for testing.', |
641 | - 'Observer Player and a box'], |
642 | + 'Here, you see Observer Player and a box.'], |
643 | ['Test Player leaves into the box.']) |
644 | |
645 | |
646 | @@ -105,7 +269,7 @@ |
647 | 'exit out', |
648 | [E('[ Test Location ]'), |
649 | 'Location for testing.', |
650 | - 'Observer Player and a box'], |
651 | + 'Here, you see Observer Player and a box.'], |
652 | ['Test Player leaves out of the box.']) |
653 | self.assertEquals(self.player.location, |
654 | self.location) |
655 | |
656 | === modified file 'Imaginary/imaginary/test/test_garments.py' |
657 | --- Imaginary/imaginary/test/test_garments.py 2011-09-16 18:52:54 +0000 |
658 | +++ Imaginary/imaginary/test/test_garments.py 2013-07-24 22:23:27 +0000 |
659 | @@ -92,7 +92,7 @@ |
660 | u'[ daisy ]\n' |
661 | u'daisy is great.\n' |
662 | u'She is naked.\n' |
663 | - u'a pair of Daisy Dukes' |
664 | + u'She is carrying a pair of Daisy Dukes.' |
665 | ) |
666 | self.assertIdentical(self.dukes.location, self.daisy) |
667 | |
668 | @@ -109,7 +109,8 @@ |
669 | u'[ daisy ]\n' |
670 | u'daisy is great.\n' |
671 | u'She is naked.\n' |
672 | - u'a pair of Daisy Dukes and a pair of lacy underwear' |
673 | + u'She is carrying a pair of Daisy Dukes and a pair of lacy ' |
674 | + u'underwear.' |
675 | ) |
676 | self.assertIdentical(self.dukes.location, self.daisy) |
677 | |
678 | @@ -265,7 +266,7 @@ |
679 | [E("[ Test Player ]"), |
680 | E("Test Player is great."), |
681 | E("She is wearing a pair of overalls."), |
682 | - E("a pair of daisy dukes"), |
683 | + E("She is carrying a pair of daisy dukes."), |
684 | ]) |
685 | |
686 | |
687 | |
688 | === modified file 'Imaginary/imaginary/test/test_illumination.py' |
689 | --- Imaginary/imaginary/test/test_illumination.py 2010-04-24 17:48:50 +0000 |
690 | +++ Imaginary/imaginary/test/test_illumination.py 2013-07-24 22:23:27 +0000 |
691 | @@ -164,7 +164,7 @@ |
692 | "look", |
693 | [commandutils.E("[ Test Location ]"), |
694 | "Location for testing.", |
695 | - "Observer Player"]) |
696 | + "Here, you see Observer Player."]) |
697 | |
698 | |
699 | def test_changeIlluminationLevel(self): |
700 | |
701 | === added file 'Imaginary/imaginary/test/test_language.py' |
702 | --- Imaginary/imaginary/test/test_language.py 1970-01-01 00:00:00 +0000 |
703 | +++ Imaginary/imaginary/test/test_language.py 2013-07-24 22:23:27 +0000 |
704 | @@ -0,0 +1,116 @@ |
705 | + |
706 | +from twisted.trial.unittest import TestCase |
707 | + |
708 | +from imaginary.objects import Thing |
709 | +from imaginary.language import Gender, ConceptTemplate, ExpressList |
710 | +from imaginary.test.commandutils import flatten |
711 | + |
712 | +class ConceptTemplateTests(TestCase): |
713 | + """ |
714 | + Tests for L{imaginary.language.ConceptTemplate}. |
715 | + """ |
716 | + def setUp(self): |
717 | + self.thing = Thing(name=u"alice", gender=Gender.FEMALE) |
718 | + |
719 | + |
720 | + def expandToText(self, template, values): |
721 | + """ |
722 | + Expand the given L{ConceptTemplate} with the given values and flatten |
723 | + the result into a L{unicode} string. |
724 | + """ |
725 | + return flatten(ExpressList(template.expand(values)).plaintext(None)) |
726 | + |
727 | + |
728 | + def test_unexpandedLiteral(self): |
729 | + """ |
730 | + A template string containing no substitution markers expands to itself. |
731 | + """ |
732 | + self.assertEqual( |
733 | + u"hello world", |
734 | + self.expandToText(ConceptTemplate(u"hello world"), {})) |
735 | + |
736 | + |
737 | + def test_expandedName(self): |
738 | + """ |
739 | + I{field:name} can be used to substitute the name of the value given by |
740 | + C{"field"}. |
741 | + """ |
742 | + template = ConceptTemplate(u"{a:name}") |
743 | + self.assertEqual( |
744 | + u"alice", |
745 | + self.expandToText(template, dict(a=self.thing))) |
746 | + |
747 | + |
748 | + def test_expandedPronoun(self): |
749 | + """ |
750 | + I{field:pronoun} can be used to substitute the personal pronoun of the |
751 | + value given by C{"field"}. |
752 | + """ |
753 | + template = ConceptTemplate(u"{b:pronoun}") |
754 | + self.assertEqual( |
755 | + u"she", |
756 | + self.expandToText(template, dict(b=self.thing))) |
757 | + |
758 | + |
759 | + def test_intermixed(self): |
760 | + """ |
761 | + Literals and subsitution markers may be combined in a single template. |
762 | + """ |
763 | + template = ConceptTemplate(u"{c:pronoun} wins.") |
764 | + self.assertEqual( |
765 | + u"she wins.", |
766 | + self.expandToText(template, dict(c=self.thing))) |
767 | + |
768 | + |
769 | + def test_multiples(self): |
770 | + """ |
771 | + Multiple substitution markers may be used in a single template. |
772 | + """ |
773 | + another = Thing(name=u"bob", gender=Gender.FEMALE) |
774 | + template = ConceptTemplate(u"{a:name} hits {b:name}.") |
775 | + self.assertEqual( |
776 | + u"alice hits bob.", |
777 | + self.expandToText(template, dict(a=self.thing, b=another))) |
778 | + |
779 | + |
780 | + def test_adjacent(self): |
781 | + """ |
782 | + Adjacent substitution markers are expanded without introducing |
783 | + extraneous intervening characters. |
784 | + """ |
785 | + another = Thing(name=u"bob", gender=Gender.FEMALE) |
786 | + template = ConceptTemplate(u"{a:name}{b:name}") |
787 | + self.assertEqual( |
788 | + u"alicebob", |
789 | + self.expandToText(template, dict(a=self.thing, b=another))) |
790 | + |
791 | + |
792 | + def test_missingTarget(self): |
793 | + """ |
794 | + A missing target is expanded to a warning about a bad template. |
795 | + """ |
796 | + template = ConceptTemplate(u"{c} wins.") |
797 | + self.assertEqual( |
798 | + u"<missing target 'c' for expansion> wins.", |
799 | + self.expandToText(template, dict())) |
800 | + |
801 | + |
802 | + def test_missingTargetWithSpecifier(self): |
803 | + """ |
804 | + A missing target is expanded to a warning about a bad template. |
805 | + """ |
806 | + template = ConceptTemplate(u"{c:pronoun} wins.") |
807 | + self.assertEqual( |
808 | + u"<missing target 'c' for 'pronoun' expansion> wins.", |
809 | + self.expandToText(template, dict())) |
810 | + |
811 | + |
812 | + def test_unsupportedSpecifier(self): |
813 | + """ |
814 | + A specifier not supported on the identified target is expanded to a |
815 | + warning about a bad template. |
816 | + """ |
817 | + template = ConceptTemplate(u"{c:glorbex} wins.") |
818 | + self.assertEqual( |
819 | + u"<'glorbex' unsupported by target 'c'> wins.", |
820 | + self.expandToText(template, dict(c=self.thing))) |
821 | |
822 | === modified file 'Imaginary/imaginary/world.py' |
823 | --- Imaginary/imaginary/world.py 2009-06-29 12:25:10 +0000 |
824 | +++ Imaginary/imaginary/world.py 2013-07-24 22:23:27 +0000 |
825 | @@ -7,7 +7,6 @@ |
826 | from axiom.item import Item |
827 | from axiom.attributes import inmemory, reference |
828 | |
829 | -from imaginary.iimaginary import IContainer |
830 | from imaginary.objects import Thing, Container, Actor |
831 | from imaginary.events import MovementArrivalEvent |
832 | |
833 | @@ -48,7 +47,9 @@ |
834 | |
835 | character = Thing(store=self.store, weight=100, |
836 | name=name, proper=True, **kw) |
837 | - Container.createFor(character, capacity=10) |
838 | + Container.createFor( |
839 | + character, capacity=10, |
840 | + contentsTemplate=u"{subject:pronoun} is carrying {contents}.") |
841 | Actor.createFor(character) |
842 | |
843 | # Unfortunately, world -> garments -> creation -> action -> |
By no means a review:
* It looks like "ConceptTemplate" is missing some method docstrings.
* If the target named in a format string to ConceptTemplate .expand does not exist, KeyError is raised (probably with an unhelpful exception) and if the property of that target (name, pronoun, etc.) does not exist then AttributeError (slightly more useful) is raised; neither of these cases appears in the tests. Custom exceptions for these cases would allow them to be more specific about what went wrong, and perhaps even where.
I also wonder about the interaction of these exceptions and the rest of the system: if there is a typo in the format string, is the entity simply imperceptible to the viewer.