← Back to team overview

divmod-dev team mailing list archive

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

 

Jean-Paul Calderone has proposed merging lp:~exarkun/divmod.org/explained-inventory-1193494 into lp:divmod.org.

Requested reviews:
  Divmod-dev (divmod-dev)
Related bugs:
  Bug #1193494 in Divmod Imaginary: "Inventory display in "look at" output is bad"
  https://bugs.launchpad.net/imaginary/+bug/1193494

For more details, see:
https://code.launchpad.net/~exarkun/divmod.org/explained-inventory-1193494/+merge/172932

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.

-- 
https://code.launchpad.net/~exarkun/divmod.org/explained-inventory-1193494/+merge/172932
Your team Divmod-dev is requested to review the proposed merge of lp:~exarkun/divmod.org/explained-inventory-1193494 into lp:divmod.org.
=== modified file 'Imaginary/ExampleGame/examplegame/test/test_furniture.py'
--- Imaginary/ExampleGame/examplegame/test/test_furniture.py	2009-08-17 02:40:03 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_furniture.py	2013-07-04 00:03:42 +0000
@@ -5,9 +5,9 @@
 
 from twisted.trial.unittest import TestCase
 
-from imaginary.test.commandutils import CommandTestCaseMixin, E
+from imaginary.test.commandutils import CommandTestCaseMixin, E, createLocation
 
-from imaginary.objects import Thing, Container, Exit
+from imaginary.objects import Thing, Exit
 from examplegame.furniture import Chair
 
 class SitAndStandTests(CommandTestCaseMixin, TestCase):
@@ -79,8 +79,7 @@
         first.
         """
         self.test_sitDown()
-        otherRoom = Thing(store=self.store, name=u'elsewhere')
-        Container.createFor(otherRoom, capacity=1000)
+        otherRoom = createLocation(self.store, u"elsewhere", None).thing
         Exit.link(self.location, otherRoom, u'north')
         self.assertCommandOutput(
             "go north",
@@ -102,6 +101,4 @@
             # at.
             [E("[ Test Location ]"),
              "Location for testing.",
-             "Observer Player and a chair"])
-
-
+             "Here, you see Observer Player and a chair."])

=== modified file 'Imaginary/ExampleGame/examplegame/test/test_glass.py'
--- Imaginary/ExampleGame/examplegame/test/test_glass.py	2009-08-17 02:40:03 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_glass.py	2013-07-04 00:03:42 +0000
@@ -50,7 +50,7 @@
             "look at box",
             [E("[ box ]"),
              "The system under test.",
-             "a ball"])
+             "It contains a ball."])
 
 
     def test_take(self):

=== modified file 'Imaginary/ExampleGame/examplegame/test/test_japanese.py'
--- Imaginary/ExampleGame/examplegame/test/test_japanese.py	2009-06-29 04:03:17 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_japanese.py	2013-07-04 00:03:42 +0000
@@ -414,8 +414,9 @@
         """
         clock = task.Clock()
 
-        closet = objects.Thing(store=self.store, name=u"Closet")
-        closetContainer = objects.Container.createFor(closet, capacity=500)
+        closetContainer = commandutils.createLocation(
+            self.store, u"Closet", None)
+        closet = closetContainer.thing
 
         mouse = mice.createHiraganaMouse(
             store=self.store,
@@ -432,7 +433,7 @@
             "north",
             [commandutils.E("[ Closet ]"),
              commandutils.E("( south )"),
-             commandutils.E(self.mouseName)],
+             commandutils.E(u"Here, you see " + self.mouseName + u".")],
             ["Test Player leaves north."])
 
         clock.advance(mousehood.challengeInterval)

=== modified file 'Imaginary/ExampleGame/examplegame/test/test_mice.py'
--- Imaginary/ExampleGame/examplegame/test/test_mice.py	2009-06-29 04:03:17 +0000
+++ Imaginary/ExampleGame/examplegame/test/test_mice.py	2013-07-04 00:03:42 +0000
@@ -14,8 +14,9 @@
     def setUp(self):
         self.store = store.Store()
 
-        self.location = objects.Thing(store=self.store, name=u"Place")
-        self.locationContainer = objects.Container.createFor(self.location, capacity=1000)
+        self.locationContainer = commandutils.createLocation(
+            self.store, u"Place", None)
+        self.location = self.locationContainer.thing
 
         self.alice = objects.Thing(store=self.store, name=u"Alice")
         self.actor = objects.Actor.createFor(self.alice)
