Skip to content

Commit

Permalink
Use a static template classname and baseclassname that are unlikely t…
Browse files Browse the repository at this point in the history
…o conflict with the namespace
  • Loading branch information
asottile committed Dec 18, 2014
1 parent 4f0b311 commit f193df0
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 166 deletions.
2 changes: 2 additions & 0 deletions Cheetah/Template.py
Expand Up @@ -133,3 +133,5 @@ def respond(self):


Template.Reserved_SearchList = set(dir(Template))
# Alias for #extends
YelpCheetahTemplate = Template
29 changes: 9 additions & 20 deletions Cheetah/compile.py
Expand Up @@ -5,36 +5,30 @@
import os.path

from Cheetah import five
from Cheetah.legacy_compiler import CLASS_NAME
from Cheetah.legacy_compiler import LegacyCompiler


def compile_source(
source,
cls_name='DynamicallyCompiledTemplate',
settings=None,
compiler_cls=LegacyCompiler,
):
"""The general case for compiling from source.
:param text source: Text representing the cheetah source.
:param text cls_name: Classname for the generated module.
:param dict settings: Compile settings
:param type compiler_cls: Class to use for the compiler.
:return: The compiled output.
:rtype: text
:raises TypeError: if source or cls_name are not text.
:raises TypeError: if source is not text.
"""
if not isinstance(source, five.text):
raise TypeError(
'`source` must be `text` but got {0!r}'.format(type(source))
)

if not isinstance(cls_name, five.text):
raise TypeError(
'`cls_name` must be `text` but got {0!r}'.format(type(cls_name))
)

compiler = compiler_cls(source, cls_name, settings=settings)
compiler = compiler_cls(source, settings=settings)
return compiler.getModuleCode()


Expand All @@ -49,18 +43,14 @@ def compile_file(filename, target=None, **kwargs):
'`filename` must be `text` but got {0!r}'.format(type(filename))
)

if 'cls_name' in kwargs:
raise ValueError('`cls_name` when compiling a file is invalid')

contents = io.open(filename, encoding='UTF-8').read()

cls_name = os.path.basename(filename).split('.', 1)[0]
compiled_source = compile_source(contents, cls_name=cls_name, **kwargs)
py_file = os.path.basename(filename).split('.', 1)[0] + '.py'
compiled_source = compile_source(contents, **kwargs)

if target is None:
# Write out to the file {cls_name}.py
dirname = os.path.dirname(filename)
target = os.path.join(dirname, '{0}.py'.format(cls_name))
target = os.path.join(dirname, py_file)

with io.open(target, 'w', encoding='UTF-8') as target_file:
target_file.write('# -*- coding: UTF-8 -*-\n')
Expand All @@ -84,18 +74,17 @@ def _create_module_from_source(source, filename='<generated cheetah module>'):
return module


def compile_to_class(source, cls_name='DynamicallyCompiledTemplate', **kwargs):
def compile_to_class(source, **kwargs):
"""Compile source directly to a `type` object. Mainly used by tests.
:param text source: Text representing the cheetah source
:param text cls_name: Classname for generated module.
:param kwargs: Keyword args passed to `compile`
:return: A `Template` class
:rtype: type
"""
compiled_source = compile_source(source, cls_name=cls_name, **kwargs)
compiled_source = compile_source(source, **kwargs)
module = _create_module_from_source(compiled_source)
cls = getattr(module, cls_name)
cls = getattr(module, CLASS_NAME)
# To prevent our module from getting gc'd
cls.__module_obj__ = module
return cls
117 changes: 37 additions & 80 deletions Cheetah/legacy_compiler.py
Expand Up @@ -10,6 +10,7 @@
from __future__ import unicode_literals

import collections
import contextlib
import copy
import re
import textwrap
Expand Down Expand Up @@ -48,6 +49,9 @@

DEFAULT_COMPILER_SETTINGS = dict((v[0], v[1]) for v in _DEFAULT_COMPILER_SETTINGS)

CLASS_NAME = 'YelpCheetahTemplate'
BASE_CLASS_NAME = 'YelpCheetahBaseClass'


def genPlainVar(nameChunks):
"""Generate Python code for a Cheetah $var without using NameMapper."""
Expand Down Expand Up @@ -410,14 +414,12 @@ def methodSignature(self):
class ClassCompiler(object):
methodCompilerClass = MethodCompiler

