Skip to content
Browse files

Now Python 3 compatible

  • Loading branch information...
1 parent 7ba9f36 commit ea21d8f213cfc8cd308305e8d60c9491ca8ea161 @smartt committed Aug 23, 2011
Showing with 317 additions and 18 deletions.
  1. +5 −1 README.markdown
  2. +13 −17 jsmacro.py
  3. +299 −0 jsmacro_25.py
View
6 README.markdown
@@ -78,7 +78,7 @@ Why bother?
Why Python?
-----------
-Besides being fun to use, Python is available on all of the development and deployment environments that I've used in the past who-knows-how-many years. This is important, since it means not installing something new on servers (which might not be mine.) While it might be fun to write this tool in Lisp, Haskell, or even JavaScript on v8, those aren't practical for a wider audience. Perl, Python, and to some degree, Java, are a pretty safe bet.
+Besides being fun to use, Python is available on all of the development and deployment environments that I've used in the past who-knows-how-many years. This is important, since it means not installing something new on servers (which might not be mine.) While it might be fun to write this tool in another language, the one's I'm interested in aren't practical for a wider audience. Perl, Python, and to some degree, Java, are a pretty safe bet for general use.
Note though, that the tool intentionally isn't called "pyjsmacro". I'm perfectly happy having implementations in other languages, provided they all pass the same test cases.
@@ -93,6 +93,10 @@ Future Ideas
Changes
-------
+v0.2.14
+
+ - jsmacro.py now runs on Python 3. Testing environments include: Python 2.6, Python 2.7, PyPy 1.6 (which is Python 2.7.1), and Python 3.2.
+
v0.2.10
- Fixed a bug where a DEFINE on the command-line wasn't overriding a DEFINE in the input file.
View
30 jsmacro.py
@@ -2,15 +2,14 @@
from datetime import datetime
import getopt
-import hashlib
import os
import re
import sys
__author__ = "Erik Smartt"
__copyright__ = "Copyright 2010, Erik Smartt"
__license__ = "MIT"
-__version__ = "0.2.12"
+__version__ = "0.2.14"
__usage__ = """Normal usage:
jsmacro.py -f [INPUT_FILE_NAME] > [OUTPUT_FILE]
@@ -61,7 +60,7 @@ def reset(self):
self.env = {}
def handle_define(self, key, value='1'):
- if self.env.has_key(key):
+ if key in self.env:
return
self.env[key] = eval(value)
@@ -101,7 +100,7 @@ def handle_ifdef(self, arg, text):
"""
parts = re.split(self.re_else_pattern, text)
- if self.env.has_key(arg):
+ if arg in self.env:
return "\n{s}".format(s=parts[0])
else:
@@ -120,7 +119,7 @@ def handle_ifndef(self, arg, text):
"""
parts = re.split(self.re_else_pattern, text)
- if self.env.has_key(arg):
+ if arg in self.env:
try:
return "{s}".format(s=parts[1])
@@ -190,7 +189,7 @@ def scan_and_parse_dir(srcdir, destdir, parser):
in_file_path = "{p}/{f}".format(p=in_path, f=filename)
out_file_path = "{p}/{f}".format(p=out_path, f=filename)
- print("{i} -> {o}".format(i=in_file_path, o=out_file_path))
+ print(("{i} -> {o}".format(i=in_file_path, o=out_file_path)))
if not(os.path.exists(out_path)):
os.mkdir(out_path)
@@ -217,13 +216,10 @@ def scan_for_test_files(dirname, parser):
out_target_output = out_file.read()
out_file.close()
- # Hopefully this doesn't come back to bite me, but I'm using a hash of the
- # output to compare it with the known TEST PASS state. The odds of a false
- # positive are pretty slim...
- if hashlib.sha224(out_target_output).hexdigest() == hashlib.sha224(in_parsed).hexdigest():
- print("PASS [{s}]".format(s=in_file_path))
+ if out_target_output == in_parsed:
+ print(("PASS [{s}]".format(s=in_file_path)))
else:
- print("FAIL [{s}]".format(s=in_file_path))
+ print(("FAIL [{s}]".format(s=in_file_path)))
if parser.save_expected_failures:
# Write the expected output file for local diffing
@@ -232,8 +228,8 @@ def scan_for_test_files(dirname, parser):
fout.close()
else:
- print("\n-- EXPECTED --\n{s}".format(s=out_target_output))
- print("\n-- GOT --\n{s}".format(s=in_parsed))
+ print(("\n-- EXPECTED --\n{s}".format(s=out_target_output)))
+ print(("\n-- GOT --\n{s}".format(s=in_parsed)))
parser.reset()
@@ -248,8 +244,8 @@ def scan_for_test_files(dirname, parser):
opts, args = getopt.getopt(sys.argv[1:],
"hf:s:d:",
["help", "file=", "srcdir=","dstdir=", "test", "def=", "savefail", "version"])
- except getopt.GetoptError, err:
- print(str(err))
+ except getopt.GetoptError as err:
+ print((str(err)))
print(__usage__)
sys.exit(2)
@@ -298,7 +294,7 @@ def scan_for_test_files(dirname, parser):
break
if o in ["-f", "--file"]:
- print(p.parse(a))
+ print((p.parse(a)))
break
View
299 jsmacro_25.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python
+
+#
+#
+# This older snapshot is Python 2.5 compatible, whereas the main jsmacro.py targets
+# Python 2.6 and newer (including Python 3.) There are no guarentees that this
+# version will be kept feature-complete in the future, but it may buy you some time
+# if you need jsmacro.py in production.
+#
+#
+
+from datetime import datetime
+import getopt
+import os
+import re
+import sys
+
+__author__ = "Erik Smartt"
+__copyright__ = "Copyright 2010, Erik Smartt"
+__license__ = "MIT"
+__version__ = "0.2.11.2"
+__usage__ = """Normal usage:
+ jsmacro.py -f [INPUT_FILE_NAME] > [OUTPUT_FILE]
+
+ Options:
+ --def [VAR] Sets the supplied variable to True in the parser environment.
+ --file [VAR] Same as -f; Used to load the input file.
+ --help Prints this Help message.
+ --savefail Saves the expected output of a failed test case to disk.
+ --test Run the test suite.
+ --version Print the version number of jsmacro being used."""
+
+
+
+class MacroEngine(object):
+ """
+ The MacroEngine is where the magic happens. It defines methods that are called
+ to handle the macros found in a document.
+ """
+ def __init__(self):
+ self.save_expected_failures = False
+
+ self.re_else_pattern = '//[\@|#]else'
+
+ # Compile the main patterns
+ self.re_define_macro = re.compile("(\s*\/\/[\@|#]define\s*)(\w*)\s*(\w*)", re.I)
+
+ self.re_date_sub_macro = re.compile("[\@|#]\_\_date\_\_", re.I)
+ self.re_time_sub_macro = re.compile("[\@|#]\_\_time\_\_", re.I)
+ self.re_datetime_sub_macro = re.compile("[\@|#]\_\_datetime\_\_", re.I)
+
+ self.re_stripline_macro = re.compile(".*\/\/[\@|#]strip.*", re.I)
+
+ # A wrapped macro takes the following form:
+ #
+ # //@MACRO <ARGUMENTS>
+ # ...some code
+ # //@end
+ self.re_wrapped_macro = re.compile("(\s*\/\/[\@|#])([a-z]+)\s+(\w*?\s)(.*?)(\s*\/\/[\@|#]end(if)?)", re.M|re.S)
+
+ self.reset()
+
+ def reset(self):
+ self.env = {}
+
+ def handle_define(self, key, value='1'):
+ if (self.env.has_key(key)):
+ return
+
+ self.env[key] = eval(value)
+
+ def handle_if(self, arg, text):
+ """
+ Returns the text to output based on the value of 'arg'. E.g., if arg evaluates to false,
+ expect to get an empty string back.
+
+ @param arg String Statement found after the 'if'. Currently expected to be a variable (i.e., key) in the env dictionary.
+ @param text String The text found between the macro statements
+ """
+ # To handle the '//@else' statement, we'll split text on the statement.
+ parts = re.split(self.re_else_pattern, text)
+
+ try:
+ if (self.env[arg]):
+ return "\n%s" % parts[0]
+ else:
+ try:
+ return "%s" % parts[1]
+ except IndexError:
+ return ''
+ except KeyError:
+ return "\n%s" % text
+
+ def handle_ifdef(self, arg, text):
+ """
+ @param arg String Statement found after the 'ifdef'. Currently expected to be a variable (i.e., key) in the env dictionary.
+ @param text String The text found between the macro statements
+
+ An ifdef is true if the variable 'arg' exists in the environment, regardless of whether
+ it resolves to True or False.
+ """
+ parts = re.split(self.re_else_pattern, text)
+
+ if (self.env.has_key(arg)):
+ return "\n%s" % parts[0]
+ else:
+ try:
+ return "%s" % parts[1]
+ except IndexError:
+ return ''
+
+ def handle_ifndef(self, arg, text):
+ """
+ @param arg String Statement found after the 'ifndef'. Currently expected to be a variable (i.e., key) in the env dictionary.
+ @param text String The text found between the macro statements
+
+ An ifndef is true if the variable 'arg' does not exist in the environment.
+ """
+ parts = re.split(self.re_else_pattern, text)
+
+ if (self.env.has_key(arg)):
+ try:
+ return "%s" % parts[1]
+ except IndexError:
+ return ''
+ else:
+ return "\n%s" % parts[0]
+
+ def handle_macro(self, mo):
+ method = mo.group(2)
+ args = mo.group(3).strip()
+ code = "\n%s" % mo.group(4)
+
+ # This is a fun line. We construct a method name using the
+ # string found in the regex, and call that method on self
+ # with the arguments we have. So, we can dynamically call
+ # methods... (and eventually, we'll support adding methods
+ # at runtime :-)
+ return getattr(self, "handle_%s" % method)(args, code)
+
+
+ def parse(self, file_name):
+ now = datetime.now()
+
+ fp = open(file_name, 'r')
+ text = fp.read()
+ fp.close()
+
+ # Replace supported __foo__ statements
+ text = self.re_date_sub_macro.sub('%s' % (now.strftime("%b %d, %Y")),
+ self.re_time_sub_macro.sub('%s' % (now.strftime("%I:%M%p")),
+ self.re_datetime_sub_macro.sub('%s' % (now.strftime("%b %d, %Y %I:%M%p")), text)))
+
+ # Parse for DEFINE statements
+ for mo in self.re_define_macro.finditer(text):
+ if mo:
+ k = mo.group(2) # key
+ v = mo.group(3) # value
+
+ self.handle_define(k, v)
+
+ # Delete the DEFINE statements
+ text = self.re_define_macro.sub('', text)
+
+ # Drop any lines containing a //@strip statement
+ text = self.re_stripline_macro.sub('', text)
+
+ # Do the magic...
+ text = self.re_wrapped_macro.sub(self.handle_macro, text)
+
+ return text
+
+def scan_and_parse_dir(srcdir, destdir, parser):
+
+ for root, dirs, files in os.walk(srcdir):
+ for filename in files:
+
+ if not(filename.endswith('.js')):
+ continue
+
+ dir = root[len(srcdir)+1:]
+
+ if srcdir != root:
+ dir = dir + '/'
+
+ inpath = "%s/%s" % (srcdir, dir)
+ outpath = "%s/%s" % (destdir, dir)
+
+ in_file_path = "%s%s" % (inpath, filename)
+ out_file_path = "%s%s" % (outpath, filename)
+ print("%s -> %s", in_file_path, out_file_path)
+
+ if not(os.path.exists(outpath)):
+ os.mkdir(outpath)
+
+ data = parser.parse(in_file_path)
+ outfile = open(out_file_path,'w')
+ outfile.write(data)
+ outfile.close()
+
+# ---------------------------------
+# MAIN
+# ---------------------------------
+
+def scan_for_test_files(dirname, parser):
+ for root, dirs, files in os.walk(dirname):
+ for in_filename in files:
+ if in_filename.endswith('in.js'):
+ in_file_path = "%s/%s" % (dirname, in_filename)
+ out_file_path = "%s/%s" % (dirname, "%sout.js" % (in_filename[:-5]))
+
+ in_parsed = parser.parse(in_file_path)
+
+ out_file = open(out_file_path, 'r')
+ out_target_output = out_file.read()
+ out_file.close()
+
+ if (out_target_output == in_parsed):
+ print "PASS [%s]" % (in_file_path)
+ else:
+ print "FAIL [%s]" % (in_file_path)
+
+ if parser.save_expected_failures:
+ # Write the expected output file for local diffing
+ fout = open('%s_expected' % out_file_path, 'w')
+ fout.write(in_parsed)
+ fout.close()
+
+ else:
+ print "\n-- EXPECTED --\n%s" % (out_target_output)
+ print "\n-- GOT --\n%s" % (in_parsed)
+
+ parser.reset()
+
+
+# --------------------------------------------------
+# MAIN
+# --------------------------------------------------
+if __name__ == "__main__":
+ p = MacroEngine()
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:],
+ "hf:s:d:",
+ ["help", "file=", "srcdir=","dstdir=", "test", "def=", "savefail", "version"])
+ except getopt.GetoptError, err:
+ print str(err)
+ print __usage__
+ sys.exit(2)
+
+
+ # First handle commands that exit
+ for o, a in opts:
+ if o in ["-h", "--help"]:
+ print __usage__
+ sys.exit(0)
+
+ if o in ["--version"]:
+ print __version__
+ sys.exit(0)
+
+
+ # Next, handle commands that config
+ for o, a in opts:
+ if o in ["--def"]:
+ p.handle_define(a)
+ continue
+
+ if o in ["--savefail"]:
+ p.save_expected_failures = True
+ continue
+
+ srcdir = None
+ dstdir = None
+ # Now handle commands the execute based on the config
+ for o, a in opts:
+
+ if o in ["-s", "--srcdir"]:
+ srcdir = a
+
+ if o in ["-d", "--dstdir"]:
+ dstdir = a
+ if srcdir == None:
+ raise Exception("you must set the srcdir when setting a dstdir.")
+ else:
+ scan_and_parse_dir(srcdir, dstdir, p)
+ break
+
+ if o in ["-f", "--file"]:
+ print p.parse(a)
+ break
+
+ if o in ["--test"]:
+ print "Testing..."
+ scan_for_test_files("testfiles", p)
+ print "Done."
+ break
+
+ sys.exit(0)

0 comments on commit ea21d8f

Please sign in to comment.
Something went wrong with that request. Please try again.