# Copyright (c) 2010 ActiveState Software Inc. All rights reserved.

import sys
import os
from os import path as P
import logging
from operator import attrgetter
import collections
import activestate

import six
from pkg_resources import Requirement
from applib import sh
from applib import log
from applib import textui
from applib import _cmdln as cmdln
from applib.misc import require_option

import pypm
from pypm.common import licensing, python, net
from pypm.common.supported import PLATNAME
from pypm.common.repository import MultiRepositoryConfig
from pypm.common.package import RepoPackage
from pypm.common.package import BinaryPackage
from pypm.common.package import PackageFile
from pypm.common.util import concise_path
from pypm.common.util import wrapped
from pypm.client.depgraph import req_name
from pypm.client.depgraph import req2str
from pypm.client.base import make_pypm_environment2
from pypm.client.base import CONF_FILE_GLOBAL
from pypm.client.base import CONF_FILE_LOCAL
from pypm.client.base import IDX_PATH
from pypm.client.base import application
from pypm.client.fs import DOWNLOAD_CACHE
from pypm.client import installer
from pypm.client import error
from pypm.client import hooks

LOG = logging.getLogger('pypm.client')


#
# facade to install, uninstall or upgrade a package
#

def uninstall(pypmenv, names, nodeps=False):
    depgraph = installer.PyPMDepGraph(pypmenv)
    for name in names:
        name = pje_let_go(name.lower())
        if not depgraph.has_package(name):
            LOG.info('skipping "%s"; not installed', name)
        else:
            depgraph.remove_package(name, nodeps=nodeps)
    return installer.Installer(pypmenv).apply(
        depgraph, _requested_rmlist=names)
    

def install(pypmenv, requirements, nodeps=False, hint_upgrade=False, skip_missing=False):
    """Install the given requirements into ``pypmenv.pyenv``
    
    - requirements: List of requirement strings
    - nodeps: don't install dependencies automatically
    - hint_upgrade: don't print 'skipping' messages
    - skip_missing: proceed installing others when some are missing.
    """
    depgraph = installer.PyPMDepGraph(pypmenv)
    for rs in requirements:
        rs = pje_let_go(rs)
        r = Requirement.parse(rs)
        try:
            added = depgraph.add_requirement(r, nodeps=nodeps)
            if not added and not hint_upgrade:
                LOG.info('skipping "%s"; already installed at %s',
                         rs, pypmenv.pyenv.printable_location)
        except error.PackageNotFound as e:
            if e.required_by is None and skip_missing:
                # just warn of missing packages (unless it is a missing
                # dependency); allow other packages to be installed.
                if not hint_upgrade:
                    LOG.warn(e)
            else:
                raise
    return installer.Installer(pypmenv).apply(depgraph)
        

def upgrade(pypmenv):
    """Upgrade all installed packages"""
    pypmenv.pyenv.ensure_write_access()
    ipkglist = pypmenv.installed_store.find_all_packages()
    if ipkglist:
        # FIXME: we are ignoring extras here.
        names = [p.name for p in ipkglist]
        marks = install(pypmenv, names, hint_upgrade=True, skip_missing=True)
        return marks
    
    
def install_local_file(pypmenv, pypmfile): # nodeps = True (always!)
    """Install a local .pypm files
    
    Dependency is not automatically resolved, although we should do this in
    future
    """
    assert pypmfile.endswith(BinaryPackage.EXTENSION) and P.exists(pypmfile)
    
    LOG.debug('Installing local package "%s"', pypmfile)
    bpkg = PackageFile(pypmfile).to_binary_package()

    # sanity check
    if bpkg.pyver != pypmenv.pyenv.pyver:
        raise ValueError(
            'cannot install a {0} package on python {1}; {2}'.format(
                bpkg.pyver, pypmenv.pyenv.pyver, pypmfile))
    if bpkg.osarch != PLATNAME:
        raise ValueError(
            'incompatible platform {0}; {1}'.format(
                bpkg.osarch, pypmfile))
    
    pkg = RepoPackage.create_from(bpkg, relpath=pypmfile, tags='local')
        
    with pypmenv.locked():
        installer.Installer(pypmenv)._install(pkg, pypmfile)
        

