This repository has been archived on 2023-02-07. You can view files and clone it, but cannot push or open issues or pull requests.
Files
blender-package-manager-addon/make_repo.py
gandalf3 3847cc877f Improve make_repo.py
This cleans up make_repo.py a bit, using file extensions to determine
file type.

This also loosens the testing repo generation, as the existing
test required matching a predifed expected output which had to be
updated on every change (essentially making it a moot test, as the
reference output was obtained from the functions output).
The new test just checks if the output has the same number of packages
as the input dir has addons.

Tips on how best to test these sorts of "higher level" functions (if at
all) would be welcome :)
2017-07-04 23:56:19 -07:00

202 lines
6.1 KiB
Python
Executable File

#!/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`.
"""
pass
for item in path.iterdir():
try:
yield(item, extract_blinfo(item))
except BadAddon as err:
log.debug("Skipping '{}': {}".format(item.name, err))
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
if not item.exists():
raise FileNotFoundError("Cannot extract blinfo from '%s'; no such file or directory" % item)
if item.is_dir():
try:
f = (item / '__init__.py').open("r")
except FileNotFoundError as err:
raise BadAddon("Directory '%s' doesn't contain __init__.py; not a python package" % item) from err
with f:
blinfo = parse_blinfo(f.read())
elif item.is_file():
ext = item.suffix.lower()
if ext == '.zip':
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)
# for now we just break after the first one
if fname.endswith('__init__.py'):
try:
blinfo = parse_blinfo(z.read(fname))
break
except BadAddon:
continue
if blinfo is None:
raise BadAddon("Zipfile '%s' doesn't contain a readable bl_info dict" % item)
except zipfile.BadZipFile as e:
raise BadAddon("Bad zipfile '%s'" % item) from e
elif ext == '.py':
with item.open() as f:
blinfo = parse_blinfo(f.read())
else:
raise BadAddon("File '%s' doesn't have a .zip or .py extension; not an addon" % item)
# This should not happen
if blinfo is None:
raise RuntimeError("Could not read addon '%s'" % item.name)
return blinfo
class Package:
def __init__(self, path: Path, bl_info: dict, baseurl=None):
self.bl_info = bl_info
self.path = path
self.url = None
def dict(self) -> dict:
return {
'bl_info': self.bl_info,
'url': self.url,
}
class Repository:
def __init__(self, name: str):
self.name = name
self.url = None
self.packages = []
def add_package(self, pkg: Package):
# if pkg.url is None:
# pkg.url =
self.packages.append(pkg)
def dict(self) -> dict:
return {
'name': self.name,
'packages': [p.dict() for p in self.packages],
'url': self.url,
}
def dump(self, path: Path):
with (path / 'repo.json').open('w', encoding='utf-8') as repo_file:
json.dump(self.dict(), repo_file, indent=4, sort_keys=True)
log.info("repo.json written to %s" % path)
def json(self) -> str:
return json.dumps(self.__dict__)
def make_repo(path: Path, name: str) -> Repository:
"""Make repo.json for files in directory 'path'"""
repo = Repository(name)
if not path.is_dir():
raise FileNotFoundError(path)
for addon, bl_info in iter_addons(path):
# Check if we have all bl_info fields we want
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 = Package(addon, bl_info)
repo.add_package(package)
log.info("Repository generation successful")
return repo
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('-n', '--name',
help="Name of repo (defaults to basename of 'path')")
parser.add_argument('-o', '--output',
help="Directory in which to write repo.json file",
type=Path,
default=Path.cwd())
parser.add_argument('path',
type=Path,
help="Path to addon directory")
args = parser.parse_args()
if args.name is None:
args.name = args.path.name
logging.basicConfig(format='%(levelname)8s: %(message)s', level=logging.INFO)
log.level += args.verbose
repo = make_repo(args.path, args.name)
repo.dump(args.output)
if __name__ == '__main__':
main()