update BAT module to 1.15 version

This commit is contained in:
pullusb
2023-01-10 11:41:55 +01:00
parent 85037c027a
commit fc32ebd453
34 changed files with 1878 additions and 1042 deletions
+69 -31
View File
@@ -24,67 +24,99 @@ import datetime
import logging
import time
from . import blocks, common, pack, list_deps
from . import blocks, common, pack, list_deps, version
def cli_main():
from blender_asset_tracer import __version__
parser = argparse.ArgumentParser(description='BAT: Blender Asset Tracer v%s' % __version__)
common.add_flag(parser, 'profile', help='Run the profiler, write to bam.prof')
parser = argparse.ArgumentParser(
description="BAT: Blender Asset Tracer v%s" % __version__
)
common.add_flag(parser, "profile", help="Run the profiler, write to bam.prof")
# func is set by subparsers to indicate which function to run.
parser.set_defaults(func=None,
loglevel=logging.WARNING)
parser.set_defaults(func=None, loglevel=logging.WARNING)
loggroup = parser.add_mutually_exclusive_group()
loggroup.add_argument('-v', '--verbose', dest='loglevel',
action='store_const', const=logging.INFO,
help='Log INFO level and higher')
loggroup.add_argument('-d', '--debug', dest='loglevel',
action='store_const', const=logging.DEBUG,
help='Log everything')
loggroup.add_argument('-q', '--quiet', dest='loglevel',
action='store_const', const=logging.ERROR,
help='Log at ERROR level and higher')
loggroup.add_argument(
"-v",
"--verbose",
dest="loglevel",
action="store_const",
const=logging.INFO,
help="Log INFO level and higher",
)
loggroup.add_argument(
"-d",
"--debug",
dest="loglevel",
action="store_const",
const=logging.DEBUG,
help="Log everything",
)
loggroup.add_argument(
"-q",
"--quiet",
dest="loglevel",
action="store_const",
const=logging.ERROR,
help="Log at ERROR level and higher",
)
parser.add_argument(
"-S",
"--strict-pointers",
default=False,
action="store_true",
help="Crash on pointers to missing data; otherwise the missing data is just ignored.",
)
subparsers = parser.add_subparsers(
help='Choose a subcommand to actually make BAT do something. '
'Global options go before the subcommand, '
'whereas subcommand-specific options go after it. '
'Use --help after the subcommand to get more info.')
help="Choose a subcommand to actually make BAT do something. "
"Global options go before the subcommand, "
"whereas subcommand-specific options go after it. "
"Use --help after the subcommand to get more info."
)
blocks.add_parser(subparsers)
pack.add_parser(subparsers)
list_deps.add_parser(subparsers)
version.add_parser(subparsers)
args = parser.parse_args()
config_logging(args)
from blender_asset_tracer import __version__
log = logging.getLogger(__name__)
# Make sure the things we log in our local logger are visible
if args.profile and args.loglevel > logging.INFO:
log.setLevel(logging.INFO)
log.debug('Running BAT version %s', __version__)
log.debug("Running BAT version %s", __version__)
if not args.func:
parser.error('No subcommand was given')
parser.error("No subcommand was given")
set_strict_pointer_mode(args.strict_pointers)
start_time = time.time()
if args.profile:
import cProfile
prof_fname = 'bam.prof'
log.info('Running profiler')
cProfile.runctx('args.func(args)',
globals=globals(),
locals=locals(),
filename=prof_fname)
log.info('Profiler exported data to %s', prof_fname)
log.info('Run "pyprof2calltree -i %r -k" to convert and open in KCacheGrind', prof_fname)
prof_fname = "bam.prof"
log.info("Running profiler")
cProfile.runctx(
"args.func(args)", globals=globals(), locals=locals(), filename=prof_fname
)
log.info("Profiler exported data to %s", prof_fname)
log.info(
'Run "pyprof2calltree -i %r -k" to convert and open in KCacheGrind',
prof_fname,
)
else:
retval = args.func(args)
duration = datetime.timedelta(seconds=time.time() - start_time)
log.info('Command took %s to complete', duration)
log.info("Command took %s to complete", duration)
def config_logging(args):
@@ -92,8 +124,14 @@ def config_logging(args):
logging.basicConfig(
level=logging.WARNING,
format='%(asctime)-15s %(levelname)8s %(name)-40s %(message)s',
format="%(asctime)-15s %(levelname)8s %(name)-40s %(message)s",
)
# Only set the log level on our own logger. Otherwise
# debug logging will be completely swamped.
logging.getLogger('blender_asset_tracer').setLevel(args.loglevel)
logging.getLogger("blender_asset_tracer").setLevel(args.loglevel)
def set_strict_pointer_mode(strict_pointers: bool) -> None:
from blender_asset_tracer import blendfile
blendfile.set_strict_pointer_mode(strict_pointers)
+56 -37
View File
@@ -34,19 +34,29 @@ class BlockTypeInfo:
self.num_blocks = 0
self.sizes = []
self.blocks = []
self.name = 'unset'
self.name = "unset"
def add_parser(subparsers):
"""Add argparser for this subcommand."""
parser = subparsers.add_parser('blocks', help=__doc__)
parser = subparsers.add_parser("blocks", help=__doc__)
parser.set_defaults(func=cli_blocks)
parser.add_argument('blendfile', type=pathlib.Path)
parser.add_argument('-d', '--dump', default=False, action='store_true',
help='Hex-dump the biggest block')
parser.add_argument('-l', '--limit', default=10, type=int,
help='Limit the number of DNA types shown, default is 10')
parser.add_argument("blendfile", type=pathlib.Path)
parser.add_argument(
"-d",
"--dump",
default=False,
action="store_true",
help="Hex-dump the biggest block",
)
parser.add_argument(
"-l",
"--limit",
default=10,
type=int,
help="Limit the number of DNA types shown, default is 10",
)
def by_total_bytes(info: BlockTypeInfo) -> int:
@@ -54,23 +64,23 @@ def by_total_bytes(info: BlockTypeInfo) -> int:
def block_key(block: blendfile.BlendFileBlock) -> str:
return '%s-%s' % (block.dna_type_name, block.code.decode())
return "%s-%s" % (block.dna_type_name, block.code.decode())
def cli_blocks(args):
bpath = args.blendfile
if not bpath.exists():
log.fatal('File %s does not exist', args.blendfile)
log.fatal("File %s does not exist", args.blendfile)
return 3
per_blocktype = collections.defaultdict(BlockTypeInfo)
print('Opening %s' % bpath)
print("Opening %s" % bpath)
bfile = blendfile.BlendFile(bpath)
print('Inspecting %s' % bpath)
print("Inspecting %s" % bpath)
for block in bfile.blocks:
if block.code == b'DNA1':
if block.code == b"DNA1":
continue
index_as = block_key(block)
@@ -81,49 +91,58 @@ def cli_blocks(args):
info.sizes.append(block.size)
info.blocks.append(block)
fmt = '%-35s %10s %10s %10s %10s'
print(fmt % ('Block type', 'Total Size', 'Num blocks', 'Avg Size', 'Median'))
print(fmt % (35 * '-', 10 * '-', 10 * '-', 10 * '-', 10 * '-'))
fmt = "%-35s %10s %10s %10s %10s"
print(fmt % ("Block type", "Total Size", "Num blocks", "Avg Size", "Median"))
print(fmt % (35 * "-", 10 * "-", 10 * "-", 10 * "-", 10 * "-"))
infos = sorted(per_blocktype.values(), key=by_total_bytes, reverse=True)
for info in infos[:args.limit]:
for info in infos[: args.limit]:
median_size = sorted(info.sizes)[len(info.sizes) // 2]
print(fmt % (info.name,
common.humanize_bytes(info.total_bytes),
info.num_blocks,
common.humanize_bytes(info.total_bytes // info.num_blocks),
common.humanize_bytes(median_size)
))
print(
fmt
% (
info.name,
common.humanize_bytes(info.total_bytes),
info.num_blocks,
common.humanize_bytes(info.total_bytes // info.num_blocks),
common.humanize_bytes(median_size),
)
)
print(70 * '-')
print(70 * "-")
# From the blocks of the most space-using category, the biggest block.
biggest_block = sorted(infos[0].blocks,
key=lambda blck: blck.size,
reverse=True)[0]
print('Biggest %s block is %s at address %s' % (
block_key(biggest_block),
common.humanize_bytes(biggest_block.size),
biggest_block.addr_old,
))
biggest_block = sorted(infos[0].blocks, key=lambda blck: blck.size, reverse=True)[0]
print(
"Biggest %s block is %s at address %s"
% (
block_key(biggest_block),
common.humanize_bytes(biggest_block.size),
biggest_block.addr_old,
)
)
print('Finding what points there')
print("Finding what points there")
addr_to_find = biggest_block.addr_old
found_pointer = False
for block in bfile.blocks:
for prop_path, prop_value in block.items_recursive():
if not isinstance(prop_value, int) or prop_value != addr_to_find:
continue
print(' ', block, prop_path)
print(" ", block, prop_path)
found_pointer = True
if not found_pointer:
print('Nothing points there')
print("Nothing points there")
if args.dump:
print('Hexdump:')
print("Hexdump:")
bfile.fileobj.seek(biggest_block.file_offset)
data = bfile.fileobj.read(biggest_block.size)
line_len_bytes = 32
import codecs
for offset in range(0, len(data), line_len_bytes):
line = codecs.encode(data[offset:offset + line_len_bytes], 'hex').decode()
print('%6d -' % offset, ' '.join(line[i:i + 2] for i in range(0, len(line), 2)))
line = codecs.encode(data[offset : offset + line_len_bytes], "hex").decode()
print(
"%6d -" % offset,
" ".join(line[i : i + 2] for i in range(0, len(line), 2)),
)
+18 -15
View File
@@ -29,11 +29,13 @@ def add_flag(argparser, flag_name: str, **kwargs):
The flag defaults to False, and when present on the CLI stores True.
"""
argparser.add_argument('-%s' % flag_name[0],
'--%s' % flag_name,
default=False,
action='store_true',
**kwargs)
argparser.add_argument(
"-%s" % flag_name[0],
"--%s" % flag_name,
default=False,
action="store_true",
**kwargs
)
def shorten(cwd: pathlib.Path, somepath: pathlib.Path) -> pathlib.Path:
@@ -44,7 +46,7 @@ def shorten(cwd: pathlib.Path, somepath: pathlib.Path) -> pathlib.Path:
return somepath
def humanize_bytes(size_in_bytes: int, precision: typing.Optional[int]=None):
def humanize_bytes(size_in_bytes: int, precision: typing.Optional[int] = None):
"""Return a humanized string representation of a number of bytes.
Source: http://code.activestate.com/recipes/577081-humanized-representation-of-a-number-of-bytes
@@ -78,22 +80,23 @@ def humanize_bytes(size_in_bytes: int, precision: typing.Optional[int]=None):
precision = size_in_bytes >= 1024
abbrevs = (
(1 << 50, 'PB'),
(1 << 40, 'TB'),
(1 << 30, 'GB'),
(1 << 20, 'MB'),
(1 << 10, 'kB'),
(1, 'B')
(1 << 50, "PB"),
(1 << 40, "TB"),
(1 << 30, "GB"),
(1 << 20, "MB"),
(1 << 10, "kB"),
(1, "B"),
)
for factor, suffix in abbrevs:
if size_in_bytes >= factor:
break
else:
factor = 1
suffix = 'B'
return '%.*f %s' % (precision, size_in_bytes / factor, suffix)
suffix = "B"
return "%.*f %s" % (precision, size_in_bytes / factor, suffix)
if __name__ == '__main__':
if __name__ == "__main__":
import doctest
doctest.testmod()
+24 -19
View File
@@ -36,27 +36,32 @@ log = logging.getLogger(__name__)
def add_parser(subparsers):
"""Add argparser for this subcommand."""
parser = subparsers.add_parser('list', help=__doc__)
parser = subparsers.add_parser("list", help=__doc__)
parser.set_defaults(func=cli_list)
parser.add_argument('blendfile', type=pathlib.Path)
common.add_flag(parser, 'json', help='Output as JSON instead of human-readable text')
common.add_flag(parser, 'sha256',
help='Include SHA256sums in the output. Note that those may differ from the '
'SHA256sums in a BAT-pack when paths are rewritten.')
common.add_flag(parser, 'timing', help='Include timing information in the output')
parser.add_argument("blendfile", type=pathlib.Path)
common.add_flag(
parser, "json", help="Output as JSON instead of human-readable text"
)
common.add_flag(
parser,
"sha256",
help="Include SHA256sums in the output. Note that those may differ from the "
"SHA256sums in a BAT-pack when paths are rewritten.",
)
common.add_flag(parser, "timing", help="Include timing information in the output")
def cli_list(args):
bpath = args.blendfile
if not bpath.exists():
log.fatal('File %s does not exist', args.blendfile)
log.fatal("File %s does not exist", args.blendfile)
return 3
if args.json:
if args.sha256:
log.fatal('--sha256 can currently not be used in combination with --json')
log.fatal("--sha256 can currently not be used in combination with --json")
if args.timing:
log.fatal('--timing can currently not be used in combination with --json')
log.fatal("--timing can currently not be used in combination with --json")
report_json(bpath)
else:
report_text(bpath, include_sha256=args.sha256, show_timing=args.timing)
@@ -66,13 +71,13 @@ def calc_sha_sum(filepath: pathlib.Path) -> typing.Tuple[str, float]:
start = time.time()
if filepath.is_dir():
for subfile in filepath.rglob('*'):
for subfile in filepath.rglob("*"):
calc_sha_sum(subfile)
duration = time.time() - start
return '-multiple-', duration
return "-multiple-", duration
summer = hashlib.sha256()
with filepath.open('rb') as infile:
with filepath.open("rb") as infile:
while True:
block = infile.read(32 * 1024)
if not block:
@@ -108,24 +113,24 @@ def report_text(bpath, *, include_sha256: bool, show_timing: bool):
for assetpath in usage.files():
assetpath = bpathlib.make_absolute(assetpath)
if assetpath in reported_assets:
log.debug('Already reported %s', assetpath)
log.debug("Already reported %s", assetpath)
continue
if include_sha256:
shasum, time_spent = calc_sha_sum(assetpath)
time_spent_on_shasums += time_spent
print(' ', shorten(assetpath), shasum)
print(" ", shorten(assetpath), shasum)
else:
print(' ', shorten(assetpath))
print(" ", shorten(assetpath))
reported_assets.add(assetpath)
if show_timing:
duration = time.time() - start_time
print('Spent %.2f seconds on producing this listing' % duration)
print("Spent %.2f seconds on producing this listing" % duration)
if include_sha256:
print('Spent %.2f seconds on calculating SHA sums' % time_spent_on_shasums)
print("Spent %.2f seconds on calculating SHA sums" % time_spent_on_shasums)
percentage = time_spent_on_shasums / duration * 100
print(' (that is %d%% of the total time' % percentage)
print(" (that is %d%% of the total time" % percentage)
class JSONSerialiser(json.JSONEncoder):
+121 -67
View File
@@ -32,36 +32,62 @@ log = logging.getLogger(__name__)
def add_parser(subparsers):
"""Add argparser for this subcommand."""
parser = subparsers.add_parser('pack', help=__doc__)
parser = subparsers.add_parser("pack", help=__doc__)
parser.set_defaults(func=cli_pack)
parser.add_argument('blendfile', type=pathlib.Path,
help='The Blend file to pack.')
parser.add_argument('target', type=str,
help="The target can be a directory, a ZIP file (does not have to exist "
"yet, just use 'something.zip' as target), "
"or a URL of S3 storage (s3://endpoint/path) "
"or Shaman storage (shaman://endpoint/#checkoutID).")
parser.add_argument("blendfile", type=pathlib.Path, help="The Blend file to pack.")
parser.add_argument(
"target",
type=str,
help="The target can be a directory, a ZIP file (does not have to exist "
"yet, just use 'something.zip' as target), "
"or a URL of S3 storage (s3://endpoint/path) "
"or Shaman storage (shaman://endpoint/#checkoutID).",
)
parser.add_argument('-p', '--project', type=pathlib.Path,
help='Root directory of your project. Paths to below this directory are '
'kept in the BAT Pack as well, whereas references to assets from '
'outside this directory will have to be rewitten. The blend file MUST '
'be inside the project directory. If this option is ommitted, the '
'directory containing the blend file is taken as the project '
'directoy.')
parser.add_argument('-n', '--noop', default=False, action='store_true',
help="Don't copy files, just show what would be done.")
parser.add_argument('-e', '--exclude', nargs='*', default='',
help="Space-separated list of glob patterns (like '*.abc *.vbo') to "
"exclude.")
parser.add_argument('-c', '--compress', default=False, action='store_true',
help='Compress blend files while copying. This option is only valid when '
'packing into a directory (contrary to ZIP file or S3 upload). '
'Note that files will NOT be compressed when the destination file '
'already exists and has the same size as the original file.')
parser.add_argument('-r', '--relative-only', default=False, action='store_true',
help='Only pack assets that are referred to with a relative path (e.g. '
'starting with `//`.')
parser.add_argument(
"-p",
"--project",
type=pathlib.Path,
help="Root directory of your project. Paths to below this directory are "
"kept in the BAT Pack as well, whereas references to assets from "
"outside this directory will have to be rewitten. The blend file MUST "
"be inside the project directory. If this option is ommitted, the "
"directory containing the blend file is taken as the project "
"directoy.",
)
parser.add_argument(
"-n",
"--noop",
default=False,
action="store_true",
help="Don't copy files, just show what would be done.",
)
parser.add_argument(
"-e",
"--exclude",
nargs="*",
default="",
help="Space-separated list of glob patterns (like '*.abc *.vbo') to "
"exclude.",
)
parser.add_argument(
"-c",
"--compress",
default=False,
action="store_true",
help="Compress blend files while copying. This option is only valid when "
"packing into a directory (contrary to ZIP file or S3 upload). "
"Note that files will NOT be compressed when the destination file "
"already exists and has the same size as the original file.",
)
parser.add_argument(
"-r",
"--relative-only",
default=False,
action="store_true",
help="Only pack assets that are referred to with a relative path (e.g. "
"starting with `//`.",
)
def cli_pack(args):
@@ -72,55 +98,70 @@ def cli_pack(args):
try:
packer.execute()
except blender_asset_tracer.pack.transfer.FileTransferError as ex:
log.error("%d files couldn't be copied, starting with %s",
len(ex.files_remaining), ex.files_remaining[0])
log.error(
"%d files couldn't be copied, starting with %s",
len(ex.files_remaining),
ex.files_remaining[0],
)
raise SystemExit(1)
def create_packer(args, bpath: pathlib.Path, ppath: pathlib.Path, target: str) -> pack.Packer:
if target.startswith('s3:/'):
def create_packer(
args, bpath: pathlib.Path, ppath: pathlib.Path, target: str
) -> pack.Packer:
if target.startswith("s3:/"):
if args.noop:
raise ValueError('S3 uploader does not support no-op.')
raise ValueError("S3 uploader does not support no-op.")
if args.compress:
raise ValueError('S3 uploader does not support on-the-fly compression')
raise ValueError("S3 uploader does not support on-the-fly compression")
if args.relative_only:
raise ValueError('S3 uploader does not support the --relative-only option')
raise ValueError("S3 uploader does not support the --relative-only option")
packer = create_s3packer(bpath, ppath, pathlib.PurePosixPath(target))
elif target.startswith('shaman+http:/') or target.startswith('shaman+https:/') \
or target.startswith('shaman:/'):
elif (
target.startswith("shaman+http:/")
or target.startswith("shaman+https:/")
or target.startswith("shaman:/")
):
if args.noop:
raise ValueError('Shaman uploader does not support no-op.')
raise ValueError("Shaman uploader does not support no-op.")
if args.compress:
raise ValueError('Shaman uploader does not support on-the-fly compression')
raise ValueError("Shaman uploader does not support on-the-fly compression")
if args.relative_only:
raise ValueError('Shaman uploader does not support the --relative-only option')
raise ValueError(
"Shaman uploader does not support the --relative-only option"
)
packer = create_shamanpacker(bpath, ppath, target)
elif target.lower().endswith('.zip'):
elif target.lower().endswith(".zip"):
from blender_asset_tracer.pack import zipped
if args.compress:
raise ValueError('ZIP packer does not support on-the-fly compression')
raise ValueError("ZIP packer does not support on-the-fly compression")
packer = zipped.ZipPacker(bpath, ppath, target, noop=args.noop,
relative_only=args.relative_only)
packer = zipped.ZipPacker(
bpath, ppath, target, noop=args.noop, relative_only=args.relative_only
)
else:
packer = pack.Packer(bpath, ppath, target, noop=args.noop,
compress=args.compress, relative_only=args.relative_only)
packer = pack.Packer(
bpath,
ppath,
target,
noop=args.noop,
compress=args.compress,
relative_only=args.relative_only,
)
if args.exclude:
# args.exclude is a list, due to nargs='*', so we have to split and flatten.
globs = [glob
for globs in args.exclude
for glob in globs.split()]
log.info('Excluding: %s', ', '.join(repr(g) for g in globs))
globs = [glob for globs in args.exclude for glob in globs.split()]
log.info("Excluding: %s", ", ".join(repr(g) for g in globs))
packer.exclude(*globs)
return packer
@@ -130,14 +171,16 @@ def create_s3packer(bpath, ppath, tpath) -> pack.Packer:
# Split the target path into 's3:/', hostname, and actual target path
parts = tpath.parts
endpoint = 'https://%s/' % parts[1]
endpoint = "https://%s/" % parts[1]
tpath = pathlib.Path(*tpath.parts[2:])
log.info('Uploading to S3-compatible storage %s at %s', endpoint, tpath)
log.info("Uploading to S3-compatible storage %s at %s", endpoint, tpath)
return s3.S3Packer(bpath, ppath, tpath, endpoint=endpoint)
def create_shamanpacker(bpath: pathlib.Path, ppath: pathlib.Path, tpath: str) -> pack.Packer:
def create_shamanpacker(
bpath: pathlib.Path, ppath: pathlib.Path, tpath: str
) -> pack.Packer:
"""Creates a package for sending files to a Shaman server.
URLs should have the form:
@@ -149,11 +192,15 @@ def create_shamanpacker(bpath: pathlib.Path, ppath: pathlib.Path, tpath: str) ->
endpoint, checkout_id = shaman.parse_endpoint(tpath)
if not checkout_id:
log.warning('No checkout ID given on the URL. Going to send BAT pack to Shaman, '
'but NOT creating a checkout')
log.warning(
"No checkout ID given on the URL. Going to send BAT pack to Shaman, "
"but NOT creating a checkout"
)
log.info('Uploading to Shaman server %s with job %s', endpoint, checkout_id)
return shaman.ShamanPacker(bpath, ppath, '/', endpoint=endpoint, checkout_id=checkout_id)
log.info("Uploading to Shaman server %s with job %s", endpoint, checkout_id)
return shaman.ShamanPacker(
bpath, ppath, "/", endpoint=endpoint, checkout_id=checkout_id
)
def paths_from_cli(args) -> typing.Tuple[pathlib.Path, pathlib.Path, str]:
@@ -163,10 +210,10 @@ def paths_from_cli(args) -> typing.Tuple[pathlib.Path, pathlib.Path, str]:
"""
bpath = args.blendfile
if not bpath.exists():
log.critical('File %s does not exist', bpath)
log.critical("File %s does not exist", bpath)
sys.exit(3)
if bpath.is_dir():
log.critical('%s is a directory, should be a blend file')
log.critical("%s is a directory, should be a blend file")
sys.exit(3)
bpath = bpathlib.make_absolute(bpath)
@@ -174,27 +221,34 @@ def paths_from_cli(args) -> typing.Tuple[pathlib.Path, pathlib.Path, str]:
if args.project is None:
ppath = bpathlib.make_absolute(bpath).parent
log.warning('No project path given, using %s', ppath)
log.warning("No project path given, using %s", ppath)
else:
ppath = bpathlib.make_absolute(args.project)
if not ppath.exists():
log.critical('Project directory %s does not exist', ppath)
log.critical("Project directory %s does not exist", ppath)
sys.exit(5)
if not ppath.is_dir():
log.warning('Project path %s is not a directory; using the parent %s', ppath, ppath.parent)
log.warning(
"Project path %s is not a directory; using the parent %s",
ppath,
ppath.parent,
)
ppath = ppath.parent
try:
bpath.relative_to(ppath)
except ValueError:
log.critical('Project directory %s does not contain blend file %s',
args.project, bpath.absolute())
log.critical(
"Project directory %s does not contain blend file %s",
args.project,
bpath.absolute(),
)
sys.exit(5)
log.info('Blend file to pack: %s', bpath)
log.info('Project path: %s', ppath)
log.info('Pack will be created in: %s', tpath)
log.info("Blend file to pack: %s", bpath)
log.info("Project path: %s", ppath)
log.info("Pack will be created in: %s", tpath)
return bpath, ppath, tpath
+32
View File
@@ -0,0 +1,32 @@
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
#
# (c) 2021, Blender Foundation - Sybren A. Stüvel
"""Print version of BAT and exit."""
from blender_asset_tracer import __version__
def add_parser(subparsers):
"""Add argparser for this subcommand."""
parser = subparsers.add_parser("version", help=__doc__)
parser.set_defaults(func=cli_version)
def cli_version(_):
print(__version__)