def __init__(self, clsname, main_method_name):
self._clsname = clsname
def __init__(self, main_method_name):
self._mainMethodName = main_method_name
self._decoratorsForNextMethod = []
self._activeMethodsList = [] # stack while parsing/generating
self._finishedMethodsList = [] # store by order
self._methodsIndex = {} # store by name
self._baseClass = 'Template'
# printed after methods in the gen class def:
self._generatedAttribs = []
methodCompiler = self._spawnMethodCompiler(
Expand All @@ -438,12 +440,6 @@ def cleanupState(self):
methCompiler = self._popActiveMethodCompiler()
self._swallowMethodCompiler(methCompiler)

def className(self):
return self._clsname

def setBaseClass(self, baseClassName):
self._baseClass = baseClassName

def setMainMethodName(self, methodName):
if methodName == self._mainMethodName:
return
Expand Down Expand Up @@ -508,7 +504,7 @@ def addSuper(self, argsList):
arg_text = arg_string_list_to_text(argsList)
self.addFilteredChunk(
'super({0}, self).{1}({2})'.format(
self._clsname, methodName, arg_text,
CLASS_NAME, methodName, arg_text,
)
)

Expand All @@ -526,9 +522,9 @@ def closeBlock(self):
# insert the code to call the block
self.addChunk('self.{0}()'.format(methodName))

def classDef(self):
def class_def(self):
return '\n'.join((
'class {0}({1}):'.format(self.className(), self._baseClass),
'class {0}({1}):'.format(CLASS_NAME, BASE_CLASS_NAME),
INDENT + '## CHEETAH GENERATED METHODS', '\n', self.methodDefs(),
INDENT + '## CHEETAH GENERATED ATTRIBUTES', '\n', self.attributes(),
))
Expand All @@ -546,34 +542,31 @@ class LegacyCompiler(SettingsManager):
parserClass = LegacyParser
classCompilerClass = ClassCompiler

def __init__(self, source, moduleName, settings=None):
def __init__(self, source, settings=None):
super(LegacyCompiler, self).__init__()
if settings:
self.updateSettings(settings)

self._mainClassName = moduleName

assert isinstance(source, five.text), 'the yelp-cheetah compiler requires text, not bytes.'

if source == '':
warnings.warn('You supplied an empty string for the source!')

self._parser = self.parserClass(source, compiler=self)
self._activeClassesList = []
self._finishedClassesList = [] # listed by ordered
self._finishedClassIndex = {} # listed by name
self._class_compiler = None
self._base_import = 'from Cheetah.Template import {0} as {1}'.format(
CLASS_NAME, BASE_CLASS_NAME,
)
self._importStatements = [
'from Cheetah.DummyTransaction import DummyTransaction',
'from Cheetah.NameMapper import valueForName as VFN',
'from Cheetah.NameMapper import valueFromFrameOrSearchList as VFFSL',
'from Cheetah.Template import NO_CONTENT',
'from Cheetah.Template import Template',
]

self._importedVarNames = [
'DummyTransaction',
'NO_CONTENT',
'Template',
'VFN',
'VFFSL',
]
Expand All @@ -584,31 +577,24 @@ def __getattr__(self, name):
"""Provide one-way access to the methods and attributes of the
ClassCompiler, and thereby the MethodCompilers as well.
"""
return getattr(self._activeClassesList[-1], name)
return getattr(self._class_compiler, name)

def _initializeSettings(self):
self.updateSettings(copy.deepcopy(DEFAULT_COMPILER_SETTINGS))

def _spawnClassCompiler(self, clsname):
def _spawnClassCompiler(self):
return self.classCompilerClass(
clsname=clsname,
main_method_name=self.setting('mainMethodName'),
)

def _addActiveClassCompiler(self, classCompiler):
self._activeClassesList.append(classCompiler)

def _getActiveClassCompiler(self):
return self._activeClassesList[-1]

def _popActiveClassCompiler(self):
return self._activeClassesList.pop()

def _swallowClassCompiler(self, classCompiler):
classCompiler.cleanupState()
self._finishedClassesList.append(classCompiler)
self._finishedClassIndex[classCompiler.className()] = classCompiler
return classCompiler
@contextlib.contextmanager
def _set_class_compiler(self, class_compiler):
orig = self._class_compiler
self._class_compiler = class_compiler
try:
yield
finally:
self._class_compiler = orig

def importedVarNames(self):
return self._importedVarNames
Expand Down Expand Up @@ -717,43 +703,17 @@ def genNameMapperVar(self, nameChunks):

return pythonCode

def setBaseClass(self, extends_name):
def set_extends(self, extends_name):
self.setMainMethodName(self.setting('mainMethodNameForSubclasses'))

if extends_name in self.importedVarNames():
raise AssertionError(
'yelp_cheetah only supports extends by module name'
)

# The #extends directive results in the base class being imported
# There are (basically) three cases:
# 1. #extends foo
# import added: from foo import foo
# baseclass: foo
# 2. #extends foo.bar
# import added: from foo.bar import bar
# baseclass: bar
# 3. #extends foo.bar.bar
# import added: from foo.bar import bar
# baseclass: bar
chunks = extends_name.split('.')
# Case 1
# If we only have one part, assume it's like from {chunk} import {chunk}
if len(chunks) == 1:
chunks *= 2

class_name = chunks[-1]
if class_name != chunks[-2]:
# Case 2
# we assume the class name to be the module name
module = '.'.join(chunks)
else:
# Case 3
module = '.'.join(chunks[:-1])
self._getActiveClassCompiler().setBaseClass(class_name)
importStatement = 'from {0} import {1}'.format(module, class_name)
self.addImportStatement(importStatement)
self.addImportedVarNames((class_name,))
self._base_import = 'from {0} import {1} as {2}'.format(
extends_name, CLASS_NAME, BASE_CLASS_NAME,
)

def setCompilerSettings(self, settingsStr):
self.updateSettingsFromConfigStr(settingsStr)
Expand All @@ -770,10 +730,10 @@ def addImportStatement(self, impStatement):
importVarNames = impStatement[impStatement.find('import') + len('import'):].split(',')
importVarNames = [var.split()[-1] for var in importVarNames] # handles aliases
importVarNames = [var for var in importVarNames if not var == '*']
self.addImportedVarNames(importVarNames, raw_statement=impStatement) # used by #extend for auto-imports
self.addImportedVarNames(importVarNames, raw_statement=impStatement)

def addAttribute(self, attribName, expr):
self._getActiveClassCompiler().addAttribute(attribName + ' = ' + expr)
self._class_compiler.addAttribute(attribName + ' = ' + expr)

def addComment(self, comm):
for line in comm.splitlines():
Expand All @@ -782,15 +742,16 @@ def addComment(self, comm):
# methods for module code wrapping

def getModuleCode(self):
classCompiler = self._spawnClassCompiler(self._mainClassName)
self._addActiveClassCompiler(classCompiler)
self._parser.parse()
self._swallowClassCompiler(self._popActiveClassCompiler())
class_compiler = self._spawnClassCompiler()
with self._set_class_compiler(class_compiler):
self._parser.parse()
class_compiler.cleanupState()

moduleDef = textwrap.dedent(
"""
from __future__ import unicode_literals
%(imports)s
%(base_import)s
# This is compiled yelp_cheetah sourcecode
__YELP_CHEETAH__ = True
Expand All @@ -803,21 +764,17 @@ def getModuleCode(self):
"""
).strip() % {
'imports': self.importStatements(),
'classes': self.classDefs(),
'base_import': self._base_import,
'classes': class_compiler.class_def(),
'scannables': self.gettextScannables(),
'footer': self.moduleFooter(),
'mainClassName': self._mainClassName,
}

return moduleDef

def importStatements(self):
return '\n'.join(self._importStatements)

def classDefs(self):
classDefs = [klass.classDef() for klass in self._finishedClassesList]
return '\n\n'.join(classDefs)

def moduleFooter(self):
return """
# CHEETAH was developed by Tavis Rudd and Mike Orr
Expand All @@ -828,7 +785,7 @@ def moduleFooter(self):
from os import environ
from sys import stdout
stdout.write({main_class_name}(searchList=[environ]).respond())
""".format(main_class_name=self._mainClassName)
""".format(main_class_name=CLASS_NAME)

def gettextScannables(self):
scannables = tuple(INDENT + nameChunks for nameChunks in self._gettextScannables)
Expand Down
7 changes: 3 additions & 4 deletions Cheetah/legacy_parser.py
Expand Up @@ -79,7 +79,6 @@ def makeTripleQuoteRe(start, end):

escapedNewlineRE = re.compile(r'(?<!\\)((\\\\)*)\\(n|012)')

# TODO(buck): audit all directives. delete with prejudice.
directiveNamesAndParsers = {
# importing and inheritance
'import': None,
Expand Down Expand Up @@ -1351,14 +1350,14 @@ def eatExtends(self):
self.getDirectiveStartToken()
self.advance(len('extends'))
self.getWhiteSpace()
basecls_name = self.readToEOL(gobble=False)
extends_value = self.readToEOL(gobble=False)

if ',' in basecls_name:
if ',' in extends_value:
raise ParseError(
self, 'yelp_cheetah does not support multiple inheritance'
)

self._compiler.setBaseClass(basecls_name)
self._compiler.set_extends(extends_value)
self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)

def eatImplements(self):
Expand Down
6 changes: 2 additions & 4 deletions Cheetah/partial_template.py
Expand Up @@ -97,13 +97,11 @@ def __new__(mcs, name, bases, attrs):
default_self_function = default_self(value)
setattr(module, attrname, default_self_function)

if name == attrname:
# If the class and function names collide, overwrite the class with the function.
result = default_self_function
assert name != attrname
return result


# Roughly stolen from six.with_metaclass
partial_template = type.__new__(
YelpCheetahTemplate = type.__new__(
PartialTemplateType, str('partial_template'), (Template,), {},
)

0 comments on commit f193df0

Please sign in to comment.