1import sys
2
3vi = sys.version_info
4if vi < (3, 5):
5    raise RuntimeError('httptools require Python 3.5 or greater')
6else:
7    import os.path
8    import pathlib
9
10    from setuptools import setup, Extension
11    from setuptools.command.build_ext import build_ext as build_ext
12
13
14CFLAGS = ['-O2']
15
16ROOT = pathlib.Path(__file__).parent
17
18CYTHON_DEPENDENCY = 'Cython(>=0.29.24,<0.30.0)'
19
20
21class httptools_build_ext(build_ext):
22    user_options = build_ext.user_options + [
23        ('cython-always', None,
24            'run cythonize() even if .c files are present'),
25        ('cython-annotate', None,
26            'Produce a colorized HTML version of the Cython source.'),
27        ('cython-directives=', None,
28            'Cythion compiler directives'),
29        ('use-system-llhttp', None,
30            'Use the system provided llhttp, instead of the bundled one'),
31        ('use-system-http-parser', None,
32            'Use the system provided http-parser, instead of the bundled one'),
33    ]
34
35    boolean_options = build_ext.boolean_options + [
36        'cython-always',
37        'cython-annotate',
38        'use-system-llhttp',
39        'use-system-http-parser',
40    ]
41
42    def initialize_options(self):
43        # initialize_options() may be called multiple times on the
44        # same command object, so make sure not to override previously
45        # set options.
46        if getattr(self, '_initialized', False):
47            return
48
49        super().initialize_options()
50        self.use_system_llhttp = False
51        self.use_system_http_parser = True
52        self.cython_always = False
53        self.cython_annotate = None
54        self.cython_directives = None
55
56    def finalize_options(self):
57        # finalize_options() may be called multiple times on the
58        # same command object, so make sure not to override previously
59        # set options.
60        if getattr(self, '_initialized', False):
61            return
62
63        need_cythonize = self.cython_always
64        cfiles = {}
65
66        for extension in self.distribution.ext_modules:
67            for i, sfile in enumerate(extension.sources):
68                if sfile.endswith('.pyx'):
69                    prefix, ext = os.path.splitext(sfile)
70                    cfile = prefix + '.c'
71
72                    if os.path.exists(cfile) and not self.cython_always:
73                        extension.sources[i] = cfile
74                    else:
75                        if os.path.exists(cfile):
76                            cfiles[cfile] = os.path.getmtime(cfile)
77                        else:
78                            cfiles[cfile] = 0
79                        need_cythonize = True
80
81        if need_cythonize:
82            try:
83                import Cython
84            except ImportError:
85                raise RuntimeError(
86                    'please install Cython to compile httptools from source')
87
88            if Cython.__version__ < '0.29':
89                raise RuntimeError(
90                    'httptools requires Cython version 0.29 or greater')
91
92            from Cython.Build import cythonize
93
94            directives = {}
95            if self.cython_directives:
96                for directive in self.cython_directives.split(','):
97                    k, _, v = directive.partition('=')
98                    if v.lower() == 'false':
99                        v = False
100                    if v.lower() == 'true':
101                        v = True
102
103                    directives[k] = v
104
105            self.distribution.ext_modules[:] = cythonize(
106                self.distribution.ext_modules,
107                compiler_directives=directives,
108                annotate=self.cython_annotate)
109
110        super().finalize_options()
111
112        self._initialized = True
113
114    def build_extensions(self):
115        mod_parser, mod_url_parser = self.distribution.ext_modules
116        if self.use_system_llhttp:
117            mod_parser.libraries.append('llhttp')
118
119            if sys.platform == 'darwin' and \
120                    os.path.exists('/opt/local/include'):
121                # Support macports on Mac OS X.
122                mod_parser.include_dirs.append('/opt/local/include')
123        else:
124            mod_parser.include_dirs.append(
125                str(ROOT / 'vendor' / 'llhttp' / 'include'))
126            mod_parser.include_dirs.append(
127                str(ROOT / 'vendor' / 'llhttp' / 'src'))
128            mod_parser.sources.append('vendor/llhttp/src/api.c')
129            mod_parser.sources.append('vendor/llhttp/src/http.c')
130            mod_parser.sources.append('vendor/llhttp/src/llhttp.c')
131
132        if self.use_system_http_parser:
133            mod_url_parser.libraries.append('http_parser')
134
135            if sys.platform == 'darwin' and \
136                    os.path.exists('/opt/local/include'):
137                # Support macports on Mac OS X.
138                mod_url_parser.include_dirs.append('/opt/local/include')
139        else:
140            mod_url_parser.include_dirs.append(
141                str(ROOT / 'vendor' / 'http-parser'))
142            mod_url_parser.sources.append(
143                'vendor/http-parser/http_parser.c')
144
145        super().build_extensions()
146
147
148with open(str(ROOT / 'README.md')) as f:
149    long_description = f.read()
150
151
152with open(str(ROOT / 'httptools' / '_version.py')) as f:
153    for line in f:
154        if line.startswith('__version__ ='):
155            _, _, version = line.partition('=')
156            VERSION = version.strip(" \n'\"")
157            break
158    else:
159        raise RuntimeError(
160            'unable to read the version from httptools/_version.py')
161
162
163setup_requires = []
164
165if (not (ROOT / 'httptools' / 'parser' / 'parser.c').exists() or
166        '--cython-always' in sys.argv):
167    # No Cython output, require Cython to build.
168    setup_requires.append(CYTHON_DEPENDENCY)
169
170
171setup(
172    name='httptools',
173    version=VERSION,
174    description='A collection of framework independent HTTP protocol utils.',
175    long_description=long_description,
176    long_description_content_type='text/markdown',
177    url='https://github.com/MagicStack/httptools',
178    classifiers=[
179        'License :: OSI Approved :: MIT License',
180        'Intended Audience :: Developers',
181        'Programming Language :: Python :: 3',
182        'Operating System :: POSIX',
183        'Operating System :: MacOS :: MacOS X',
184        'Environment :: Web Environment',
185        'Development Status :: 5 - Production/Stable',
186    ],
187    platforms=['macOS', 'POSIX', 'Windows'],
188    python_requires='>=3.5.0',
189    zip_safe=False,
190    author='Yury Selivanov',
191    author_email='yury@magic.io',
192    license='MIT',
193    packages=['httptools', 'httptools.parser'],
194    cmdclass={
195        'build_ext': httptools_build_ext,
196    },
197    ext_modules=[
198        Extension(
199            "httptools.parser.parser",
200            sources=[
201                "httptools/parser/parser.pyx",
202            ],
203            extra_compile_args=CFLAGS,
204        ),
205        Extension(
206            "httptools.parser.url_parser",
207            sources=[
208                "httptools/parser/url_parser.pyx",
209            ],
210            extra_compile_args=CFLAGS,
211        ),
212    ],
213    include_package_data=True,
214    test_suite='tests.suite',
215    setup_requires=setup_requires,
216    extras_require={
217        'test': [
218            CYTHON_DEPENDENCY
219        ]
220    }
221)
222