# Copyright (c) 2009 ActiveState Software Inc.
# See http://www.activestate.com/activepython/license/ for licensing
# information.

"""
    pypm.common.python
    ~~~~~~~~~~~~~~~~
    
    Functions and classes dealing with the actual Python interpreters
"""

import os
from os.path import expanduser
import sys
import re
from abc import ABCMeta, abstractmethod

from pypm.common.util import lazyproperty, xjoin, existing
from pypm.common import proc, console

WIN = sys.platform.startswith('win')


class NoWriteAccess(EnvironmentError):
    pass

class BasePythonEnvironment(object):
    """Represent a generic Python installation"""
    __metaclass__ = ABCMeta

    def __init__(self, root_dir):
        "Initialize a local environment based in current Python interpreter"
        self.root_dir = os.path.abspath(root_dir)
        
    def ensure_write_access(self):
        """Ensure that this Python environment can be modified

        Typically, this is expected to raise ``NoWriteAccess`` for global
        installations like /opt/ActivePython-2.6.
        """
        if not os.access(self.root_dir, os.W_OK):
            raise NoWriteAccess, 'no write access to "%s"' % self.root_dir

    def eval(self, expr, with_modules=None, preimport_stmt=None,
             python_args=None):
        """Evaluate `expr` and return the *printed* value

        Before evaluating `expr` execute `preimport_stmt` and import the
        mentioned modules.
        
        If `python_args` is None, use the default args (-B), otherwise use the
        argument value as Python args.

        Note that the returned value will include a newline at the end - which
        is the default behavior of `print`. XXX: we should change this?
        """
        statements = []

        if preimport_stmt:
            statements.append(preimport_stmt)
            
        if with_modules:
            assert isinstance(with_modules, (list, tuple))
            statements.extend(
                ['import %s' % mod for mod in with_modules]
            )
            
        if python_args is None:
            python_args = '-B' # no bytecode generation
        
        statements.append('print ' + expr)
        
        return proc.run_command(
            '{0.python_exe} {2} -s -c "{1}"'.format(
                self,
                ';'.join(statements).replace('"', '\\"'),
                python_args)
        )[0]

    def has_module(self, module, python_args=None):
        # remove PWD from sys.path to avoid conflict with modules in PWD
        preimport_stmt = "import sys; sys.path.remove('')"
        try:
            self.eval('', with_modules=[module],
                      preimport_stmt=preimport_stmt,
                      python_args=python_args)
            return True
        except proc.NonZeroReturnCode, e:
            if 'ImportError' in e.stderr:
                return False
            else:
                raise

    @lazyproperty
    def pyver(self):
        return self.eval("'%d.%d' % sys.version_info[:2]", ['sys']).strip()

    @lazyproperty
    def pyversion(self):
        """Short and concise string-representation of Python version
        
        >>> pyenv.pyversion
        '2.6.4'
        """
        return self.eval("platform.python_version()", ['platform']).strip()

    @lazyproperty
    def pyversion_info(self):
        """``sys.version_info``"""
        return eval(self.eval("sys.version_info", ['sys']).strip())

    @abstractmethod
    def python_exe(self):
        """Return the path to the Python executable"""

    @abstractmethod
    def site_packages_dir(self):
        """Return the path to site-packages directory"""

    @abstractmethod
    def scripts_dir(self):
        """Return the path to the scripts directory

        This is the directory where 'setup.py install' would install the scripts
        to.
        """

    @abstractmethod
    def get_abspath(self, relpath):
        """Get absolute path to a file described by `relpath`

        `relpath` is supposed to be inside the Python installation.
        """

    def __str__(self):
        return '{0.__class__.__name__}<{0.python_exe}>'.format(self)
    __repr__ = __str__ # make it appear in py.test errors