@@ -136,8 +137,8 @@
         intelligence = iimaginary.IActor(mouse).getIntelligence()
         intelligence._callLater = clock.callLater
 
-        elsewhere = objects.Thing(store=self.store, name=u"Mouse Hole")
-        objects.Container.createFor(elsewhere, capacity=1000)
+        elsewhere = commandutils.createLocation(
+            self.store, u"Mouse Hole", None).thing
 
         objects.Exit.link(self.location, elsewhere, u"south")
 
@@ -147,7 +148,7 @@
             "south",
             [commandutils.E("[ Mouse Hole ]"),
              commandutils.E("( north )"),
-             commandutils.E("a squeaker")],
+             commandutils.E("Here, you see a squeaker.")],
             ['Test Player leaves south.'])
 
         clock.advance(0)

=== modified file 'Imaginary/imaginary/language.py'
--- Imaginary/imaginary/language.py	2009-08-17 02:40:03 +0000
+++ Imaginary/imaginary/language.py	2013-07-04 00:03:42 +0000
@@ -6,8 +6,9 @@
 
 """
 import types
+from string import Formatter
 
-from zope.interface import implements
+from zope.interface import implements, implementer
 
 from twisted.python.components import registerAdapter
 
@@ -115,8 +116,9 @@
 def flattenWithoutColors(vt102):
     return T.flatten(vt102, useColors=False)
 
+
+@implementer(iimaginary.IConcept)
 class BaseExpress(object):
-    implements(iimaginary.IConcept)
 
     def __init__(self, original):
         self.original = original
@@ -310,3 +312,37 @@
         yield u'and '
         yield desc[-1]
 
+
+
+class ConceptTemplate(object):
+    """
+    A L{ConceptTemplate} wraps a text template which may intersperse literal
+    strings with markers for substitution.
+
+    Substitution markers follow U{the syntax for str.format<http://docs.python.org/2/library/string.html#format-string-syntax>}.
+
+    Values for field names are supplied to the L{expand} method.
+    """
+    def __init__(self, templateText):
+        self.templateText = templateText
+
+
+    def expand(self, values):
+        parts = Formatter().parse(self.templateText)
+        for (literalText, fieldName, formatSpec, conversion) in parts:
+            if literalText:
+                yield ExpressString(literalText)
+            if fieldName:
+                target = values[fieldName.lower()]
+                if formatSpec:
+                    yield getattr(self, '_expand_' + formatSpec.upper())(target)
+                else:
+                    yield target
+
+
+    def _expand_NAME(self, target):
+        return target.name
+
+
+    def _expand_PRONOUN(self, target):
+        return Noun(target).heShe()

=== modified file 'Imaginary/imaginary/objects.py'
--- Imaginary/imaginary/objects.py	2011-09-16 20:39:43 +0000
+++ Imaginary/imaginary/objects.py	2013-07-04 00:03:42 +0000
@@ -708,9 +708,7 @@
         @return: an L{ExpressSurroundings} with an iterable of all visible
         contents of this container.
         """
-        return ExpressSurroundings(
-            self.thing.idea.obtain(
-                _ContainedBy(CanSee(ProviderOf(iimaginary.IThing)), self)))
+        return ExpressContents(self)
 
 
 
@@ -838,6 +836,14 @@
     """
     A generic L{_Enhancement} that implements containment.
     """
+    contentsTemplate = attributes.text(
+        doc="""
+        Define how the contents of this container are presented to observers.
+        Certain substrings will be given special treatment.
+
+        @see: L{imaginary.language.ConceptTemplate}
+        """,
+        allowNone=True, default=None)
 
     capacity = attributes.integer(
         doc="""
@@ -856,6 +862,70 @@
 
 
 
+class ExpressContents(language.Sentence):
+    """
+    A concept representing the things contained by another thing - excluding
+    the observer of the concept.
+    """
+    _CONDITION = CanSee(ProviderOf(iimaginary.IThing))
+
+    def _contentConcepts(self, observer):
+        """
+        Get concepts for the contents of the thing wrapped by this concept.
+
+        @param observer: The L{objects.Thing} which will observe these
+            concepts.
+
+        @return: A L{list} of the contents of C{self.original}, excluding
+            C{observer}.
+        """
+        container = self.original
+        idea = container.thing.idea
+        return [
+            concept
+            for concept
+            in idea.obtain(_ContainedBy(self._CONDITION, container))
+            if concept is not observer]
+
+
+    @property
+    def template(self):
+        """
+        This is the template string which is used to construct the overall
+        concept, indicating what the container is and what its contents are.
+        """
+        template = self.original.contentsTemplate
+        if template is None:
+            template = u"{subject:pronoun} contains {contents}."
+        return template
+
+
+    def expand(self, template, observer):
+        """
+        Expand the given template using the wrapped container's L{Thing} as the
+        subject.
+
+        C{u"contents"} is also available for substitution with the contents of
+        the container.
+
+        @return: An iterator of concepts derived from the given template.
+        """
+        return language.ConceptTemplate(template).expand(dict(
+                subject=self.original.thing,
+                contents=language.ItemizedList(self._contentConcepts(observer))))
+
+
+    def concepts(self, observer):
+        """
+        Return a L{list} of L{IConcept} providers which express the contents of
+        the wrapped container.
+        """
+        if self._contentConcepts(observer):
+            return list(self.expand(self.template, observer))
+        return u""
+
+
+
 class ExpressCondition(language.BaseExpress):
     implements(iimaginary.IConcept)
 

=== modified file 'Imaginary/imaginary/test/commandutils.py'
--- Imaginary/imaginary/test/commandutils.py	2009-08-17 02:40:03 +0000
+++ Imaginary/imaginary/test/commandutils.py	2013-07-04 00:03:42 +0000
@@ -38,21 +38,14 @@
     @ivar observer: The L{Thing} representing the observer who sees the main
         player's actions.
     """
-
     def setUp(self):
         """
         Set up a store with a location, a player and an observer.
         """
         self.store = store.Store()
-
-        self.location = objects.Thing(
-            store=self.store,
-            name=u"Test Location",
-            description=u"Location for testing.",
-            proper=True)
-
-        locContainer = objects.Container.createFor(self.location, capacity=1000)
-
+        locContainer = createLocation(
+            self.store, u"Test Location", u"Location for testing.")
+        self.location = locContainer.thing
         self.world = ImaginaryWorld(store=self.store, origin=self.location)
         self.player = self.world.create(
             u"Test Player", gender=language.Gender.FEMALE)
@@ -216,6 +209,28 @@
 
 
 
+def createLocation(store, name, description):
+    """
+    Create a new L{Thing} and create an L{objects.Container} for it.
+
+    @param name: The name given to the created L{Thing}.
+    @type name: L{unicode}
+
+    @param description: The description given to the created L{Thing}.
+    @type description: L{unicode}
+
+    @return: The containment enhancement of the created L{Thing}.
+    @rtype: L{objects.Container}.
+    """
+    location = objects.Thing(
+        store=store, name=name, description=description, proper=True)
+
+    return objects.Container.createFor(
+        location, capacity=1000,
+        contentsTemplate=u"Here, you see {contents}.")
+
+
+
 def createPlayer(store, name):
     """
     Create a mock player with a mock intelligence with the given

=== modified file 'Imaginary/imaginary/test/test_actions.py'
--- Imaginary/imaginary/test/test_actions.py	2009-08-17 02:40:03 +0000
+++ Imaginary/imaginary/test/test_actions.py	2013-07-04 00:03:42 +0000
@@ -191,13 +191,13 @@
             "look",
             [E("[ Test Location ]"),
              "Location for testing.",
-             "Observer Player"])
+             "Here, you see Observer Player."])
 
         self._test(
             "look here",
             [E("[ Test Location ]"),
              "Location for testing.",
-             "Observer Player"])
+             "Here, you see Observer Player."])
 
         objects.Exit.link(self.location, self.location, u"north")
         self._test(
@@ -205,7 +205,7 @@
             [E("[ Test Location ]"),
              E("( north south )"),
              "Location for testing.",
-             "Observer Player"])
+             "Here, you see Observer Player."])
 
         self._test(
             "look me",
@@ -284,7 +284,7 @@
             "search here",
             [E("[ Test Location ]"),
              "Location for testing.",
-             "Observer Player",
+             "Here, you see Observer Player.",
              ""])
 
         self._test(
@@ -496,7 +496,7 @@
             [E("[ Test Location ]"),
              E("( west )"),
              "Location for testing.",
-             "Observer Player"],
+             "Here, you see Observer Player."],
             ["Test Player arrives from the west."])
 
 
@@ -515,7 +515,7 @@
             "north",
             [E("[ Test Location ]"),
              "Location for testing.",
-             "Observer Player"],
+             "Here, you see Observer Player."],
             ["Test Player arrives from the south."])
         self.player.moveTo(secretRoom)
         myExit.name = u'elsewhere'
@@ -523,7 +523,7 @@
             "go elsewhere",
             [E("[ Test Location ]"),
              "Location for testing.",
-             "Observer Player"],
+             "Here, you see Observer Player."],
             ["Test Player arrives."])
 
 

=== modified file 'Imaginary/imaginary/test/test_container.py'
--- Imaginary/imaginary/test/test_container.py	2009-08-17 02:40:03 +0000
+++ Imaginary/imaginary/test/test_container.py	2013-07-04 00:03:42 +0000
@@ -1,11 +1,15 @@
 # -*- test-case-name: imaginary.test -*-
 
+from zope.interface.verify import verifyObject
+
 from twisted.trial import unittest
 
 from axiom import store
 
-from imaginary import eimaginary, objects
-from imaginary.test.commandutils import CommandTestCaseMixin, E
+from imaginary import iimaginary, eimaginary, objects
+from imaginary.test.commandutils import (
+    CommandTestCaseMixin, E, createLocation, flatten)
+from imaginary.language import ExpressList
 
 class ContainerTestCase(unittest.TestCase):
     def setUp(self):
@@ -69,6 +73,165 @@
 
 
 
+class ExpressContentsTests(unittest.TestCase):
+    """
+    Tests for L{ExpressContents}.
+    """
+    def setUp(self):
+        self.store = store.Store()
+        self.box = objects.Thing(store=self.store, name=u"box")
+        self.container = objects.Container.createFor(self.box, capacity=123)
+        self.concept = objects.ExpressContents(self.container)
+        self.observer = objects.Thing(store=self.store, name=u"observer")
+
+
+    def test_interface(self):
+        """
+        An instance of L{ExpressContents} provides L{IConcept}.
+        """
+        self.assertTrue(verifyObject(iimaginary.IConcept, self.concept))
+
+
+    def test_contentConceptsEmpty(self):
+        """
+        L{ExpressContents._contentConcepts} returns an empty L{list} if the
+        L{Container} the L{ExpressContents} instance is initialized with has no
+        contents.
+        """
+        contents = self.concept._contentConcepts(self.observer)
+        self.assertEqual([], contents)
+
+
+    def test_contentConcepts(self):
+        """
+        L{ExpressContents._contentConcepts} returns a L{list} of L{IConcept}
+        providers representing the things contained by the L{Container} the
+        L{ExpressContents} instance is initialized with.
+        """
+        something = objects.Thing(store=self.store, name=u"something")
+        something.moveTo(self.container)
+
+        contents = self.concept._contentConcepts(self.observer)
+        self.assertEqual([something], contents)
+
+
+    def test_contentConceptsExcludesObserver(self):
+        """
+        The L{list} returned by L{ExpressContents._contentConcepts} does not
+        include the observer, even if the observer is contained by the
+        L{Container} the L{ExpressContents} instance is initialized with.
+        """
+        something = objects.Thing(store=self.store, name=u"something")
+        something.moveTo(self.container)
+        self.observer.moveTo(self.container)
+
+        concepts = self.concept._contentConcepts(self.observer)
+        self.assertEqual([something], concepts)
+
+
+    def test_contentConceptsExcludesUnseen(self):
+        """
+        If the L{Container} used to initialize L{ExpressContents} cannot be
+        seen by the observer passed to L{ExpressContents._contentConcepts}, it
+        is not included in the returned L{list}.
+        """
+        objects.LocationLighting.createFor(self.box, candelas=0)
+
+        something = objects.Thing(store=self.store, name=u"something")
+        something.moveTo(self.container)
+
+        concepts = self.concept._contentConcepts(self.observer)
+        self.assertEqual([], concepts)
+
+
+    def test_template(self):
+        """
+        L{ExpressContents.template} evaluates to the value of the
+        C{contentsTemplate} attribute of the L{Container} used to initialize
+        the L{ExpressContents} instance.
+        """
+        template = u"{pronoun} is carrying {contents}."
+        self.container.contentsTemplate = template
+        self.assertEqual(template, self.concept.template)
+
+
+    def test_defaultTemplate(self):
+        """
+        If the wrapped L{Container}'s C{contentsTemplate} is C{None},
+        L{ExpressContents.template} evaluates to a string giving a simple,
+        generic English-language template.
+        """
+        self.container.contentsTemplate = None
+        self.assertEqual(
+            u"{subject:pronoun} contains {contents}.", self.concept.template)
+
+
+    def test_expandSubject(self):
+        """
+        L{ExpressContents.expand} expands a concept template string using
+        the wrapped L{Container}'s L{Thing} as I{subject}.
+        """
+        self.assertEqual(
+            [self.box.name],
+            list(self.concept.expand(u"{subject:name}", self.observer)))
+
+
+    def addContents(self, names):
+        """
+        Add a new L{Thing} to C{self.container} for each element of C{names}.
+
+        @param names: An iterable of L{unicode} giving the names of the things
+            to create and add.
+        """
+        for name in names:
+            thing = objects.Thing(store=self.store, name=name, proper=True)
+            thing.moveTo(self.container)
+
+
+    def conceptAsText(self, concept, observer):
+        """
+        Express C{concept} to C{observer} and flatten the result into a
+        L{unicode} string.
+
+        @return: The text result expressing the concept.
+        """
+        return flatten(
+            ExpressList(concept.concepts(observer)).plaintext(observer))
+
+
+    def test_expandContents(self):
+        """
+        L{ExpressContents.expand} expands a concept template string using the
+        contents of the L{Thing} as I{contents}.
+        """
+        self.addContents([u"something", u"something else"])
+
+        contents = self.concept.expand(u"{contents}", self.observer)
+        self.assertEqual(
+            u"something and something else",
+            self.conceptAsText(ExpressList(contents), self.observer))
+
+
+    def test_concepts(self):
+        """
+        L{ExpressContents.concepts} returns a L{list} expressing the contents
+        of the wrapped container to the given observer.
+        """
+        self.addContents([u"red fish", u"blue fish"])
+        self.assertEqual(
+            u"it contains red fish and blue fish.",
+            self.conceptAsText(self.concept, self.observer))
+
+
+    def test_emptyConcepts(self):
+        """
+        If the wrapped container is empty, L{ExpressContents.concepts} returns
+        an empty string.
+        """
+        self.assertEqual(u"", self.concept.concepts(self.observer))
+
+
+
 class IngressAndEgressTestCase(CommandTestCaseMixin, unittest.TestCase):
     """
     I should be able to enter and exit containers that are sufficiently big.
@@ -79,8 +242,9 @@
         Create a container, C{self.box} that is large enough to stand in.
         """
         CommandTestCaseMixin.setUp(self)
-        self.box = objects.Thing(store=self.store, name=u'box')
-        self.container = objects.Container.createFor(self.box, capacity=1000)
+        self.container = createLocation(self.store, u"box", None)
+        self.box = self.container.thing
+        self.box.proper = False
         self.box.moveTo(self.location)
 
 
@@ -92,7 +256,7 @@
             'enter box',
             [E('[ Test Location ]'),
              'Location for testing.',
-             'Observer Player and a box'],
+             'Here, you see Observer Player and a box.'],
             ['Test Player leaves into the box.'])
 
 
@@ -105,7 +269,7 @@
             'exit out',
             [E('[ Test Location ]'),
              'Location for testing.',
-             'Observer Player and a box'],
+             'Here, you see Observer Player and a box.'],
             ['Test Player leaves out of the box.'])
         self.assertEquals(self.player.location,
                           self.location)

=== modified file 'Imaginary/imaginary/test/test_garments.py'
--- Imaginary/imaginary/test/test_garments.py	2011-09-16 18:52:54 +0000
+++ Imaginary/imaginary/test/test_garments.py	2013-07-04 00:03:42 +0000
@@ -92,7 +92,7 @@
             u'[ daisy ]\n'
             u'daisy is great.\n'
             u'She is naked.\n'
-            u'a pair of Daisy Dukes'
+            u'She is carrying a pair of Daisy Dukes.'
             )
         self.assertIdentical(self.dukes.location, self.daisy)
 
@@ -109,7 +109,8 @@
             u'[ daisy ]\n'
             u'daisy is great.\n'
             u'She is naked.\n'
-            u'a pair of Daisy Dukes and a pair of lacy underwear'
+            u'She is carrying a pair of Daisy Dukes and a pair of lacy '
+            u'underwear.'
             )
         self.assertIdentical(self.dukes.location, self.daisy)
 
@@ -265,7 +266,7 @@
                    [E("[ Test Player ]"),
                     E("Test Player is great."),
                     E("She is wearing a pair of overalls."),
-                    E("a pair of daisy dukes"),
+                    E("She is carrying a pair of daisy dukes."),
                     ])
 
 

=== modified file 'Imaginary/imaginary/test/test_illumination.py'
--- Imaginary/imaginary/test/test_illumination.py	2010-04-24 17:48:50 +0000
+++ Imaginary/imaginary/test/test_illumination.py	2013-07-04 00:03:42 +0000
@@ -164,7 +164,7 @@
             "look",
             [commandutils.E("[ Test Location ]"),
              "Location for testing.",
-             "Observer Player"])
+             "Here, you see Observer Player."])
 
 
     def test_changeIlluminationLevel(self):

=== added file 'Imaginary/imaginary/test/test_language.py'
--- Imaginary/imaginary/test/test_language.py	1970-01-01 00:00:00 +0000
+++ Imaginary/imaginary/test/test_language.py	2013-07-04 00:03:42 +0000
@@ -0,0 +1,62 @@
+
+from twisted.trial.unittest import TestCase
+
+from imaginary.objects import Thing
+from imaginary.language import Gender, ConceptTemplate, ExpressList
+from imaginary.test.commandutils import flatten
+
+class ConceptTemplateTests(TestCase):
+    """
+    Tests for L{imaginary.language.ConceptTemplate}.
+    """
+    def setUp(self):
+        self.thing = Thing(name=u"alice", gender=Gender.FEMALE)
+
+
+    def expandToText(self, template, values):
+        """
+        Expand the given L{ConceptTemplate} with the given values and flatten
+        the result into a L{unicode} string.
+        """
+        return flatten(ExpressList(template.expand(values)).plaintext(None))
+
+
+    def test_unexpandedLiteral(self):
+        """
+        A template string containing no substitution markers expands to itself.
+        """
+        self.assertEqual(
+            u"hello world",
+            self.expandToText(ConceptTemplate(u"hello world"), {}))
+
+
+    def test_expandedName(self):
+        """
+        I{field:name} can be used to substitute the name of the value given by
+        C{"field"}.
+        """
+        template = ConceptTemplate(u"{a:name}")
+        self.assertEqual(
+            u"alice",
+            self.expandToText(template, dict(a=self.thing)))
+
+
+    def test_expandedPronoun(self):
+        """
+        I{field:pronoun} can be used to substitute the personal pronoun of the
+        value given by C{"field"}.
+        """
+        template = ConceptTemplate(u"{b:pronoun}")
+        self.assertEqual(
+            u"she",
+            self.expandToText(template, dict(b=self.thing)))
+
+
+    def test_intermixed(self):
+        """
+        Literals and subsitution markers may be combined in a single template.
+        """
+        template = ConceptTemplate(u"{c:pronoun} wins.")
+        self.assertEqual(
+            u"she wins.",
+            self.expandToText(template, dict(c=self.thing)))

=== modified file 'Imaginary/imaginary/world.py'
--- Imaginary/imaginary/world.py	2009-06-29 12:25:10 +0000
+++ Imaginary/imaginary/world.py	2013-07-04 00:03:42 +0000
@@ -7,7 +7,6 @@
 from axiom.item import Item
 from axiom.attributes import inmemory, reference
 
-from imaginary.iimaginary import IContainer
 from imaginary.objects import Thing, Container, Actor
 from imaginary.events import MovementArrivalEvent
 
@@ -48,7 +47,9 @@
 
         character = Thing(store=self.store, weight=100,
                           name=name, proper=True, **kw)
-        Container.createFor(character, capacity=10)
+        Container.createFor(
+            character, capacity=10,
+            contentsTemplate=u"{subject:pronoun} is carrying {contents}.")
         Actor.createFor(character)
 
         # Unfortunately, world -> garments -> creation -> action ->


Follow ups