#!/usr/bin/env python3 from pathlib import Path import argparse import zipfile import ast import json import logging from utils import * log = logging.getLogger(__name__) REQUIRED_KEYS = set(['name', 'blender', 'version']) SCHEMA_VERSION = 1 class BadAddon(Exception): pass def iter_addons(path: Path) -> (Path, dict): """ Generator, yields (path, bl_info) of blender addons in `path` (non recursive). """ pass # for item in addons_dir.iterdir(): # base = item.name # # yield (base, fname, '.zip') # else: # yield (base, item.path, '.py') def parse_blinfo(source: str) -> dict: """Parse a python file and return its bl_info dict, if there is one (else return None)""" try: tree = ast.parse(source) except SyntaxError as e: raise BadAddon("Syntax error") from e for body in tree.body: if body.__class__ != ast.Assign: continue if len(body.targets) != 1: continue if getattr(body.targets[0], 'id', '') != 'bl_info': continue return ast.literal_eval(body.value) raise BadAddon('No bl_info found') def extract_blinfo(item: Path) -> dict: """ Extract bl_info dict from addon at path (can be single file, python package, or zip) """ blinfo = None addon_name = item.name if not item.exists(): raise FileNotFoundError("Cannot extract blinfo from '%s'; no such file or directory" % item) if item.is_dir(): fname = item / '__init__.py' try: with fname.open("r") as f: blinfo = parse_blinfo(f.read()) except FileNotFoundError as err: # directory with no __init__.py: not an addon raise BadAddon("Directory '%s' doesn't contain __init__.py; not a python package" % item) from err elif item.is_file(): try: with zipfile.ZipFile(str(item), 'r') as z: if len(z.namelist()) == 1: # zipfile with one item: just read that item blinfo = parse_blinfo(z.read(z.namelist()[0])) else: # zipfile with multiple items: try all __init__.py files for fname in z.namelist(): # TODO: zips with multiple bl_infos might be a problem, # not sure how such cases should be handled (if at all) if fname.endswith('__init__.py'): try: blinfo = parse_blinfo(z.read(fname)) break except BadAddon: continue raise BadAddon("Zipfile '%s' doesn't contain any readable bl_info dict" % item) except zipfile.BadZipFile: # Assume file is with item.open() as f: blinfo = parse_blinfo(f.read()) # This should not happen if blinfo == None: raise RuntimeError("Could not read addon '%s'" % addon_name) return blinfo def make_repo(repopath: Path): """Make repo.json for files in directory 'repopath'""" repo_data = {} package_data = [] if not repopath.is_dir(): raise FileNotFoundError(repopath) for addon_path in repopath.iterdir(): package_datum = {} addon = addon_path.name try: bl_info = extract_blinfo(addon_path) except BadAddon as err: log.warning('Could not extract bl_info from {}: {}'.format(addon_path, err)) continue if not REQUIRED_KEYS.issubset(set(bl_info)): log.warning( "Required key(s) '{}' not found in bl_info of '{}'".format( "', '".join(REQUIRED_KEYS.difference(set(bl_info))), addon) ) package_datum['bl_info'] = bl_info package_datum['type'] = 'addon' package_data.append(package_datum) repo_data['packages'] = package_data log.info("Repository generation successful") cwd = Path.cwd() dump_repo(cwd, repo_data) log.info("repo.json written to %s" % cwd) def main(): parser = argparse.ArgumentParser(description='Generate a blender package repository from a directory of addons') parser.add_argument('-v', '--verbose', help="Increase verbosity (can be used multiple times)", action="count", default=0) parser.add_argument('path', type=Path, nargs='?', default=Path.cwd(), help="Path to addon directory") args = parser.parse_args() log.level = args.verbose logging.basicConfig(level=logging.INFO, format='%(levelname)8s: %(message)s') make_repo(args.path) if __name__ == '__main__': main()