"""Tests for packaging.config."""
import os
import sys
from io import StringIO

from packaging import command
from packaging.dist import Distribution
from packaging.errors import PackagingFileError, PackagingOptionError
from packaging.compiler import new_compiler, _COMPILERS
from packaging.command.sdist import sdist

from packaging.tests import unittest, support
from packaging.tests.support import requires_zlib


SETUP_CFG = """
[metadata]
name = RestingParrot
version = 0.6.4
author = Carl Meyer
author_email = carl@oddbird.net
maintainer = Éric Araujo
maintainer_email = merwok@netwok.org
summary = A sample project demonstrating packaging
description-file = %(description-file)s
keywords = packaging, sample project

classifier =
  Development Status :: 4 - Beta
  Environment :: Console (Text Based)
  Environment :: X11 Applications :: GTK; python_version < '3'
  License :: OSI Approved :: MIT License
  Programming Language :: Python
  Programming Language :: Python :: 2
  Programming Language :: Python :: 3

requires_python = >=2.4, <3.2

requires_dist =
  PetShoppe
  MichaelPalin (> 1.1)
  pywin32; sys.platform == 'win32'
  pysqlite2; python_version < '2.5'
  inotify (0.0.1); sys.platform == 'linux2'

requires_external = libxml2

provides_dist = packaging-sample-project (0.2)
                unittest2-sample-project

project_url =
  Main repository, http://bitbucket.org/carljm/sample-distutils2-project
  Fork in progress, http://bitbucket.org/Merwok/sample-distutils2-project

[files]
packages_root = src

packages = one
           two
           three

modules = haven

scripts =
  script1.py
  scripts/find-coconuts
  bin/taunt

package_data =
  cheese = data/templates/*

extra_files = %(extra-files)s

# Replaces MANIFEST.in
sdist_extra =
  include THANKS HACKING
  recursive-include examples *.txt *.py
  prune examples/sample?/build

resources=
  bm/ {b1,b2}.gif = {icon}
  Cf*/ *.CFG = {config}/baBar/
  init_script = {script}/JunGle/

[global]
commands =
    packaging.tests.test_config.FooBarBazTest

compilers =
    packaging.tests.test_config.DCompiler

setup_hooks = %(setup-hooks)s



[install_dist]
sub_commands = foo
"""

# Can not be merged with SETUP_CFG else install_dist
# command will fail when trying to compile C sources
# TODO use a DummyCommand to mock build_ext
EXT_SETUP_CFG = """
[files]
packages = one
           two
           parent.undeclared

[extension:one.speed_coconuts]
sources = c_src/speed_coconuts.c
extra_link_args = "`gcc -print-file-name=libgcc.a`" -shared
define_macros = HAVE_CAIRO HAVE_GTK2
libraries = gecodeint gecodekernel -- sys.platform != 'win32'
    GecodeInt GecodeKernel -- sys.platform == 'win32'

[extension: two.fast_taunt]
sources = cxx_src/utils_taunt.cxx
          cxx_src/python_module.cxx
include_dirs = /usr/include/gecode
    /usr/include/blitz
extra_compile_args = -fPIC -O2
    -DGECODE_VERSION=$(./gecode_version) -- sys.platform != 'win32'
    /DGECODE_VERSION='win32' -- sys.platform == 'win32'
language = cxx

# corner case: if the parent package of an extension is declared but
# not its grandparent, it's legal
[extension: parent.undeclared._speed]
sources = parent/undeclared/_speed.c
"""

EXT_SETUP_CFG_BUGGY_1 = """
[extension: realname]
name = crash_here
"""

EXT_SETUP_CFG_BUGGY_2 = """
[files]
packages = ham

[extension: spam.eggs]
"""

EXT_SETUP_CFG_BUGGY_3 = """
[files]
packages = ok
           ok.works

[extension: ok.works.breaks._ext]
"""

HOOKS_MODULE = """
import logging

logger = logging.getLogger('packaging')

def logging_hook(config):
    logger.warning('logging_hook called')
"""


class DCompiler:
    name = 'd'
    description = 'D Compiler'

    def __init__(self, *args):
        pass


def version_hook(config):
    config['metadata']['version'] += '.dev1'


