# -*- test-case-name: twisted.python.test.test_setup -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

# pylint: disable=I0011,C0103,C9302,W9401,W9402

"""
Setuptools convenience functionality.

This file must not import anything from Twisted, as it is loaded by C{exec} in
C{setup.py}. If you need compatibility functions for this code, duplicate them
here.

@var _EXTRA_OPTIONS: These are the actual package names and versions that will
    be used by C{extras_require}.  This is not passed to setup directly so that
    combinations of the packages can be created without the need to copy
    package names multiple times.

@var _EXTRAS_REQUIRE: C{extras_require} is a dictionary of items that can be
    passed to setup.py to install optional dependencies.  For example, to
    install the optional dev dependencies one would type::

        pip install -e ".[dev]"

    This has been supported by setuptools since 0.5a4.

@var _PLATFORM_INDEPENDENT: A list of all optional cross-platform dependencies,
    as setuptools version specifiers, used to populate L{_EXTRAS_REQUIRE}.

@var _EXTENSIONS: The list of L{ConditionalExtension} used by the setup
    process.

@var notPortedModules: Modules that are not yet ported to Python 3.
"""

import io
import os
import platform
import re
import sys

from distutils.command import build_ext
from distutils.errors import CompileError
from setuptools import Extension, find_packages
from setuptools.command.build_py import build_py

# Do not replace this with t.p.compat imports, this file must not import
# from Twisted. See the docstring.
if sys.version_info < (3, 0):
    _PY3 = False
else:
    _PY3 = True

STATIC_PACKAGE_METADATA = dict(
    name="Twisted",
    description="An asynchronous networking framework written in Python",
    author="Twisted Matrix Laboratories",
    author_email="twisted-python@twistedmatrix.com",
    maintainer="Glyph Lefkowitz",
    maintainer_email="glyph@twistedmatrix.com",
    url="https://twistedmatrix.com/",
    project_urls={
        'Documentation': 'https://twistedmatrix.com/documents/current/',
        'Source': 'https://github.com/twisted/twisted',
        'Issues': 'https://twistedmatrix.com/trac/report',
    },
    license="MIT",
    classifiers=[
        "Programming Language :: Python :: 2.7",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.5",
        "Programming Language :: Python :: 3.6",
        "Programming Language :: Python :: 3.7",
    ],
    python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*',
)


_dev = [
    'pyflakes >= 1.0.0',
    'twisted-dev-tools >= 0.0.2',
    'python-subunit',
    'sphinx >= 1.3.1',
    'towncrier >= 17.4.0'
]

if not _PY3:
    # These modules do not yet work on Python 3.
    _dev += [
        'twistedchecker >= 0.4.0',
        'pydoctor >= 16.2.0',
    ]

_EXTRA_OPTIONS = dict(
    dev=_dev,
    tls=[
        'pyopenssl >= 16.0.0',
        # service_identity 18.1.0 added support for validating IP addresses in
        # certificate subjectAltNames
        'service_identity >= 18.1.0',
        # idna 2.3 introduced some changes that break a few things.  Avoid it.
        # The problems were fixed in 2.4.
        'idna >= 0.6, != 2.3',
    ],
    conch=[
        'pyasn1',
        'cryptography >= 2.5',
        'appdirs >= 1.4.0',
        'bcrypt >= 3.0.0',
    ],
    soap=['soappy'],
    serial=['pyserial >= 3.0',
            'pywin32 != 226; platform_system == "Windows"'],
    macos=['pyobjc-core',
           'pyobjc-framework-CFNetwork',
           'pyobjc-framework-Cocoa'],
    windows=['pywin32 != 226'],
    http2=['h2 >= 3.0, < 4.0',
           'priority >= 1.1.0, < 2.0'],
)

_PLATFORM_INDEPENDENT = (
    _EXTRA_OPTIONS['tls'] +
    _EXTRA_OPTIONS['conch'] +
    _EXTRA_OPTIONS['soap'] +
    _EXTRA_OPTIONS['serial'] +
    _EXTRA_OPTIONS['http2']
)

_EXTRAS_REQUIRE = {
    'dev': _EXTRA_OPTIONS['dev'],
    'tls': _EXTRA_OPTIONS['tls'],
    'conch': _EXTRA_OPTIONS['conch'],
    'soap': _EXTRA_OPTIONS['soap'],
    'serial': _EXTRA_OPTIONS['serial'],
    'http2': _EXTRA_OPTIONS['http2'],
    'all_non_platform': _PLATFORM_INDEPENDENT,
    'macos_platform': (
        _EXTRA_OPTIONS['macos'] + _PLATFORM_INDEPENDENT
    ),
    'windows_platform': (
        _EXTRA_OPTIONS['windows'] + _PLATFORM_INDEPENDENT
    ),
}
_EXTRAS_REQUIRE['osx_platform'] = _EXTRAS_REQUIRE['macos_platform']

