Created
December 20, 2013 11:33
-
-
Save Cilyan/8053594 to your computer and use it in GitHub Desktop.
A class to insert content between two tags in a file (draft)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Copyright 2013 Cilyan Olowen <[email protected]> | |
# | |
# Redistribution and use in source and binary forms, with or without | |
# modification, are permitted provided that the following conditions are | |
# met: | |
# | |
# * Redistributions of source code must retain the above copyright | |
# notice, this list of conditions and the following disclaimer. | |
# * Redistributions in binary form must reproduce the above | |
# copyright notice, this list of conditions and the following disclaimer | |
# in the documentation and/or other materials provided with the | |
# distribution. | |
# * Neither the name of the nor the names of its | |
# contributors may be used to endorse or promote products derived from | |
# this software without specific prior written permission. | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
# | |
import random | |
import os | |
import os.path | |
import sys | |
class FileInsert: | |
""" | |
File-like interface that writes its content between two tags inside a | |
template file. This works by copying the content of input to output | |
until the start tag is met. Then the specific data is written. When the | |
:py:func:`~FileInsert.close` function is called, the end tag is searched | |
and from there, the rest of the file is copied to the output. | |
.. sourcecode: python | |
fout = FileInsert.open(template_path, result_path) | |
fout.write(some_data) | |
fout.close() | |
You need to call :py:func:`~FileInsert.close`, or the content of the | |
file after the end tag will not be copied to the output. | |
The object is also a context manager. If ``outputfile`` is ``None`` or | |
not specified, the input file is overwritten. The class tries its best | |
not to lose any data. The inputfile will be overwritten only at the very | |
end when calling the :py:func:`~FileInsert.close` function or when | |
exiting a ``with`` clause. | |
.. sourcecode: python | |
with FileInsert.open(input_file_path) as fout: | |
fout.write(some_data) | |
""" | |
starttag = "/* <-- SPECIALTAG START --> */" | |
endtag = "/* <-- SPECIALTAG END --> */" | |
def __init__(self, inputfile, outputfile=None, encoding="utf-8"): | |
self.inputfile = inputfile | |
# If output is the same as input, prepare a temporary file | |
if outputfile is None or self._samefile(inputfile, outputfile): | |
randsuffix = "".join(random.sample(string.ascii_letters, 8)) | |
self.outputfile = inputfile + "." + randsuffix | |
self.in_is_out = True | |
else: | |
self.outputfile = outputfile | |
self.in_is_out = False | |
self.encoding = encoding | |
self.in_block = False | |
self._openfiles() | |
def _openfiles(self): | |
# Open the two files | |
self._fin = open(self.inputfile, "r", encoding=self.encoding) | |
try: | |
self._fout = open(self.outputfile, "w", encoding=self.encoding) | |
except IOError as e: | |
self._fin.close() | |
raise | |
def _samefile(self, src, dst): | |
# Internal function to decide whether two paths point to the same | |
# file. Taken from shutil. | |
# Macintosh, Unix. | |
if hasattr(os.path, 'samefile'): | |
try: | |
return os.path.samefile(src, dst) | |
except OSError: | |
return False | |
# All other platforms: check for same pathname. | |
return (os.path.normcase(os.path.abspath(src)) == | |
os.path.normcase(os.path.abspath(dst))) | |
def _writebegining(self): | |
# Internal function that copy all the content of input file to output | |
# file until the start tag | |
for line in self._fin: | |
# Keep the tag in the result file, that's why the check is later | |
self._fout.write(line) | |
if line.strip() == self.starttag: | |
break | |
else: | |
self._close(True) | |
raise SyntaxError("Start markup tag not found in input file") | |
self.in_block = True | |
def _close(self, error=False): | |
# Internal function to close the files in the cleanest manner possible | |
if self._fin is not None: | |
self._fin.close() | |
self._fin = None | |
if self._fout is not None: | |
self._fout.close() | |
self._fout = None | |
if error and self.in_is_out: | |
os.unlink(self.outputfile) | |
def __moveouttoin(self): | |
# Internal function that moves the temporary output file to replace the | |
# input file. Only versions older than 3.3, the rename function fails | |
# on Windows if the destination already exists (and it does exist). | |
if sys.hexversion >= 0x03030000: | |
os.replace(self.outputfile, self.inputfile) | |
else: | |
if not sys.platform.startswith("win32"): | |
os.rename(self.outputfile, self.inputfile) | |
else: | |
# Push the input file to a temporary location | |
randsuffix = "".join(random.sample(string.ascii_letters, 8)) | |
newinputfile = self.inputfile + "." + randsuffix | |
os.rename(self.inputfile, newinputfile) | |
# Try to push the output to the input file | |
try: | |
os.rename(self.outputfile, self.inputfile) | |
except OSError: | |
# If it failed, try at least to replace the input file where | |
# it was. | |
os.rename(newinputfile, self.inputfile) | |
raise | |
# Finally remove the old input file | |
os.unlink(newinputfile) | |
def close(self): | |
""" | |
End insertion of data and close the different file handles. | |
This is the step where data placed after the end tag is copied to | |
the output. | |
""" | |
if self.in_block: | |
# Find the end tag, ignoring the content in-between | |
for line in self._fin: | |
if line.strip() == self.endtag: | |
# Keep the tag in the result file | |
self._fout.write(line) | |
break | |
else: | |
self._close(True) | |
raise SyntaxError("End markup tag not found in input file") | |
self.in_block = False | |
# Copy the content after the end block | |
for line in self._fin: | |
self._fout.write(line) | |
# Close the file handles | |
self._close() | |
# If necessary, move the output to the input | |
if self.in_is_out: | |
self.__moveouttoin() | |
@classmethod | |
def open(cls, inputfile, outputfile=None, encoding="utf-8"): | |
""" | |
Creates a new :py:class:`FileInsert` object | |
""" | |
return cls(inputfile, outputfile, encoding) | |
def __enter__(self): | |
# Enter context manager. Prepare the file. | |
self._writebegining() | |
return self | |
def write(self, data): | |
""" | |
Write data to output file between the two tags | |
""" | |
# Verify that the first step has been done | |
if not self.in_block: | |
self._writebegining() | |
self._fout.write(data) | |
def __exit__(self, exc_type, exc_value, traceback): | |
# Exit context manager. Close the file handles. | |
self.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment