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/bpkg-repogen

229 lines
6.7 KiB
Plaintext
Raw Normal View History

#!/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):
"""
2017-07-02 16:55:17 -07:00
Generator, yields (path, bl_info) of blender addons in `path`.
"""
2017-07-02 16:55:17 -07:00
for item in path.iterdir():
try:
2017-07-05 18:40:32 -07:00
yield (item, extract_blinfo(item))
2017-07-02 16:55:17 -07:00
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')
2017-07-05 18:40:32 -07:00
def blinfo_from_zip(item: Path) -> dict:
try:
with zipfile.ZipFile(str(item), 'r') as z:
if len(z.namelist()) == 1:
# zipfile with one item: just read that item
return 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:
return parse_blinfo(z.read(fname))
except BadAddon:
continue
raise BadAddon("Zipfile '%s' doesn't contain a readable bl_info dict" % item)
except zipfile.BadZipFile as err:
raise BadAddon("Bad zipfile '%s'" % item) from err
def blinfo_from_py(item: Path) -> dict:
with item.open() as f:
return parse_blinfo(f.read())
def blinfo_from_dir(item: Path) -> dict:
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:
return parse_blinfo(f.read())
def extract_blinfo(item: Path) -> dict:
"""
Extract bl_info dict from addon at path (can be single file, python package, or zip)
"""
if not item.exists():
raise FileNotFoundError("Cannot extract blinfo from '%s'; no such file or directory" % item)
if item.is_dir():
2017-07-05 18:40:32 -07:00
return blinfo_from_dir(item)
2017-07-05 18:40:32 -07:00
if item.is_file():
ext = item.suffix.lower()
2017-07-05 18:40:32 -07:00
if ext == '.zip':
return blinfo_from_zip(item)
elif ext == '.py':
2017-07-05 18:40:32 -07:00
return blinfo_from_py(item)
else:
raise BadAddon("File '%s' doesn't have a .zip or .py extension; not an addon" % item)
# This should not happen
2017-07-05 18:40:32 -07:00
raise RuntimeError("Could not read addon '%s'" % item.name)
class Package:
2017-07-05 18:40:32 -07:00
"""
Stores package path and metadata
"""
def __init__(self, path: Path, bl_info: dict):
self.bl_info = bl_info
self.path = path
self.url = None
2017-07-05 18:40:32 -07:00
def to_dict(self) -> dict:
"""
Return a dict representation of the package
"""
return {
'bl_info': self.bl_info,
'url': self.url,
}
class Repository:
2017-07-05 18:40:32 -07:00
"""
Stores repository metadata (including a list of packages)
"""
def __init__(self, name: str):
self.name = name
self.url = None
self.packages = []
def add_package(self, pkg: Package):
2017-07-05 18:40:32 -07:00
"""
Add a package to the repository
"""
# if pkg.url is None:
# pkg.url =
self.packages.append(pkg)
2017-07-05 18:40:32 -07:00
def to_dict(self) -> dict:
"""
Return a dict representation of the repository
"""
return {
'name': self.name,
2017-07-05 18:40:32 -07:00
'packages': [p.to_dict() for p in self.packages],
'url': self.url,
}
def dump(self, path: Path):
2017-07-05 18:40:32 -07:00
"""
Dump repository as a repo.json file in 'path'
"""
with (path / 'repo.json').open('w', encoding='utf-8') as repo_file:
2017-07-05 18:40:32 -07:00
json.dump(self.to_dict(), repo_file, indent=4, sort_keys=True)
log.info("repo.json written to %s" % path)
def make_repo(path: Path, name: str, baseurl: str) -> Repository:
2017-07-02 16:55:17 -07:00
"""Make repo.json for files in directory 'path'"""
repo = Repository(name)
2017-07-02 16:55:17 -07:00
if not path.is_dir():
raise FileNotFoundError(path)
2017-07-02 16:55:17 -07:00
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)
package.url = baseurl + package.path.name
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('-u', '--baseurl',
#TODO: support relative URLs
help="Component of URL leading up to the package filename.")
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()
logging.basicConfig(format='%(levelname)8s: %(message)s', level=logging.INFO)
log.level += args.verbose
2017-07-05 18:40:32 -07:00
if args.name is None:
args.name = args.path.name
if args.baseurl is None:
log.warning("No baseurl given, package URLs will just be filenames")
args.baseurl = ""
2017-07-05 18:40:32 -07:00
log.debug("Repository name: '%s'" % args.name)
repo = make_repo(args.path, args.name, args.baseurl)
repo.dump(args.output)
if __name__ == '__main__':
main()