Any comments about style (especially any "non-pythonic" code), performance, readability, etc would be much appreciated! I've tested this on CentOS and OSX 10.9. Works for Python 2.7 and 3.
#!/usr/bin/env python
#
# This script will run on Python 2.7.x and later
#
# By default, iter_modules looks in sys.path which is initialized from
# the environment variable PYTHONPATH, plus an installation-dependent default.
from pkgutil import iter_modules
import sys
def parse_command_line():
import argparse # added to Python 2.7 to replace optparse
python_version = sys.version
parser = argparse.ArgumentParser(description='Use this script to view the installed packages and/or modules for Python version: '+python_version)
parser.add_argument('--type',dest='list_type',type=str,default='packages',help='Can be \'packages\' (default), \'modules\', or \'both\'')
parser.add_argument('--ncols',type=int,default=2,help='Number of columns in output (default: 2)')
args = parser.parse_args()
return args
def select_packages():
return [ module[1:] for module in iter_modules() if module[2] ],'Packages'
def select_modules():
return [ module[1:] for module in iter_modules() if not module[2] ],'Modules'
def select_both():
return [ module[1:] for module in iter_modules() ],'Packages and Modules'
options = { 'packages' : select_packages,
'modules' : select_modules,
'both' : select_both,
}
def get_printed_names(pack_tuple):
p_name = []
for code in pack_tuple:
if ( code[1] ):
p_name.append(code[0]+' (Package)')
else:
p_name.append(code[0]+' (Module)')
return p_name
def fmt_cols(my_list, cols):
import math
max_width = max(map(lambda x: len(x), my_list))
col_length = int(math.ceil(len(my_list)/cols+1))
justify_list = list(map(lambda x: x.ljust(max_width), my_list))
lines = [ ' '.join(justify_list[i::col_length]) for i in range(col_length) ]
print("\n".join(lines))
def sort_func(a_str):
tmp_str = a_str.lower() # fine with ASCII subset of characters
for index,letter in enumerate(a_str):
if letter == '_':
continue
else:
return tmp_str[index:]
# main()
arguments = parse_command_line()
detail_level = arguments.list_type
try:
installed_code,tag = options[detail_level]()
except KeyError:
print('Error: detail_level argument must be \'packages\', \'modules\', or \'both\'')
sys.exit()
ncols = arguments.ncols
print('\nFor output options type \'python_pkginfo.py -h\'')
print('\nInstalled '+tag+' for Python version '+sys.version+':\n')
print('To see package versions try: pip freeze | sort')
print('after issuing the appropriate \"setpkgs -a python\" command\n')
print_list = get_printed_names(installed_code)
print_list.sort(key=sort_func)
fmt_cols(print_list,ncols)
2 Answers 2
#!/usr/bin/env python
#
# This script will run on Python 2.7.x and later
#
# By default, iter_modules looks in sys.path which is initialized from
# the environment variable PYTHONPATH, plus an installation-dependent default.
from pkgutil import iter_modules
import sys
def parse_command_line():
import argparse # added to Python 2.7 to replace optparse
Its typically considered best to import all modules at the beginning. Avoid importing inside functions. Also, the python history lesson was pointless.
python_version = sys.version
Why are you doing this? Why not just use sys.version when you need this?
parser = argparse.ArgumentParser(description='Use this script to view the installed packages and/or modules for Python version: '+python_version)
parser.add_argument('--type',dest='list_type',type=str,default='packages',help='Can be \'packages\' (default), \'modules\', or \'both\'')
parser.add_argument('--ncols',type=int,default=2,help='Number of columns in output (default: 2)')
args = parser.parse_args()
return args
I'd combine these last two lines.
def select_packages():
return [ module[1:] for module in iter_modules() if module[2] ],'Packages'
def select_modules():
return [ module[1:] for module in iter_modules() if not module[2] ],'Modules'
def select_both():
return [ module[1:] for module in iter_modules() ],'Packages and Modules'
These three functions are awkward. For one, when looping over tuples, its generally best to unpack the tuples. This will make the code easier to follow. They are also largely duplicate of each other.
options = { 'packages' : select_packages,
'modules' : select_modules,
'both' : select_both,
}
The python standard is to use ALL_CAPS for global constants.
def get_printed_names(pack_tuple):
pack_tuple is a bad name, I don't know what its in it.
p_name = []
avoid abbreviations like p, it just makes it harder to see what's going on.
for code in pack_tuple:
if ( code[1] ):
You don't need the parens. It'd also be easier to follow if you used for name, ispkg in pack_tuple:
p_name.append(code[0]+' (Package)')
else:
p_name.append(code[0]+' (Module)')
return p_name
Why does this function operate on a list of packages?
def fmt_cols(my_list, cols):
my_list
is a bad name. I have no idea what's in the list.
import math
max_width = max(map(lambda x: len(x), my_list))
lamba x: len(x)
is just len(x)
. So you can do max(map(len, my_list))
col_length = int(math.ceil(len(my_list)/cols+1))
justify_list = list(map(lambda x: x.ljust(max_width), my_list))
In python 2.x, map
produces a list already. (Perhaps you're aiming to work on Python 3 as well?) It's also probably better to use a list comprehension.
lines = [ ' '.join(justify_list[i::col_length]) for i in range(col_length) ]
print("\n".join(lines))
There's not much point in putting everything in a line just to print it. I'd just print it directly in a for loop.
def sort_func(a_str):
tmp_str = a_str.lower() # fine with ASCII subset of characters
for index,letter in enumerate(a_str):
if letter == '_':
continue
else:
return tmp_str[index:]
It seems to me that you are just stripping off the _
so instead, you can use return a_str.lower().lstrip('_')
# main()
Normal style is to put this in a main function.
arguments = parse_command_line()
detail_level = arguments.list_type
try:
installed_code,tag = options[detail_level]()
except KeyError:
print('Error: detail_level argument must be \'packages\', \'modules\', or \'both\'')
sys.exit()
Why don't you have argument parser check this?
ncols = arguments.ncols
Why are you copying this in a local?
print('\nFor output options type \'python_pkginfo.py -h\'')
print('\nInstalled '+tag+' for Python version '+sys.version+':\n')
print('To see package versions try: pip freeze | sort')
print('after issuing the appropriate \"setpkgs -a python\" command\n')
print_list = get_printed_names(installed_code)
print_list.sort(key=sort_func)
fmt_cols(print_list,ncols)
My rewrite of your script:
#!/usr/bin/env python
#
# This script will run on Python 2.7.x and later
#
# By default, iter_modules looks in sys.path which is initialized from
# the environment variable PYTHONPATH, plus an installation-dependent default.
from pkgutil import iter_modules
import sys
import argparse
import math
def parse_command_line():
parser = argparse.ArgumentParser(description='Use this script to view the installed packages and/or modules for Python version: ' + sys.version)
parser.add_argument('--type',dest='list_type',type=str,default='packages',help='What to list', choices = ['packages', 'modules', 'both'])
parser.add_argument('--ncols',type=int,default=2,help='Number of columns in output (default: 2)')
args = parser.parse_args()
return args
LIST_TYPE = {
'packages': (True, False, 'Packages'),
'modules': (False, True, 'Modules'),
'both': (True, True, 'Packages and Modules')
}
def find_modules(include_packages, include_modules):
for module_loader, module_name, is_package in iter_modules():
if is_package and include_packages:
yield module_name, is_package
elif not is_package and include_modules:
yield module_name, is_package
def formatted_name(module):
name, is_package = module
if is_package:
return name + ' (Package)'
else:
return name + ' (Module)'
def print_cols(cells, cols):
max_width = max(map(len, cells))
rows = int(math.ceil(len(cells)/cols+1))
for row in range(rows):
print(' '.join(x.ljust(max_width) for x in cells[row::rows]))
def main():
arguments = parse_command_line()
include_packages, include_modules, tag = LIST_TYPE[arguments.list_type]
installed_code = sorted(
find_modules(include_packages, include_modules),
key = lambda module: module[0].lower().lstrip('_'))
print('\nFor output options type \'python_pkginfo.py -h\'')
print('\nInstalled '+tag+' for Python version '+sys.version+':\n')
print('To see package versions try: pip freeze | sort')
print('after issuing the appropriate \"setpkgs -a python\" command\n')
print_cols(list(map(formatted_name, installed_code)), arguments.ncols)
if __name__ == '__main__':
main()
Help message.
A standard practice is to print help (
For output options ...
) only if a help option is selected, or if the command line arguments contain an error. It is also customary not to hardcode the program name but usesys.argv[0]
instead.Output
The words
Module
andPackage
take a lot of valuable screen real estate. I'd limit myself to printing justM
andP
, giving more columns available. It would be also nice to query the screen width and calculate number of columns accordingly, likels
does; I understand it is quite an involved task.Also, you should not default for multiple columns if an output is not a tty. When your program is pipelined to another program, multiple columns are harder to parse.
fmt_cols
should beformat_columns
really.