2
\$\begingroup\$

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)
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$
#!/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()
\$\endgroup\$
0
2
\$\begingroup\$
  • 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 use sys.argv[0] instead.

  • Output

    The words Module and Package take a lot of valuable screen real estate. I'd limit myself to printing just M and P, giving more columns available. It would be also nice to query the screen width and calculate number of columns accordingly, like ls 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 be format_columns really.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.