forked from SeattleTestbed/portability
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrepyhelper.py
498 lines (388 loc) · 17.6 KB
/
repyhelper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
"""
<Program Name>
repyhelper.py
<Started>
November 2008
<Author>
Andreas Sekine
Heavily revised by: Justin Cappos
<Purpose>
Make porting Repy code to regular python easier. The main interface
is through repyhelper.translate and repyhelper.translate_and_import
JAC Note: I wanted to add an interface that allowed a user to specify
a repyhelper path that is separate from the Python path. This seems to
be impossible because you can't always use absolute names when importing
Python modules and because you can't prevent other imports from happening.
This prevent me from writing modules in a location other than the python
path. As jsamuel pointed out, it's not clear how this interacts with
non-relative path names (#291). My solution is to write these files into
the first item in the Python path.
"""
import os # for file checks
import inspect # for fiddling with callstack/module namespaces
# JAC / JS: to get the Python path
import sys
TRANSLATION_TAGLINE = "### Automatically generated by repyhelper.py ###"
WARNING_LABEL = """
### THIS FILE WILL BE OVERWRITTEN!
### DO NOT MAKE CHANGES HERE, INSTEAD EDIT THE ORIGINAL SOURCE FILE
###
### If changes to the src aren't propagating here, try manually deleting this file.
### Deleting this file forces regeneration of a repy translation
"""
# add 'coding: utf-8' to deal with ASCII characters problem
ENCODING_STRING = """# -*- coding: utf-8 -*-"""
class TranslationError(Exception):
""" An error occurred during translation """
#For keeping a truly shared context between translated files
shared_context = {}
def get_shared_context():
""" Ensure all imported repy code has a common 'mycontext' dict """
global shared_context
return shared_context
# this specifies where the preprocessed files should end up. By default, they
# will be written to the same directory as they are in. If there is
# a relative path name, it will be written in sys.path[0]
_importcachedir = None
def set_importcachedir(newimportcachedir):
"""
<Purpose>
Repyhelper creates Python versions of repy files. This function sets
the location where those all files will be stored. By default, files are
stored wherever they are found in the python path. If a relative path
name is specified, by default, files are instead stored in the first
directory in the Python path sys.path[0] (usually the current directory)
<Arguments>
newimportcachedir:
The location where all files should be stored. Use None to restore
the default behavior
<Exceptions>
TypeError if the path is invalid.
ValueError is thrown if the newimportcachedir isn't in the path
<Side Effects>
None.
<Returns>
None.
"""
global _importcachedir
# handle None...
if newimportcachedir == None:
_importcachedir = None
return
# else, is this a valid path?
if type(newimportcachedir) != str:
raise TypeError("Type of newimportcachedir '"+str(newimportcachedir)+"' is not a string")
# If it's an empty string, assume it's '.'
if newimportcachedir == '':
newimportcachedir = '.'
if not os.path.isdir(newimportcachedir):
raise TypeError("Path given for newimportcachedir '"+str(newimportcachedir)+"' is not a directory")
if newimportcachedir not in sys.path:
raise ValueError, "The import cache dir '"+newimportcachedir+"' isn't in the Python path"
# set the path... We're done.
_importcachedir = newimportcachedir
def set_shared_context(context):
"""
<Purpose>
Set the shared mycontext dictionary
<Arguments>
context:
A dict to use as the new mycontext
<Exceptions>
TypeError if context is none
<Side Effects>
Creates a python file correspond to the repy file, overwriting previously
generated files that exists with that name
<Returns>
The name of the Python module that was created in the current directory. This
string can be used with __import__ to import the translated module.
"""
global shared_context
if context is None:
raise TypeError("Context can't be none")
shared_context = context
#Ensure the generated module has a safe name
# Can't use . in the name because import uses it for scope, so convert to _
def _get_module_name(repyfilename):
head,tail = os.path.split(repyfilename)
tail = tail.replace('.', '_')
return os.path.join(head, tail)
def _translation_is_needed(repyfilename, generatedfile):
""" Checks if generatedfile needs to be regenerated. Does several checks to
decide if generating generatedfilename based on repyfilename is a good idea.
--does file already exist?
--was it automatically generated?
--was it generated from the same source file?
--was the original modified since the last translation?
"""
if not os.path.isfile(repyfilename):
raise TranslationError("no such file:", repyfilename)
if not os.path.isfile(generatedfile):
return True
#Read the first line
try:
fh = open(generatedfile, "r")
first_line = fh.readline().rstrip()
second_line = fh.readline().rstrip()
current_line = ''
for line in fh:
current_line = line
last_line = current_line
fh.close()
except IOError, e:
raise TranslationError("Error opening old generated file: " + generatedfile + ": " + str(e))
#Check to see if the file was generated by repyhelper, to prevent
#clobbering a file that we didn't create
if not first_line.startswith(ENCODING_STRING) or not second_line.startswith(TRANSLATION_TAGLINE):
# it also could have been created by an earlier version of repyhelper.
if not first_line.startswith(TRANSLATION_TAGLINE):
raise TranslationError("File name exists but wasn't automatically generated: " + generatedfile)
if not last_line.startswith(TRANSLATION_TAGLINE):
# The file generation wasn't completed... I think this means we should
# silently regenerate (#617)
return True
#Check to see if the generated file has the same original source
old_translation_path = first_line[len(TRANSLATION_TAGLINE):].strip()
generated_abs_path = os.path.abspath(repyfilename)
if old_translation_path != generated_abs_path:
#It doesn't match, but the other file was also a translation! Regen then...
return True
#If we get here and modification time of orig is older than gen, this is still
#a valid generation
repystat = os.stat(repyfilename)
genstat = os.stat(generatedfile)
if repystat.st_mtime < genstat.st_mtime:
return False
return True
def _generate_python_file_from_repy(repyfilename, generatedfilename, shared_mycontext, callfunc, callargs):
""" Generate a python module from a repy file so it can be imported
The first line is TRANSLATION_TAGLINE, so it's easy to detect that
the file was automatically generated
"""
#Start the generation! Print out the header and portability stuff, then include
#the original data and translations
try:
# Create path if it doesn't exist.
# JAC: due to #814, we check for the empty directory too...
if os.path.dirname(generatedfilename) != '' and not os.path.isdir(os.path.dirname(generatedfilename)):
os.makedirs(os.path.dirname(generatedfilename))
fh = open(generatedfilename, "w")
except IOError, e:
# this is likely a directory permissions error
raise TranslationError("Cannot open file for translation '" + repyfilename + "': " + str(e))
# always close the file
try:
print >> fh, ENCODING_STRING
print >> fh, TRANSLATION_TAGLINE, os.path.abspath(repyfilename)
print >> fh, WARNING_LABEL
print >> fh, "from repyportability import *"
print >> fh, "from repyportability import _context"
print >> fh, "import repyhelper"
if shared_mycontext:
print >> fh, "mycontext = repyhelper.get_shared_context()"
else:
print >> fh, "mycontext = {}"
print >> fh, "callfunc =", repr(callfunc)
#Properly format the callargs list. Assume it only contains python strings
print >> fh, "callargs =", repr(callargs)
print >> fh
_process_output_file(fh, repyfilename, generatedfilename)
# append the TRANSLATION_TAGLINE so that we can see if the operation was
# interrupted (#617)
print >> fh
print >> fh, TRANSLATION_TAGLINE, os.path.abspath(repyfilename)
except IOError, e:
raise TranslationError("Error translating file " + repyfilename + ": " + str(e))
finally:
fh.close()
def _process_output_file(outfh, filename, generatedfilename):
""" Read filename and print it to outfh, except convert includes into calls to
repyhelper.translate
"""
try:
repyfh = open(filename, "r")
repyfiledata = repyfh.readlines()
repyfh.close()
except IOError, e:
#Delete the partially translated file, to ensure this partial translation
#doesn't get used
try:
os.remove(generatedfilename)
except (IOError, OSError):
pass
raise TranslationError("Error opening " + filename + ": " + str(e))
#Having read all the data, lets output it again, performing translations
#as needed
for line in repyfiledata:
#look for includes, and substitute them with calls to translate
if line.startswith('include '):
includename = line[len('include '):].strip()
modulename = _get_module_name(includename)
print >> outfh, "repyhelper.translate_and_import('" + includename + "')"
else:
print >> outfh, line, #line includes a newline, so dont add another
def translate(filename, shared_mycontext=True, callfunc="import", callargs=None, force_overwrite=False):
"""
<Purpose>
Translate a Repy file into a valid python module that can be imported by
the standard "import" statement.
Creates a python file correspond to the repy file in the current directory,
with all '.' in the name replaced with "_", and ".py" appended to it to
make it a valid python module name.
Performs several checks to only perform a translation when necessary, and
to prevent accidentally clobbering other files.
The repyhelper and repyportability modules must be in the Python path for
the translated files
Note that the optional arguments used to set variables are only used
if the file is retranslated--otherwise they are ignored. To ensure they're
used, manually delete the translation to force regeneration
<Arguments>
repyfilename:
A valid repy file name that exists in the Python path (sys.path). If the
filename contains a directory separator, it is used instead of the path.
shared_mycontext:
Optional parameter whether or not the mycontext of this translation
should be shared, or the translation should have it's own. Default True
callfunc:
Optional parameter for what the callfunc of this translation should be.
Should be valid python string. Default "import"
callargs:
A list of strings to use as the repy's "callargs" variable. Default empty
list.
force_overwrite:
If set to True, will skip all file checks and just overwrite any file
with the same name as the generated file. Dangerous, so use cautiously.
Default False
<Exceptions>
TranslationError if there was an error during file generation
ValueError if the file can't be found or directory is invalid
<Side Effects>
Creates a python file correspond to the repy file, overwriting previously
generated files that exists with that name
<Returns>
The name of the Python module that was created in the current directory. This
string can be used with __import__ to import the translated module.
"""
global _importcachedir
filedir = None # The directory the file is in.
filenamewithpath = None # The full path to the file including the filename.
destdir = None # where the file should be written when generated
# If the file name contains a directory, honor that exactly...
if filename != os.path.basename(filename):
# since the name contains a directory, that's the filename + path
filenamewithpath = filename
# I need to use the absolute path because python doesn't handle '..' in
# directory / module names
filedir = os.path.abspath(os.path.dirname(filename))
# write it to the first directory in the python path (by default)
destdir = sys.path[0]
# Let's verify these exist and if not exit...
if not os.path.isdir(filedir):
raise ValueError("In repyhelper, the directory '" + filedir + "' does not exist for file '"+filename+"'")
if not os.path.isfile(filename):
raise ValueError("In repyhelper, the file '" + filename + "' does not exist.")
else:
# Determine in which directory in the file is located (using the
# Python path)
for pathdir in sys.path:
possiblefilenamewithpath = os.path.join(pathdir, filename)
if os.path.isfile(possiblefilenamewithpath):
filenamewithpath = possiblefilenamewithpath
filedir = pathdir
break
# make sure we found something.
if filenamewithpath is None:
raise ValueError("File " + filename + " does not exist in the Python path.")
# write it where it was (by default)
destdir = filedir
if callargs is None:
callargs = []
# expand the name from foo.repy to foo_repy (change '.' to '_')
modulenameonly = _get_module_name(os.path.basename(filename))
generatedfilenameonly = modulenameonly + ".py"
# if it shouldn't be in the default location, put it in the correct dir
if _importcachedir != None:
destdir = _importcachedir
# let's generate it
generatedfilenamewithpath = os.path.join(destdir, generatedfilenameonly)
if force_overwrite or _translation_is_needed(filenamewithpath, generatedfilenamewithpath):
_generate_python_file_from_repy(filenamewithpath, generatedfilenamewithpath, shared_mycontext, callfunc, callargs)
# return the name so that we can import it
return modulenameonly
def translate_and_import(filename, shared_mycontext=True, callfunc="import", callargs=None,
force_overwrite=False, preserve_globals=False):
"""
<Purpose>
Translate a repy file to python (see repyhelper.translate), but also import
it to the current global namespace. This import behaves similarly to python's
"from <module> import *", to mimic repy's include semantics, in which
included files are in-lined. Globals starting with "_" aren't imported.
<Arguments>
filename:
The name of the repy filename to translate and import
shared_mycontext:
Whether or not the mycontext of this translation should be shared, or
the translation should have it's own. Default True
callfunc:
Optional parameter for what the callfunc of this translation should be.
Should be valid python string. Deafault "import"
callargs:
A list of strings to use as the repy's "callargs" variable. Default empty list.
force_overwrite:
If set to True, will skip all file checks and just overwrite any file with
the same name as the generated file. Dangerous, so use cautiously.
Default False
preserve_globals:
Whether or not to preserve globals in the current namespace.
False means globals in current context will get overwritten by globals
in filename if the names clash, True means to keep current globals in the
event of a collision. Default False
<Exceptions>
TranslationError if there was an error during translation
<Side Effects>
Creates/updates a python module corresponding to the repy file argument,
and places references to that module in the current global namespace
<Returns>
None
"""
modulename = translate(filename, shared_mycontext, callfunc, callargs, force_overwrite)
_import_file_contents_to_caller_namespace(modulename, preserve_globals)
#List of globals to skip; we want to make sure to ignore these when
#inserting the imported module's vars into the caller's namespace
# Could also blacklist the repyportability things here....
GLOBAL_VARS_BLACKLIST = set(['mycontext', 'callfunc', 'callargs', 'repyhelper'])
def _import_file_contents_to_caller_namespace(modulename, preserve_globals):
"""
Responsible for importing modulename, and taking the contents and
injecting them into the caller's namespace. If overwrite_globals is set to
false, then globals that are already defined in the callers namespace get
skipped.
Doesn't include objects that start with "_"
BIG HACK WARNING:
The idea here is to use inspect to get a handle to the caller's module, and
start inserting elements from the imported module into the caller's global
namespace. This is to simulate the repy behavior of inlining includes, which
puts everything in the same namespace.
"""
#DEBUG
#caller_file = os.path.basename(inspect.currentframe().f_back.f_back.f_code.co_filename)
#print "*** IMPORTNG", modulename, "INTO FILE", caller_file, "***"
#Let python handle the initial import
import_module = __import__(modulename)
#To get a handle on the caller's module navigate back up the stack:
#Go back 2 frames: back to translate_and_import, and another to
#whoever called that
caller_globals = inspect.currentframe().f_back.f_back.f_globals
#Now iterate over the import's members, and insert them into the
#caller's namespace
for name,definition in inspect.getmembers(import_module):
#like normal python from imports, don't import names starting with "_"
if name.startswith('_'):
continue
#Skip blacklisted items
if name in GLOBAL_VARS_BLACKLIST:
continue
#skip already defined vars if told to do so
if name in caller_globals and preserve_globals:
continue
caller_globals[name] = definition