diff --git a/make_repo.py b/make_repo.py index 40ab33c..a05eed0 100755 --- a/make_repo.py +++ b/make_repo.py @@ -20,10 +20,9 @@ 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)) + yield (item, extract_blinfo(item)) except BadAddon as err: log.debug("Skipping '{}': {}".format(item.name, err)) @@ -48,104 +47,122 @@ def parse_blinfo(source: str) -> dict: raise BadAddon('No bl_info found') +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) """ - 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()) + return blinfo_from_dir(item) - elif item.is_file(): + if 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 - + return blinfo_from_zip(item) elif ext == '.py': - with item.open() as f: - blinfo = parse_blinfo(f.read()) - + 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 - if blinfo is None: - raise RuntimeError("Could not read addon '%s'" % item.name) - - return blinfo - + raise RuntimeError("Could not read addon '%s'" % item.name) class Package: - def __init__(self, path: Path, bl_info: dict, baseurl=None): + """ + Stores package path and metadata + """ + + def __init__(self, path: Path, bl_info: dict): self.bl_info = bl_info self.path = path self.url = None - def dict(self) -> dict: + def to_dict(self) -> dict: + """ + Return a dict representation of the package + """ return { 'bl_info': self.bl_info, 'url': self.url, } class Repository: + """ + 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): + """ + Add a package to the repository + """ # if pkg.url is None: # pkg.url = self.packages.append(pkg) - def dict(self) -> dict: + def to_dict(self) -> dict: + """ + Return a dict representation of the repository + """ return { 'name': self.name, - 'packages': [p.dict() for p in self.packages], + 'packages': [p.to_dict() for p in self.packages], 'url': self.url, } def dump(self, path: Path): + """ + Dump repository as a repo.json file in 'path' + """ with (path / 'repo.json').open('w', encoding='utf-8') as repo_file: - json.dump(self.dict(), repo_file, indent=4, sort_keys=True) + json.dump(self.to_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'""" @@ -188,12 +205,15 @@ def main(): 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 + if args.name is None: + args.name = args.path.name + + log.debug("Repository name: '%s'" % args.name) + repo = make_repo(args.path, args.name) repo.dump(args.output) diff --git a/tests/test_make_repo.py b/tests/test_make_repo.py index 603f8fa..a3c866d 100644 --- a/tests/test_make_repo.py +++ b/tests/test_make_repo.py @@ -10,7 +10,7 @@ import make_repo logging.basicConfig(level=logging.DEBUG, format='%(levelname)8s: %(message)s') -class test_make_repo(unittest.TestCase): +class TestRepoGeneration(unittest.TestCase): helper_path = Path('tests', 'test_helpers') addon_path = helper_path / 'addons' @@ -20,21 +20,6 @@ class test_make_repo(unittest.TestCase): with self.assertRaises(FileNotFoundError): make_repo.extract_blinfo(self.addon_path / test_file) - # def test_make_repo_valid(self): - # reference_repo = make_repo.make_repo(self.helper_path / 'addons', "test repo") - # repojson = Path.cwd() / 'repo.json' - # reference_repojson = self.helper_path / 'repo.json' - # - # try: - # with repojson.open('r') as repolist_f: - # with reference_repojson.open('r') as ref_repolist_f: - # repolist = json.loads(repolist_f.read()) - # ref_repolist = json.loads(ref_repolist_f.read()) - # self.assertEqual(repolist, ref_repolist) - # finally: - # # repojson.unlink() - # pass - def test_package_quantity(self): repo = make_repo.make_repo(self.addon_path, "name of the repo") acceptible_addons = [ @@ -49,12 +34,12 @@ class test_make_repo(unittest.TestCase): # addons which should contain bl_infos yes_blinfo = [ - f for f in test_make_repo.addon_path.iterdir() + f for f in TestRepoGeneration.addon_path.iterdir() if not f.match('*nonaddon*') and not f.match('*invalid_addon*') ] # addons which should throw BadAddon because they have no blinfo no_blinfo = [ - f for f in test_make_repo.addon_path.iterdir() + f for f in TestRepoGeneration.addon_path.iterdir() if f.match('*nonaddon*') ] @@ -81,6 +66,6 @@ def add_generated_tests(test_generator, params, destclass): test_func = test_generator(param) setattr(destclass, 'test_{}'.format(param), test_func) -add_generated_tests(generate_good_blinfo_test, yes_blinfo, test_make_repo) -add_generated_tests(generate_bad_blinfo_test, no_blinfo, test_make_repo) +add_generated_tests(generate_good_blinfo_test, yes_blinfo, TestRepoGeneration) +add_generated_tests(generate_bad_blinfo_test, no_blinfo, TestRepoGeneration)