Skip to content

Commit

Permalink
Optional cffi dependency (#156)
Browse files Browse the repository at this point in the history
* Update build.py

* Update utils.py

* Update tox.ini

* add cffi extra

* optional cffi without extra

* Update 1_usage.rst

* Update FUNDING.yml

* Update CHANGELOG.rst
  • Loading branch information
jannikmi authored Sep 23, 2022
1 parent 51efdd3 commit 5dc365c
Show file tree
Hide file tree
Showing 9 changed files with 645 additions and 365 deletions.
1 change: 0 additions & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# These are supported funding model platforms
github: [jannikmi]
issuehunt: mrminimal64
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Future TODOs:
* parameterised tests


6.1.3 (2022-09-23)
------------------

* bugfix broken package build in the case of a broken ``cffi`` installation (GitHub issue #155). Skip build process if ``cffi`` fails. For performance reasons using the C extension should remain the default behavior. Hence the ``cffi`` dependency should not be optional.


6.1.2 (2022-09-13)
------------------

Expand Down
56 changes: 37 additions & 19 deletions build.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" builds inside polygon algorithm C extension
""" optionally builds inside polygon algorithm C extension
Resources:
https://github.com/FirefoxMetzger/mini-extension
Expand All @@ -7,10 +7,11 @@
"""
import pathlib
import re
import warnings

import cffi
import cffi.setuptools_ext
import setuptools
from cffi import FFI
from cffi.setuptools_ext import cffi_modules

EXTENSION_NAME = "inside_polygon_ext"
H_FILE_NAME = "inside_polygon_int.h"
Expand All @@ -20,30 +21,43 @@
h_file_path = EXTENSION_PATH / H_FILE_NAME
c_file_path = EXTENSION_PATH / C_FILE_NAME

ffibuilder = FFI()
ffibuilder.set_source(
EXTENSION_NAME, # name of the output C extension
f'#include "{h_file_path}"',
sources=[str(c_file_path)],
)
try:
ffibuilder = cffi.FFI()
except Exception as exc:
warnings.warn(f"C lang extension cannot be build, since cffi failed with this error: {exc}")
# Clang extension should be fully optional
ffibuilder = None

with open(h_file_path) as h_file:
# cffi does not like our preprocessor directives, so we remove them
lns = h_file.read().splitlines()
flt = filter(lambda ln: not re.match(r" *#", ln), lns)
if ffibuilder is not None:
ffibuilder.set_source(
EXTENSION_NAME, # name of the output C extension
f'#include "{h_file_path}"',
sources=[str(c_file_path)],
)

ffibuilder.cdef("\n".join(flt))
with open(c_file_path) as c_file:
# cffi does not like our preprocessor directives, so we remove them
c_file_content = c_file.read()
with open(h_file_path) as h_file:
# cffi does not like our preprocessor directives, so we remove them
lns = h_file.read().splitlines()
flt = filter(lambda ln: not re.match(r" *#", ln), lns)

ffibuilder.cdef("\n".join(flt))

# with open(c_file_path) as c_file:
# # cffi does not like our preprocessor directives, so we remove them
# c_file_content = c_file.read()


def build_c_extension():
if ffibuilder is None:
""""""
return

if __name__ == "__main__":
# not required
# ffibuilder.compile(verbose=True)

# Note: built into "timezonefinder" package folder
distribution = setuptools.Distribution({"package_dir": {"": "timezonefinder"}})
cffi_modules(distribution, "cffi_modules", ["build.py:ffibuilder"])
cffi.setuptools_ext.cffi_modules(distribution, "cffi_modules", ["build.py:ffibuilder"])
cmd = distribution.cmdclass["build_ext"](distribution)
cmd.inplace = 1
cmd.ensure_finalized()
Expand All @@ -54,3 +68,7 @@
# a build failure in the extension (e.g. C compile is not installed) must not abort the build process,
# but instead simply not install the failing extension.
pass


if __name__ == "__main__":
build_c_extension()
2 changes: 2 additions & 0 deletions docs/1_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ If no timezone has been matched, ``None`` is being returned.
tz = tf.timezone_at(lng=1.0, lat=50.5) # 'Etc/GMT'
.. note::

To reduce the risk of mixing up the coordinates, the arguments ``lng`` and ``lat`` have to be given as keyword arguments

.. note::

This function is optimized for speed: The last possible timezone in proximity is always returned (without checking if the point is really included).


Expand Down
8 changes: 5 additions & 3 deletions docs/7_performance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ Performance
C extension
-----------

During installation ``timezonefinder`` tries to compile a C extension with an implementation of the time critical point in polygon check algorithm.
If compilation fails ``timezonefinder`` will silently fall back to a pure Python implementation (~400x slower, cf. :ref:`speed test results <speed-tests>` below).
During installation ``timezonefinder`` automatically tries to compile a C extension with an implementation of the time critical point in polygon check algorithm.
In order for this to work, a Clang compiler has to be installed.

Please make sure to have C compiler installed when speed matters to you.
.. note::

If compilation fails (due to e.g. missing C compiler or broken ``cffi`` installation) ``timezonefinder`` will silently fall back to a pure Python implementation (~400x slower, cf. :ref:`speed test results <speed-tests>` below).


For testing if the compiled C implementation of the point in polygon algorithm is being used:
Expand Down
Loading

0 comments on commit 5dc365c

Please sign in to comment.