def install_remote_file(pypmenv, url):
    """Install a .pypm file from an URL

    Dependency is not automatically resolved, although we should do this in
    future
    """
    sh.mkdirs(DOWNLOAD_CACHE)
    local_file, _ = net.download_file(url, DOWNLOAD_CACHE)
    return install_local_file(pypmenv, local_file)
    
    
def pje_let_go(req):
    """Setuptools is dead; long live Distribute!
    
    Just a hack to replace 'setuptools' with 'distribute'
    """
    return 'distribute' if req == 'setuptools' else req

    

#
# sub-commands
#

@cmdln.option('-E', '--virtualenv', default=None, metavar='DIR',
              help='Install/uninstall in the virtualenv or an arbitrary (Active)Python install at DIR.')
@cmdln.option('-g', '--globalinstall', action="store_true",
              help='Install/uninstall in global site-packages')
@cmdln.option('-R', '--repository-locations', # refers to RepositorySet
                                              # location(s) or name(S)
              default=None, metavar='NAME/URL',
              help='Comma separated list of repository URLs or names')
@cmdln.option('-n', '--no-autosync', action="store_true",
              help='Do not auto sync repositories')
@cmdln.option('-s', '--always-sync', action="store_true",
              help='Always force sync repositories before doing anything')
@cmdln.option('-y', '--non-interactive', action="store_true",
              help=('Run non-interactively; Automatic yes to prompts, no progress bars'))
@cmdln.option('-f', '--force', action="store_true", default=False,
              help='Force overwrite of files during installation')
