chore: type formatting + refactor help text

This commit is contained in:
relikd
2023-03-08 00:28:58 +01:00
parent 4f565b6de1
commit 3b430740dc
6 changed files with 104 additions and 70 deletions

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
from typing import Union, Iterator, Optional
from typing import Union, Optional
from math import sqrt
from . import IcnsType, PackBytes, RawData
try:
@@ -16,6 +16,8 @@ class ArgbImage:
def from_mono(cls, data: bytes, iType: IcnsType.Media) -> 'ArgbImage':
''' Load monochrome 1-bit image with or without mask. '''
assert(iType.bits == 1)
assert(iType.size)
assert(iType.channels)
img = []
for byte in data:
for i in range(7, -1, -1):
@@ -31,9 +33,13 @@ class ArgbImage:
self.r, self.g, self.b = img, img, img
return self
def __init__(self, *, data: Optional[bytes] = None,
def __init__(
self,
*,
data: Optional[bytes] = None,
file: Optional[str] = None,
mask: Union[bytes, str, None] = None) -> None:
mask: Union[bytes, str, None] = None,
) -> None:
'''
Provide either a filename or raw binary data.
- mask : Optional, may be either binary data or filename
@@ -91,8 +97,9 @@ class ArgbImage:
self.g = uncompressed_data[(i + 1) * per_channel:(i + 2) * per_channel]
self.b = uncompressed_data[(i + 2) * per_channel:(i + 3) * per_channel]
def load_mask(self, *, file: Optional[str] = None,
data: Optional[bytes] = None) -> None:
def load_mask(
self, *, file: Optional[str] = None, data: Optional[bytes] = None,
) -> None:
''' Data must be uncompressed and same length as a single channel! '''
if file:
with open(file, 'rb') as fp:

View File

@@ -79,14 +79,18 @@ class IcnsFile:
x, y)
@staticmethod
def description(fname: str, *, verbose: bool = False, indent: int = 0) -> \
str:
def description(fname: str, *, verbose: bool = False, indent: int = 0) \
-> str:
return IcnsFile._description(
RawData.parse_icns_file(fname), verbose=verbose, indent=indent)
@staticmethod
def _description(enumerator: Iterable[Tuple[IcnsType.Media.KeyT, bytes]],
*, verbose: bool = False, indent: int = 0) -> str:
def _description(
enumerator: Iterable[Tuple[IcnsType.Media.KeyT, bytes]],
*,
verbose: bool = False,
indent: int = 0,
) -> str:
''' Expects an enumerator with (key, size, data) '''
txt = ''
offset = 8 # already with icns header
@@ -117,7 +121,7 @@ class IcnsFile:
except RawData.ParserError as e:
return ' ' * indent + str(e) + os.linesep
def __init__(self, file: str = None) -> None:
def __init__(self, file: Optional[str] = None) -> None:
''' Read .icns file and load bundled media files into memory. '''
self.media = {} # type: Dict[IcnsType.Media.KeyT, bytes]
self.infile = file
@@ -134,9 +138,14 @@ class IcnsFile:
def has_toc(self) -> bool:
return 'TOC ' in self.media.keys()
def add_media(self, key: Optional[IcnsType.Media.KeyT] = None, *,
file: Optional[str] = None, data: Optional[bytes] = None,
force: bool = False) -> None:
def add_media(
self,
key: Optional[IcnsType.Media.KeyT] = None,
*,
file: Optional[str] = None,
data: Optional[bytes] = None,
force: bool = False,
) -> None:
'''
If you provide both, data and file, data takes precedence.
However, the filename is still used for type-guessing.
@@ -182,11 +191,16 @@ class IcnsFile:
for key in order:
RawData.icns_header_write_data(fp, key, self.media[key])
def export(self, outdir: Optional[str] = None, *,
allowed_ext: str = '*', key_suffix: bool = False,
convert_png: bool = False, decompress: bool = False,
recursive: bool = False) -> Dict[IcnsType.Media.KeyT,
Union[str, Dict]]:
def export(
self,
outdir: Optional[str] = None,
*,
allowed_ext: str = '*',
key_suffix: bool = False,
convert_png: bool = False,
decompress: bool = False,
recursive: bool = False,
) -> Dict[IcnsType.Media.KeyT, Union[str, Dict]]:
'''
Write all bundled media files to output directory.
@@ -266,9 +280,14 @@ class IcnsFile:
return order
def _export_single(self, outdir: str, key: IcnsType.Media.KeyT,
key_suffix: bool, decompress: bool,
allowed: List[str]) -> Optional[str]:
def _export_single(
self,
outdir: str,
key: IcnsType.Media.KeyT,
key_suffix: bool,
decompress: bool,
allowed: List[str],
) -> Optional[str]:
''' You must ensure that keys exist in self.media '''
data = self.media[key]
ext = RawData.determine_file_ext(data)
@@ -294,9 +313,13 @@ class IcnsFile:
fp.write(data)
return fname
def _export_to_png(self, outdir: str, img_key: IcnsType.Media.KeyT,
def _export_to_png(
self,
outdir: str,
img_key: IcnsType.Media.KeyT,
mask_key: Optional[IcnsType.Media.KeyT],
key_suffix: bool) -> Optional[str]:
key_suffix: bool,
) -> Optional[str]:
''' You must ensure key and mask_key exists! '''
data = self.media[img_key]
if RawData.determine_file_ext(data) not in ['argb', None]:

View File

@@ -4,7 +4,7 @@ Namespace for the ICNS format.
@see https://en.wikipedia.org/wiki/Apple_Icon_Image_format
'''
import os # path
from typing import Union, Optional, Tuple, Iterator, List, Iterable, Dict
from typing import Union, Optional, Tuple, Iterator, List, Iterable
from . import PackBytes, RawData
@@ -17,10 +17,17 @@ class Media:
__slots__ = ['key', 'types', 'size', 'channels', 'bits', 'availability',
'desc', 'compressable', 'retina', 'maxsize', 'ext_certain']
def __init__(self, key: KeyT, types: list,
size: Optional[Union[int, Tuple[int, int]]] = None,
*, ch: Optional[int] = None, bits: Optional[int] = None,
os: Optional[float] = None, desc: str = '') -> None:
def __init__(
self,
key: KeyT,
types: List[str],
size: Union[int, Tuple[int, int], None] = None,
*,
ch: Optional[int] = None,
bits: Optional[int] = None,
os: Optional[float] = None,
desc: str = '',
) -> None:
self.key = key
self.types = types
self.size = (size, size) if isinstance(size, int) else size
@@ -54,8 +61,8 @@ class Media:
return self.desc # guaranteed to be icon, mask, or iconmask
return self.types[-1]
def decompress(self, data: bytes, ext: Optional[str] = '-?-') -> Optional[
List[int]]:
def decompress(self, data: bytes, ext: Optional[str] = '-?-') \
-> Optional[List[int]]:
''' Returns None if media is not decompressable. '''
if self.compressable:
if ext == '-?-':
@@ -165,11 +172,11 @@ _TYPES = {x.key: x for x in (
Media('icnV', ['bin'], desc='4-byte Icon Composer.app bundle version'),
Media('name', ['bin'], desc='Unknown'),
Media('info', ['plist'], desc='Info binary plist'),
)} # type: Dict[Media.KeyT, Media]
)}
def enum_img_mask_pairs(available_keys: Iterable[Media.KeyT]) -> Iterator[
Tuple[Optional[str], Optional[str]]]:
def enum_img_mask_pairs(available_keys: Iterable[Media.KeyT]) \
-> Iterator[Tuple[Optional[str], Optional[str]]]:
for mask_k, *imgs in [ # list probably never changes, ARGB FTW
('s8mk', 'is32', 'ics8', 'ics4', 'icp4'),
('l8mk', 'il32', 'icl8', 'icl4', 'icp5'),
@@ -186,8 +193,8 @@ def enum_img_mask_pairs(available_keys: Iterable[Media.KeyT]) -> Iterator[
yield None, mk
def enum_png_convertable(available_keys: Iterable[Media.KeyT]) -> Iterator[
Tuple[Media.KeyT, Optional[Media.KeyT]]]:
def enum_png_convertable(available_keys: Iterable[Media.KeyT]) \
-> Iterator[Tuple[Media.KeyT, Optional[Media.KeyT]]]:
''' Yield (image-key, mask-key or None) '''
for img in _TYPES.values():
if img.key not in available_keys:
@@ -219,8 +226,8 @@ def key_from_readable(key: str) -> Media.KeyT:
'selected': 'slct',
'template': 'sbtp',
'toc': 'TOC ',
} # type: Dict[str, Media.KeyT]
return key_mapping.get(key.lower(), key)
}
return key_mapping.get(key.lower(), key) # type: ignore[return-value]
def match_maxsize(total: int, typ: str) -> Media:

View File

@@ -7,7 +7,7 @@ def pack(data: List[int]) -> bytes:
buf = [] # type: List[int]
i = 0
def flush_buf():
def flush_buf() -> None:
# write out non-repeating bytes
if len(buf) > 0:
ret.append(len(buf) - 1)

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
import struct # pack, unpack
from typing import Union, Optional, Tuple, Iterator, BinaryIO
from typing import Optional, Tuple, Iterator, BinaryIO
from . import IcnsType, PackBytes
@@ -85,8 +85,9 @@ def icns_header_read(data: bytes) -> Tuple[IcnsType.Media.KeyT, int]:
return data[:4], length # Fallback to bytes-string key
def icns_header_write_data(fp: BinaryIO, key: IcnsType.Media.KeyT,
data: bytes) -> None:
def icns_header_write_data(
fp: BinaryIO, key: IcnsType.Media.KeyT, data: bytes,
) -> None:
''' Calculates length from data. '''
fp.write(key.encode('utf8') if isinstance(key, str) else key)
fp.write(struct.pack('>I', len(data) + 8))

View File

@@ -4,9 +4,8 @@ Export existing icns files or compose new ones.
'''
import os # path, makedirs
import sys # path, stderr
from typing import Iterator, Optional, Callable
from argparse import ArgumentParser, ArgumentTypeError, RawTextHelpFormatter
from argparse import Namespace as ArgParams
from typing import Iterator, Optional, Callable, List
from argparse import ArgumentParser, ArgumentTypeError, Namespace as ArgParams
if __name__ == '__main__':
sys.path[0] = os.path.dirname(sys.path[0])
from icnsutil import __version__, IcnsFile, IcnsType, ArgbImage
@@ -53,16 +52,15 @@ def cli_update(args: ArgParams) -> None:
has_changes |= icns.remove_media(IcnsType.key_from_readable(x))
# add media
for key_val in args.set or []:
def fail():
raise ArgumentTypeError(
'Expected arg format KEY=FILE - got "{}"'.format(key_val))
if key_val.lower() == 'toc':
key_val = 'toc=1'
if '=' not in key_val:
fail()
key, val = key_val.split('=')
raise ArgumentTypeError(
'Expected arg format KEY=FILE - got "{}"'.format(key_val))
key, val = key_val.split('=', 1)
if not val:
fail()
raise ArgumentTypeError(
'Expected arg format KEY=FILE - got "{}"'.format(key_val))
has_changes = True
if key.lower() == 'toc':
@@ -130,7 +128,7 @@ def cli_convert(args: ArgParams) -> None:
exit(1)
def enum_with_stdin(file_arg: list) -> Iterator[str]:
def enum_with_stdin(file_arg: List[str]) -> Iterator[str]:
for x in file_arg:
if x == '-':
for line in sys.stdin.readlines():
@@ -155,18 +153,18 @@ def main() -> None:
return path
# Args Parser
parser = ArgumentParser(description=__doc__,
formatter_class=RawTextHelpFormatter)
parser = ArgumentParser(description=__doc__)
parser.set_defaults(func=lambda _: parser.print_help(sys.stdout))
parser.add_argument(
'-v', '--version', action='version', version='icnsutil ' + __version__)
sub_parser = parser.add_subparsers(metavar='command')
# helper method
def add_command(name: str, alias: str, fn: Callable[[ArgParams], None]):
def add_command(
name: str, alias: str, fn: Callable[[ArgParams], None]
) -> ArgumentParser:
desc = fn.__doc__ or ''
cmd = sub_parser.add_parser(name, aliases=[alias],
formatter_class=RawTextHelpFormatter,
help=desc, description=desc.strip())
cmd.set_defaults(func=fn)
return cmd
@@ -189,22 +187,20 @@ def main() -> None:
# Compose
cmd = add_command('compose', 'c', cli_compose)
cmd.add_argument('-f', '--force', action='store_true',
help='force overwrite output file')
cmd.add_argument('--toc', action='store_true',
help='write table of contents to file')
help='Force overwrite output file')
cmd.add_argument('--toc', action='store_true', help='''
Write table of contents to file.
TOC is optional and uses just a few bytes (8b per media entry).''')
cmd.add_argument('target', type=str, metavar='destination',
help='Output file for newly created icns file.')
cmd.add_argument('source', type=PathExist('f', stdin=True), nargs='+',
metavar='src',
help='One or more media files: png, argb, rgb, jp2, icns')
cmd.epilog = '''
Notes:
- TOC is optional but only a few bytes long (8b per media entry).
- Icon dimensions are read directly from file.
- Filename suffix "@2x.png" or "@2x.jp2" sets the retina flag.
- Use one of these suffixes to automatically assign icns files:
template, selected, dark
'''
metavar='src', help='''
One or more media files: png, argb, rgb, jp2, icns.
--
Icon dimensions are read directly from file.
Filename suffixes "@2x.png" and "@2x.jp2" will set the retina flag.
If the suffix ends on one of these (template, selected, dark),
the file is automatically assigned to an icns file field.''')
# Update
cmd = add_command('update', 'u', cli_update)