# Scripts provided by Twisted on Python 2 and 3.
_CONSOLE_SCRIPTS = [
    "ckeygen = twisted.conch.scripts.ckeygen:run",
    "cftp = twisted.conch.scripts.cftp:run",
    "conch = twisted.conch.scripts.conch:run",
    "mailmail = twisted.mail.scripts.mailmail:run",
    "pyhtmlizer = twisted.scripts.htmlizer:run",
    "tkconch = twisted.conch.scripts.tkconch:run",
    "trial = twisted.scripts.trial:run",
    "twist = twisted.application.twist._twist:Twist.main",
    "twistd = twisted.scripts.twistd:run",
    ]



class ConditionalExtension(Extension, object):
    """
    An extension module that will only be compiled if certain conditions are
    met.

    @param condition: A callable of one argument which returns True or False to
        indicate whether the extension should be built. The argument is an
        instance of L{build_ext_twisted}, which has useful methods for checking
        things about the platform.
    """
    def __init__(self, *args, **kwargs):
        self.condition = kwargs.pop("condition", lambda builder: True)
        Extension.__init__(self, *args, **kwargs)



# The C extensions used for Twisted.
_EXTENSIONS = [
    ConditionalExtension(
        "twisted.test.raiser",
        sources=["src/twisted/test/raiser.c"],
        condition=lambda _: _isCPython),

    ConditionalExtension(
        "twisted.internet.iocpreactor.iocpsupport",
        sources=[
            "src/twisted/internet/iocpreactor/iocpsupport/iocpsupport.c",
            "src/twisted/internet/iocpreactor/iocpsupport/winsock_pointers.c",
            ],
        libraries=["ws2_32"],
        condition=lambda _: _isCPython and sys.platform == "win32"),

    ConditionalExtension(
        "twisted.python._sendmsg",
        sources=["src/twisted/python/_sendmsg.c"],
        condition=lambda _: not _PY3 and sys.platform != "win32"),
    ]



def _longDescriptionArgsFromReadme(readme):
    """
    Generate a PyPI long description from the readme.

    @param readme: Path to the readme reStructuredText file.
    @type readme: C{str}

    @return: Keyword arguments to be passed to C{setuptools.setup()}.
    @rtype: C{str}
    """
    with io.open(readme, encoding='utf-8') as f:
        readmeRst = f.read()

    # Munge links of the form `NEWS <NEWS.rst>`_ to point at the appropriate
    # location on GitHub so that they function when the long description is
    # displayed on PyPI.
    longDesc = re.sub(
        r'`([^`]+)\s+<(?!https?://)([^>]+)>`_',
        r'`\1 <https://github.com/twisted/twisted/blob/trunk/\2>`_',
        readmeRst,
        flags=re.I,
    )

    return {
        'long_description': longDesc,
        'long_description_content_type': 'text/x-rst',
    }



def getSetupArgs(extensions=_EXTENSIONS, readme='README.rst'):
    """
    Generate arguments for C{setuptools.setup()}

    @param extensions: C extension modules to maybe build. This argument is to
        be used for testing.
    @type extensions: C{list} of C{ConditionalExtension}

    @param readme: Path to the readme reStructuredText file. This argument is
        to be used for testing.
    @type readme: C{str}

    @return: The keyword arguments to be used by the setup method.
    @rtype: L{dict}
    """
    arguments = STATIC_PACKAGE_METADATA.copy()
    if readme:
        arguments.update(_longDescriptionArgsFromReadme(readme))

    # This is a workaround for distutils behavior; ext_modules isn't
    # actually used by our custom builder.  distutils deep-down checks
    # to see if there are any ext_modules defined before invoking
    # the build_ext command.  We need to trigger build_ext regardless
    # because it is the thing that does the conditional checks to see
    # if it should build any extensions.  The reason we have to delay
    # the conditional checks until then is that the compiler objects
    # are not yet set up when this code is executed.
    arguments["ext_modules"] = extensions
    # Use custome class to build the extensions.
    class my_build_ext(build_ext_twisted):
        conditionalExtensions = extensions
    command_classes = {
        'build_ext': my_build_ext,
    }

    if sys.version_info[0] >= 3:
        command_classes['build_py'] = BuildPy3

    requirements = [
        "zope.interface >= 4.4.2",
        "constantly >= 15.1",
        "incremental >= 16.10.1",
        "Automat >= 0.3.0",
        "hyperlink >= 17.1.1",
        # PyHamcrest 1.10.0 is Python 3 only, but lacks package metadata that
        # says so. This condition can be dropped when Twisted drops support for
        # Python 2.7.
        "PyHamcrest >= 1.9.0, != 1.10.0",
        "attrs >= 19.2.0",
    ]

    arguments.update(dict(
        packages=find_packages("src"),
        use_incremental=True,
        setup_requires=["incremental >= 16.10.1"],
        install_requires=requirements,
        entry_points={
            'console_scripts': _CONSOLE_SCRIPTS
        },
        cmdclass=command_classes,
        include_package_data=True,
        exclude_package_data={
            "": ["*.c", "*.h", "*.pxi", "*.pyx", "build.bat"],
        },
        zip_safe=False,
        extras_require=_EXTRAS_REQUIRE,
        package_dir={"": "src"},
    ))

    return arguments