class PythonEnvironment(BasePythonEnvironment):
    """A Python environment representing ordinary Python installations

    Examples:

      /usr
      /opt/ActivePython-2.6
      /tmp/Python-2.6.2/buildimage # ./configure --prefix=./buildimage
    """
        
    @lazyproperty
    def python_exe(self):
        # Note: we cannot use `self.scripts_dir` here.
        if WIN:
            # virtualenv creates the python exe inside Scripts/ directory
            python_exe = xjoin(self.root_dir, 'Scripts', 'Python.exe')
            if not os.path.exists(python_exe):
                # but normal Python installation has it on root directory
                python_exe = xjoin(self.root_dir, 'Python.exe')
        else:
            python_exe = xjoin(self.root_dir, 'bin', 'python')
        
        return existing(python_exe)

    @lazyproperty
    def lib_dir(self):
        """Return the path to Python's lib directory

        This is the directory where the core Python modules (os, sys, logging,
        etc..) reside .. along with the site-packages/ directory.
        """
        if WIN:
            return existing(xjoin(self.root_dir, 'Lib'))
        else:
            return existing(xjoin(
                    self.root_dir, 'lib', 'python{0}'.format(self.pyver)))

    @lazyproperty
    def site_packages_dir(self):
        return existing(xjoin(self.lib_dir, 'site-packages'))

    @lazyproperty
    def scripts_dir(self):
        """Return the path to directory where scripts will be installed"""
        return existing(xjoin(self.root_dir, 'Scripts' if WIN else 'bin'))

    def get_abspath(self, relpath):
        return xjoin(self.root_dir, relpath)



#
# Typically used Python environments
#

class CurrentPythonEnvironment(PythonEnvironment):
    """Represent the environment of the current Python process"""
    def __init__(self):
        "Initialize a local environment based in current Python interpreter"
        super(CurrentPythonEnvironment, self).__init__(
            root_dir = sys.prefix
        )
        
class VirtualPythonEnvironment(PythonEnvironment):
    """A Python environment based on a virtualenv directory"""
    pass




#
# Support for PEP 370
#

class UserLocalPythonEnvironment(PythonEnvironment):
    """Represent PEP 370 style like environment

    PEP 370 made it possible to install packages into $HOME using the --user
    option to 'setup.py install':

      ~/.local in Unix
      $APPDATA/Python in Windows
    """

    def __init__(self):
        super(PythonEnvironment, self).__init__(sys.prefix)
        self.global_env = PythonEnvironment(sys.prefix)

    def ensure_write_access(self):
        if not os.path.exists(self.user_base_dir):
            # user may not have a ~/.local directory *yet*
            console.mkdirs(self.user_base_dir)

        # This is unlikely to happen
        if not os.access(self.user_base_dir, os.W_OK):
            raise NoWriteAccess, (
                'no write access to user base "%s"' % self.user_base_dir)

    def get_abspath(self, relpath):
        return xjoin(self.user_base_dir, relpath)


    @lazyproperty
    def user_base_dir(self):
        return self.eval('site.USER_BASE', ['site']).strip()

    @lazyproperty
    def user_site_dir(self):
        return self.eval('site.USER_SITE', ['site']).strip()
    site_packages_dir = user_site_dir

    # Apparently an user local environment is pretty much same as a virtualenv
    # created one - except for the fact that on Windows lib directory is in
    # %APPDATA%/Python/Python26 (as opposed to /Lib)
    @lazyproperty
    def lib_dir(self):
        return existing(xjoin(self.site_packages_dir, os.pardir))

    @lazyproperty
    def scripts_dir(self):
        return existing(xjoin(self.user_base_dir, 'Scripts' if WIN else 'bin'))

    def __str__(self):
        return '{0.__class__.__name__}<{0.python_exe},{0.user_base_dir}>'.format(
            self)        



if __name__ == '__main__':
    # debug
    pyenv = UserLocalPythonEnvironment()
    print pyenv.root_dir
    print pyenv.python_exe
    print pyenv.scripts_dir
    print pyenv.site_packages_dir
    print pyenv.lib_dir
    print pyenv.pyver
