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()
def __addContent(self, content): Console.debug("Adding manual content") Console.indent() for fileId in content: fileContent = content[fileId] if len(fileContent) == 0: raise UserError("Empty content!") # If the user defines a file extension for JS public idenfiers # (which is not required) we filter them out if fileId.endswith(".js"): raise UserError( "JavaScript files should define the exported name, not a file name: %s" % fileId) fileExtension = os.path.splitext(fileContent[0])[1] # Support for joining text content if len(fileContent) == 1: filePath = os.path.join(self.__path, fileContent[0]) else: filePath = [ os.path.join(self.__path, filePart) for filePart in fileContent ] # Structure files if fileExtension in classExtensions: construct = jasy.item.Class.ClassItem dist = self.classes elif fileExtension in translationExtensions: construct = jasy.item.Translation.TranslationItem dist = self.translations else: construct = jasy.item.Asset.AssetItem dist = self.assets # Check for duplication if fileId in dist: raise UserError("Item ID was registered before: %s" % fileId) # Create instance item = construct(self, fileId).attach(filePath) Console.debug("Registering %s %s" % (item.kind, fileId)) dist[fileId] = item Console.outdent()
def addProfile(self, name, root=None, config=None, items=None): """ Adds a new profile to the manager. This is basically the plain version of addSourceProfile/addBuildProfile which gives complete manual control of where to load the assets from. This is useful for e.g. supporting a complete custom loading scheme aka complex CDN based setup. """ profiles = self.__profiles for entry in profiles: if entry["name"] == name: raise UserError("Asset profile %s was already defined!" % name) profile = {"name": name} if root: if not root.endswith("/"): root += "/" profile["root"] = root if config is not None: profile.update(config) unique = len(profiles) profiles.append(profile) if items: for fileId in items: items[fileId]["p"] = unique self.__addRuntimeData(items) return unique
def __resolveConstructor(self, itemType): construct = self.__session.getItemType(itemType) if not construct: raise UserError("Could not resolve item type %s" % itemType) return construct
def task(*args, **kwargs): """Specifies that this function is a task.""" if len(args) == 1: func = args[0] if isinstance(func, Task): return func elif isinstance(func, types.FunctionType): return Task(func) # Compat to old Jasy 0.7.x task declaration elif isinstance(func, str): return task(**kwargs) else: raise UserError("Invalid task") else: def wrapper(func): return Task(func, **kwargs) return wrapper
def executeScript(self, fileName, autoDelete=True, optional=False, encoding="utf-8"): """ Executes the given script for configuration proposes and deletes the file afterwards (by default). Returns True when the file was found and processed. """ if not os.path.exists(fileName): if optional: return False else: raise UserError("Could not find configuration script: %s" % fileName) env = {"config": self, "file": File} code = open(fileName, "r", encoding=encoding).read() exec(compile(code, os.path.abspath(fileName), "exec"), globals(), env) if autoDelete: File.rm(fileName) return True
def write(self, filename, debug=False): if Image is None: raise UserError("Missing Python PIL which is required to create sprite sheets!") img = Image.new('RGBA', (self.width, self.height)) draw = ImageDraw.Draw(img) #draw.rectangle((0, 0, self.width, self.height), fill=(255, 255, 0, 255)) # Load images and pack them in for block in self.blocks: res = Image.open(block.image.src) x, y = block.fit.x, block.fit.y if block.rotated: Console.debug('%s is rotated' % block.image.src) res = res.rotate(90) img.paste(res, (x, y)) del res if debug: x, y, w, h = block.fit.x, block.fit.y, block.w, block.h draw.rectangle((x, y , x + w , y + h), outline=(0, 0, 255, 255) if block.rotated else (255, 0, 0, 255)) if debug: for i, block in enumerate(self.packer.getUnused()): x, y, w, h = block.x, block.y, block.w, block.h draw.rectangle((x, y , x + w , y + h), fill=(255, 255, 0, 255)) img.save(filename)
def writeConfig(data, fileName, indent=2, encoding="utf-8"): """ Writes the given data structure to the given file name. Based on the given extension a different file format is choosen. Currently use either .yaml or .json. """ fileHandle = open(fileName, mode="w", encoding=encoding) fileExt = os.path.splitext(fileName)[1] if fileExt == ".yaml": yaml.dump(data, fileHandle, default_flow_style=False, indent=indent, allow_unicode=True) fileHandle.close() elif fileExt == ".json": json.dump(data, fileHandle, indent=indent, ensure_ascii=False) fileHandle.close() else: fileHandle.close() raise UserError("Unsupported config type: %s" % fileExt)
def runTask(project, task, **kwargs): """ Executes the given task of the given projects. This happens inside a new sandboxed session during which the current session is paused/resumed automatically. """ remote = session.getProjectByName(project) if remote is not None: remotePath = remote.getPath() remoteName = remote.getName() elif os.path.isdir(project): remotePath = project remoteName = os.path.basename(project) else: raise UserError("Unknown project or invalid path: %s" % project) Console.info("Running %s of project %s...", Console.colorize(task, "bold"), Console.colorize(remoteName, "bold")) # Pauses this session to allow sub process fully accessing the same projects session.pause() # Build parameter list from optional arguments params = ["--%s=%s" % (key, kwargs[key]) for key in kwargs] if not "prefix" in kwargs: params.append("--prefix=%s" % session.getCurrentPrefix()) # Full list of args to pass to subprocess args = [__command, task] + params # Change into sub folder and execute jasy task oldPath = os.getcwd() os.chdir(remotePath) returnValue = subprocess.call(args, shell=sys.platform == "win32") os.chdir(oldPath) # Resumes this session after sub process was finished session.resume() # Error handling if returnValue != 0: raise UserError("Executing of sub task %s from project %s failed" % (task, project))
def setDefaultLocale(self, locale): """ Sets the default locale """ if not "locale" in self.__fields: raise UserError("Define locales first!") self.__fields["locale"]["default"] = locale
def mkdir(name): """Creates directory (works recursively)""" if os.path.isdir(name): return elif os.path.exists(name): raise UserError("Error creating directory %s - File exists!" % name) return os.makedirs(name)
def __hasDir(self, directory): full = os.path.join(self.__path, directory) if os.path.exists(full): if not os.path.isdir(full): raise UserError("Expecting %s to be a directory: %s" % full) return True return False
def __addContent(self, content): Console.info("Adding manual content") Console.indent() for fileId in content: entry = content[fileId] if not isinstance(entry, dict): raise UserError( "Invalid manual content section for file %s. Requires a dict with type and source definition!" % fileId) itemType = entry["type"] fileContent = entry["source"] if len(fileContent) == 0: raise UserError("Empty content!") # Support for joining text content if len(fileContent) == 1: filePath = os.path.join(self.__path, fileContent[0]) else: filePath = [ os.path.join(self.__path, filePart) for filePart in fileContent ] name, construct = self.__resolveConstructor(itemType) item = construct(self, fileId).attach(filePath) Console.debug("Registering %s %s" % (item.kind, fileId)) if not itemType in self.items: self.items[itemType] = {} # Check for duplication if fileId in self.items[itemType]: raise UserError("Item ID was registered before: %s" % fileId) self.items[itemType][fileId] = item Console.outdent()
def getHtml(self, highlight=True): """ Returns the comment text converted to HTML. :param highlight: Whether to highlight the code :type highlight: bool """ if not Text.supportsMarkdown: raise UserError( "Markdown is not supported by the system. Documentation comments could converted to HTML." ) if highlight: if self.__highlightedText is None: highlightedText = "" for block in self.__blocks: if block["type"] == "comment": highlightedText += Text.highlightCodeBlocks( Text.markdownToHtml(block["processed"])) else: highlightedText += "\n%s" % Text.highlightCodeBlocks( Text.markdownToHtml(block["text"])) self.__highlightedText = highlightedText return self.__highlightedText else: if self.__processedText is None: processedText = "" for block in self.__blocks: if block["type"] == "comment": processedText += Text.markdownToHtml( block["processed"]) else: processedText += "\n%s\n\n" % block["text"] self.__processedText = processedText.strip() return self.__processedText
def executeTask(taskname, **kwargs): """Executes the given task by name with any optional named arguments.""" if taskname in __taskRegistry: try: camelCaseArgs = {Util.camelize(key) : kwargs[key] for key in kwargs} return __taskRegistry[taskname](**camelCaseArgs) except UserError as err: raise except: Console.error("Unexpected error! Could not finish task %s successfully!" % taskname) raise else: raise UserError("No such task: %s" % taskname)
def __splitTemplate(value, valueParams): """ Split string into plus-expression(s) - patchParam: string node containing the placeholders - valueParams: list of params to inject """ # Convert list with nodes into Python dict # [a, b, c] => {0:a, 1:b, 2:c} mapper = {pos: value for pos, value in enumerate(valueParams)} result = [] splits = __replacer.split(value) if len(splits) == 1: return None pair = Node.Node(None, "plus") for entry in splits: if entry == "": continue if len(pair) == 2: newPair = Node.Node(None, "plus") newPair.append(pair) pair = newPair if __replacer.match(entry): pos = int(entry[1]) - 1 # Items might be added multiple times. Copy to protect original. try: repl = mapper[pos] except KeyError: raise UserError("Invalid positional value: %s in %s" % (entry, value)) copied = copy.deepcopy(mapper[pos]) if copied.type not in ("identifier", "call"): copied.parenthesized = True pair.append(copied) else: child = Node.Node(None, "string") child.value = entry pair.append(child) return pair
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
def loadConfig(fileName, encoding="utf-8"): """Loads the given configuration file (filename without extension) and returns the parsed object structure.""" configName = findConfig(fileName) if configName is None: raise UserError("Unsupported config file: %s" % fileName) fileHandle = open(configName, mode="r", encoding=encoding) fileExt = os.path.splitext(configName)[1] if fileExt == ".yaml": result = yaml.load(fileHandle) elif fileExt == ".json": result = json.load(fileHandle) fileHandle.close() return result
def addFile(self, relPath, fullPath, itemType, package, override=False): fileName = os.path.basename(relPath) fileExtension = os.path.splitext(fileName)[1] name, construct = self.__resolveConstructor(itemType) item = construct.fromPath(self, relPath, package).attach(fullPath) fileId = item.getId() Console.debug("Registering %s %s" % (item.kind, fileId)) if not itemType in self.items: self.items[itemType] = {} # Check for duplication if fileId in self.items[itemType] and not override: raise UserError("Item ID was registered before: %s" % fileId) self.items[itemType][fileId] = item
def getHighlightedCode(self): field = "highlighted[%s]" % self.id source = self.project.getCache().read(field, self.mtime) if source is None: if highlight is None: raise UserError( "Could not highlight JavaScript code! Please install Pygments." ) lexer = JavascriptLexer(tabsize=2) formatter = HtmlFormatter(full=True, style="autumn", linenos="table", lineanchors="line") source = highlight(self.getText(), lexer, formatter) self.project.getCache().store(field, source, self.mtime) return source
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 # 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.debug("Importing %s shared methods under %s...", counter, objectName) self.__scriptEnvironment[objectName] = exportedModule return counter
def getApi(self): field = "api[%s]" % self.id apidata = self.project.getCache().read(field, self.getModificationTime()) if not Text.supportsMarkdown: raise UserError( "Missing Markdown feature to convert package docs into HTML.") if apidata is None: apidata = Data.ApiData(self.id) apidata.main["type"] = "Package" apidata.main["doc"] = Text.highlightCodeBlocks( Text.markdownToHtml(self.getText())) self.project.getCache().store(field, apidata, self.getModificationTime()) return apidata
def readQuestions(self, fileName, force=False, autoDelete=True, optional=False, encoding="utf-8"): """ Reads the given configuration file with questions and deletes the file afterwards (by default). Returns True when the file was found and processed. """ configFile = findConfig(fileName) if configFile is None: if optional: return False else: raise UserError( "Could not find configuration file (questions): %s" % configFile) data = loadConfig(configFile, encoding=encoding) for entry in data: question = entry["question"] name = entry["name"] accept = getKey(entry, "accept", None) required = getKey(entry, "required", True) default = getKey(entry, "default", None) force = getKey(entry, "force", False) self.ask(question, name, accept=accept, required=required, default=default, force=force) if autoDelete: File.rm(configFile) return True
def __resolveScanConfig(self, configs): scan = [] for path, config in configs.items(): if isinstance(config, str): config = {"type": config, "package": self.__package} else: config = copy.deepcopy(config) if not "type" in config: raise UserError( "No type configured in jasyproject configuration (scan section)" ) if not "package" in config: config["package"] = self.__package if config["package"] == "": config["package"] = None config["origpath"] = path config["regex"], config["paths"] = self.__createPathRe(path) scan.append(config) def specificitySort(item): """Sorts for specificy of given scan path.""" origPath = item["origpath"] if not "*" in origPath: num = 10000 elif not origPath.endswith("*"): num = 1000 else: num = 0 num += len(origPath) return -num scan.sort(key=specificitySort) return scan
def addFile(self, relPath, fullPath, distname, override=False): fileName = os.path.basename(relPath) fileExtension = os.path.splitext(fileName)[1] # Prepand package if self.__package: fileId = "%s/" % self.__package else: fileId = "" # Structure files if fileExtension in classExtensions and distname == "classes": fileId += os.path.splitext(relPath)[0] construct = jasy.item.Class.ClassItem dist = self.classes elif fileExtension in translationExtensions and distname == "translations": fileId += os.path.splitext(relPath)[0] construct = jasy.item.Translation.TranslationItem dist = self.translations elif fileName in docFiles: fileId += os.path.dirname(relPath) fileId = fileId.strip("/") # edge case when top level directory construct = jasy.item.Doc.DocItem dist = self.docs else: fileId += relPath construct = jasy.item.Asset.AssetItem dist = self.assets # Only assets keep unix style paths identifiers if construct != jasy.item.Asset.AssetItem: fileId = fileId.replace("/", ".") # Check for duplication if fileId in dist and not override: raise UserError("Item ID was registered before: %s" % fileId) # Create instance item = construct(self, fileId).attach(fullPath) Console.debug("Registering %s %s" % (item.kind, fileId)) dist[fileId] = item
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()
def __structurize(self, data): """ This method structurizes the incoming data into a cascaded structure representing the file system location (aka file IDs) as a tree. It further extracts the extensions and merges files with the same name (but different extensions) into the same entry. This is especially useful for alternative formats like audio files, videos and fonts. It only respects the data of the first entry! So it is not a good idea to have different files with different content stored with the same name e.g. content.css and content.png. """ root = {} # Easier to debug and understand when sorted for fileId in sorted(data): current = root splits = fileId.split("/") # Extract the last item aka the filename itself basename = splits.pop() # Find the current node to store info on for split in splits: if split not in current: current[split] = {} elif not isinstance(current[split], dict): raise UserError( "Invalid asset structure. Folder names must not be identical to any filename without extension: \"%s\" in %s" % (split, fileId)) current = current[split] # Create entry Console.debug("Adding %s..." % fileId) current[basename] = data[fileId] return root
def attach(self, path): self.__path = path entry = None try: if isinstance(path, list): mtime = 0 for entry in path: entryTime = os.stat(entry).st_mtime if entryTime > mtime: mtime = entryTime self.mtime = mtime else: entry = path self.mtime = os.stat(entry).st_mtime except OSError as oserr: raise UserError("Invalid item path: %s" % entry) return self
def loadValues(self, fileName, optional=False, encoding="utf-8"): """ Imports the values of the given config file Returns True when the file was found and processed. Note: Supports dotted names to store into sub trees Note: This method overrides keys when they are already defined! """ configFile = findConfig(fileName) if configFile is None: if optional: return False else: raise UserError( "Could not find configuration file (values): %s" % configFile) data = loadConfig(configFile, encoding=encoding) for key in data: self.set(key, data[key]) return True
def create(name="myproject", origin=None, originVersion=None, skeleton=None, destination=None, session=None, **argv): """ Creates a new project from a defined skeleton or an existing project's root directory (only if there is a jasycreate config file). :param name: The name of the new created project :type name: string :param origin: Path or git url to the base project :type origin: string :param originVersion: Version of the base project from wich will be created. :type originVersion: string :param skeleton: Name of a defined skeleton. None for creating from root :type skeleton: string :param destination: Destination path for the new created project :type destination: string :param session: An optional session to use as origin project :type session: object """ if not validProjectName.match(name): raise UserError( "Invalid project name: %s (Use lowercase characters and numbers only for broadest compabibility)" % name) # # Initial Checks # # Figuring out destination folder if destination is None: destination = name destinationPath = os.path.abspath(os.path.expanduser(destination)) if os.path.exists(destinationPath): raise UserError( "Cannot create project %s in %s. File or folder exists!" % (name, destinationPath)) # Origin can be either: # 1) None, which means a skeleton from the current main project # 2) An repository URL # 3) A project name known inside the current session # 4) Relative or absolute folder path originPath = None originName = None if origin is None: originProject = session and session.getMain() if originProject is None: raise UserError( "Auto discovery failed! No Jasy projects registered!") originPath = originProject.getPath() originName = originProject.getName() originRevision = None elif Repository.isUrl(origin): Console.info("Using remote skeleton") tempDirectory = tempfile.TemporaryDirectory() originPath = os.path.join(tempDirectory.name, "clone") originUrl = origin Console.indent() originRevision = Repository.update(originUrl, originVersion, originPath) Console.outdent() if originRevision is None: raise UserError("Could not clone origin repository!") Console.debug("Cloned revision: %s" % originRevision) if findConfig(os.path.join( originPath, "jasycreate")) or os.path.isfile( os.path.join(originPath, "jasycreate.py")): originProject = None else: originProject = getProjectFromPath(originPath, session) originName = originProject.getName() else: originProject = session and session.getProjectByName(origin) originVersion = None originRevision = None if originProject is not None: originPath = originProject.getPath() originName = origin elif os.path.isdir(origin): originPath = origin if findConfig(os.path.join( originPath, "jasycreate")) or os.path.isfile( os.path.join(originPath, "jasycreate.py")): originProject = None else: originProject = getProjectFromPath(originPath, session) originName = originProject.getName() else: raise UserError("Invalid value for origin: %s" % origin) # Figure out the skeleton root folder if originProject is not None: skeletonDir = os.path.join( originPath, originProject.getConfigValue("skeletonDir", "skeleton")) else: skeletonDir = originPath if not os.path.isdir(skeletonDir): raise UserError('The project %s offers no skeletons!' % originName) # For convenience: Use first skeleton in skeleton folder if no other selection was applied if skeleton is None: if originProject is not None: skeleton = getFirstSubFolder(skeletonDir) else: skeleton = skeletonDir # Finally we have the skeleton path (the root folder to copy for our app) skeletonPath = os.path.join(skeletonDir, skeleton) if not os.path.isdir(skeletonPath): raise UserError('Skeleton %s does not exist in project "%s"' % (skeleton, originName)) # # Actual Work # # Prechecks done if originName: Console.info('Creating %s from %s %s...', Console.colorize(name, "bold"), Console.colorize(skeleton + " @", "bold"), Console.colorize(originName, "magenta")) else: Console.info('Creating %s from %s...', Console.colorize(name, "bold"), Console.colorize(skeleton, "bold")) Console.debug('Skeleton: %s', Console.colorize(skeletonPath, "grey")) Console.debug('Destination: %s', Console.colorize(destinationPath, "grey")) # Copying files to destination Console.info("Copying files...") shutil.copytree(skeletonPath, destinationPath) Console.debug("Files were copied successfully.") # Close origin project if originProject: originProject.close() # Change to directory before continuing os.chdir(destinationPath) # Create configuration file from question configs and custom scripts Console.info("Starting configuration...") config = Config() config.set("name", name) config.set("jasy.version", jasy.__version__) if originName: config.set("origin.name", originName) config.set("origin.version", originVersion) config.set("origin.revision", originRevision) config.set("origin.skeleton", os.path.basename(skeletonPath)) config.injectValues(**argv) if originProject is not None: config.readQuestions("jasycreate", optional=True) config.executeScript("jasycreate.py", optional=True) # Do actual replacement of placeholders massFilePatcher(destinationPath, config) Console.debug("Files were patched successfully.") # Done Console.info('Your application %s was created successfully!', Console.colorize(name, "bold"))