class BuildPy3(build_py, object):
    """
    A version of build_py that doesn't install the modules that aren't yet
    ported to Python 3.
    """
    def find_package_modules(self, package, package_dir):
        modules = [
            module for module
            in build_py.find_package_modules(self, package, package_dir)
            if ".".join([module[0], module[1]]) not in notPortedModules]
        return modules



## Helpers and distutil tweaks


class build_ext_twisted(build_ext.build_ext, object):
    """
    Allow subclasses to easily detect and customize Extensions to
    build at install-time.
    """

    def prepare_extensions(self):
        """
        Prepare the C{self.extensions} attribute (used by
        L{build_ext.build_ext}) by checking which extensions in
        I{conditionalExtensions} should be built.  In addition, if we are
        building on NT, define the WIN32 macro to 1.
        """
        # always define WIN32 under Windows
        if os.name == 'nt':
            self.define_macros = [("WIN32", 1)]
        else:
            self.define_macros = []

        # On Solaris 10, we need to define the _XOPEN_SOURCE and
        # _XOPEN_SOURCE_EXTENDED macros to build in order to gain access to
        # the msg_control, msg_controllen, and msg_flags members in
        # sendmsg.c. (according to
        # https://stackoverflow.com/questions/1034587).  See the documentation
        # of X/Open CAE in the standards(5) man page of Solaris.
        if sys.platform.startswith('sunos'):
            self.define_macros.append(('_XOPEN_SOURCE', 1))
            self.define_macros.append(('_XOPEN_SOURCE_EXTENDED', 1))

        self.extensions = [
            x for x in self.conditionalExtensions if x.condition(self)
        ]

        for ext in self.extensions:
            ext.define_macros.extend(self.define_macros)


    def build_extensions(self):
        """
        Check to see which extension modules to build and then build them.
        """
        self.prepare_extensions()
        build_ext.build_ext.build_extensions(self)


    def _remove_conftest(self):
        for filename in ("conftest.c", "conftest.o", "conftest.obj"):
            try:
                os.unlink(filename)
            except EnvironmentError:
                pass


    def _compile_helper(self, content):
        conftest = open("conftest.c", "w")
        try:
            with conftest:
                conftest.write(content)

            try:
                self.compiler.compile(["conftest.c"], output_dir='')
            except CompileError:
                return False
            return True
        finally:
            self._remove_conftest()


    def _check_header(self, header_name):
        """
        Check if the given header can be included by trying to compile a file
        that contains only an #include line.
        """
        self.compiler.announce("checking for {} ...".format(header_name), 0)
        return self._compile_helper("#include <{}>\n".format(header_name))



def _checkCPython(sys=sys, platform=platform):
    """
    Checks if this implementation is CPython.

    This uses C{platform.python_implementation}.

    This takes C{sys} and C{platform} kwargs that by default use the real
    modules. You shouldn't care about these -- they are for testing purposes
    only.

    @return: C{False} if the implementation is definitely not CPython, C{True}
        otherwise.
    """
    return platform.python_implementation() == "CPython"


_isCPython = _checkCPython()

notPortedModules = [
    "twisted.mail.alias",
    "twisted.mail.bounce",
    "twisted.mail.mail",
    "twisted.mail.maildir",
    "twisted.mail.pb",
    "twisted.mail.relaymanager",
    "twisted.mail.scripts.__init__",
    "twisted.mail.tap",
    "twisted.mail.test.test_bounce",
    "twisted.mail.test.test_mail",
    "twisted.mail.test.test_options",
    "twisted.mail.test.test_scripts",
    "twisted.news.__init__",
    "twisted.news.database",
    "twisted.news.news",
    "twisted.news.nntp",
    "twisted.news.tap",
    "twisted.news.test.__init__",
    "twisted.news.test.test_database",
    "twisted.news.test.test_news",
    "twisted.news.test.test_nntp",
    "twisted.plugins.twisted_mail",
    "twisted.plugins.twisted_news",
    "twisted.protocols.shoutcast",
    "twisted.python._pydoctor",
    "twisted.python.finalize",
    "twisted.python.hook",
    "twisted.python.test.cmodulepullpipe",
    "twisted.python.test.test_pydoctor",
    "twisted.python.test.test_win32",
    "twisted.test.test_hook",
    "twisted.web.soap",
    "twisted.web.test.test_soap",
]