def first_hook(config):
    config['files']['modules'] += '\n first'


def third_hook(config):
    config['files']['modules'] += '\n third'


class FooBarBazTest:

    def __init__(self, dist):
        self.distribution = dist

    @classmethod
    def get_command_name(cls):
        return 'foo'

    def run(self):
        self.distribution.foo_was_here = True

    def nothing(self):
        pass

    def get_source_files(self):
        return []

    ensure_finalized = finalize_options = initialize_options = nothing


class ConfigTestCase(support.TempdirManager,
                     support.EnvironRestorer,
                     support.LoggingCatcher,
                     unittest.TestCase):

    restore_environ = ['PLAT']

    def setUp(self):
        super(ConfigTestCase, self).setUp()
        self.addCleanup(setattr, sys, 'stdout', sys.stdout)
        self.addCleanup(setattr, sys, 'stderr', sys.stderr)
        sys.stdout = StringIO()
        sys.stderr = StringIO()

        self.addCleanup(os.chdir, os.getcwd())
        tempdir = self.mkdtemp()
        self.working_dir = os.getcwd()
        os.chdir(tempdir)
        self.tempdir = tempdir

    def tearDown(self):
        os.chdir(self.working_dir)
        super(ConfigTestCase, self).tearDown()

    def write_setup(self, kwargs=None):
        opts = {'description-file': 'README', 'extra-files': '',
                'setup-hooks': 'packaging.tests.test_config.version_hook'}
        if kwargs:
            opts.update(kwargs)
        self.write_file('setup.cfg', SETUP_CFG % opts, encoding='utf-8')

    def get_dist(self):
        dist = Distribution()
        dist.parse_config_files()
        return dist

    def test_config(self):
        self.write_setup()
        self.write_file('README', 'yeah')
        os.mkdir('bm')
        self.write_file(('bm', 'b1.gif'), '')
        self.write_file(('bm', 'b2.gif'), '')
        os.mkdir('Cfg')
        self.write_file(('Cfg', 'data.CFG'), '')
        self.write_file('init_script', '')

        # try to load the metadata now
        dist = self.get_dist()

        # check what was done
        self.assertEqual(dist.metadata['Author'], 'Carl Meyer')
        self.assertEqual(dist.metadata['Author-Email'], 'carl@oddbird.net')

        # the hook adds .dev1
        self.assertEqual(dist.metadata['Version'], '0.6.4.dev1')

        wanted = [
            'Development Status :: 4 - Beta',
            'Environment :: Console (Text Based)',
            "Environment :: X11 Applications :: GTK; python_version < '3'",
            'License :: OSI Approved :: MIT License',
            'Programming Language :: Python',
            'Programming Language :: Python :: 2',
            'Programming Language :: Python :: 3']
        self.assertEqual(dist.metadata['Classifier'], wanted)

        wanted = ['packaging', 'sample project']
        self.assertEqual(dist.metadata['Keywords'], wanted)

        self.assertEqual(dist.metadata['Requires-Python'], '>=2.4, <3.2')

        wanted = ['PetShoppe',
                  'MichaelPalin (> 1.1)',
                  "pywin32; sys.platform == 'win32'",
                  "pysqlite2; python_version < '2.5'",
                  "inotify (0.0.1); sys.platform == 'linux2'"]

        self.assertEqual(dist.metadata['Requires-Dist'], wanted)
        urls = [('Main repository',
                 'http://bitbucket.org/carljm/sample-distutils2-project'),
                ('Fork in progress',
                 'http://bitbucket.org/Merwok/sample-distutils2-project')]
        self.assertEqual(dist.metadata['Project-Url'], urls)

        self.assertEqual(dist.packages, ['one', 'two', 'three'])
        self.assertEqual(dist.py_modules, ['haven'])
        self.assertEqual(dist.package_data, {'cheese': 'data/templates/*'})
        self.assertEqual(
            {'bm/b1.gif': '{icon}/b1.gif',
             'bm/b2.gif': '{icon}/b2.gif',
             'Cfg/data.CFG': '{config}/baBar/data.CFG',
             'init_script': '{script}/JunGle/init_script'},
             dist.data_files)

        self.assertEqual(dist.package_dir, 'src')

        # Make sure we get the foo command loaded.  We use a string comparison
        # instead of assertIsInstance because the class is not the same when
        # this test is run directly: foo is packaging.tests.test_config.Foo
        # because get_command_class uses the full name, but a bare "Foo" in
        # this file would be __main__.Foo when run as "python test_config.py".
        # The name FooBarBazTest should be unique enough to prevent
        # collisions.
        self.assertEqual('FooBarBazTest',
                         dist.get_command_obj('foo').__class__.__name__)

        # did the README got loaded ?
        self.assertEqual(dist.metadata['description'], 'yeah')

        # do we have the D Compiler enabled ?
        self.assertIn('d', _COMPILERS)
        d = new_compiler(compiler='d')
        self.assertEqual(d.description, 'D Compiler')

    def test_multiple_description_file(self):
        self.write_setup({'description-file': 'README  CHANGES'})
        self.write_file('README', 'yeah')
        self.write_file('CHANGES', 'changelog2')
        dist = self.get_dist()
        self.assertEqual(dist.metadata.requires_files, ['README', 'CHANGES'])

    def test_multiline_description_file(self):
        self.write_setup({'description-file': 'README\n  CHANGES'})
        self.write_file('README', 'yeah')
        self.write_file('CHANGES', 'changelog')
        dist = self.get_dist()
        self.assertEqual(dist.metadata['description'], 'yeah\nchangelog')
        self.assertEqual(dist.metadata.requires_files, ['README', 'CHANGES'])

    def test_parse_extensions_in_config(self):
        self.write_file('setup.cfg', EXT_SETUP_CFG)
        dist = self.get_dist()

        ext_modules = dict((mod.name, mod) for mod in dist.ext_modules)
        self.assertEqual(len(ext_modules), 3)
        ext = ext_modules.get('one.speed_coconuts')
        self.assertEqual(ext.sources, ['c_src/speed_coconuts.c'])
        self.assertEqual(ext.define_macros, ['HAVE_CAIRO', 'HAVE_GTK2'])
        libs = ['gecodeint', 'gecodekernel']
        if sys.platform == 'win32':
            libs = ['GecodeInt', 'GecodeKernel']
        self.assertEqual(ext.libraries, libs)
        self.assertEqual(ext.extra_link_args,
            ['`gcc -print-file-name=libgcc.a`', '-shared'])

        ext = ext_modules.get('two.fast_taunt')
        self.assertEqual(ext.sources,
            ['cxx_src/utils_taunt.cxx', 'cxx_src/python_module.cxx'])
        self.assertEqual(ext.include_dirs,
            ['/usr/include/gecode', '/usr/include/blitz'])
        cargs = ['-fPIC', '-O2']
        if sys.platform == 'win32':
            cargs.append("/DGECODE_VERSION=win32")
        else:
            cargs.append('-DGECODE_VERSION=$(./gecode_version)')
        self.assertEqual(ext.extra_compile_args, cargs)
        self.assertEqual(ext.language, 'cxx')

        self.write_file('setup.cfg', EXT_SETUP_CFG_BUGGY_1)
        self.assertRaises(PackagingOptionError, self.get_dist)

        self.write_file('setup.cfg', EXT_SETUP_CFG_BUGGY_2)
        self.assertRaises(PackagingOptionError, self.get_dist)

        self.write_file('setup.cfg', EXT_SETUP_CFG_BUGGY_3)
        self.assertRaises(PackagingOptionError, self.get_dist)

    def test_project_setup_hook_works(self):
        # Bug #11637: ensure the project directory is on sys.path to allow
        # project-specific hooks
        self.write_setup({'setup-hooks': 'hooks.logging_hook'})
        self.write_file('README', 'yeah')
        self.write_file('hooks.py', HOOKS_MODULE)
        self.get_dist()
        self.assertEqual(['logging_hook called'], self.get_logs())
        self.assertIn('hooks', sys.modules)

    def test_missing_setup_hook_warns(self):
        self.write_setup({'setup-hooks': 'this.does._not.exist'})
        self.write_file('README', 'yeah')
        self.get_dist()
        logs = self.get_logs()
        self.assertEqual(1, len(logs))
        self.assertIn('cannot find setup hook', logs[0])

    def test_multiple_setup_hooks(self):
        self.write_setup({
            'setup-hooks': '\n  packaging.tests.test_config.first_hook'
                           '\n  packaging.tests.test_config.missing_hook'
                           '\n  packaging.tests.test_config.third_hook',
        })
        self.write_file('README', 'yeah')
        dist = self.get_dist()

        self.assertEqual(['haven', 'first', 'third'], dist.py_modules)
        logs = self.get_logs()
        self.assertEqual(1, len(logs))
        self.assertIn('cannot find setup hook', logs[0])

    def test_metadata_requires_description_files_missing(self):
        self.write_setup({'description-file': 'README README2'})
        self.write_file('README', 'yeah')
        self.write_file('README2', 'yeah')
        os.mkdir('src')
        self.write_file(('src', 'haven.py'), '#')
        self.write_file('script1.py', '#')
        os.mkdir('scripts')
        self.write_file(('scripts', 'find-coconuts'), '#')
        os.mkdir('bin')
        self.write_file(('bin', 'taunt'), '#')

        for pkg in ('one', 'two', 'three'):
            pkg = os.path.join('src', pkg)
            os.mkdir(pkg)
            self.write_file((pkg, '__init__.py'), '#')

        dist = self.get_dist()
        cmd = sdist(dist)
        cmd.finalize_options()
        cmd.get_file_list()
        self.assertRaises(PackagingFileError, cmd.make_distribution)

    @requires_zlib
    def test_metadata_requires_description_files(self):
        # Create the following file structure:
        #   README
        #   README2
        #   script1.py
        #   scripts/
        #       find-coconuts
        #   bin/
        #       taunt
        #   src/
        #       haven.py
        #       one/__init__.py
        #       two/__init__.py
        #       three/__init__.py

        self.write_setup({'description-file': 'README\n  README2',
                          'extra-files': '\n  README3'})
        self.write_file('README', 'yeah 1')
        self.write_file('README2', 'yeah 2')
        self.write_file('README3', 'yeah 3')
        os.mkdir('src')
        self.write_file(('src', 'haven.py'), '#')
        self.write_file('script1.py', '#')
        os.mkdir('scripts')
        self.write_file(('scripts', 'find-coconuts'), '#')
        os.mkdir('bin')
        self.write_file(('bin', 'taunt'), '#')

        for pkg in ('one', 'two', 'three'):
            pkg = os.path.join('src', pkg)
            os.mkdir(pkg)
            self.write_file((pkg, '__init__.py'), '#')

        dist = self.get_dist()
        self.assertIn('yeah 1\nyeah 2', dist.metadata['description'])

        cmd = sdist(dist)
        cmd.finalize_options()
        cmd.get_file_list()
        self.assertRaises(PackagingFileError, cmd.make_distribution)

        self.write_setup({'description-file': 'README\n  README2',
                          'extra-files': '\n  README2\n    README'})
        dist = self.get_dist()
        cmd = sdist(dist)
        cmd.finalize_options()
        cmd.get_file_list()
        cmd.make_distribution()
        with open('MANIFEST') as fp:
            self.assertIn('README\nREADME2\n', fp.read())

    def test_sub_commands(self):
        self.write_setup()
        self.write_file('README', 'yeah')
        os.mkdir('src')
        self.write_file(('src', 'haven.py'), '#')
        self.write_file('script1.py', '#')
        os.mkdir('scripts')
        self.write_file(('scripts', 'find-coconuts'), '#')
        os.mkdir('bin')
        self.write_file(('bin', 'taunt'), '#')

        for pkg in ('one', 'two', 'three'):
            pkg = os.path.join('src', pkg)
            os.mkdir(pkg)
            self.write_file((pkg, '__init__.py'), '#')

        # try to run the install command to see if foo is called
        dist = self.get_dist()
        self.assertIn('foo', command.get_command_names())
        self.assertEqual('FooBarBazTest',
                         dist.get_command_obj('foo').__class__.__name__)


def test_suite():
    return unittest.makeSuite(ConfigTestCase)

if __name__ == '__main__':
    unittest.main(defaultTest='test_suite')
