Source code for jasy.core.Session

#
# Jasy - Web Tooling Framework
# Copyright 2010-2012 Zynga Inc.
# Copyright 2013-2014 Sebastian Werner
#

import atexit
import os
import zlib
import shutil

import jasy.core.Config as Config
import jasy.core.Project as Project
import jasy.core.Util as Util
import jasy.core.Console as Console

import jasy.item.Asset
import jasy.item.Script
import jasy.item.Doc
import jasy.item.Style
import jasy.item.Template
import jasy.item.Translation

from jasy import UserError


[docs]class Session(): """Manages all projects.""" # Environment object used for executing jasyscript.py. # Contains all items from the main projects jasyscript.py + # all shared (namespaced) commands from all jasylibrary.py files. __scriptEnvironment = None # List of all projects in priority order __projects = None # Virtual project to store dynamically created classes __virtualProject = None # Whether repositories should be auto updated before projects should be initialized __updateRepositories = True # All (active) fields as defined by the active projects __fields = None # Dictionary which maps command names to the implementation function __commands = None # Translation bundles created by merged data from active projects __translationBundles = None # # Core # def __init__(self): atexit.register(self.close) self.__projects = [] self.__fields = {} self.__commands = {} self.__translationBundles = {} self.__postscans = [] self.__itemType = {} self.addItemType("jasy.Asset", "Assets", jasy.item.Asset.AssetItem) self.addItemType("jasy.Script", "Classes", jasy.item.Script.ScriptItem) self.addItemType("jasy.Doc", "Docs", jasy.item.Doc.DocItem) self.addItemType("jasy.Style", "Styles", jasy.item.Style.StyleItem) self.addItemType("jasy.Template", "Templates", jasy.item.Template.TemplateItem) self.addItemType("jasy.Translation", "Translations", jasy.item.Translation.TranslationItem)
[docs] def init(self, autoInitialize=True, updateRepositories=True, scriptEnvironment=None): """ Initialize the actual session with projects. :param autoInitialize: Whether the projects should be automatically added when the current folder contains a valid Jasy project. :param updateRepositories: Whether to update repositories of all project dependencies. :param scriptEnvironment: API object as being used for loadLibrary to add Python features offered by projects. :param commandEnvironment: API object as being used for loadCommands to add Python features for any item nodes. """ self.__scriptEnvironment = scriptEnvironment self.__updateRepositories = updateRepositories if autoInitialize and Config.findConfig("jasyproject"): Console.info("Initializing session...") Console.indent() try: self.addProject(Project.getProjectFromPath(".", self)) except UserError as err: Console.outdent(True) Console.error(err) raise UserError("Critical: Could not initialize session!") self.getVirtualProject() Console.debug("Active projects (%s):", len(self.__projects)) Console.indent() for project in self.__projects: if project.version: Console.debug("%s @ %s", Console.colorize(project.getName(), "bold"), Console.colorize(project.version, "magenta")) else: Console.debug(Console.colorize(project.getName(), "bold")) Console.outdent() Console.outdent()
[docs] def setCurrentTask(self, name=None): if name: Console.header(name) self.__currentTask = name
[docs] def getCurrentTask(self): return self.__currentTask
[docs] def scan(self): """Scans all registered projects.""" # Check for whether session is still alive if not self.__projects: return Console.info("Scanning projects...") Console.indent() for project in self.__projects: project.scan() for postscan in self.__postscans: postscan() Console.outdent()
[docs] def clean(self): """Clears all caches of all registered projects.""" if not self.__projects: return Console.info("Cleaning session...") Console.indent() for project in self.__projects: project.clean() path = os.path.abspath(os.path.join(".jasy", "locale")) if os.path.exists(path): Console.info("Cleaning up locale project...") shutil.rmtree(path) path = os.path.abspath(os.path.join(".jasy", "virtual")) if os.path.exists(path): Console.info("Cleaning up virtual project...") shutil.rmtree(path) Console.outdent()
[docs] def close(self): """Closes the session and stores cache to the harddrive.""" if not self.__projects: return Console.debug("Closing session...") Console.indent() for project in self.__projects: project.close() self.__projects = None Console.outdent()
[docs] def pause(self): """ Pauses the session. This release cache files etc. and makes it possible to call other jasy processes on the same projects. """ Console.info("Pausing session...") for project in self.__projects: project.pause()
[docs] def resume(self): """Resumes the session after it has been paused.""" Console.info("Resuming session...") for project in self.__projects: project.resume()
[docs] def getFields(self): return self.__fields
[docs] def getScriptByName(self, className): """ Queries all currently registered projects for the given class and returns the class item. Returns None when no matching class item was found. :param className: Any valid classname from any of the projects. :type className: str """ for project in self.__projects: classes = project.getScripts() if className in classes: return classes[className] return None
[docs] def getStyleByName(self, styleName): """ Queries all currently registered projects for the given style and returns the style item. Returns None when no matching style item was found. :param styleName: Any valid styleName from any of the projects. :type styleName: str """ for project in self.__projects: styles = project.getStyles() if styleName in styles: return styles[styleName] return None
# # Item type handling #
[docs] def addItemType(self, itemType, name, cls): self.__itemType[itemType] = (name, cls)
[docs] def getItemType(self, itemType): if not itemType in self.__itemType: return None return self.__itemType[itemType]
[docs] def getItemTypes(self): return self.__itemType
# # Project Managment #
[docs] def addProject(self, project): """ Adds the given project to the list of known projects. Projects should be added in order of their priority. This adds the field configuration of each project to the session fields. Fields must not conflict between different projects (same name). :param project: Instance of Project to append to the list :type project: object """ result = Project.getProjectDependencies(project, "external", self.__updateRepositories) for project in result: Console.info("Adding %s...", Console.colorize(project.getName(), "bold")) Console.indent() # Append to session list self.__projects.append(project) # Import library methods libraryPath = os.path.join(project.getPath(), "jasylibrary.py") if os.path.exists(libraryPath): self.loadLibrary(project.getName(), libraryPath, doc="Library of project %s" % project.getName()) # Import command methods commandPath = os.path.join(project.getPath(), "jasycommand.py") if os.path.exists(commandPath): self.loadCommands(project.getName(), commandPath) # Import project defined fields which might be configured using "activateField()" fields = project.getFields() for name in fields: entry = fields[name] if name in self.__fields: raise UserError("Field '%s' was already defined!" % (name)) if "check" in entry: check = entry["check"] if check in ["Boolean", "String", "Number"] or isinstance(check, list): pass else: raise UserError("Unsupported check: '%s' for field '%s'" % (check, name)) self.__fields[name] = entry Console.outdent()
[docs] def loadLibrary(self, objectName, fileName, encoding="utf-8", doc=None): """ Creates a new object inside the user API (jasyscript.py) with the given name containing all @share'd functions and fields loaded from the given file. """ if objectName in self.__scriptEnvironment: raise UserError("Could not import library %s as the object name %s is already used." % (fileName, objectName)) # Create internal class object for storing shared methods class Shared(object): pass exportedModule = Shared() exportedModule.__doc__ = doc or "Imported from %s" % os.path.relpath(fileName, os.getcwd()) counter = 0 # Method for being used as a decorator to share methods to the outside def share(func): nonlocal counter setattr(exportedModule, func.__name__, func) counter += 1 return func def itemtype(type, name): def wrap(cls): id = "%s.%s" % (objectName, type[0].upper() + type[1:]) self.addItemType(id, name, cls) return cls return wrap def postscan(): def wrap(f): self.__postscans.append(f) return f return wrap # Execute given file. Using clean new global environment # but add additional decorator for allowing to define shared methods # and the session object (self). code = open(fileName, "r", encoding=encoding).read() exec(compile(code, os.path.abspath(fileName), "exec"), {"share" : share, "itemtype": itemtype, "postscan": postscan, "session" : self}) # Export destination name as global self.__scriptEnvironment[objectName] = exportedModule Console.info("Imported %s.", Console.colorize("%s methods" % counter, "magenta")) return counter
[docs] def loadCommands(self, objectName, fileName, encoding="utf-8"): """Loads new commands into the session wide command registry.""" counter = 0 commands = self.__commands # Method for being used as a decorator to share methods to the outside def share(func): name = "%s.%s" % (objectName, func.__name__) if name in commands: raise Exception("Command %s already exists!" % name) commands[name] = func nonlocal counter counter += 1 return func # Execute given file. Using clean new global environment # but add additional decorator for allowing to define shared methods # and the session object (self). code = open(fileName, "r", encoding=encoding).read() exec(compile(code, os.path.abspath(fileName), "exec"), {"share" : share, "session" : self}) # Export destination name as global Console.info("Imported %s.", Console.colorize("%s commands" % counter, "magenta")) return counter
[docs] def addCommand(self, name, func, resultType=None, globalName=False): """Registers the given function as a new command.""" if globalName and "." in name: raise Exception("Invalid global name: %s!" % name) elif not globalName and len(name.split(".")) != 2: raise Exception("Command names should always match namespace.name! Tried with: %s!" % name) commands = self.__commands if name in commands: raise Exception("Command %s already exists!" % name) commands[name] = { "func" : func, "type" : resultType }
[docs] def getCommands(self): """Returns a dictionary of all commands.""" return self.__commands
[docs] def executeCommand(self, name, params): commands = self.__commands if name not in commands: raise Exception("Unknown command %s!" % name) entry = commands[name] resultType = entry["type"] if params: result = entry["func"](*params) else: result = entry["func"]() return result, resultType
[docs] def getProjects(self): """Returns all currently registered projects.""" return self.__projects
[docs] def getProjectByName(self, name): """Returns a project by its name.""" for project in self.__projects: if project.getName() == name: return project return None
[docs] def getRelativePath(self, project): """Returns the relative path of any project to the main project.""" mainPath = self.__projects[0].getPath() projectPath = project.getPath() return os.path.relpath(projectPath, mainPath)
[docs] def getMain(self): """Returns the main project which is the first project added to the session and the one with the highest priority.""" if self.__projects: return self.__projects[0] else: return None
[docs] def getVirtualProject(self): """ Returns the virtual project for this application. The project offers storage for dynamically created JavaScript classes and other files. Storage is kept intact between different Jasy sessions. """ # Create only once per session if self.__virtualProject: return self.__virtualProject # Place virtual project in application's ".jasy" folder path = os.path.abspath(os.path.join(".jasy", "virtual")) # Set package to empty string to allow for all kind of namespaces in this virtual project jasy.core.File.write(os.path.join(path, "jasyproject.yaml"), 'name: virtual\npackage: ""\n') # Generate project instance from path, store and return project = Project.getProjectFromPath(path, self) self.__virtualProject = project self.__projects.append(project) return project
[docs] def getVirtualFilePathFromId(self, fileId, extension=None): """ Returns a valid virtual path for the given file item ID. Supports adding an optional extension for files where the extension is not part of the idea (effectively this are most of them, but not assets) """ virtualProject = self.getVirtualProject() fileName = fileId.replace(".", os.sep) if extension is not None: fileName += extension # Place file into "src" folder return os.path.join(virtualProject.getPath(), "src", fileName)
[docs] def getVirtualItem(self, baseName, itemClass, text, extension): virtualProject = self.getVirtualProject() # Tweak name by content checksum to make all results of the # same content being cachable by the normal infrastructure. checksum = zlib.adler32(text.encode("utf-8")) fileId = "%s-%s" % (baseName, checksum) # Try to reuse existing item e.g. from previous run # TODO: Use same type as item class here... not just scripts! item = virtualProject.getScriptByName(fileId) if item: return item # Generate path from file ID. filePath = self.getVirtualFilePathFromId(fileId, extension) # Create a class dynamically and add it to both, # the virtual project and our requirements list. item = itemClass(virtualProject, fileId) item.saveText(text, filePath) return item
# # Translation Support #
[docs] def getAvailableTranslations(self): """ Returns a set of all available translations. This is the sum of all projects so even if only one project supports "fr_FR" then it will be included here. """ supported = set() for project in self.__projects: supported.update(project.getTranslations().keys()) return supported
[docs] def getTranslationBundle(self, language=None): """Returns a translation object for the given language containing all relevant translation files for the current project set.""" if language is None: return None if language in self.__translationBundles: return self.__translationBundles[language] Console.info("Creating translation bundle: %s", language) Console.indent() # Initialize new Translation object with no project assigned # This object is used to merge all seperate translation instances later on. combined = jasy.item.Translation.TranslationItem(None, id=language) relevantLanguages = self.__expandLanguage(language) # Loop structure is build to prefer finer language matching over project priority for currentLanguage in reversed(relevantLanguages): for project in self.__projects: for translation in project.getTranslations().values(): if translation.getLanguage() == currentLanguage: Console.debug("Adding %s entries from %s @ %s...", len(translation.getTable()), currentLanguage, project.getName()) combined += translation Console.info("Combined number of translations: %s", len(combined.getTable())) Console.outdent() self.__translationBundles[language] = combined return combined
def __expandLanguage(self, language): """Expands the given language into a list of languages being used in priority order (highest first)""" # Priority Chain: # de_DE => de => C (default language) => code all = [language] if "_" in language: all.append(language[:language.index("_")]) all.append("C") return all