diff --git a/.github/workflows/create-wheels.yaml b/.github/workflows/create-wheels.yaml index 1edb08d43..ba260e23e 100644 --- a/.github/workflows/create-wheels.yaml +++ b/.github/workflows/create-wheels.yaml @@ -15,6 +15,7 @@ jobs: # four jobs are defined make-wheel-win-osx, make-wheel-linux, make-source-dist and make-emulated-wheels # the wheels jobs do the the same steps, but linux wheels need to be build to target manylinux make-wheel-win-osx: + needs: make-source-dist name: ${{ matrix.python-version }}-${{ matrix.architecture }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: @@ -50,14 +51,13 @@ jobs: architecture: ${{ matrix.architecture }} - name: Create wheel - # create the wheel using --no-use-pep517 since locally we have pyproject - # this flag should be removed once falcon supports pep517 - # `--no-deps` is used to only generate the wheel for the current library. Redundant in falcon since it has no dependencies + # `--no-deps` is used to only generate the wheel for the current library. + # Redundant in falcon since it has no dependencies run: | python -m pip install --upgrade pip pip --version - pip install 'setuptools>=44' 'wheel>=0.34' 'cython>=0.29.21' - pip wheel -w dist --no-use-pep517 -v --no-deps . + pip install 'setuptools>=47' 'wheel>=0.34' + pip wheel -w dist -v --no-deps . - name: Check created wheel # - install the created wheel without using the pypi index @@ -90,6 +90,7 @@ jobs: twine upload --skip-existing dist/* make-wheel-linux: + needs: make-source-dist # see also comments in the make-wheel-win-osx job for details on the steps name: ${{ matrix.python-version }}-${{ matrix.architecture }}-${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -142,11 +143,9 @@ jobs: # this action generates 3 wheels in dist/. linux manylinux1 and manylinux2010 with: python-versions: ${{ matrix.python-version }} - build-requirements: "setuptools>=44 wheel>=0.34 cython>=0.29.21" - # Create the wheel using --no-use-pep517 since locally we have pyproject - # This flag should be removed once falcon supports pep517 + build-requirements: "setuptools>=47 wheel>=0.34" # `--no-deps` is used to only generate the wheel for the current library. Redundant in falcon since it has no dependencies - pip-wheel-args: "-w ./dist --no-use-pep517 -v --no-deps" + pip-wheel-args: "-w ./dist -v --no-deps" - name: Create wheel for manylinux 2014 # as previous step but for manylinux2014 @@ -156,8 +155,8 @@ jobs: # Remove previous original wheel just to be sure it is recreated. Should not be needed pre-build-command: "rm ./dist/*-linux*.whl" python-versions: ${{ matrix.python-version }} - build-requirements: "setuptools>=44 wheel>=0.34 cython>=0.29.21" - pip-wheel-args: "-w ./dist --no-use-pep517 -v --no-deps" + build-requirements: "setuptools>=47 wheel>=0.34" + pip-wheel-args: "-w ./dist -v --no-deps" - name: Check created wheel # - install the created wheel without using the pypi index @@ -216,12 +215,10 @@ jobs: architecture: ${{ matrix.architecture }} - name: Create sdist - # TODO should we generate the c files? Only for pyx files or also for py files? run: | python -m pip install --upgrade pip pip --version - pip install setuptools>=44 wheel>=0.34 cython>=0.29.21 - # cythonize -3 falcon/**/*.pyx + pip install setuptools>=47 wheel>=0.34 python setup.py sdist --dist-dir dist python .github/workflows/scripts/verify_tag.py dist @@ -243,6 +240,7 @@ jobs: twine upload --skip-existing dist/* make-emulated-wheels: + needs: make-source-dist # see also comments in the make-wheel-linux job for details on the steps name: ${{ matrix.python-version }}-${{ matrix.architecture }} runs-on: ubuntu-latest @@ -289,8 +287,8 @@ jobs: # this action generates 2 wheels in dist/. linux manylinux1 and manylinux2010 with: python-versions: ${{ matrix.python-version }} - build-requirements: "setuptools>=44 wheel>=0.34 cython>=0.29.21" - pip-wheel-args: "-w ./dist --no-use-pep517 -v --no-deps" + build-requirements: "setuptools>=47 wheel>=0.34" + pip-wheel-args: "-w ./dist -v --no-deps" - name: Check created wheel for arm if: ${{ matrix.architecture == 'aarch64' }} @@ -314,8 +312,8 @@ jobs: # this action generates 2 wheels in dist/. linux manylinux1 and manylinux2010 with: python-versions: ${{ matrix.python-version }} - build-requirements: "setuptools>=44 wheel>=0.34 cython>=0.29.21" - pip-wheel-args: "-w ./dist --no-use-pep517 -v --no-deps" + build-requirements: "setuptools>=47 wheel>=0.34" + pip-wheel-args: "-w ./dist -v --no-deps" - name: Check created wheel for s390x if: ${{ matrix.architecture == 's390x' }} diff --git a/MANIFEST.in b/MANIFEST.in index bd2cfd5a6..54339263c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,7 +6,6 @@ include tox.ini include README.rst include LICENSE include docs/conf.py docs/Makefile -exclude pyproject.toml graft docs/_static graft docs/_templates graft tools diff --git a/README.rst b/README.rst index 920874cd5..43424c505 100644 --- a/README.rst +++ b/README.rst @@ -236,29 +236,30 @@ Or, to install the latest beta or release candidate, if any: In order to provide an extra speed boost, Falcon can compile itself with Cython. Wheels containing pre-compiled binaries are available from PyPI for several common platforms. However, if a wheel for your platform of choice is not -available, you can choose to stick with the source distribution, or use the -instructions below to cythonize Falcon for your environment. - -The following commands tell pip to install Cython, and then to invoke -Falcon's ``setup.py``, which will in turn detect the presence of Cython -and then compile (AKA cythonize) the Falcon framework with the system's -default C compiler. +available, you can install the source distribution. The installation process +will automatically try to cythonize Falcon for your environment, falling back to +a normal pure-Python install if any issues are encountered during the +cythonization step: .. code:: bash - $ pip install cython - $ pip install --no-build-isolation --no-binary :all: falcon - -Note that ``--no-build-isolation`` is necessary to override pip's default -PEP 517 behavior that can cause Cython not to be found in the build -environment. + $ pip install --no-binary :all: falcon If you want to verify that Cython is being invoked, simply -pass `-v` to pip in order to echo the compilation commands: +pass the verbose flag `-v` to pip in order to echo the compilation commands. + +The cythonization step is only active when using the ``CPython`` Python +implementation, so installing using ``PyPy`` will skip it. +If you want to skip Cython compilation step and install +the pure-Python version directly you can set the environment variable +``FALCON_DISABLE_CYTHON`` to a non empty value before install: .. code:: bash - $ pip install -v --no-build-isolation --no-binary :all: falcon + $ FALCON_DISABLE_CYTHON=Y pip install -v --no-binary :all: falcon + +Please note that ``pip>=10`` is required to be able to install Falcon from +source. **Installing on OS X** @@ -335,7 +336,7 @@ available to your app without having to reinstall the package: .. code:: bash $ cd falcon - $ pip install --no-use-pep517 -e . + $ pip install -e . You can manually test changes to the Falcon framework by switching to the directory of the cloned repo and then running pytest: diff --git a/pyproject.toml b/pyproject.toml index 30457b0e4..55568d34b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,11 @@ +[build-system] + build-backend = "setuptools.build_meta" + requires = [ + "setuptools>=47", + "wheel>=0.34", + "cython>=0.29.21; python_implementation == 'CPython'", # Skip cython when using pypy + ] + [tool.towncrier] package = "falcon" package_dir = "" diff --git a/setup.cfg b/setup.cfg index d01eddfec..41305e24c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,72 @@ +[metadata] +name = falcon +version = attr: falcon.__version__ +description = An unladen web framework for building APIs and app backends. +long_description_content_type = text/x-rst +url = https://falconframework.org +author = Kurt Griffiths +author_email = mail@kgriffs.com +license = Apache 2.0 +license_file = LICENSE +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Web Environment + Natural Language :: English + Intended Audience :: Developers + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: MacOS :: MacOS X + Operating System :: Microsoft :: Windows + Operating System :: POSIX + Topic :: Internet :: WWW/HTTP :: WSGI + Topic :: Software Development :: Libraries :: Application Frameworks + Programming Language :: Python + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Cython +keywords = + asgi + wsgi + web + api + framework + rest + http + cloud +project_urls = + Documentation=https://falcon.readthedocs.io/en/stable/ + Issue Tracker=https://github.com/falconry/falcon + +[options] +zip_safe = False +include_package_data = True +packages = find: +python_requires = >=3.5 +install_requires = +tests_require = + testtools + requests + pyyaml + pytest + pytest-runner + +[options.packages.find] +exclude = + examples + tests + +[options.entry_points] +console_scripts = + falcon-bench = falcon.cmd.bench:main + falcon-inspect-app = falcon.cmd.inspect_app:main + falcon-print-routes = falcon.cmd.inspect_app:route_main + [egg_info] # TODO replace tag_build = dev1 diff --git a/setup.py b/setup.py index 1748e71e7..ebe7fae8b 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,14 @@ import glob -import importlib import io import os from os import path import re import sys -from setuptools import Extension, find_packages, setup +from setuptools import Extension, setup MYDIR = path.abspath(os.path.dirname(__file__)) -VERSION = importlib.import_module('falcon.version') -VERSION = VERSION.__version__ - -REQUIRES = [] - try: sys.pypy_version_info PYPY = True @@ -30,7 +24,39 @@ except ImportError: CYTHON = False -if CYTHON: + +class BuildFailed(Exception): + pass + + +def get_cython_options(): + # from sqlalchemy setup.py + from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatformError + ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError) + if sys.platform == 'win32': + # Work around issue https://github.com/pypa/setuptools/issues/1902 + ext_errors += (IOError, TypeError) + + class ve_build_ext(build_ext): + # This class allows Cython building to fail. + + def run(self): + try: + super().run() + except DistutilsPlatformError: + raise BuildFailed() + + def build_extension(self, ext): + try: + super().build_extension(ext) + except ext_errors as e: + raise BuildFailed() from e + except ValueError as e: + # this can happen on Windows 64 bit, see Python issue 7511 + if "'path'" in str(e): + raise BuildFailed() from e + raise + def list_modules(dirname, pattern): filenames = glob.glob(path.join(dirname, pattern)) @@ -92,11 +118,8 @@ def list_modules(dirname, pattern): for ext_mod in ext_modules: ext_mod.cython_directives = {'language_level': '3'} - cmdclass = {'build_ext': build_ext} - -else: - cmdclass = {} - ext_modules = [] + cmdclass = {'build_ext': ve_build_ext} + return cmdclass, ext_modules def load_description(): @@ -134,56 +157,47 @@ def load_description(): return ''.join(description_lines) -setup( - name='falcon', - version=VERSION, - description='An unladen web framework for building APIs and app backends.', - long_description=load_description(), - long_description_content_type='text/x-rst', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Natural Language :: English', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: POSIX :: Linux', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: WSGI', - # 'Topic :: Internet :: WWW/HTTP :: ASGI', - 'Topic :: Software Development :: Libraries :: Application Frameworks', - 'Programming Language :: Python', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Cython', - ], - keywords='wsgi web api framework rest http cloud', - author='Kurt Griffiths', - author_email='mail@kgriffs.com', - url='https://falconframework.org', - license='Apache 2.0', - packages=find_packages(exclude=['examples', 'tests']), - include_package_data=True, - zip_safe=False, - python_requires='>=3.5', - install_requires=REQUIRES, - cmdclass=cmdclass, - ext_modules=ext_modules, - tests_require=['testtools', 'requests', 'pyyaml', 'pytest', 'pytest-runner'], - entry_points={ - 'console_scripts': [ - 'falcon-bench = falcon.cmd.bench:main', - 'falcon-inspect-app = falcon.cmd.inspect_app:main', - 'falcon-print-routes = falcon.cmd.inspect_app:route_main', - ] - } -) +def run_setup(CYTHON): + if CYTHON: + cmdclass, ext_modules = get_cython_options() + else: + cmdclass, ext_modules = {}, [] + + setup( + long_description=load_description(), + cmdclass=cmdclass, + ext_modules=ext_modules, + ) + + +def status_msgs(*msgs): + print('*' * 75, *msgs, '*' * 75, sep='\n') + + +if not CYTHON: + run_setup(False) + if not PYPY: + status_msgs('Cython compilation not supported in this environment') +elif os.environ.get('FALCON_DISABLE_CYTHON'): + run_setup(False) + status_msgs( + 'FALCON_DISABLE_CYTHON is set, skipping cython compilation.', + 'Pure-Python build succeeded.', + ) +else: + try: + run_setup(True) + except BuildFailed as exc: + status_msgs( + exc.__cause__, + 'Cython compilation could not be completed, speedups are not enabled.', + 'Failure information, if any, is above.', + 'Retrying the build without the C extension now.' + ) + + run_setup(False) + + status_msgs( + 'Cython compilation could not be completed, speedups are not enabled.', + 'Pure-Python build succeeded.' + ) diff --git a/tox.ini b/tox.ini index 66f392267..05d237025 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,7 @@ envlist = py38, setenv = PIP_CONFIG_FILE={toxinidir}/pip.conf PYTHONASYNCIODEBUG=1 + FALCON_DISABLE_CYTHON=Y deps = -r{toxinidir}/requirements/tests commands = {toxinidir}/tools/clean.sh {toxinidir}/falcon pytest tests [] @@ -132,10 +133,9 @@ commands = {toxinidir}/tools/clean.sh {toxinidir}/falcon [with-cython] deps = -r{toxinidir}/requirements/tests - cython setenv = PIP_CONFIG_FILE={toxinidir}/pip.conf - FALCON_CYTHON=Y + FALCON_DISABLE_CYTHON= FALCON_ASGI_WRAP_NON_COROUTINES=Y FALCON_TESTING_SESSION=Y PYTHONASYNCIODEBUG=1 @@ -183,6 +183,7 @@ commands = {[with-cython]commands} [testenv:wsgi_servers] install_command = {[with-cython]install_command} +setenv = {[with-cython]setenv} deps = {[with-cython]deps} gunicorn meinheld @@ -449,6 +450,7 @@ setenv = PYTHONNOUSERSITE=1 FALCON_ASGI_WRAP_NON_COROUTINES=Y FALCON_TESTING_SESSION=Y + FALCON_DISABLE_CYTHON=Y commands = pip uninstall --yes falcon pip install --find-links {toxinidir}/dist --no-index --ignore-installed falcon