class Commands(log.LogawareCmdln):
    name = "pypm"
    version = '%s\nType "pypm info" for more details' % pypm.__version__

    def initialize(self):
        require_option(self.options, 'repository-locations')
        venv = self.options.virtualenv

        if venv:
            if self.options.globalinstall:
                LOG.warn('ignoring -g/--globalinstall')
            pyenv = python.VirtualPythonEnvironment(venv)
        else:
            if self.options.globalinstall:
                pyenv = python.GlobalPythonEnvironment()
            else:
                pyenv = python.UserLocalPythonEnvironment()

        # Should we be interactive in CLI?
        interactive = not self.options.non_interactive

        # Find the actual URLs of repositories being used
        mrc = MultiRepositoryConfig(CONF_FILE_GLOBAL, self.options.configfile)
        repo_urls = mrc.get_urls(self.options.repository_locations)
        self.pypmenv = make_pypm_environment2(
            repo_urls,
            pyenv,
            interactive=interactive,
            force=self.options.force,
            )

        LOG.debug("using pythonenv: %s", self.pypmenv.pyenv.root_dir)
        LOG.debug('using repo list: %s', repo_urls)

    @cmdln.option('-f', '--force', action="store_true", default=False,
                  help='force downloading of the index')
    def do_sync(self, subcmd, opts):
        """${cmd_name}: Download lists of new/upgradable packages

        PyPM uses a private index database to keep track of which packages are
        installed, which are not installed and which are available for
        installation. It uses this database to find out how to install packages
        requested by the user and to find out which additional packages are
        needed in order for a selected package to work properly.

        To update this list, you would use the command "pypm sync". This command
        downloads the index from the repositories specified in the "client.conf"
        (which is parsed by the ``ConfigParser`` module) file inside the
        ``pypm.client`` package.

        This command is automatically run once a day, so you do not have to run
        it manually.

        To find where the local index directory is located, run::
        
          $ pypm info
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            self.pypmenv.repo_store.sync(
                force=opts.force,
                interactive=self.pypmenv.options['interactive'])

    @cmdln.option('-w', '--web', action="store_true",
                  help="Search online using your web browser")
    def do_search(self, subcmd, opts, *keywords):
        """${cmd_name}: Search for a package by name or description

        In the search results, the first column (if available) may show the
        package status marker: 'i' means the package is installed; 'u' means a
        newer version of that package is available.

        If no KEYWORDS are given, all available packages are listed.
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        u = 'http://code.activestate.com/pypm/search:{0}/'.format('+'.join(keywords))
        with self.bootstrapped():
            if opts.web:
                import webbrowser
                webbrowser.open(u)
            else:
                self._autosync()
                
                results = list(self.pypmenv.repo_store.search(*keywords))
                
                # Find the list of installed packages; useful to mark the
                # search results later
                installed = {}
                for pkg in self.pypmenv.installed_store.find_all_packages():
                    installed[pkg.name] = pkg

                # prune duplicates
                verlists = collections.defaultdict(list)
                for pkg in results:
                    verlists[pkg.name].append(pkg)
                for k, verlist in verlists.items():
                    verlist.sort(key=attrgetter('version_key'), reverse=True)
                    
                # prepare what needs to be printed
                output_table = []
                is_be_user = licensing.user_has_be_license()
                print_be_warning = False
                for (name, verlist) in sorted(verlists.items()):
                    pkg = verlist[0] # pick the latest version
                    mark = []
                    
                    # BE mark
                    if pkg.requires_be_license and not is_be_user:
                        mark.append('[BE]')
                        print_be_warning = True
                        
                    # Status mark (installed, upgrade)
                    if pkg.name in installed:
                        available_key = pkg.version_key
                        installed_key = installed[pkg.name].version_key
                        if available_key > installed_key:
                            mark.append('u')
                        else:
                            mark.append('i')
                    
                    output_table.append([
                        ' '.join(mark), name, pkg.summary or ''])
                    
                # If the user is not a BE user and there are BE-only packages
                # in the search results, show the standard BE warning message.
                if print_be_warning:
                    sys.stderr.write((
                        '*** Packages marked [BE] below require a valid \n'
                        '*** Business Edition license to install. Please visit\n'
                        '*** {0} for more details.\n\n').format(
                            licensing.BE_HOME_PAGE))

                if output_table:
                    textui.colprint(output_table)
                else:
                    LOG.warn("no matches found for `%s`; try PyPM Index:\n  %s",
                             ' '.join(keywords), u)
                
                return results

    @cmdln.option('-w', '--open-home-page', action="store_true",
                  help=('Also opens the project home page, '
                        'if available, in your webbrowser'))
    @cmdln.option('', '--rdepends', action="store_true",
                  help="Show list of packages depending on the given package")
    def do_show(self, subcmd, opts, name, version=None):
        """${cmd_name}: Display detailed information about a package

        If the package is already installed, show the location of the
        site-packages directory under which it is installed.
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            self._autosync()
            
            name = pje_let_go(name)
            pkg = self.pypmenv.repo_store.find_package(name, version)
            dependencies = pkg.install_requires['']
            extra_dependencies = []
            for extra in pkg.install_requires:
                if extra == '': continue
                extra_dependencies.append('`pypm install {0}[{1}]` ->\n    {2}'.format(
                    pkg.name,
                    extra,
                    ',\n    '.join(pkg.install_requires[extra])))
            
            # Show package metadata
            LOG.info('Name: %s', pkg.name)
            LOG.info('Latest version: %s', pkg.printable_version)
            if pkg.author or pkg.author_email:
                LOG.info('Author: %s %s',
                         pkg.author,
                         pkg.author_email and '<%s>' % pkg.author_email or '')
            LOG.info('Summary: %s', pkg.summary)
            if pkg.home_page:
                LOG.info('Home Page: %s', pkg.home_page)
            if pkg.license:
                LOG.info('License: %s', pkg.license)
            
            # Show package dependencies
            if dependencies:
                LOG.info('Dependencies:')
                for dep in dependencies:
                    LOG.info(' %s', dep)
                if extra_dependencies:
                    LOG.info('Optional dependencies:')
                    for ed in extra_dependencies:
                        LOG.info(' %s', ed)
                        
            # Optionally, show packages depending on it
            if opts.rdepends:
                LOG.info('Depended by:')
                rdependencies = {}
                for rpkg in self.pypmenv.repo_store._query():
                    for req in rpkg.install_requires['']:
                        req = Requirement.parse(req)
                        if pkg.name == req_name(req):
                            if pkg.version in req:
                                prev_rpkg = rdependencies.get(rpkg.name, None)
                                if (not prev_rpkg) or (
                                    rpkg.version_key > prev_rpkg.version_key):
                                        rdependencies[rpkg.name] = rpkg
                if rdependencies:
                    LOG.info(wrapped(
                        ', '.join(sorted(
                            [p.full_name for p in rdependencies.values()])),
                        prefix=' ',
                        break_on_hyphens=False))
            
            # Show list of (older) versions available in the repository
            if not version:
                pkglist = self.pypmenv.repo_store.find_package_releases(name)
                LOG.info('Available versions: %s', ', '.join(
                    [p.printable_version for p in pkglist]))
            
            # Status: is this package installed or not? Does it require BE?
            try:
                ipkg = self.pypmenv.installed_store.find_only_package(name)
            except error.NoPackageInstalled:
                LOG.info('Status: Not installed')
                if pkg.requires_be_license and not licensing.user_has_be_license():
                    LOG.info(
                        'NOTE: This package requires a valid Business Edition '
                        'license. Please visit %s for more details.',
                        licensing.BE_HOME_PAGE)
            else:
                # Is this package installed because another package?
                depgraph = installer.PyPMDepGraph(self.pypmenv)
                required_by = depgraph.edges[ipkg.name].items()
                if required_by:
                    LOG.info('Required by:')
                    for rpkg, rl in required_by:
                        LOG.info(' %s [%s]', rpkg, req2str(*rl))
                    
                LOG.info('Status: Already installed (%s) at %s',
                         ipkg.printable_version,
                         self.pypmenv.pyenv.printable_location)
                # Is a newer version available for upgrade?
                if ipkg.version_key < pkg.version_key:
                    # TODO: Need --force
                    LOG.info('Status: '
                             'A newer version (%s) is available. '
                             'Type "pypm install %s" to upgrade',
                             pkg.printable_version,
                             pkg.name)

            # Show postinstall notes for this package
            for note in pkg.get_notes(postinstall=True):
                LOG.info('***NOTE***: %s', note['content'].strip())
                    
            if opts.open_home_page:
                import webbrowser
                u = 'http://code.activestate.com/pypm/{0}/'.format(pkg.name)
                webbrowser.open(pkg.home_page or u)
                
            return pkg

    @cmdln.option('', '--full-path', action="store_true",
                  help='Show full absolute path, instead of a concise one')
    def do_files(self, subcmd, opts, name):
        """${cmd_name}: Display the list of files in an installed package

        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            pyenv = self.pypmenv.pyenv
            
            name = pje_let_go(name)
            pkg = self.pypmenv.installed_store.find_only_package(name)
            files = list(sorted(pkg.get_files_list()))

            for f in files:
                display_f = f = P.join(pyenv.base_dir, f)
                if not opts.full_path:
                    display_f = concise_path(f)
                LOG.info('%s %s', display_f, '' if P.exists(f) else '(missing)')
            return files

    def do_reconfigure(self, subcmd, opts, package):
        """${cmd_name}: Re-configure an already installed package

        This command runs the postinstall hooks for PACKAGE once again.

        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            ipkg = self.pypmenv.installed_store.find_only_package(package)
            for hook in hooks.POSTINSTALL:
                hook(self.pypmenv, ipkg)

    @cmdln.option('-r', '--remove', action="store_true",
                  help='Mark packages for removal instead of instalation')
    def _do_depgraph(self, subcmd, opts, *names):
        """${cmd_name}: Show the dependency graph

        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            depgraph = installer.PyPMDepGraph(self.pypmenv)
            for name in names:
                name = six.u(name)
                if opts.remove:
                    depgraph.remove_package(name)
                else:
                    depgraph.add_requirement(Requirement.parse(name))
            depgraph.display()
            return depgraph
        
    @cmdln.option(
        '-r', '--requirement', action='append', dest='requirements', 
        default=[], metavar='FILENAME',
        help='Install all the packages listed in the given requirements file')
    @cmdln.option('', '--nodeps', action="store_true", default=False,
                  help='Do not install dependencies automatically')
    @cmdln.option('', '--no-ignore', action="store_true", default=False,
                  help='Do not ignore missing packages; fail immediately')
    def do_install(self, subcmd, opts, *names):
        """${cmd_name}: Install/upgrade packages

        NAME can be one of the following:

          - Name of the package to install (eg: "django")
          - Setuptools-style requirement string (eg: "cherrypy<3")
          - Path to a local .pypm file
          - URL to a remote .pypm file

        Installing a package will automatically install its dependencies that
        are specified via setuptools' "install_requires".
        
        Packages are by default installed to PEP 370 style user directories
        (~/.local or %APPDATA%\Python) unless the -g or -E option is used.
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            self._autosync()
            
            # Read requirement files
            if opts.requirements:
                names = list(names)
                for requirement in opts.requirements:
                    with open(requirement) as f:
                        for r in f:
                            r = r.strip()
                            if r and not r.startswith('#'):
                                names.append(r)
            elif not names:
                raise cmdln.CmdlnUserError(
                    'insufficient arguments; see `pypm help install`')

            # Ask the user if he wants to add user local bin directory to his
            # PATH. Do this only when `pyenv` is a user site environment; not
            # when it is, say, the global one (-g).
            # On Windows, try to expand the unexpanded %APPDATA%
            if isinstance(self.pypmenv.pyenv, python.UserLocalPythonEnvironment):
                pep370_fix_path()

            if len(names) == 1:
                if '://' in names[0]:
                    return install_remote_file(self.pypmenv, names[0])
                elif names[0].endswith(BinaryPackage.EXTENSION) and P.exists(names[0]):
                    return install_local_file(self.pypmenv, names[0])

            return install(self.pypmenv, names, nodeps=opts.nodeps, skip_missing=not opts.no_ignore)
    
    @cmdln.alias('rm', 'remove', 'del', 'delete')
    @cmdln.option('', '--nodeps', action="store_true", default=False,
                  help='Skip uninstalling depending packages')
    def do_uninstall(self, subcmd, opts, *names):
        """${cmd_name}: Uninstall packages

        Removing a package will automatically uninstall other packages that
        depend on the package to be uninstall.
        
        Packages are by default uninstalled from PEP 370 style user directories
        (~/.local or %APPDATA%\Python) unless the -E option is used (type 'pypm
        help virtualenv' for more information).
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            if not names:
                raise cmdln.CmdlnUserError(
                    'insufficient arguments; see `pypm help uninstall`')

            return uninstall(self.pypmenv, names, nodeps=opts.nodeps)
     
    @cmdln.alias('update')
    def do_upgrade(self, subcmd, opts):
        """${cmd_name}: Upgrade installed packages
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            self._autosync()
            upgrade(self.pypmenv)
    
    @cmdln.alias('ls')
    @cmdln.option('', '--short', action="store_true", default=False,
                  help='Show only package names; no columns')
    def do_list(self, subcmd, opts):
        """${cmd_name}: List the currently installed packages

        As of now, PyPM only lists packages installed via PyPM
        itself. Eventually, the plan is to list every packages installed by all
        package managers.
        
        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            l = list(self.pypmenv.installed_store.find_all_packages())
            l = list(sorted(l, key=attrgetter('name'))) # sort by name
            if opts.short:
                for ipkg in l: print(ipkg.name)
            else:
                textui.colprint([
                    [ipkg.name, ipkg.printable_version, ipkg.summary or '']
                    for ipkg in l])
            return l

    def do_freeze(self, subcmd, opts):
        """${cmd_name}: Write installed requirements to stdout

        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            l = list(self.pypmenv.installed_store.find_all_packages())
            l = list(sorted(l, key=attrgetter('name'))) # sort by name
            for ipkg in l:
                print('{0}=={1}'.format(ipkg.name, ipkg.version))
            return l

    def do_log(self, subcmd, opts):
        """${cmd_name}: Print entries from the log file for last operation

        ${cmd_usage}
        ${cmd_option_list}
        """
        marker = '"pypm log" marker'
        # read from the current log file; failing which use the rolled over one
        logfiles = [application.locations.log_file_path,
                    application.locations.log_file_path + '.old']
        
        for logfile in logfiles:
            if not P.exists(logfile): continue
            with open(logfile) as f:
                log = f.read()
            operations = log.split('\n\n')
            LOG.info(marker)
            if operations:
                # ignore current log entry
                if '\n' not in operations[-1].strip():
                    del operations[-1]
            for op in reversed(operations):
                if marker not in op:
                    # must use `print` instead of `LOG.info` to avoid printing
                    # to log file
                    print(op)
                    return
        print('No log entries found')
        
    @cmdln.option('', '--full', action='store_true', default=False,
                  help='Show detailed information')
    def do_info(self, subcmd, opts):
        """${cmd_name}: Show version and other diagnostic details

        ${cmd_usage}
        ${cmd_option_list}
        """
        with self.bootstrapped():
            pyenv = python.GlobalPythonEnvironment()

            if opts.full is False:
                LOG.info('PyPM {0} ({1[product_type]} {2})'.format(
                    pypm.__version__,
                    activestate.version_info,
                    activestate.version))
                LOG.info('Installation target: {0}'.format(
                    self.pypmenv.pyenv.printable_location))
                LOG.info('(type "pypm info --full" for detailed information)')
            else:
                LOG.info('PyPM: %s', pypm.__version__)
                LOG.info('ActivePython %s built on %s <%s>',
                         activestate.version,
                         activestate.version_info['build_time'],
                         concise_path(sys.prefix))
                LOG.info('Installation target: Python %s <%s>',
                         self.pypmenv.pyenv.pyver,
                         concise_path(self.pypmenv.pyenv.site_packages_dir))
                LOG.info('Platform: %s', PLATNAME)
                LOG.info('Repositories:\n %s', '\n '.join([
                        r.url for r in self.pypmenv.repo_store.repository_list]))
                LOG.info('Business Edition: %s',
                         licensing.user_has_be_license())
                LOG.info('Config file:\n (current) %s\n (global) %s',
                         concise_path(self.options.configfile),
                         concise_path(CONF_FILE_GLOBAL))
                LOG.info('Log file: %s',
                         concise_path(application.locations.log_file_path))
                LOG.info('Repository cache: %s', concise_path(IDX_PATH))
                LOG.info('Download cache: %s', concise_path(DOWNLOAD_CACHE))
                LOG.info('Install database: %s', concise_path(self.pypmenv.installed_store.storepath))
                import applib, appdirs, sqlalchemy, zclockfile
                LOG.info('Imports:\n %s', '\n '.join([
                            concise_path(pypm.__file__),
                            concise_path(applib.__file__),
                            concise_path(appdirs.__file__),
                            concise_path(six.__file__),
                            concise_path(zclockfile.__file__),
                            concise_path(sqlalchemy.__file__)]))
        return self.pypmenv

    def _autosync(self):
        if self.options.always_sync:
            self.pypmenv.repo_store.sync()
        elif not self.options.no_autosync:
            self.pypmenv.repo_store.autosync()

    def help_virtualenv(self):
        """Virtualenv integration

        PyPM supports virtualenv_ (a tool for creating isolated Python
        environments) with the ``-E`` option::

          pypm -E C:\myvirtualenv install pylons

        .. _virtualenv: http://code.activestate.com/pypm/virtualenv
        """
        return self.help_virtualenv.__doc__

    def help_proxy(self):
        """Proxies and Firewalls

        If you connect to the internet through a proxy server, you may need to
        set the ``http_proxy`` environment variable.

        Set the http_proxy variable with the hostname or IP address of the proxy
        server::

          http_proxy=http://proxy.example.org

        If the proxy server requires a user name and password, include them in the
        following form::

          http_proxy=http://username:password@proxy.example.org

        If the proxy server uses a port other than 80, include the port number::

          http_proxy=http://username:password@proxy.example.org:8080
        """
        return self.help_proxy.__doc__


def main(arguments=None):
    """Invoke the client command

    `arguments` is the list of arguments to PyPM; eg::
    
        >>> from pypm.client.command import main
        >>> ret = main(['install', 'ipython'])
        [... installs ipython ...]
        >>>

    Note that the sub-commands are designed to return the Python objects for
    further use; `main()` returns them as-it-is::

        >>> installed_packages = main(['ls'])

    The PyPM script invokes `main()` without any arguments - therby using
    sys.argv (expected.
    """
    l = logging.getLogger('')
    log.setup_trace(l, application.locations.log_file_path)
    api_use = arguments is not None
    if api_use:
        assert isinstance(arguments, list), (
            'invalid type for `arguments` -- '
            'expected a list, got %s' % type(arguments))
        argv = ['pypm'] + arguments
    else:
        argv = sys.argv
        
    if len(argv) < 2:
        # The user has just typed 'pypm'; likely he is a newbie
        # We should not intimidate him with our huge help text.
        concise_help = r"""PyPM {0} (Python Package Manager) brief help:
  Type "pypm install PACKAGE" to install a package.
  Type "pypm search KEYWORDS" to search for packages.
  Type "pypm upgrade" to upgrade installed packages.
  Type "pypm help" to show full help."""
        print(concise_help.format(pypm.__version__))
    else:
        if P.exists(CONF_FILE_LOCAL):
            LOG.debug('using local config file: %s', CONF_FILE_LOCAL)
            conf_file = CONF_FILE_LOCAL
        else:
            conf_file = CONF_FILE_GLOBAL

        ret = Commands(
            install_console=True,
            default_configfile=conf_file,
        ).main(argv=argv)

        # Do not return objects returned by do_* when invoked for setuptools'
        # entry points, which treat the return values as stuff to be passed to
        # sys.exit (madness!)
        if api_use:
            return ret


#
# Ensure that PEP 370's scripts directory is in user's path. Do all we can to
# better the user's experience with PyPM
# 

def pep370_fix_path():
    """Ensure that PEP 370's scripts/bin directory is in PATH"""
    if sys.platform == 'win32':
        _pep370_fix_path_win()
    else:
        _pep370_fix_path_unix()
        
        
def _pep370_fix_path_win():
    """Workaround for http://support.microsoft.com/kb/329308
    
    The ActivePython installer adds %APPDATA%\Python\Scripts to the user's
    %PATH%, but a bug in Windows causes this to remain unexpanded, especially
    after reboots.
    
    We work around this by expanding it ourself (using Windows registry)
    
    The user can disable this by setting PYPM_NO_APPDATA_EXPAND environment
    variable.
    """
    if os.environ.get('PYPM_NO_APPDATA_EXPAND'):
        return
    user_scripts = r'%APPDATA%\Python\Scripts'  # as set by APy installer
    PATH = os.environ.get('PATH', '').split(';')
    try:
        idx = PATH.index(user_scripts)
    except ValueError:
        return  # either already expanded, or removed by the user
    
    PATH[idx] = P.expandvars(user_scripts)
    
    winenv = Win32Environment(scope='user')
    LOG.info('Expanding "%s" in user\'s %%PATH%%; please launch a new Command Window',
             user_scripts)
    winenv.setenv('PATH', ';'.join(PATH))
    
    
# http://code.activestate.com/recipes/577621-manage-environment-variables-on-windows/
class Win32Environment:
    """Utility class to get/set windows environment variable"""
    
    def __init__(self, scope):
        assert scope in ('user', 'system')
        self.scope = scope
        if scope == 'user':
            self.root = six.moves.winreg.HKEY_CURRENT_USER
            self.subkey = 'Environment'
        else:
            self.root = six.moves.winreg.HKEY_LOCAL_MACHINE
            self.subkey = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
            
    def getenv(self, name):
        r = six.moves.winreg
        key = r.OpenKey(self.root, self.subkey, 0, r.KEY_READ)
        try:
            value, _ = r.QueryValueEx(key, name)
        except WindowsError:
            value = ''
        return value
    
    def setenv(self, name, value):
        import win32con
        from win32gui import SendMessage
        r = six.moves.winreg
        assert self.scope == 'user', 'setenv supported only for user env'
        key = r.OpenKey(self.root, self.subkey, 0, r.KEY_ALL_ACCESS)
        r.SetValueEx(key, name, 0, r.REG_EXPAND_SZ, value)
        # LOG.info('SetValueEx %s == %s', name, value)
        r.CloseKey(key)
        SendMessage(
            win32con.HWND_BROADCAST, win32con.WM_SETTINGCHANGE, 0, self.subkey)
    

def _pep370_fix_path_unix():
    """If ~/.local/bin is not in $PATH, automatically add them
    
    Do this only with the user's consent. And do not run this check more than
    once (i.e., run only when PyPM is *first run*).
    """
    if sys.platform.startswith('win'):
        return # MSI does this on Windows

    # Proceed only when the terminal is interactive and was never run before
    isatty = (sys.stdin.isatty() and sys.stdout.isatty())
    firstrun_file = P.join(application.locations.user_cache_dir, '.firstrun-pep370')
    if (not isatty) or P.exists(firstrun_file):
        return
        
    import site
    from datetime import datetime
    pathenv = [P.abspath(x.strip()) for x in os.environ.get('PATH', '').split(':')]
    binpath = P.abspath(P.join(site.USER_BASE, 'bin'))
    profile = P.expanduser('~/.profile' if sys.platform == 'darwin' else '~/.bashrc')
    profile_lines = [
        '# PEP 370 PATH added by PyPM on %s' % datetime.now(),
        'export PATH=%s:$PATH' % binpath,
    ]
    already_in_profile = P.exists(profile) and profile_lines[1] in [
        l.strip() for l in open(profile).readlines()
    ]
    
    # Proceed only if ~/.local/bin is neither in $PATH, nor added to profile
    if binpath in pathenv or already_in_profile:
        return
    
    # Add to profile on the user's consent
    msg = (
        'Packages will install their script files to "%s" '
        '(as per PEP 370). This directory is not yet in your $PATH. '
        'Would you like PyPM to add it by appending to "%s"?'
    ) % (concise_path(binpath), concise_path(profile))
    if textui.askyesno(wrapped(msg, '*** '), default=True):
        if P.exists(profile):
            sh.cp(profile, profile+'.bak') # take a backup first
            
        with open(profile, 'a') as f:
            f.write('\n%s\n' % '\n'.join(profile_lines))
        print ('You may now reopen your shell for the changes to take effect.')
            
    sh.mkdirs(P.dirname(firstrun_file))
    with open(firstrun_file, 'w') as f: pass # prevent future runs


#  Make `python -m "pypm.client.command" ...` work like `pypm ...`
if __name__ == '__main__':
    main()
