On Jul 13, 2009, at 5:21 PM, Francis J. Lacoste wrote:
> On July 11, 2009, Gary Poster wrote:
>> Gary Poster has proposed merging
>> lp:~gary/z3c.recipe.filetemplate/support-system-python into
>> lp:~gary/z3c.recipe.filetemplate/trunk.
>>
>> Requested reviews:
>> Francis J. Lacoste (flacoste)
>>
>> Support the new zc.buildout 1.4.0+ include-site-packages option,
>> and more.
>>
>
> Hi Gary,
>
> This all looks good. My only concern is the new write_and_wait which
> I don't
> understand why it's necessary.
Cool. I'll discuss below.
>
>> === modified file 'z3c/recipe/filetemplate/__init__.py'
>
>> @@ -195,17 +202,19 @@
>> 'Destinations already exist: %s. Please make sure
>> that '
>> 'you really want to generate these automatically.
>> Then '
>> 'move them away.', ', '.join(already_exists))
>> + seen = [] # we throw this away right now, but could move
>> template
>
> Sentence should start with a capital.
Ack. See incremental diff.
>
>> + # processing up to __init__ if valuable. That would mean
>> that
> templates
>> + # would be rewritten even if a value in another section
>> had been
>> + # referenced; however, it would also mean that __init__
>> would do
>> + # virtually all of the work, with install only doing the
>> writing.
>> for rel_path, last_mod, st_mode in self.actions:
>> source = os.path.join(self.source_dir, rel_path)
>> dest = os.path.join(self.destination_dir, rel_path[:-3])
>> mode=stat.S_IMODE(st_mode)
>> - template=open(source).read()
>> - template=re.sub(r"\$\{([^:]+?)\}", r"${%s:\1}" %
>> self.name,
>> - template)
>> - self._create_paths(os.path.dirname(dest))
>> # we process the file first so that it won't be created
>> if
> there
>> # is a problem.
>> - processed = self.options._sub(template, [])
>> + processed = Template(source).substitute(self, seen)
>> + self._create_paths(os.path.dirname(dest))
>> result=open(dest, "wt")
>> result.write(processed)
>> result.close()
>> @@ -221,3 +230,74 @@
>>
>> def update(self):
>> pass
>> +
>> +
>> +class Template:
>> + # hacked from string.Template
>> + pattern = re.compile(r"""
>> + \$(?:
>> + \${(?P<escaped>[^}]*)} | # Escape sequence
>> of two
> delimiters.
>> + {(?P<braced_single>[-a-z0-9 ._]+)} |
>> + # Delimiter and a braced
>> local
> option
>> + {(?P<braced_double>[-a-z0-9 ._]+:[-a-z0-9 ._]+)} |
>> + # Delimiter and a braced
>> fully
>> + # qualified option (that
>> is, with
>> + # explicit section).
>> + {(?P<invalid>[^}]*}) # Other ill-formed
>> delimiter
> exprs.
>> + )
>> + """, re.IGNORECASE | re.VERBOSE)
>> +
>> + def __init__(self, source):
>> + self.source = source
>> + self.template = open(source).read()
>> +
>> + def _get_colno_lineno(self, i):
>> + lines = self.template[:i].splitlines(True)
>> + if not lines:
>> + colno = 1
>> + lineno = 1
>> + else:
>> + colno = i - len(''.join(lines[:-1]))
>> + lineno = len(lines)
>> + return colno, lineno
>> +
>> + def _get(self, options, section, option, seen, start):
>> + value = options.get(option, None, seen)
>> + if value is None:
>> + colno, lineno = self._get_colno_lineno(start)
>> + raise zc.buildout.buildout.MissingOption(
>> + "Option '%s:%s', referenced in line %d, col %d of
>> %s, "
>> + "does not exist." %
>> + (section, option, lineno, colno, self.source))
>> + return value
>> +
>> + def substitute(self, recipe, seen):
>> + # Helper function for .sub()
>> + def convert(mo):
>> + # Check the most common path first.
>> + option = mo.group('braced_single')
>> + if option is not None:
>> + val = self._get(recipe.options, recipe.name,
>> option, seen,
>> + mo.start('braced_single'))
>> + # We use this idiom instead of str() because the
>> latter
> will
>> + # fail if val is a Unicode containing non-ASCII
>> characters.
>> + return '%s' % (val,)
>> + double = mo.group('braced_double')
>> + if double is not None:
>> + section, option = double.split(':')
>> + val = self._get(recipe.buildout[section], section,
>> option,
> seen,
>> + mo.start('braced_double'))
>> + return '%s' % (val,)
>> + escaped = mo.group('escaped')
>> + if escaped is not None:
>> + return '${%s}' % (escaped,)
>> + invalid = mo.group('invalid')
>> + if invalid is not None:
>> + colno, lineno =
>> self._get_colno_lineno(mo.start('invalid'))
>> + raise ValueError(
>> + 'Invalid placeholder %r in line %d, col %d of
>> %s' %
>> + (mo.group('invalid'), lineno, colno,
>> self.source))
>> + raise ValueError('Unrecognized named group in pattern',
>> + self.pattern) # programmer error,
>> AFAICT
>> + return self.pattern.sub(convert, self.template)
>> +
>
> Trailing spaces.
Ack, done.
>
>
>> === modified file 'z3c/recipe/filetemplate/tests.py'
>> --- z3c/recipe/filetemplate/tests.py 2009-04-30 17:56:10 +0000
>> +++ z3c/recipe/filetemplate/tests.py 2009-07-09 18:20:56 +0000
>> @@ -12,12 +12,24 @@
>> #
>>
> ##############################################################################
>>
>> +import os
>> import zc.buildout.testing
>> import zc.buildout.tests
>> from zope.testing import doctest
>>
>> +def write_and_wait(dir, *args):
>> + path = os.path.join(dir, *(args[:-1]))
>> + original = os.stat(path).st_mtime
>> + while os.stat(path).st_mtime == original:
>> + f = open(path, 'w')
>> + f.write(args[-1])
>> + f.flush()
>> + os.fsync(f.fileno())
>> + f.close()
>> +
>
> Why is this needed? Your CHANGES.txt file mentioned making tests
> more robust
> against timing issues, but I don't see how that makes thing more
> robust?
>
> I mean the mtime of path should be modified right after open(path,
> 'w'). I
> don't see the rationale behind the loop, nor the necessity of
> f.flush() nor
> fsysnc. Unless you are running things on a NFS-mounted volume... and
> even
> there, I'm not sure it would help.
In general, the necessity is caused by the mtime precision. On my
Mac, the precision is 1 second. Here's an example run of a slightly
modified version of the helper (adding a print), in the interactive
prompt.
>>> def write_and_wait(dir, *args):
... path = os.path.join(dir, *(args[:-1]))
... original = os.stat(path).st_mtime
... while os.stat(path).st_mtime == original:
... print original
... f = open(path, 'w')
... f.write(args[-1])
... f.flush()
... os.fsync(f.fileno())
... f.close()
...
>>> f = open('/Users/gary/dev/deleteme', 'w')
>>> f.write('test')
>>> f.close()
>>> write_and_wait('/Users/gary/dev/deleteme', 'test')
1247536625.0
>>> write_and_wait('/Users/gary/dev/deleteme', 'test')
1247536627.0
>>> write_and_wait('/Users/gary/dev/deleteme', 'test')
1247536628.0
>>> write_and_wait('/Users/gary/dev/deleteme', 'test')
1247536629.0
1247536629.0
1247536629.0
[...there are 299 of these...]
>>>
So, the precision is the point. I tried a variety of things to
trigger the mtime, but writing an empty string didn't work, and
directly writing the mtime is OS-specific/dependent, and I didn't
really want to go there. This is a test harness. Good enough, was my
thinking.
As far as all the flushing and so on, that's what Jim had for his
``write`` helper function, so I suspect there's a point there too. It
could probably be somewhat refactored but...it's a test harness.
Happy to change if you want, though.
Incremental diff follows.
Thanks
Gary
Index: z3c/recipe/filetemplate/__init__.py
===================================================================
--- z3c/recipe/filetemplate/__init__.py (revision 101788)
+++ z3c/recipe/filetemplate/__init__.py (working copy)
@@ -202,7 +202,7 @@ 'Destinations already exist: %s. Please make sure
that ' 'you really want to generate these automatically.
Then ' 'move them away.', ', '.join(already_exists))
- seen = [] # we throw this away right now, but could move
template
+ seen = [] # We throw this away right now, but could move
template
# processing up to __init__ if valuable. That would mean
that templates
# would be rewritten even if a value in another section had
been
# referenced; however, it would also mean that __init__
would do
@@ -300,4 +300,4 @@
raise ValueError('Unrecognized named group in pattern', self.pattern) # programmer error, AFAICT
return self.pattern.sub(convert, self.template)
-
+
On Jul 13, 2009, at 5:21 PM, Francis J. Lacoste wrote:
> On July 11, 2009, Gary Poster wrote: site-packages option,
>> Gary Poster has proposed merging
>> lp:~gary/z3c.recipe.filetemplate/support-system-python into
>> lp:~gary/z3c.recipe.filetemplate/trunk.
>>
>> Requested reviews:
>> Francis J. Lacoste (flacoste)
>>
>> Support the new zc.buildout 1.4.0+ include-
>> and more.
>>
>
> Hi Gary,
>
> This all looks good. My only concern is the new write_and_wait which
> I don't
> understand why it's necessary.
Cool. I'll discuss below.
> filetemplate/ __init_ _.py' already_ exists) )
>> === modified file 'z3c/recipe/
>
>> @@ -195,17 +202,19 @@
>> 'Destinations already exist: %s. Please make sure
>> that '
>> 'you really want to generate these automatically.
>> Then '
>> 'move them away.', ', '.join(
>> + seen = [] # we throw this away right now, but could move
>> template
>
> Sentence should start with a capital.
Ack. See incremental diff.
> join(self. source_ dir, rel_path) join(self. destination_ dir, rel_path[:-3]) S_IMODE( st_mode) open(source) .read() re.sub( r"\$\{( [^:]+?) \}", r"${%s:\1}" % paths(os. path.dirname( dest)) _sub(template, []) source) .substitute( self, seen) paths(os. path.dirname( dest)) write(processed ) escaped> [^}]*)} | # Escape sequence single> [-a-z0- 9 ._]+)} | double> [-a-z0- 9 ._]+:[-a-z0-9 ._]+)} | [^}]*}) # Other ill-formed lineno( self, i): :i].splitlines( True) join(lines[ :-1])) colno_lineno( start) buildout. MissingOption( 'braced_ single' ) recipe. options, recipe.name, 'braced_ single' )) 'braced_ double' ) recipe. buildout[ section] , section, 'braced_ double' )) colno_lineno( mo.start( 'invalid' )) 'invalid' ), lineno, colno, 'Unrecognized named group in pattern', sub(convert, self.template)
>> + # processing up to __init__ if valuable. That would mean
>> that
> templates
>> + # would be rewritten even if a value in another section
>> had been
>> + # referenced; however, it would also mean that __init__
>> would do
>> + # virtually all of the work, with install only doing the
>> writing.
>> for rel_path, last_mod, st_mode in self.actions:
>> source = os.path.
>> dest = os.path.
>> mode=stat.
>> - template=
>> - template=
>> self.name,
>> - template)
>> - self._create_
>> # we process the file first so that it won't be created
>> if
> there
>> # is a problem.
>> - processed = self.options.
>> + processed = Template(
>> + self._create_
>> result=open(dest, "wt")
>> result.
>> result.close()
>> @@ -221,3 +230,74 @@
>>
>> def update(self):
>> pass
>> +
>> +
>> +class Template:
>> + # hacked from string.Template
>> + pattern = re.compile(r"""
>> + \$(?:
>> + \${(?P<
>> of two
> delimiters.
>> + {(?P<braced_
>> + # Delimiter and a braced
>> local
> option
>> + {(?P<braced_
>> + # Delimiter and a braced
>> fully
>> + # qualified option (that
>> is, with
>> + # explicit section).
>> + {(?P<invalid>
>> delimiter
> exprs.
>> + )
>> + """, re.IGNORECASE | re.VERBOSE)
>> +
>> + def __init__(self, source):
>> + self.source = source
>> + self.template = open(source).read()
>> +
>> + def _get_colno_
>> + lines = self.template[
>> + if not lines:
>> + colno = 1
>> + lineno = 1
>> + else:
>> + colno = i - len(''.
>> + lineno = len(lines)
>> + return colno, lineno
>> +
>> + def _get(self, options, section, option, seen, start):
>> + value = options.get(option, None, seen)
>> + if value is None:
>> + colno, lineno = self._get_
>> + raise zc.buildout.
>> + "Option '%s:%s', referenced in line %d, col %d of
>> %s, "
>> + "does not exist." %
>> + (section, option, lineno, colno, self.source))
>> + return value
>> +
>> + def substitute(self, recipe, seen):
>> + # Helper function for .sub()
>> + def convert(mo):
>> + # Check the most common path first.
>> + option = mo.group(
>> + if option is not None:
>> + val = self._get(
>> option, seen,
>> + mo.start(
>> + # We use this idiom instead of str() because the
>> latter
> will
>> + # fail if val is a Unicode containing non-ASCII
>> characters.
>> + return '%s' % (val,)
>> + double = mo.group(
>> + if double is not None:
>> + section, option = double.split(':')
>> + val = self._get(
>> option,
> seen,
>> + mo.start(
>> + return '%s' % (val,)
>> + escaped = mo.group('escaped')
>> + if escaped is not None:
>> + return '${%s}' % (escaped,)
>> + invalid = mo.group('invalid')
>> + if invalid is not None:
>> + colno, lineno =
>> self._get_
>> + raise ValueError(
>> + 'Invalid placeholder %r in line %d, col %d of
>> %s' %
>> + (mo.group(
>> self.source))
>> + raise ValueError(
>> + self.pattern) # programmer error,
>> AFAICT
>> + return self.pattern.
>> +
>
> Trailing spaces.
Ack, done.
> filetemplate/ tests.py' filetemplate/ tests.py 2009-04-30 17:56:10 +0000 filetemplate/ tests.py 2009-07-09 18:20:56 +0000 ####### ####### ####### ####### ####### ####### ####### ####### ####### ####### # path).st_ mtime path).st_ mtime == original: f.fileno( ))
>
>> === modified file 'z3c/recipe/
>> --- z3c/recipe/
>> +++ z3c/recipe/
>> @@ -12,12 +12,24 @@
>> #
>>
> #######
>>
>> +import os
>> import zc.buildout.testing
>> import zc.buildout.tests
>> from zope.testing import doctest
>>
>> +def write_and_wait(dir, *args):
>> + path = os.path.join(dir, *(args[:-1]))
>> + original = os.stat(
>> + while os.stat(
>> + f = open(path, 'w')
>> + f.write(args[-1])
>> + f.flush()
>> + os.fsync(
>> + f.close()
>> +
>
> Why is this needed? Your CHANGES.txt file mentioned making tests
> more robust
> against timing issues, but I don't see how that makes thing more
> robust?
>
> I mean the mtime of path should be modified right after open(path,
> 'w'). I
> don't see the rationale behind the loop, nor the necessity of
> f.flush() nor
> fsysnc. Unless you are running things on a NFS-mounted volume... and
> even
> there, I'm not sure it would help.
In general, the necessity is caused by the mtime precision. On my
Mac, the precision is 1 second. Here's an example run of a slightly
modified version of the helper (adding a print), in the interactive
prompt.
>>> def write_and_wait(dir, *args): path).st_ mtime path).st_ mtime == original: f.fileno( )) Users/gary/ dev/deleteme' , 'w') wait('/ Users/gary/ dev/deleteme' , 'test') wait('/ Users/gary/ dev/deleteme' , 'test') wait('/ Users/gary/ dev/deleteme' , 'test') wait('/ Users/gary/ dev/deleteme' , 'test')
... path = os.path.join(dir, *(args[:-1]))
... original = os.stat(
... while os.stat(
... print original
... f = open(path, 'w')
... f.write(args[-1])
... f.flush()
... os.fsync(
... f.close()
...
>>> f = open('/
>>> f.write('test')
>>> f.close()
>>> write_and_
1247536625.0
>>> write_and_
1247536627.0
>>> write_and_
1247536628.0
>>> write_and_
1247536629.0
1247536629.0
1247536629.0
[...there are 299 of these...]
>>>
So, the precision is the point. I tried a variety of things to dependent, and I didn't
trigger the mtime, but writing an empty string didn't work, and
directly writing the mtime is OS-specific/
really want to go there. This is a test harness. Good enough, was my
thinking.
As far as all the flushing and so on, that's what Jim had for his
``write`` helper function, so I suspect there's a point there too. It
could probably be somewhat refactored but...it's a test harness.
Happy to change if you want, though.
Incremental diff follows.
Thanks
Gary
Index: z3c/recipe/ filetemplate/ __init_ _.py ======= ======= ======= ======= ======= ======= ======= ======= ==== filetemplate/ __init_ _.py (revision 101788) filetemplate/ __init_ _.py (working copy)
'Destinatio ns already exist: %s. Please make sure
'you really want to generate these automatically.
'move them away.', ', '.join( already_ exists) ) 'Unrecognized named group in pattern',
self. pattern) # programmer error, AFAICT sub(convert, self.template)
=======
--- z3c/recipe/
+++ z3c/recipe/
@@ -202,7 +202,7 @@
that '
Then '
- seen = [] # we throw this away right now, but could move
template
+ seen = [] # We throw this away right now, but could move
template
# processing up to __init__ if valuable. That would mean
that templates
# would be rewritten even if a value in another section had
been
# referenced; however, it would also mean that __init__
would do
@@ -300,4 +300,4 @@
raise ValueError(
return self.pattern.
-
+