From 6ece750d9aeceeefa85a3a3bf5db6ca0a0428a3d Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 15 Dec 2011 22:56:53 +0100 Subject: [PATCH] appy.bin: updated publish.py, that is now able to generate a DistUtils tar.gz for Appy; publish.py can now be called with option '-s' (silent): in this mode no question is asked to the user, default values are used; updated new.py that generates a better Plone4-ready simple Zope instance; appy: moved FileWrapper from appy.gen.utils to appy.shared.utils to avoid circular package dependencies; appy.gen: use of .pyt extensions for template Python classes in appy.gen.templates in order to avoid byte-compilation errors when distutils installs the package; appy.pod: when using function 'document' in 'from' statements, first arg can now be a appy.shared.utils.FileWrapper instance. --- bin/new.py | 56 +++--- bin/publish.py | 181 +++++++++++--------- gen/__init__.py | 14 +- gen/generator.py | 15 +- gen/mixins/ToolMixin.py | 2 +- gen/templates/{Class.py => Class.pyt} | 0 gen/templates/{__init__.py => __init__.pyt} | 0 gen/templates/{config.py => config.pyt} | 0 gen/templates/{testAll.py => testAll.pyt} | 0 gen/templates/{wrappers.py => wrappers.pyt} | 0 gen/utils.py | 81 +-------- install.txt | 11 -- pod/doc_importers.py | 6 +- pod/renderer.py | 3 + shared/utils.py | 78 +++++++++ 15 files changed, 237 insertions(+), 210 deletions(-) mode change 100755 => 100644 bin/publish.py rename gen/templates/{Class.py => Class.pyt} (100%) rename gen/templates/{__init__.py => __init__.pyt} (100%) rename gen/templates/{config.py => config.pyt} (100%) rename gen/templates/{testAll.py => testAll.pyt} (100%) rename gen/templates/{wrappers.py => wrappers.pyt} (100%) delete mode 100644 install.txt diff --git a/bin/new.py b/bin/new.py index 6470712..dfc1af8 100644 --- a/bin/new.py +++ b/bin/new.py @@ -109,8 +109,9 @@ class NewScript: f.write(self.patchRex.sub('',fileContent)) f.close() - filesToPatch2 = ('profiles/default/skins.xml') - def patchPlone4(self): + missingIncludes = ('plone.app.upgrade', 'plonetheme.sunburst', + 'plonetheme.classic') + def patchPlone4(self, versions): '''Patches Plone 4 that can't live without buildout as-is.''' self.patchPlone3x() # We still need this for Plone 4 as well. # bin/zopectl @@ -124,42 +125,44 @@ class NewScript: f.write(content) f.close() j = os.path.join - themeFolder = '%s/plonetheme' % self.libFolder - for theme in os.listdir(themeFolder): - # Create a simlink to every theme in self.productsFolder - tFolder = j(themeFolder, theme) - if not os.path.isdir(tFolder): continue - os.system('ln -s %s %s/%s' % (tFolder, self.productsFolder, theme)) - # Patch skins.xml - fileName = '%s/profiles/default/skins.xml' % tFolder - f = file(fileName) - content = f.read() - f.close() - f = file(fileName, 'w') - f.write(content.replace('plonetheme.%s:' % theme, '%s/' % theme)) - f.close() - # As eggs have been deleted, Plone can't tell which version of Zope and - # Plone are there. So we patch the code that tries to get Plone and Zope - # versions. + # As eggs have been deleted, versions of components are lost. Reify + # them from p_versions. + dVersions = ['"%s":"%s"' % (n, v) for n, v in versions.iteritems()] + sVersions = 'appyVersions = {' + ','.join(dVersions) + '}' codeFile = "%s/pkg_resources.py" % self.libFolder f = file(codeFile) - content = f.read() + content = sVersions + '\n' + f.read() f.close() content = content.replace("raise DistributionNotFound(req)", "dist = Distribution(project_name=req.project_name, " \ - "version='1.1.1', platform='linux2', location='%s')" % \ - self.instancePath) + "version=appyVersions[req.project_name], platform='linux2', " \ + "location='%s')" % self.instancePath) f = file(codeFile, 'w') f.write(content) f.close() + # Some 'include' directives must be added with our install. + configPlone = j(self.productsFolder, 'CMFPlone', 'configure.zcml') + f = file(configPlone) + content = f.read() + f.close() + missing = '' + for missingInclude in self.missingIncludes: + missing += ' \n' % missingInclude + content = content.replace('', '%s\n' % missing) + f = file(configPlone, 'w') + f.write(content) + f.close() def copyEggs(self): - '''Copy content of eggs into the Zope instance.''' + '''Copy content of eggs into the Zope instance. This method also + retrieves every egg version and returns a dict {s_egg:s_version}.''' j = os.path.join eggsFolder = j(self.plonePath, 'buildout-cache/eggs') - self.ploneThemes = [] + res = {} for name in os.listdir(eggsFolder): if name == 'EGG-INFO': continue + splittedName = name.split('-') + res[splittedName[0]] = splittedName[1] absName = j(eggsFolder, name) # Copy every file or sub-folder into self.libFolder or # self.productsFolder. @@ -175,6 +178,7 @@ class NewScript: copyFolder(absFileName, j(self.libFolder, fileName)) else: shutil.copy(absFileName, self.libFolder) + return res def createInstance(self, linksForProducts): '''Calls the Zope script that allows to create a Zope instance and copy @@ -224,7 +228,7 @@ class NewScript: if self.ploneVersion in ('plone25', 'plone30'): self.installPlone25or30Stuff(linksForProducts) elif self.ploneVersion in ('plone3x', 'plone4'): - self.copyEggs() + versions = self.copyEggs() if self.ploneVersion == 'plone3x': self.patchPlone3x() elif self.ploneVersion == 'plone4': @@ -234,7 +238,7 @@ class NewScript: j(self.instancePath, 'lib/python')) print cmd os.system(cmd) - self.patchPlone4() + self.patchPlone4(versions) # Remove .bat files under Linux if os.name == 'posix': cleanFolder(j(self.instancePath, 'bin'), exts=('.bat',)) diff --git a/bin/publish.py b/bin/publish.py old mode 100755 new mode 100644 index eedda85..2b3e6ef --- a/bin/publish.py +++ b/bin/publish.py @@ -1,6 +1,6 @@ -#!/usr/bin/python2.4.4 +#!/usr/bin/python # Imports ---------------------------------------------------------------------- -import os, os.path, shutil, re, zipfile, sys, ftplib, time +import os, os.path, sys, shutil, re, zipfile, sys, ftplib, time import appy from appy.shared import appyPath from appy.shared.utils import FolderDeleter, LinesCounter @@ -9,17 +9,23 @@ from appy.gen.utils import produceNiceMessage # ------------------------------------------------------------------------------ versionRex = re.compile('(\d+\.\d+\.\d+)') -eggInfo = '''import os, setuptools -setuptools.setup( - name = "appy", version = "%s", description = "The Appy framework", - long_description = "See http://appyframework.org", - author = "Gaetan Delannay", author_email = "gaetan.delannay AT gmail.com", - license = "GPL", keywords = "plone, pod, pdf, odt, document", - url = 'http://appyframework.org', - classifiers = ['Development Status :: 4 - Beta', "License :: OSI Approved"], - packages = setuptools.find_packages('src'), include_package_data = True, - package_dir = {'':'src'}, data_files = [('.', [])], - namespace_packages = ['appy'], zip_safe = False)''' +distInfo = '''from distutils.core import setup +setup(name = "appy", version = "%s", + description = "The Appy framework", + long_description = "Appy builds simple but complex web Python apps.", + author = "Gaetan Delannay", + author_email = "gaetan.delannay AT geezteem.com", + license = "GPL", platforms="all", + url = 'http://appyframework.org', + packages = [%s], + package_data = {'':["*.*"]}) +''' +manifestInfo = ''' +recursive-include appy/bin * +recursive-include appy/gen * +recursive-include appy/pod * +recursive-include appy/shared * +''' def askLogin(): print 'Login: ', @@ -28,29 +34,6 @@ def askLogin(): passwd = sys.stdin.readline().strip() return (login, passwd) -def askQuestion(question, default='yes'): - '''Asks a question to the user (yes/no) and returns True if the user - answered "yes".''' - defaultIsYes = (default.lower() in ('y', 'yes')) - if defaultIsYes: - yesNo = '[Y/n]' - else: - yesNo = '[y/N]' - print question + ' ' + yesNo + ' ', - response = sys.stdin.readline().strip().lower() - res = False - if response in ('y', 'yes'): - res = True - elif response in ('n', 'no'): - res = False - elif not response: - # It depends on default value - if defaultIsYes: - res = True - else: - res = False - return res - class FtpFolder: '''Represents a folder on a FTP site.''' def __init__(self, name): @@ -247,52 +230,95 @@ class Publisher: self.versionLong = '%s (%s)' % (self.versionShort, time.strftime('%Y/%m/%d %H:%M')) f.close() + # In silent mode (option -s), no question is asked, default answers are + # automatically given. + if (len(sys.argv) > 1) and (sys.argv[1] == '-s'): + self.silent = True + else: + self.silent = False + + def askQuestion(self, question, default='yes'): + '''Asks a question to the user (yes/no) and returns True if the user + answered "yes".''' + if self.silent: return (default == 'yes') + defaultIsYes = (default.lower() in ('y', 'yes')) + if defaultIsYes: + yesNo = '[Y/n]' + else: + yesNo = '[y/N]' + print question + ' ' + yesNo + ' ', + response = sys.stdin.readline().strip().lower() + res = False + if response in ('y', 'yes'): + res = True + elif response in ('n', 'no'): + res = False + elif not response: + # It depends on default value + if defaultIsYes: + res = True + else: + res = False + return res def executeCommand(self, cmd): '''Executes the system command p_cmd.''' print 'Executing %s...' % cmd os.system(cmd) - def createCodeAndEggReleases(self): - '''Publishes the egg on pypi.python.org.''' + distExcluded = ('appy/doc', 'appy/temp', 'appy/versions', 'appy/gen/test') + def isDistExcluded(self, name): + '''Returns True if folder named p_name must be included in the + distribution.''' + if '.bzr' in name: return True + for prefix in self.distExcluded: + if name.startswith(prefix): return True + + def createDistRelease(self): + '''Create the distutils package.''' curdir = os.getcwd() - if askQuestion('Upload eggs on PyPI?', default='no'): - # Create egg structure - eggFolder = '%s/egg' % self.genFolder - os.mkdir(eggFolder) - f = file('%s/setup.py' % eggFolder, 'w') - f.write(eggInfo % self.versionShort) - f.close() - os.mkdir('%s/docs' % eggFolder) - os.mkdir('%s/src' % eggFolder) - os.mkdir('%s/src/appy' % eggFolder) - shutil.copy('%s/doc/version.txt' % appyPath, - '%s/docs/HISTORY.txt' % eggFolder) - shutil.copy('%s/doc/license.txt' % appyPath, - '%s/docs/LICENSE.txt' % eggFolder) - # Move appy sources within the egg - os.rename('%s/appy' % self.genFolder, '%s/src/appy' % eggFolder) - # Create eggs and publish them on pypi - os.chdir(eggFolder) - print 'Uploading appy%s source egg on PyPI...' % self.versionShort - #self.executeCommand('python setup.py sdist upload') - self.executeCommand('python setup.py sdist') - for pythonTarget in self.pythonTargets: - print 'Uploading appy%s binary egg for python%s...' % \ - (self.versionShort, pythonTarget) - #self.executeCommand('python%s setup.py bdist_egg upload' % \ - # pythonTarget) - self.executeCommand('python%s setup.py bdist_egg' % \ - pythonTarget) + distFolder = '%s/dist' % self.genFolder + # Create setup.py + os.mkdir(distFolder) + f = file('%s/setup.py' % distFolder, 'w') + # List all packages to include + packages = [] + os.chdir(os.path.dirname(appyPath)) + for dir, dirnames, filenames in os.walk('appy'): + if self.isDistExcluded(dir): continue + packageName = dir.replace('/', '.') + packages.append('"%s"' % packageName) + f.write(distInfo % (self.versionShort, ','.join(packages))) + f.close() + # Create MANIFEST.in + f = file('%s/MANIFEST.in' % distFolder, 'w') + f.write(manifestInfo) + f.close() + # Move appy sources within the dist folder + os.rename('%s/appy' % self.genFolder, '%s/appy' % distFolder) + # Create the source distribution + os.chdir(distFolder) + self.executeCommand('python setup.py sdist') + # DistUtils has created the .tar.gz file. Copy it into folder "versions" + name = 'appy-%s.tar.gz' % self.versionShort + os.rename('%s/dist/%s' % (distFolder, name), + '%s/versions/%s' % (appyPath, name)) + # Clean temp files os.chdir(curdir) + FolderDeleter.delete(os.path.join(self.genFolder, 'dist')) + return name + + def uploadOnPypi(self, name): + print 'Uploading %s on PyPI...' % name + #self.executeCommand('python setup.py sdist upload') def createZipRelease(self): '''Creates a zip file with the appy sources.''' newZipRelease = '%s/versions/appy%s.zip' % (appyPath, self.versionShort) if os.path.exists(newZipRelease): - if not askQuestion('"%s" already exists. Replace it?' % \ - newZipRelease, default='yes'): - print 'Publication cancelled.' + if not self.askQuestion('"%s" already exists. Replace it?' % \ + newZipRelease, default='yes'): + print 'Publication canceled.' sys.exit(1) print 'Removing obsolete %s...' % newZipRelease os.remove(newZipRelease) @@ -306,8 +332,6 @@ class Publisher: # [2:] is there to avoid havin './' in the path in the zip file. zipFile.close() os.chdir(curdir) - # Remove the "appy" folder within the gen folder. - FolderDeleter.delete(os.path.join(self.genFolder, 'appy')) def applyTemplate(self): '''Decorates each page with the template.''' @@ -405,7 +429,7 @@ class Publisher: # Create a temp clean copy of appy sources (without .svn folders, etc) genSrcFolder = '%s/appy' % self.genFolder os.mkdir(genSrcFolder) - for aFile in ('__init__.py', 'install.txt'): + for aFile in ('__init__.py',): shutil.copy('%s/%s' % (appyPath, aFile), genSrcFolder) for aFolder in ('gen', 'pod', 'shared', 'bin'): shutil.copytree('%s/%s' % (appyPath, aFolder), @@ -438,17 +462,18 @@ class Publisher: # Perform a small analysis on the Appy code LinesCounter(appy).run() print 'Generating site in %s...' % self.genFolder - minimalist = askQuestion('Minimalist (shipped without tests)?', - default='no') + minimalist = self.askQuestion('Minimalist (shipped without tests)?', + default='no') self.prepareGenFolder(minimalist) self.createDocToc() self.applyTemplate() self.createZipRelease() - #self.createCodeAndEggReleases() - if askQuestion('Do you want to publish the site on ' \ - 'appyframework.org?', default='no'): + tarball = self.createDistRelease() + if self.askQuestion('Upload %s on PyPI?' % tarball, default='no'): + self.uploadOnPypi(tarball) + if self.askQuestion('Publish on appyframework.org?', default='no'): AppySite().publish() - if askQuestion('Delete locally generated site ?', default='no'): + if self.askQuestion('Delete locally generated site ?', default='yes'): FolderDeleter.delete(self.genFolder) # ------------------------------------------------------------------------------ diff --git a/gen/__init__.py b/gen/__init__.py index 7cb9a3a..2955325 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -5,12 +5,13 @@ from appy import Object from appy.gen.layout import Table from appy.gen.layout import defaultFieldLayouts from appy.gen.po import PoMessage -from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \ - getClassName, SomeObjects +from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, getClassName, \ + SomeObjects import appy.pod from appy.pod.renderer import Renderer from appy.shared.data import countries -from appy.shared.utils import Traceback, getOsTempFolder, formatNumber +from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \ + FileWrapper # Default Appy permissions ----------------------------------------------------- r, w, d = ('read', 'write', 'delete') @@ -1505,7 +1506,7 @@ class File(Type): def getRequestValue(self, request): return request.get('%s_file' % self.name) - def getDefaultLayouts(self): return {'view':'lf','edit':'lrv-f'} + def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'} def isEmptyValue(self, value, obj=None): '''Must p_value be considered as empty?''' @@ -1536,8 +1537,9 @@ class File(Type): * an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In this case, it is file content coming from a HTTP POST; * an instance of Zope class OFS.Image.File; - * an instance of appy.gen.utils.FileWrapper, which wraps an instance - of OFS.Image.File and adds useful methods for manipulating it; + * an instance of appy.shared.utils.FileWrapper, which wraps an + instance of OFS.Image.File and adds useful methods for manipulating + it; * a string. In this case, the string represents the path of a file on disk; * a 2-tuple (fileName, fileContent) where: diff --git a/gen/generator.py b/gen/generator.py index 8ca3c75..390db3a 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -675,7 +675,7 @@ class ZopeGenerator(Generator): repls['languages'] = ','.join('"%s"' % l for l in self.config.languages) repls['languageSelector'] = self.config.languageSelector repls['sourceLanguage'] = self.config.sourceLanguage - self.copyFile('config.py', repls) + self.copyFile('config.pyt', repls, destName='config.py') def generateInit(self): # Compute imports @@ -690,7 +690,7 @@ class ZopeGenerator(Generator): repls['imports'] = '\n'.join(imports) repls['classes'] = ','.join(classNames) repls['totalNumberOfTests'] = self.totalNumberOfTests - self.copyFile('__init__.py', repls) + self.copyFile('__init__.pyt', repls, destName='__init__.py') def getClassesInOrder(self, allClasses): '''When generating wrappers, classes mut be dumped in order (else, it @@ -757,7 +757,7 @@ class ZopeGenerator(Generator): for klass in self.getClasses(include='predefined'): modelClass = klass.modelClass repls['%s' % modelClass.__name__] = modelClass._appy_getBody() - self.copyFile('wrappers.py', repls) + self.copyFile('wrappers.pyt', repls, destName='wrappers.py') def generateTests(self): '''Generates the file needed for executing tests.''' @@ -765,7 +765,8 @@ class ZopeGenerator(Generator): modules = self.modulesWithTests repls['imports'] = '\n'.join(['import %s' % m for m in modules]) repls['modulesWithTests'] = ','.join(modules) - self.copyFile('testAll.py', repls, destFolder='tests') + self.copyFile('testAll.pyt', repls, destName='testAll.py', + destFolder='tests') def generateTool(self): '''Generates the tool that corresponds to this application.''' @@ -790,7 +791,7 @@ class ZopeGenerator(Generator): repls.update({'methods': klass.methods, 'genClassName': klass.name, 'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem', 'classDoc': 'Standard Appy class', 'icon':'object.gif'}) - self.copyFile('Class.py', repls, destName='%s.py' % klass.name) + self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name) # Before generating the Tool class, finalize it with query result # columns, with fields to propagate, workflow-related fields. @@ -817,7 +818,7 @@ class ZopeGenerator(Generator): 'genClassName': self.tool.name, 'baseMixin':'ToolMixin', 'parents': 'ToolMixin, Folder', 'icon': 'folder.gif', 'classDoc': 'Tool class for %s' % self.applicationName}) - self.copyFile('Class.py', repls, destName='%s.py' % self.tool.name) + self.copyFile('Class.pyt', repls, destName='%s.py' % self.tool.name) def generateClass(self, classDescr): '''Is called each time an Appy class is found in the application, for @@ -863,7 +864,7 @@ class ZopeGenerator(Generator): if poMsg not in self.labels: self.labels.append(poMsg) # Generate the resulting Zope class. - self.copyFile('Class.py', repls, destName=fileName) + self.copyFile('Class.pyt', repls, destName=fileName) def generateWorkflow(self, wfDescr): '''This method creates the i18n labels related to the workflow described diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index f613f4d..c5df7c6 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -781,7 +781,7 @@ class ToolMixin(BaseMixin): if brain: sibling = brain.getObject() res[urlKey] = sibling.getUrl(nav=newNav % (index + 1), - page='main') + page=self.REQUEST.get('page', 'main')) return res def tabularize(self, data, numberOfRows): diff --git a/gen/templates/Class.py b/gen/templates/Class.pyt similarity index 100% rename from gen/templates/Class.py rename to gen/templates/Class.pyt diff --git a/gen/templates/__init__.py b/gen/templates/__init__.pyt similarity index 100% rename from gen/templates/__init__.py rename to gen/templates/__init__.pyt diff --git a/gen/templates/config.py b/gen/templates/config.pyt similarity index 100% rename from gen/templates/config.py rename to gen/templates/config.pyt diff --git a/gen/templates/testAll.py b/gen/templates/testAll.pyt similarity index 100% rename from gen/templates/testAll.py rename to gen/templates/testAll.pyt diff --git a/gen/templates/wrappers.py b/gen/templates/wrappers.pyt similarity index 100% rename from gen/templates/wrappers.py rename to gen/templates/wrappers.pyt diff --git a/gen/utils.py b/gen/utils.py index 5389cfd..783f1c4 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -1,7 +1,6 @@ # ------------------------------------------------------------------------------ -import re, os, os.path, time +import re, os, os.path import appy.pod -from appy.shared.utils import getOsTempFolder, normalizeString, executeCommand sequenceTypes = (list, tuple) # Function for creating a Zope object ------------------------------------------ @@ -242,84 +241,6 @@ class Keywords: return op.join(self.keywords)+'*' return '' -# ------------------------------------------------------------------------------ -CONVERSION_ERROR = 'An error occurred while executing command "%s". %s' -class FileWrapper: - '''When you get, from an appy object, the value of a File attribute, you - get an instance of this class.''' - def __init__(self, zopeFile): - '''This constructor is only used by Appy to create a nice File instance - from a Zope corresponding instance (p_zopeFile). If you need to - create a new file and assign it to a File attribute, use the - attribute setter, do not create yourself an instance of this - class.''' - d = self.__dict__ - d['_zopeFile'] = zopeFile # Not for you! - d['name'] = zopeFile.filename - d['content'] = zopeFile.data - d['mimeType'] = zopeFile.content_type - d['size'] = zopeFile.size # In bytes - - def __setattr__(self, name, v): - d = self.__dict__ - if name == 'name': - self._zopeFile.filename = v - d['name'] = v - elif name == 'content': - self._zopeFile.update_data(v, self.mimeType, len(v)) - d['content'] = v - d['size'] = len(v) - elif name == 'mimeType': - self._zopeFile.content_type = self.mimeType = v - else: - raise 'Impossible to set attribute %s. "Settable" attributes ' \ - 'are "name", "content" and "mimeType".' % name - - def dump(self, filePath=None, format=None, tool=None): - '''Writes the file on disk. If p_filePath is specified, it is the - path name where the file will be dumped; folders mentioned in it - must exist. If not, the file will be dumped in the OS temp folder. - The absolute path name of the dumped file is returned. - If an error occurs, the method returns None. If p_format is - specified, OpenOffice will be called for converting the dumped file - to the desired format. In this case, p_tool, a Appy tool, must be - provided. Indeed, any Appy tool contains parameters for contacting - OpenOffice in server mode.''' - if not filePath: - filePath = '%s/file%f.%s' % (getOsTempFolder(), time.time(), - normalizeString(self.name)) - f = file(filePath, 'w') - if self.content.__class__.__name__ == 'Pdata': - # The file content is splitted in several chunks. - f.write(self.content.data) - nextPart = self.content.next - while nextPart: - f.write(nextPart.data) - nextPart = nextPart.next - else: - # Only one chunk - f.write(self.content) - f.close() - if format: - if not tool: return - # Convert the dumped file using OpenOffice - errorMessage = tool.convert(filePath, format) - # Even if we have an "error" message, it could be a simple warning. - # So we will continue here and, as a subsequent check for knowing if - # an error occurred or not, we will test the existence of the - # converted file (see below). - os.remove(filePath) - # Return the name of the converted file. - baseName, ext = os.path.splitext(filePath) - if (ext == '.%s' % format): - filePath = '%s.res.%s' % (baseName, format) - else: - filePath = '%s.%s' % (baseName, format) - if not os.path.exists(filePath): - tool.log(CONVERSION_ERROR % (cmd, errorMessage), type='error') - return - return filePath - # ------------------------------------------------------------------------------ def getClassName(klass, appName=None): '''Generates, from appy-class p_klass, the name of the corresponding diff --git a/install.txt b/install.txt deleted file mode 100644 index bbae2a1..0000000 --- a/install.txt +++ /dev/null @@ -1,11 +0,0 @@ -Installation under Windows or MacOS ------------------------------------ -Copy the content of this folder to - \Lib\site-packages\appy - -Installation under Linux ------------------------- -Copy the content of this folder wherever you want (in /opt/appy for example) -and make a symbolic link in your Python lib folder (for example: -"ln -s /opt/appy /usr/lib/python2.5/site-packages/appy"). - diff --git a/pod/doc_importers.py b/pod/doc_importers.py index 5c3a180..0040cc7 100644 --- a/pod/doc_importers.py +++ b/pod/doc_importers.py @@ -20,6 +20,7 @@ import os, os.path, time, shutil, struct, random from appy.pod import PodError from appy.pod.odf_parser import OdfEnvironment +from appy.shared.utils import FileWrapper # ------------------------------------------------------------------------------ FILE_NOT_FOUND = "'%s' does not exist or is not a file." @@ -59,9 +60,12 @@ class DocImporter: self.importPath = self.moveFile(at, self.importPath) else: # We need to dump the file content (in self.content) in a temp file - # first. self.content may be binary or a file handler. + # first. self.content may be binary, a file handler or a + # FileWrapper. if isinstance(self.content, file): fileContent = self.content.read() + elif isinstance(self.content, FileWrapper): + fileContent = content.content else: fileContent = self.content f = file(self.importPath, 'wb') diff --git a/pod/renderer.py b/pod/renderer.py index 034cf47..d0cb701 100644 --- a/pod/renderer.py +++ b/pod/renderer.py @@ -26,6 +26,7 @@ from appy.pod import PodError from appy.shared import mimeTypesExts from appy.shared.xml_parser import XmlElement from appy.shared.utils import FolderDeleter, executeCommand +from appy.shared.utils import FileWrapper from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert from appy.pod.converter import FILE_TYPES from appy.pod.buffers import FileBuffer @@ -281,6 +282,8 @@ class Renderer: if not content and not at: raise PodError(DOC_NOT_SPECIFIED) # Guess document format + if isinstance(content, FileWrapper): + format = content.mimeType if not format: # It should be deduced from p_at if not at: diff --git a/shared/utils.py b/shared/utils.py index 000a407..c347768 100644 --- a/shared/utils.py +++ b/shared/utils.py @@ -410,4 +410,82 @@ class LinesCounter: elif ext in exts['pt']: self.zpt[self.inTest].analyseFile(j(root, fileName)) self.printReport() + +# ------------------------------------------------------------------------------ +CONVERSION_ERROR = 'An error occurred while executing command "%s". %s' +class FileWrapper: + '''When you get, from an appy object, the value of a File attribute, you + get an instance of this class.''' + def __init__(self, zopeFile): + '''This constructor is only used by Appy to create a nice File instance + from a Zope corresponding instance (p_zopeFile). If you need to + create a new file and assign it to a File attribute, use the + attribute setter, do not create yourself an instance of this + class.''' + d = self.__dict__ + d['_zopeFile'] = zopeFile # Not for you! + d['name'] = zopeFile.filename + d['content'] = zopeFile.data + d['mimeType'] = zopeFile.content_type + d['size'] = zopeFile.size # In bytes + + def __setattr__(self, name, v): + d = self.__dict__ + if name == 'name': + self._zopeFile.filename = v + d['name'] = v + elif name == 'content': + self._zopeFile.update_data(v, self.mimeType, len(v)) + d['content'] = v + d['size'] = len(v) + elif name == 'mimeType': + self._zopeFile.content_type = self.mimeType = v + else: + raise 'Impossible to set attribute %s. "Settable" attributes ' \ + 'are "name", "content" and "mimeType".' % name + + def dump(self, filePath=None, format=None, tool=None): + '''Writes the file on disk. If p_filePath is specified, it is the + path name where the file will be dumped; folders mentioned in it + must exist. If not, the file will be dumped in the OS temp folder. + The absolute path name of the dumped file is returned. + If an error occurs, the method returns None. If p_format is + specified, OpenOffice will be called for converting the dumped file + to the desired format. In this case, p_tool, a Appy tool, must be + provided. Indeed, any Appy tool contains parameters for contacting + OpenOffice in server mode.''' + if not filePath: + filePath = '%s/file%f.%s' % (getOsTempFolder(), time.time(), + normalizeString(self.name)) + f = file(filePath, 'w') + if self.content.__class__.__name__ == 'Pdata': + # The file content is splitted in several chunks. + f.write(self.content.data) + nextPart = self.content.next + while nextPart: + f.write(nextPart.data) + nextPart = nextPart.next + else: + # Only one chunk + f.write(self.content) + f.close() + if format: + if not tool: return + # Convert the dumped file using OpenOffice + errorMessage = tool.convert(filePath, format) + # Even if we have an "error" message, it could be a simple warning. + # So we will continue here and, as a subsequent check for knowing if + # an error occurred or not, we will test the existence of the + # converted file (see below). + os.remove(filePath) + # Return the name of the converted file. + baseName, ext = os.path.splitext(filePath) + if (ext == '.%s' % format): + filePath = '%s.res.%s' % (baseName, format) + else: + filePath = '%s.%s' % (baseName, format) + if not os.path.exists(filePath): + tool.log(CONVERSION_ERROR % (cmd, errorMessage), type='error') + return + return filePath # ------------------------------------------------------------------------------