Merge lp:~exarkun/divmod.org/explained-inventory-1193494 into lp:divmod.org

Proposed by Jean-Paul Calderone
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
Reviewer Review Type Date Requested Status
Jean-Paul Calderone Approve
Review via email: mp+172932@code.launchpad.net

Description of the change

This introduces a new API, imaginary.language.ConceptTemplate. This is used to transform text like "{subject:name} hits you in the face." into text like "Alice hits you in the face." This is useful because it allows some customization of the description of an action or some state without requiring new code for each variation.

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.

To post a comment you must log in.
Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

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.

Revision history for this message
Jean-Paul Calderone (exarkun) wrote :

Jonathan, thank you very much for your input!

Revision history for this message
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.contents() returns a list of sentence fragments or an empty string. Given that the docstring explicitly mentions returning a list, I think the final line should be `return []` (or maybe `return [u""]` if that makes more sense) rather than `return u""`.

 * ConceptTemplateTests doesn't have a test case for multiple substitutions or format specifiers that aren't at the start of the string. A test for something like u"foo {c:pronoun}{c:name} bar {c:pronoun}" would hit a bunch of edge cases in the formatter.

 * ExpressContentsTests.test_template() uses a valid but incorrect template to test with. That's okay for the test, since the template isn't being expanded at all, but it's probably better to have a correct template in the test.

 * You could probably move ExpressContentsTests.addContents() up a bit and use it in some earlier tests.

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.

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 ->

Subscribers

People subscribed via source and target branches