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
c8a1f2e1
Commit
c8a1f2e1
authored
May 05, 2012
by
Chris Jerdonek
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'decouple-parser' into 'development':
Parsing a template no longer requires a RenderEngine instance.
parents
7eef0a68
5dd7fa7c
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
195 additions
and
168 deletions
+195
-168
pystache/defaults.py
+3
-0
pystache/parsed.py
+4
-5
pystache/parser.py
+148
-29
pystache/renderengine.py
+38
-133
pystache/tests/test_parser.py
+2
-1
No files found.
pystache/defaults.py
View file @
c8a1f2e1
...
@@ -38,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding()
...
@@ -38,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding()
# strings that arise from files.
# strings that arise from files.
FILE_ENCODING
=
sys
.
getdefaultencoding
()
FILE_ENCODING
=
sys
.
getdefaultencoding
()
# The delimiters to start with when parsing.
DELIMITERS
=
(
u'{{'
,
u'}}'
)
# How to handle missing tags when rendering a template.
# How to handle missing tags when rendering a template.
MISSING_TAGS
=
MissingTags
.
ignore
MISSING_TAGS
=
MissingTags
.
ignore
...
...
pystache/parsed.py
View file @
c8a1f2e1
...
@@ -38,18 +38,17 @@ class ParsedTemplate(object):
...
@@ -38,18 +38,17 @@ class ParsedTemplate(object):
def
add
(
self
,
node
):
def
add
(
self
,
node
):
self
.
_parse_tree
.
append
(
node
)
self
.
_parse_tree
.
append
(
node
)
def
render
(
self
,
context
):
def
render
(
self
,
engine
,
context
):
"""
"""
Returns: a string of type unicode.
Returns: a string of type unicode.
"""
"""
# We avoid use of the ternary operator for Python 2.4 support.
# We avoid use of the ternary operator for Python 2.4 support.
def
get_unicode
(
val
):
def
get_unicode
(
val
):
if
callable
(
val
)
:
if
type
(
val
)
is
unicode
:
return
val
(
context
)
return
val
return
val
return
val
.
render
(
engine
,
context
)
parts
=
map
(
get_unicode
,
self
.
_parse_tree
)
parts
=
map
(
get_unicode
,
self
.
_parse_tree
)
s
=
''
.
join
(
parts
)
s
=
''
.
join
(
parts
)
return
unicode
(
s
)
return
unicode
(
s
)
pystache/parser.py
View file @
c8a1f2e1
# coding: utf-8
# coding: utf-8
"""
"""
Provides a class for parsing template strings.
Exposes a parse() function to parse template strings.
This module is only meant for internal use by the renderengine module.
"""
"""
import
re
import
re
from
pystache.defaults
import
DELIMITERS
from
pystache.parsed
import
ParsedTemplate
from
pystache.parsed
import
ParsedTemplate
DEFAULT_DELIMITERS
=
(
u'{{'
,
u'}}'
)
END_OF_LINE_CHARACTERS
=
[
u'
\r
'
,
u'
\n
'
]
END_OF_LINE_CHARACTERS
=
[
u'
\r
'
,
u'
\n
'
]
NON_BLANK_RE
=
re
.
compile
(
ur'^(.)'
,
re
.
M
)
def
_compile_template_re
(
delimiters
=
None
):
# TODO: add some unit tests for this.
def
parse
(
template
,
delimiters
=
None
):
"""
"""
Return a regular expresssion object (re.RegexObject) instance.
Parse a unicode template string and return a ParsedTemplate instance.
Arguments:
template: a unicode template string.
delimiters: a 2-tuple of delimiters. Defaults to the package default.
"""
parser
=
_Parser
(
delimiters
)
return
parser
.
parse
(
template
)
def
_compile_template_re
(
delimiters
):
"""
"""
if
delimiters
is
None
:
Return a regular expresssion object (re.RegexObject) instance.
delimiters
=
DEFAULT_DELIMITERS
"""
# The possible tag type characters following the opening tag,
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
# excluding "=" and "{".
tag_types
=
"!>&/#^"
tag_types
=
"!>&/#^"
...
@@ -52,25 +64,131 @@ class ParsingError(Exception):
...
@@ -52,25 +64,131 @@ class ParsingError(Exception):
pass
pass
class
Parser
(
object
):
## Node types
_delimiters
=
None
_template_re
=
None
def
__init__
(
self
,
engine
,
delimiters
=
None
):
class
_CommentNode
(
object
):
"""
Construct an instance.
Arguments:
def
render
(
self
,
engine
,
context
):
return
u''
engine: a RenderEngine instance.
"""
class
_ChangeNode
(
object
):
def
__init__
(
self
,
delimiters
):
self
.
delimiters
=
delimiters
def
render
(
self
,
engine
,
context
):
return
u''
class
_TagNode
(
object
):
def
__init__
(
self
,
key
):
self
.
key
=
key
def
render
(
self
,
engine
,
context
):
s
=
engine
.
fetch_string
(
context
,
self
.
key
)
return
engine
.
escape
(
s
)
class
_LiteralNode
(
object
):
def
__init__
(
self
,
key
):
self
.
key
=
key
def
render
(
self
,
engine
,
context
):
s
=
engine
.
fetch_string
(
context
,
self
.
key
)
return
engine
.
literal
(
s
)
class
_PartialNode
(
object
):
def
__init__
(
self
,
key
,
indent
):
self
.
key
=
key
self
.
indent
=
indent
def
render
(
self
,
engine
,
context
):
template
=
engine
.
resolve_partial
(
self
.
key
)
# Indent before rendering.
template
=
re
.
sub
(
NON_BLANK_RE
,
self
.
indent
+
ur'\1'
,
template
)
return
engine
.
render
(
template
,
context
)
class
_InvertedNode
(
object
):
def
__init__
(
self
,
key
,
parsed_section
):
self
.
key
=
key
self
.
parsed_section
=
parsed_section
def
render
(
self
,
engine
,
context
):
# TODO: is there a bug because we are not using the same
# logic as in fetch_string()?
data
=
engine
.
resolve_context
(
context
,
self
.
key
)
# Note that lambdas are considered truthy for inverted sections
# per the spec.
if
data
:
return
u''
return
engine
.
render_parsed
(
self
.
parsed_section
,
context
)
class
_SectionNode
(
object
):
# TODO: the template_ and parsed_template_ arguments don't both seem
# to be necessary. Can we remove one of them? For example, if
# callable(data) is True, then the initial parsed_template isn't used.
def
__init__
(
self
,
key
,
parsed_contents
,
delimiters
,
template
,
section_begin_index
,
section_end_index
):
self
.
delimiters
=
delimiters
self
.
key
=
key
self
.
parsed_contents
=
parsed_contents
self
.
template
=
template
self
.
section_begin_index
=
section_begin_index
self
.
section_end_index
=
section_end_index
def
render
(
self
,
engine
,
context
):
data
=
engine
.
fetch_section_data
(
context
,
self
.
key
)
parts
=
[]
for
val
in
data
:
if
callable
(
val
):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
val
=
val
(
self
.
template
[
self
.
section_begin_index
:
self
.
section_end_index
])
val
=
engine
.
_render_value
(
val
,
context
,
delimiters
=
self
.
delimiters
)
parts
.
append
(
val
)
continue
context
.
push
(
val
)
parts
.
append
(
engine
.
render_parsed
(
self
.
parsed_contents
,
context
))
context
.
pop
()
return
unicode
(
''
.
join
(
parts
))
class
_Parser
(
object
):
_delimiters
=
None
_template_re
=
None
def
__init__
(
self
,
delimiters
=
None
):
if
delimiters
is
None
:
if
delimiters
is
None
:
delimiters
=
DE
FAULT_DE
LIMITERS
delimiters
=
DELIMITERS
self
.
_delimiters
=
delimiters
self
.
_delimiters
=
delimiters
self
.
engine
=
engine
def
_compile_delimiters
(
self
):
def
_compile_delimiters
(
self
):
self
.
_template_re
=
_compile_template_re
(
self
.
_delimiters
)
self
.
_template_re
=
_compile_template_re
(
self
.
_delimiters
)
...
@@ -172,8 +290,9 @@ class Parser(object):
...
@@ -172,8 +290,9 @@ class Parser(object):
parsed_template
.
add
(
node
)
parsed_template
.
add
(
node
)
# Add the remainder of the template.
# Avoid adding spurious empty strings to the parse tree.
parsed_template
.
add
(
template
[
start_index
:])
if
start_index
!=
len
(
template
):
parsed_template
.
add
(
template
[
start_index
:])
return
parsed_template
return
parsed_template
...
@@ -184,21 +303,21 @@ class Parser(object):
...
@@ -184,21 +303,21 @@ class Parser(object):
"""
"""
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if
tag_type
==
'!'
:
if
tag_type
==
'!'
:
return
u''
return
_CommentNode
()
if
tag_type
==
'='
:
if
tag_type
==
'='
:
delimiters
=
tag_key
.
split
()
delimiters
=
tag_key
.
split
()
self
.
_change_delimiters
(
delimiters
)
self
.
_change_delimiters
(
delimiters
)
return
u''
return
_ChangeNode
(
delimiters
)
if
tag_type
==
''
:
if
tag_type
==
''
:
return
self
.
engine
.
_make_get_escaped
(
tag_key
)
return
_TagNode
(
tag_key
)
if
tag_type
==
'&'
:
if
tag_type
==
'&'
:
return
self
.
engine
.
_make_get_literal
(
tag_key
)
return
_LiteralNode
(
tag_key
)
if
tag_type
==
'>'
:
if
tag_type
==
'>'
:
return
self
.
engine
.
_make_get_partial
(
tag_key
,
leading_whitespace
)
return
_PartialNode
(
tag_key
,
leading_whitespace
)
raise
Exception
(
"Invalid symbol for interpolation tag:
%
s"
%
repr
(
tag_type
))
raise
Exception
(
"Invalid symbol for interpolation tag:
%
s"
%
repr
(
tag_type
))
...
@@ -209,10 +328,10 @@ class Parser(object):
...
@@ -209,10 +328,10 @@ class Parser(object):
"""
"""
if
tag_type
==
'#'
:
if
tag_type
==
'#'
:
return
self
.
engine
.
_make_get_section
(
tag_key
,
parsed_section
,
self
.
_delimiters
,
return
_SectionNode
(
tag_key
,
parsed_section
,
self
.
_delimiters
,
template
,
section_start_index
,
section_end_index
)
template
,
section_start_index
,
section_end_index
)
if
tag_type
==
'^'
:
if
tag_type
==
'^'
:
return
self
.
engine
.
_make_get_invers
e
(
tag_key
,
parsed_section
)
return
_InvertedNod
e
(
tag_key
,
parsed_section
)
raise
Exception
(
"Invalid symbol for section tag:
%
s"
%
repr
(
tag_type
))
raise
Exception
(
"Invalid symbol for section tag:
%
s"
%
repr
(
tag_type
))
pystache/renderengine.py
View file @
c8a1f2e1
...
@@ -8,10 +8,7 @@ Defines a class responsible for rendering logic.
...
@@ -8,10 +8,7 @@ Defines a class responsible for rendering logic.
import
re
import
re
from
pystache.common
import
is_string
from
pystache.common
import
is_string
from
pystache.parser
import
Parser
from
pystache.parser
import
parse
NON_BLANK_RE
=
re
.
compile
(
ur'^(.)'
,
re
.
M
)
def
context_get
(
stack
,
name
):
def
context_get
(
stack
,
name
):
...
@@ -90,12 +87,12 @@ class RenderEngine(object):
...
@@ -90,12 +87,12 @@ class RenderEngine(object):
# The returned value MUST be rendered against the default delimiters,
# The returned value MUST be rendered against the default delimiters,
# then interpolated in place of the lambda.
# then interpolated in place of the lambda.
#
#
def
_get_string_value
(
self
,
context
,
tag_
name
):
def
fetch_string
(
self
,
context
,
name
):
"""
"""
Get a value from the given context as a basestring instance.
Get a value from the given context as a basestring instance.
"""
"""
val
=
self
.
resolve_context
(
context
,
tag_
name
)
val
=
self
.
resolve_context
(
context
,
name
)
if
callable
(
val
):
if
callable
(
val
):
# Return because _render_value() is already a string.
# Return because _render_value() is already a string.
...
@@ -106,134 +103,40 @@ class RenderEngine(object):
...
@@ -106,134 +103,40 @@ class RenderEngine(object):
return
val
return
val
def
_make_get_literal
(
self
,
name
):
def
fetch_section_data
(
self
,
context
,
name
):
def
get_literal
(
context
):
data
=
self
.
resolve_context
(
context
,
name
)
"""
Returns: a string of type unicode.
# From the spec:
#
"""
# If the data is not of a list type, it is coerced into a list
s
=
self
.
_get_string_value
(
context
,
name
)
# as follows: if the data is truthy (e.g. `!!data == true`),
return
self
.
literal
(
s
)
# use a single-element list containing the data, otherwise use
# an empty list.
return
get_literal
#
if
not
data
:
def
_make_get_escaped
(
self
,
name
):
data
=
[]
get_literal
=
self
.
_make_get_literal
(
name
)
else
:
# The least brittle way to determine whether something
def
get_escaped
(
context
):
# supports iteration is by trying to call iter() on it:
"""
Returns: a string of type unicode.
"""
s
=
self
.
_get_string_value
(
context
,
name
)
return
self
.
escape
(
s
)
return
get_escaped
def
_make_get_partial
(
self
,
tag_key
,
leading_whitespace
):
template
=
self
.
resolve_partial
(
tag_key
)
# Indent before rendering.
template
=
re
.
sub
(
NON_BLANK_RE
,
leading_whitespace
+
ur'\1'
,
template
)
def
get_partial
(
context
):
"""
Returns: a string of type unicode.
"""
# TODO: can we do the parsing before calling this function?
return
self
.
render
(
template
,
context
)
return
get_partial
def
_make_get_inverse
(
self
,
name
,
parsed_template
):
def
get_inverse
(
context
):
"""
Returns a string with type unicode.
"""
# TODO: is there a bug because we are not using the same
# logic as in _get_string_value()?
data
=
self
.
resolve_context
(
context
,
name
)
# Per the spec, lambdas in inverted sections are considered truthy.
if
data
:
return
u''
return
parsed_template
.
render
(
context
)
return
get_inverse
# TODO: the template_ and parsed_template_ arguments don't both seem
# to be necessary. Can we remove one of them? For example, if
# callable(data) is True, then the initial parsed_template isn't used.
def
_make_get_section
(
self
,
name
,
parsed_template
,
delims
,
template
,
section_start_index
,
section_end_index
):
def
get_section_value
(
context
):
"""
Returns: a string of type unicode.
"""
data
=
self
.
resolve_context
(
context
,
name
)
# From the spec:
#
#
# If the data is not of a list type, it is coerced into a list
# http://docs.python.org/library/functions.html#iter
# as follows: if the data is truthy (e.g. `!!data == true`),
# use a single-element list containing the data, otherwise use
# an empty list.
#
#
if
not
data
:
# It is not sufficient, for example, to check whether the item
data
=
[]
# implements __iter__ () (the iteration protocol). There is
# also __getitem__() (the sequence protocol). In Python 2,
# strings do not implement __iter__(), but in Python 3 they do.
try
:
iter
(
data
)
except
TypeError
:
# Then the value does not support iteration.
data
=
[
data
]
else
:
else
:
# The least brittle way to determine whether something
if
is_string
(
data
)
or
isinstance
(
data
,
dict
):
# supports iteration is by trying to call iter() on it:
# Do not treat strings and dicts (which are iterable) as lists.
#
# http://docs.python.org/library/functions.html#iter
#
# It is not sufficient, for example, to check whether the item
# implements __iter__ () (the iteration protocol). There is
# also __getitem__() (the sequence protocol). In Python 2,
# strings do not implement __iter__(), but in Python 3 they do.
try
:
iter
(
data
)
except
TypeError
:
# Then the value does not support iteration.
data
=
[
data
]
data
=
[
data
]
else
:
# Otherwise, treat the value as a list.
if
is_string
(
data
)
or
isinstance
(
data
,
dict
):
# Do not treat strings and dicts (which are iterable) as lists.
return
data
data
=
[
data
]
# Otherwise, treat the value as a list.
parts
=
[]
for
val
in
data
:
if
callable
(
val
):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
#
# When used as the data value for a Section tag, the
# lambda MUST be treatable as an arity 1 function, and
# invoked as such (passing a String containing the
# unprocessed section contents). The returned value
# MUST be rendered against the current delimiters, then
# interpolated in place of the section.
#
# Also see--
#
# https://github.com/defunkt/pystache/issues/113
#
# TODO: should we check the arity?
val
=
val
(
template
[
section_start_index
:
section_end_index
])
val
=
self
.
_render_value
(
val
,
context
,
delimiters
=
delims
)
parts
.
append
(
val
)
continue
context
.
push
(
val
)
parts
.
append
(
parsed_template
.
render
(
context
))
context
.
pop
()
return
unicode
(
''
.
join
(
parts
))
return
get_section_value
def
_render_value
(
self
,
val
,
context
,
delimiters
=
None
):
def
_render_value
(
self
,
val
,
context
,
delimiters
=
None
):
"""
"""
...
@@ -247,6 +150,9 @@ class RenderEngine(object):
...
@@ -247,6 +150,9 @@ class RenderEngine(object):
val
=
self
.
literal
(
val
)
val
=
self
.
literal
(
val
)
return
self
.
render
(
val
,
context
,
delimiters
)
return
self
.
render
(
val
,
context
,
delimiters
)
def
render_parsed
(
self
,
parsed_template
,
context_stack
):
return
parsed_template
.
render
(
self
,
context_stack
)
def
render
(
self
,
template
,
context_stack
,
delimiters
=
None
):
def
render
(
self
,
template
,
context_stack
,
delimiters
=
None
):
"""
"""
Render a unicode template string, and return as unicode.
Render a unicode template string, and return as unicode.
...
@@ -259,7 +165,6 @@ class RenderEngine(object):
...
@@ -259,7 +165,6 @@ class RenderEngine(object):
context_stack: a ContextStack instance.
context_stack: a ContextStack instance.
"""
"""
parser
=
Parser
(
self
,
delimiters
=
delimiters
)
parsed_template
=
parse
(
template
,
delimiters
)
parsed_template
=
parser
.
parse
(
template
)
return
parsed_template
.
render
(
context_stack
)
return
self
.
render_parsed
(
parsed_template
,
context_stack
)
pystache/tests/test_parser.py
View file @
c8a1f2e1
...
@@ -7,6 +7,7 @@ Unit tests of parser.py.
...
@@ -7,6 +7,7 @@ Unit tests of parser.py.
import
unittest
import
unittest
from
pystache.defaults
import
DELIMITERS
from
pystache.parser
import
_compile_template_re
as
make_re
from
pystache.parser
import
_compile_template_re
as
make_re
...
@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase):
...
@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase):
Test getting a key from a dictionary.
Test getting a key from a dictionary.
"""
"""
re
=
make_re
()
re
=
make_re
(
DELIMITERS
)
match
=
re
.
search
(
"b {{test}}"
)
match
=
re
.
search
(
"b {{test}}"
)
self
.
assertEqual
(
match
.
start
(),
1
)
self
.
assertEqual
(
match
.
start
(),
1
)
...
...
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