Skip to content

Commit

Permalink
Further refinement of table handling with packages. More unit tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
dabeaz committed Apr 24, 2015
1 parent cbea715 commit daa1160
Show file tree
Hide file tree
Showing 20 changed files with 470 additions and 53 deletions.
28 changes: 19 additions & 9 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
Version 3.6
---------------------
04/24/15: beazley
Fixed some problems related to use of packages and table file
modules. Just to emphasize, if you use a command such as this:
Fixed some issues related to use of packages and table file
modules. Just to emphasize, PLY now generates its special
files such as 'parsetab.py' and 'lextab.py' in the *SAME*
directory as the source file that uses lex() and yacc().

# lexer.py
parser = yacc.yacc(tabmodule='foo.bar.parsetab')
If for some reason, you want to change the name of the table
module, use the tabmodule and lextab options:

You need to make sure that the file 'lexer.py' is located at
foo/bar/lexer.py. If this is not the same location, then
you need to also include the outputdir option
lexer = lex.lex(lextab='spamlextab')
parser = yacc.yacc(tabmodule='spamparsetab')

parse = yacc.yacc(tabmodule='foo.bar.parsetab',
outputdir='foo/bar')
If you specify a simple name as shown, the module will still be
created in the same directory as the file invoking lex() or yacc().
If you want the table files to be placed into a different package,
then give a fully qualified package name. For example:

lexer = lex.lex(lextab='pkgname.files.lextab')
parser = yacc.yacc(tabmodule='pkgname.files.parsetab')

For this to work, 'pkgname.files' must already exist as a valid
Python package (i.e., the directories must already exist and be
set up with the proper __init__.py files, etc.).

Version 3.5
---------------------
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PLY (Python Lex-Yacc) Version 3.5
PLY (Python Lex-Yacc) Version 3.6

Copyright (C) 2001-2015,
David M. Beazley (Dabeaz LLC)
Expand Down
2 changes: 1 addition & 1 deletion doc/ply.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h1>PLY (Python Lex-Yacc)</h1>
</b>

<p>
<b>PLY Version: 3.5</b>
<b>PLY Version: 3.6</b>
<p>

<!-- INDEX -->
Expand Down
54 changes: 31 additions & 23 deletions ply/lex.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,10 @@ def clone(self, object=None):
# ------------------------------------------------------------
# writetab() - Write lexer information to a table file
# ------------------------------------------------------------
def writetab(self, tabfile, outputdir=''):
if isinstance(tabfile, types.ModuleType):
return
parts = tabfile.split('.')
basetabfilename = parts[-1]
if not outputdir and len(parts) > 1:
# If no explicit output directory was given, then set it to the location of the tabfile
packagename = '.'.join(parts[:-1])
exec('import %s' % packagename)
package = sys.modules[packagename]
outputdir = os.path.dirname(package.__file__)

