Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pystache_custom
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
pystache_custom
Commits
ee579e21
Commit
ee579e21
authored
Jan 17, 2012
by
Chris Jerdonek
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Addresses issue #81: not to call methods on instances of built-in types.
parent
4b8a6952
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
193 additions
and
163 deletions
+193
-163
pystache/context.py
+97
-105
tests/test_context.py
+96
-58
No files found.
pystache/context.py
View file @
ee579e21
...
...
@@ -5,11 +5,12 @@ Defines a Context class to represent mustache(5)'s notion of context.
"""
class
NotFound
(
object
):
pass
# We use this private global variable as a return value to represent a key
# not being found on lookup. This lets us distinguish between the case
# of a key's value being None with the case of a key not being found --
# without having to rely on exceptions (e.g. KeyError) for flow control.
_NOT_FOUND
=
object
()
_NOT_FOUND
=
NotFound
()
# TODO: share code with template.check_callable().
...
...
@@ -17,40 +18,35 @@ def _is_callable(obj):
return
hasattr
(
obj
,
'__call__'
)
def
_get_
item
(
obj
,
key
):
def
_get_
value
(
item
,
key
):
"""
Ret
urn a key's value, or _NOT_FOUND if the key does not exist
.
Ret
rieve a key's value from an item
.
The obj argument should satisfy the same conditions as those
described for the arguments passed to Context.__init__(). These
conditions are described in Context.__init__()'s docstring.
Returns _NOT_FOUND if the key does not exist.
The rules for looking up the value of a key are the same as the rules
described in Context.get()'s docstring for querying a single item.
The behavior of this function is undefined if obj is None.
The Context.get() docstring documents this function's intended behavior.
"""
if
hasattr
(
obj
,
'__getitem__'
):
# We do a membership test to avoid using exceptions for flow control
# (e.g. catching KeyError). In addition, we call __contains__()
# explicitly as opposed to using the membership operator "in" to
# avoid triggering the following Python fallback behavior:
if
isinstance
(
item
,
dict
):
# Then we consider the argument a "hash" for the purposes of the spec.
#
# "For objects that don’t define __contains__(), the membership test
# first tries iteration via __iter__(), then the old sequence
# iteration protocol via __getitem__()...."
# We do a membership test to avoid using exceptions for flow control
# (e.g. catching KeyError).
if
key
in
item
:
return
item
[
key
]
elif
type
(
item
)
.
__module__
!=
'__builtin__'
:
# Then we consider the argument an "object" for the purposes of
# the spec.
#
# (from http://docs.python.org/reference/datamodel.html#object.__contains__ )
if
obj
.
__contains__
(
key
):
return
obj
[
key
]
elif
hasattr
(
obj
,
key
):
attr
=
getattr
(
obj
,
key
)
if
_is_callable
(
attr
):
return
attr
()
return
attr
# The elif test above lets us avoid treating instances of built-in
# types like integers and strings as objects (cf. issue #81).
# Instances of user-defined classes on the other hand, for example,
# are considered objects by the test above.
if
hasattr
(
item
,
key
):
attr
=
getattr
(
item
,
key
)
if
_is_callable
(
attr
):
return
attr
()
return
attr
return
_NOT_FOUND
...
...
@@ -61,18 +57,18 @@ class Context(object):
Provides dictionary-like access to a stack of zero or more items.
Instances of this class are meant to act as the rendering context
when rendering mustache templates in accordance with mustache(5).
Instances encapsulate a private stack of objects and dictionaries.
Querying the stack for the value of a key queries the items in the
stack in order from last-added objects to first (last in, first out).
when rendering Mustache templates in accordance with mustache(5)
and the Mustache spec.
*Caution*:
Instances encapsulate a private stack of hashes, objects, and built-in
type instances. Querying the stack for the value of a key queries
the items in the stack in order from last-added objects to first
(last in, first out).
This class currently does not support recursive nesting in that
items in the stack cannot themselves be Context instances.
Caution: this class does not currently support recursive nesting in
that
items in the stack cannot themselves be Context instances.
See the docstrings of the methods of this class for more
information
.
See the docstrings of the methods of this class for more
details
.
"""
...
...
@@ -87,27 +83,8 @@ class Context(object):
stack in order so that, in particular, items at the end of
the argument list are queried first when querying the stack.
Each item should satisfy the following condition:
* If the item implements __getitem__(), it should also implement
__contains__(). Failure to implement __contains__() will cause
an AttributeError to be raised when the item is queried during
calls to self.get().
Python dictionaries, in particular, satisfy this condition.
An item satisfying this condition we informally call a "mapping
object" because it shares some characteristics of the Mapping
abstract base class (ABC) in Python's collections package:
http://docs.python.org/library/collections.html#collections-abstract-base-classes
It is not necessary for an item to implement __getitem__().
In particular, an item can be an ordinary object with no
mapping-like characteristics.
*Caution*:
Items should not themselves be Context instances, as recursive
nesting does not behave as one might expect.
Caution: items should not themselves be Context instances, as
recursive nesting does not behave as one might expect.
"""
self
.
_stack
=
list
(
items
)
...
...
@@ -128,11 +105,11 @@ class Context(object):
@staticmethod
def
create
(
*
context
,
**
kwargs
):
"""
Build a Context instance from a sequence of
"mapping-like" object
s.
Build a Context instance from a sequence of
context-like item
s.
This factory-style method is more general than the Context class's
constructor in that
Context instances can themselves appear in the
argument list. This is not true of the constructor
.
constructor in that
, unlike the constructor, the argument list
can itself contain Context instances
.
Here is an example illustrating various aspects of this method:
...
...
@@ -185,56 +162,71 @@ class Context(object):
"""
Query the stack for the given key, and return the resulting value.
Querying for a key queries items in the stack in order from last-
added objects to first (last in, first out). The value returned
is the value of the key for the first item for which the item
contains the key. If the key is not found in any item in the
stack, then this method returns the default value. The default
value defaults to None.
Querying an item in the stack is done in the following way:
(1) If the item defines __getitem__() and the item contains the
key (i.e. __contains__() returns True), then the corresponding
value is returned.
(2) Otherwise, the method looks for an attribute with the same
name as the key. If such an attribute exists, the value of
this attribute is returned. If the attribute is callable,
however, the attribute is first called with no arguments.
(3) If there is no attribute with the same name as the key, then
the key is considered not found in the item.
This method queries items in the stack in order from last-added
objects to first (last in, first out). The value returned is
the value of the key in the first item that contains the key.
If the key is not found in any item in the stack, then the default
value is returned. The default value defaults to None.
When speaking about returning values from a context, the Mustache
spec distinguishes between two types of context stack elements:
hashes and objects.
In accordance with the spec, this method queries items in the
stack for a key in the following way. For the purposes of querying,
each item is classified into one of the following three mutually
exclusive categories: a hash, an object, or neither:
(1) Hash: if the item's type is a subclass of dict, then the item
is considered a hash (in the terminology of the spec), and
the key's value is the dictionary value of the key. If the
dictionary doesn't contain the key, the key is not found.
(2) Object: if the item isn't a hash and isn't an instance of a
built-in type, then the item is considered an object (again
using the language of the spec). In this case, the method
looks for an attribute with the same name as the key. If an
attribute with that name exists, the value of the attribute is
returned. If the attribute is callable, however (i.e. if the
attribute is a method), then the attribute is called with no
arguments and instead that value returned. If there is no
attribute with the same name as the key, then the key is
considered not found.
(3) Neither: if the item is neither a hash nor an object, then
the key is considered not found.
*Caution*:
Callables resulting from a call to __getitem__ (as in (1)
above) are handled differently from callables that are merely
attributes (as in (2) above).
The former are returned as-is, while the latter are first called
and that value returned. Here is an exampl
e:
>>> def greet():
... return "Hi Bob!"
>>>
>>> class Greeter(object):
... greet = None
>>>
>>> obj = Greeter()
>>> obj.greet = greet
>>> dct = {'greet': greet}
>>>
>>> obj.greet is dct['greet']
True
>>> Context(obj).get('greet')
'Hi Bob!'
>>> Context(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...>
TODO: explain the rationale for this difference in treatment.
Callables are handled differently depending on whether they are
dictionary values, as in (1) above, or attributes, as in (2).
The former are returned as-is, while the latter are first
called and that value returned.
Here is an example to illustrat
e:
>>> def greet():
... return "Hi Bob!"
>>>
>>> class Greeter(object):
... greet = None
>>>
>>> dct = {'greet': greet}
>>> obj = Greeter()
>>> obj.greet = greet
>>>
>>> dct['greet'] is obj.greet
True
>>> Context(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...>
>>> Context(obj).get('greet')
'Hi Bob!'
TODO: explain the rationale for this difference in treatment.
"""
for
obj
in
reversed
(
self
.
_stack
):
val
=
_get_
item
(
obj
,
key
)
val
=
_get_
value
(
obj
,
key
)
if
val
is
_NOT_FOUND
:
continue
# Otherwise, the key was found.
...
...
tests/test_context.py
View file @
ee579e21
...
...
@@ -5,23 +5,14 @@ Unit tests of context.py.
"""
from
datetime
import
datetime
import
unittest
from
pystache.context
import
_NOT_FOUND
from
pystache.context
import
_get_
item
from
pystache.context
import
_get_
value
from
pystache.context
import
Context
class
AssertIsMixin
:
"""A mixin for adding assertIs() to a unittest.TestCase."""
# unittest.assertIs() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone
def
assertIs
(
self
,
first
,
second
):
self
.
assertTrue
(
first
is
second
,
msg
=
"
%
s is not
%
s"
%
(
repr
(
first
),
repr
(
second
)))
class
SimpleObject
(
object
):
"""A sample class that does not define __getitem__()."""
...
...
@@ -33,7 +24,7 @@ class SimpleObject(object):
return
"called..."
class
MappingObject
(
object
):
class
DictLike
(
object
):
"""A sample class that implements __getitem__() and __contains__()."""
...
...
@@ -48,26 +39,36 @@ class MappingObject(object):
return
self
.
_dict
[
key
]
class
GetItemTestCase
(
unittest
.
TestCase
,
AssertIsMixin
):
class
AssertIsMixin
:
"""A mixin for adding assertIs() to a unittest.TestCase."""
# unittest.assertIs() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone
def
assertIs
(
self
,
first
,
second
):
self
.
assertTrue
(
first
is
second
,
msg
=
"
%
s is not
%
s"
%
(
repr
(
first
),
repr
(
second
)))
"""Test context._get_item()."""
class
GetValueTests
(
unittest
.
TestCase
,
AssertIsMixin
):
def
assertNotFound
(
self
,
obj
,
key
):
"""Test context._get_value()."""
def
assertNotFound
(
self
,
item
,
key
):
"""
Assert that a call to _get_
item
() returns _NOT_FOUND.
Assert that a call to _get_
value
() returns _NOT_FOUND.
"""
self
.
assertIs
(
_get_
item
(
obj
,
key
),
_NOT_FOUND
)
self
.
assertIs
(
_get_
value
(
item
,
key
),
_NOT_FOUND
)
### Case:
obj
is a dictionary.
### Case:
the item
is a dictionary.
def
test_dictionary__key_present
(
self
):
"""
Test getting a key from a dictionary.
"""
obj
=
{
"foo"
:
"bar"
}
self
.
assertEquals
(
_get_
item
(
obj
,
"foo"
),
"bar"
)
item
=
{
"foo"
:
"bar"
}
self
.
assertEquals
(
_get_
value
(
item
,
"foo"
),
"bar"
)
def
test_dictionary__callable_not_called
(
self
):
"""
...
...
@@ -77,95 +78,132 @@ class GetItemTestCase(unittest.TestCase, AssertIsMixin):
def
foo_callable
(
self
):
return
"bar"
obj
=
{
"foo"
:
foo_callable
}
self
.
assertNotEquals
(
_get_
item
(
obj
,
"foo"
),
"bar"
)
self
.
assertTrue
(
_get_
item
(
obj
,
"foo"
)
is
foo_callable
)
item
=
{
"foo"
:
foo_callable
}
self
.
assertNotEquals
(
_get_
value
(
item
,
"foo"
),
"bar"
)
self
.
assertTrue
(
_get_
value
(
item
,
"foo"
)
is
foo_callable
)
def
test_dictionary__key_missing
(
self
):
"""
Test getting a missing key from a dictionary.
"""
obj
=
{}
self
.
assertNotFound
(
obj
,
"missing"
)
item
=
{}
self
.
assertNotFound
(
item
,
"missing"
)
def
test_dictionary__attributes_not_checked
(
self
):
"""
Test that dictionary attributes are not checked.
"""
obj
=
{}
item
=
{}
attr_name
=
"keys"
self
.
assertEquals
(
getattr
(
obj
,
attr_name
)(),
[])
self
.
assertNotFound
(
obj
,
attr_name
)
self
.
assertEquals
(
getattr
(
item
,
attr_name
)(),
[])
self
.
assertNotFound
(
item
,
attr_name
)
def
test_dictionary__dict_subclass
(
self
):
"""
Test that subclasses of dict are treated as dictionaries.
"""
class
DictSubclass
(
dict
):
pass
### Case: obj does not implement __getitem__().
item
=
DictSubclass
()
item
[
"foo"
]
=
"bar"
self
.
assertEquals
(
_get_value
(
item
,
"foo"
),
"bar"
)
### Case: the item is an object.
def
test_object__attribute_present
(
self
):
"""
Test getting an attribute from an object.
"""
obj
=
SimpleObject
()
self
.
assertEquals
(
_get_
item
(
obj
,
"foo"
),
"bar"
)
item
=
SimpleObject
()
self
.
assertEquals
(
_get_
value
(
item
,
"foo"
),
"bar"
)
def
test_object__attribute_missing
(
self
):
"""
Test getting a missing attribute from an object.
"""
obj
=
SimpleObject
()
self
.
assertNotFound
(
obj
,
"missing"
)
item
=
SimpleObject
()
self
.
assertNotFound
(
item
,
"missing"
)
def
test_object__attribute_is_callable
(
self
):
"""
Test getting a callable attribute from an object.
"""
obj
=
SimpleObject
()
self
.
assertEquals
(
_get_item
(
obj
,
"foo_callable"
),
"called..."
)
item
=
SimpleObject
()
self
.
assertEquals
(
_get_value
(
item
,
"foo_callable"
),
"called..."
)
def
test_object__non_built_in_type
(
self
):
"""
Test getting an attribute from an instance of a type that isn't built-in.
### Case: obj implements __getitem__() (i.e. a "mapping object").
"""
item
=
datetime
(
2012
,
1
,
2
)
self
.
assertEquals
(
_get_value
(
item
,
"day"
),
2
)
def
test_
mapping__key_present
(
self
):
def
test_
object__dict_like
(
self
):
"""
Test getting a key from a
mapping object
.
Test getting a key from a
dict-like object (an object that implements '__getitem__')
.
"""
obj
=
MappingObject
()
self
.
assertEquals
(
_get_item
(
obj
,
"foo"
),
"bar"
)
item
=
DictLike
()
self
.
assertEquals
(
item
[
"foo"
],
"bar"
)
self
.
assertNotFound
(
item
,
"foo"
)
### Case: the item is an instance of a built-in type.
def
test_
mapping__key_missing
(
self
):
def
test_
built_in_type__integer
(
self
):
"""
Test getting
a missing key from a mapping object
.
Test getting
from an integer
.
"""
obj
=
MappingObject
()
self
.
assertNotFound
(
obj
,
"missing"
)
class
MyInt
(
int
):
pass
item1
=
MyInt
(
10
)
item2
=
10
self
.
assertEquals
(
item1
.
real
,
10
)
self
.
assertEquals
(
item2
.
real
,
10
)
self
.
assertEquals
(
_get_value
(
item1
,
'real'
),
10
)
self
.
assertNotFound
(
item2
,
'real'
)
def
test_
mapping__get_attribute
(
self
):
def
test_
built_in_type__string
(
self
):
"""
Test getting
an attribute from a mapping object
.
Test getting
from a string
.
"""
obj
=
MappingObject
()
key
=
"fuzz"
self
.
assertEquals
(
getattr
(
obj
,
key
),
"buzz"
)
# As desired, __getitem__()'s presence causes obj.fuzz not to be checked.
self
.
assertNotFound
(
obj
,
key
)
class
MyStr
(
str
):
pass
def
test_mapping_object__not_implementing_contains
(
self
):
item1
=
MyStr
(
'abc'
)
item2
=
'abc'
self
.
assertEquals
(
item1
.
upper
(),
'ABC'
)
self
.
assertEquals
(
item2
.
upper
(),
'ABC'
)
self
.
assertEquals
(
_get_value
(
item1
,
'upper'
),
'ABC'
)
self
.
assertNotFound
(
item2
,
'upper'
)
def
test_built_in_type__list
(
self
):
"""
Test
querying a mapping object that doesn't define __contains__()
.
Test
getting from a list
.
"""
class
Sample
(
object
):
class
MyList
(
list
):
pass
item1
=
MyList
([
1
,
2
,
3
])
item2
=
[
1
,
2
,
3
]
def
__getitem__
(
self
,
key
):
return
"bar"
self
.
assertEquals
(
item1
.
pop
(),
3
)
self
.
assertEquals
(
item2
.
pop
(),
3
)
obj
=
Sample
(
)
self
.
assert
Raises
(
AttributeError
,
_get_item
,
obj
,
"foo"
)
self
.
assertEquals
(
_get_value
(
item1
,
'pop'
),
2
)
self
.
assert
NotFound
(
item2
,
'pop'
)
class
ContextTests
(
unittest
.
TestCase
,
AssertIsMixin
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment