Template for command line application
from __future__ import annotations
import logging
import sys
from pathlib import Path
from argparse import Namespace , ArgumentParser
class UserError (Exception ):
pass
def parse_args () -> Namespace :
parser = ArgumentParser ()
parser .add_argument ('files' , type = Path , nargs = '+' )
### For subcommands
subparsers = parser .add_subparsers (dest = 'command' , required = True )
foo_parser = subparsers .add_parser ('foo' )
foo_parser .add_argument ('count' , type = int )
###
return parser .parse_args ()
### For subcommands
def foo_command (count : int ) -> None :
print (count )
###
def main (command : str , paths : list [Path ], ** kwargs ) -> None :
print (paths )
### For subcommands
commands = {
'foo' : foo_command }
commands [command ](** kwargs )
###
def entry_point () -> None :
logging .basicConfig (level = logging .INFO , format = '%(message)s' )
try :
main (** vars (parse_args ()))
except UserError as e :
logging .error (f'error: { e } ' )
sys .exit (1 )
except KeyboardInterrupt :
logging .error ('Operation interrupted.' )
sys .exit (130 )
Convert a file size in bytes into a human-readable string
def format_size (size ):
if size < 1000 :
return '{} bytes' .format (size )
for unit in 'KMGTPEZY' :
size = size / 1000
if size < 10 :
return '{:.2f} {}B' .format (size , unit )
elif size < 100 :
return '{:.1f} {}B' .format (size , unit )
elif size < 1000 or unit == 'Y' :
return '{:.0f} {}B' .format (size , unit )
print (format_size (9 ))
print (format_size (89 ))
print (format_size (789 ))
print (format_size (6789 ))
print (format_size (56789 ))
print (format_size (456789 ))
print (format_size (3456789 ))
print (format_size (23456789 ))
print (format_size (123456789 ))
print (format_size (1234567890 ))
print (format_size (12345678900 ))
print (format_size (123456789000 ))
print (format_size (2 ** 100 ))
# 9 bytes
# 89 bytes
# 789 bytes
# 6.79 KB
# 56.8 KB
# 457 KB
# 3.46 MB
# 23.5 MB
# 123 MB
# 1.23 GB
# 12.3 GB
# 123 GB
# 1267651 YB
Implement __eq__()
and __hash__()
by providing a single _hashable_key()
method
class Hashable :
def __eq__ (self , other ):
return type (self ) is type (other ) and self ._hashable_key () == other ._hashable_key ()
def __hash__ (self ):
return hash (self ._hashable_key ())
def _hashable_key (self ):
raise NotImplementedError ()
Implement __lt__()
and co. by providing a single _hashable_key()
method
@total_ordering
class Ordered (Hashable ):
def __lt__ (self , other ):
if type (self ) == type (other ):
return self ._hashable_key () < other ._hashable_key ()
else :
return NotImplemented
File-like object which hashes data written to it. E.g. useful in conjunction with shutil.copyfileobj()
class _HashFile (io .RawIOBase ):
"""
A simple file-like object which calculates a hash of the data written to
it.
"""
def __init__ (self , hash = hashlib .sha256 ):
self .hash = hash ()
def write (self , b ):
self .hash .update (b )
Context managers to atomically replace a file
import contextlib
import os
import pathlib
from typing import ContextManager , BinaryIO
@contextlib .contextmanager
def atomically_replaced_path (dest_path : pathlib .Path ) -> ContextManager [pathlib .Path ]:
temp_path = dest_path .with_name (dest_path .name + '~' )
yield temp_path
temp_path .rename (dest_path )
@contextlib .contextmanager
def atomically_replaced_file (dest_path : pathlib .Path ) -> ContextManager [BinaryIO ]:
with atomically_replaced_path (dest_path ) as temp_path :
with temp_path .open ('wb' ) as file :
try :
yield file
except Exception :
temp_path .unlink ()
raise
os .fsync (file .fileno ())
def atomically_replace_symlink (dest_path : pathlib .Path , target_path : pathlib .Path ):
with atomically_replaced_path (dest_path ) as temp_path :
temp_path .symlink_to (target_path )