filename = os.path.join(outputdir, basetabfilename) + '.py'
def writetab(self, basetabmodule, outputdir=''):
filename = os.path.join(outputdir, basetabmodule) + '.py'
with open(filename, 'w') as tf:
tf.write('# %s.py. This file automatically created by PLY (version %s). Don\'t edit!\n' % (tabfile, __version__))
tf.write('# %s.py. This file automatically created by PLY (version %s). Don\'t edit!\n' % (basetabmodule, __version__))
tf.write('_tabversion = %s\n' % repr(__tabversion__))
tf.write('_lextokens = %s\n' % repr(self.lextokens))
tf.write('_lexreflags = %s\n' % repr(self.lexreflags))
Expand Down Expand Up @@ -886,19 +875,38 @@ def lex(module=None, object=None, debug=False, optimize=False, lextab='lextab',
if object:
module = object

# Get the module dictionary used for the parser
if module:
_items = [(k, getattr(module, k)) for k in dir(module)]
ldict = dict(_items)
if outputdir is None:
srcfile = getattr(module, '__file__', None)
if srcfile is None:
if hasattr(module, '__module__'):
srcfile = getattr(sys.modules[module.__module__], '__file__', '')
outputdir = os.path.dirname(srcfile)
# If no __file__ attribute is available, try to obtain it from the __module__ instead
if '__file__' not in ldict:
ldict['__file__'] = sys.modules[ldict['__module__']].__file__
else:
ldict = get_caller_module_dict(2)
if outputdir is None:
outputdir = os.path.dirname(ldict.get('__file__', ''))

if outputdir is None:
# If no output directory is set, the location of the output files
# is determined according to the following rules:
# - If lextab specifies a package, files go into that package directory
# - Otherwise, files go in the same directory as the specifying module
if '.' not in lextab:
srcfile = ldict['__file__']
else:
parts = lextab.split('.')
pkgname = '.'.join(parts[:-1])
exec('import %s' % pkgname)
srcfile = getattr(sys.modules[pkgname], '__file__', '')
outputdir = os.path.dirname(srcfile)

# Determine if the module is package of a package or not.
# If so, fix the tabmodule setting so that tables load correctly
pkg = ldict.get('__package__')
if pkg:
if '.' not in lextab:
lextab = pkg + '.' + lextab

baselextab = lextab.split('.')[-1]

# Collect parser information from the dictionary
linfo = LexerReflect(ldict, log=errorlog, reflags=reflags)
Expand Down Expand Up @@ -1021,7 +1029,7 @@ def lex(module=None, object=None, debug=False, optimize=False, lextab='lextab',

# If in optimize mode, we write the lextab
if lextab and optimize:
lexobj.writetab(lextab, outputdir)
lexobj.writetab(baselextab, outputdir)

return lexobj

Expand Down
45 changes: 27 additions & 18 deletions ply/yacc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2692,15 +2692,7 @@ def lr_parse_table(self):
# This function writes the LR parsing tables to a file
# -----------------------------------------------------------------------------

def write_table(self, modulename, outputdir='', signature=''):
parts = modulename.split('.')
basemodulename = parts[-1]
if not outputdir and len(parts) > 1:
# If no explicit output directory was given, then set it to the location of the tabfile
packagename = '.'.join(parts[:-1])
exec('import %s' % packagename)
package = sys.modules[packagename]
outputdir = os.path.dirname(package.__file__)
def write_table(self, basemodulename, outputdir='', signature=''):
filename = os.path.join(outputdir, basemodulename) + '.py'
try:
f = open(filename, 'w')
Expand Down Expand Up @@ -3203,16 +3195,33 @@ def yacc(method='LALR', debug=yaccdebug, module=None, tabmodule=tab_module, star
if module:
_items = [(k, getattr(module, k)) for k in dir(module)]
pdict = dict(_items)
if outputdir is None:
srcfile = getattr(module, '__file__', None)
if srcfile is None:
if hasattr(module, '__module__'):
srcfile = getattr(sys.modules[module.__module__], '__file__', '')
outputdir = os.path.dirname(srcfile)
# If no __file__ attribute is available, try to obtain it from the __module__ instead
if '__file__' not in pdict:
pdict['__file__'] = sys.modules[pdict['__module__']].__file__
else:
pdict = get_caller_module_dict(2)
if outputdir is None:
outputdir = os.path.dirname(pdict.get('__file__', ''))

if outputdir is None:
# If no output directory is set, the location of the output files
# is determined according to the following rules:
# - If tabmodule specifies a package, files go into that package directory
# - Otherwise, files go in the same directory as the specifying module
if '.' not in tabmodule:
srcfile = pdict['__file__']
else:
parts = tabmodule.split('.')
pkgname = '.'.join(parts[:-1])
exec('import %s' % pkgname)
srcfile = getattr(sys.modules[pkgname], '__file__', '')
outputdir = os.path.dirname(srcfile)

# Determine if the module is package of a package or not.
# If so, fix the tabmodule setting so that tables load correctly
pkg = pdict.get('__package__')
if pkg and '.' not in tabmodule:
tabmodule = pkg + '.' + tabmodule

basetabmodule = tabmodule.split('.')[-1]

# Set start symbol if it's specified directly using an argument
if start is not None:
Expand Down Expand Up @@ -3420,7 +3429,7 @@ def yacc(method='LALR', debug=yaccdebug, module=None, tabmodule=tab_module, star

# Write the table file if requested
if write_tables:
lr.write_table(tabmodule, outputdir, signature)
lr.write_table(basetabmodule, outputdir, signature)

# Write a pickled version of the tables
if picklefile:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
It is compatible with both Python 2 and Python 3.
""",
license="""BSD""",
version = "3.5",
version = "3.6",
author = "David Beazley",
author_email = "[email protected]",
maintainer = "David Beazley",
Expand Down
9 changes: 9 additions & 0 deletions test/pkg_test1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Tests proper handling of lextab and parsetab files in package structures

# Here for testing purposes
import sys
if '..' not in sys.path:
sys.path.insert(0, '..')

from .parsing.calcparse import parser

Empty file.
47 changes: 47 additions & 0 deletions test/pkg_test1/parsing/calclex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -----------------------------------------------------------------------------
# calclex.py
# -----------------------------------------------------------------------------

import ply.lex as lex

tokens = (
'NAME','NUMBER',
'PLUS','MINUS','TIMES','DIVIDE','EQUALS',
'LPAREN','RPAREN',
)

# Tokens

t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_EQUALS = r'='
t_LPAREN = r'\('
t_RPAREN = r'\)'
t_NAME = r'[a-zA-Z_][a-zA-Z0-9_]*'

def t_NUMBER(t):
r'\d+'
try:
t.value = int(t.value)
except ValueError:
print("Integer value too large %s" % t.value)
t.value = 0
return t

t_ignore = " \t"

def t_newline(t):
r'\n+'
t.lexer.lineno += t.value.count("\n")

def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)

# Build the lexer
lexer = lex.lex(optimize=True)



66 changes: 66 additions & 0 deletions test/pkg_test1/parsing/calcparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# -----------------------------------------------------------------------------
# yacc_simple.py
#
# A simple, properly specifier grammar
# -----------------------------------------------------------------------------

from .calclex import tokens
from ply import yacc

# Parsing rules
precedence = (
('left','PLUS','MINUS'),
('left','TIMES','DIVIDE'),
('right','UMINUS'),
)

# dictionary of names
names = { }

def p_statement_assign(t):
'statement : NAME EQUALS expression'
names[t[1]] = t[3]

def p_statement_expr(t):
'statement : expression'
t[0] = t[1]

def p_expression_binop(t):
'''expression : expression PLUS expression
| expression MINUS expression
| expression TIMES expression
| expression DIVIDE expression'''
if t[2] == '+' : t[0] = t[1] + t[3]
elif t[2] == '-': t[0] = t[1] - t[3]
elif t[2] == '*': t[0] = t[1] * t[3]
elif t[2] == '/': t[0] = t[1] / t[3]

def p_expression_uminus(t):
'expression : MINUS expression %prec UMINUS'
t[0] = -t[2]

def p_expression_group(t):
'expression : LPAREN expression RPAREN'
t[0] = t[2]

def p_expression_number(t):
'expression : NUMBER'
t[0] = t[1]

def p_expression_name(t):
'expression : NAME'
try:
t[0] = names[t[1]]
except LookupError:
print("Undefined name '%s'" % t[1])
t[0] = 0

def p_error(t):
print("Syntax error at '%s'" % t.value)

parser = yacc.yacc()





9 changes: 9 additions & 0 deletions test/pkg_test2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Tests proper handling of lextab and parsetab files in package structures

# Here for testing purposes
import sys
if '..' not in sys.path:
sys.path.insert(0, '..')

from .parsing.calcparse import parser

Empty file.
47 changes: 47 additions & 0 deletions test/pkg_test2/parsing/calclex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -----------------------------------------------------------------------------
# calclex.py
# -----------------------------------------------------------------------------

import ply.lex as lex

tokens = (
'NAME','NUMBER',
'PLUS','MINUS','TIMES','DIVIDE','EQUALS',
'LPAREN','RPAREN',
)

# Tokens

t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_EQUALS = r'='
t_LPAREN = r'\('
t_RPAREN = r'\)'
t_NAME = r'[a-zA-Z_][a-zA-Z0-9_]*'

def t_NUMBER(t):
r'\d+'
try:
t.value = int(t.value)
except ValueError:
print("Integer value too large %s" % t.value)
t.value = 0
return t

t_ignore = " \t"

def t_newline(t):
r'\n+'
t.lexer.lineno += t.value.count("\n")

def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)

# Build the lexer
lexer = lex.lex(optimize=True, lextab='calclextab')



Loading

0 comments on commit daa1160

Please sign in to comment.