1import builtins
2import logging
3import os
4import subprocess
5import sys
6from distutils.version import LooseVersion
7from pathlib import Path
8
9from setuptools import Extension, setup, find_packages
10from setuptools.command.build_ext import build_ext as _build_ext
11
12import versioneer
13
14# Skip Cython build if not available
15try:
16    from Cython.Build import cythonize
17except ImportError:
18    cythonize = None
19
20
21log = logging.getLogger(__name__)
22ch = logging.StreamHandler()
23log.addHandler(ch)
24
25MIN_GEOS_VERSION = "3.5"
26
27if "all" in sys.warnoptions:
28    # show GEOS messages in console with: python -W all
29    log.setLevel(logging.DEBUG)
30
31
32def get_geos_config(option):
33    """Get configuration option from the `geos-config` development utility
34
35    The PATH environment variable should include the path where geos-config is
36    located, or the GEOS_CONFIG environment variable should point to the
37    executable.
38    """
39    cmd = os.environ.get("GEOS_CONFIG", "geos-config")
40    try:
41        stdout, stderr = subprocess.Popen(
42            [cmd, option], stdout=subprocess.PIPE, stderr=subprocess.PIPE
43        ).communicate()
44    except OSError:
45        return
46    if stderr and not stdout:
47        log.warning("geos-config %s returned '%s'", option, stderr.decode().strip())
48        return
49    result = stdout.decode().strip()
50    log.debug("geos-config %s returned '%s'", option, result)
51    return result
52
53
54def get_geos_paths():
55    """Obtain the paths for compiling and linking with the GEOS C-API
56
57    First the presence of the GEOS_INCLUDE_PATH and GEOS_INCLUDE_PATH environment
58    variables is checked. If they are both present, these are taken.
59
60    If one of the two paths was not present, geos-config is called (it should be on the
61    PATH variable). geos-config provides all the paths.
62
63    If geos-config was not found, no additional paths are provided to the extension. It is
64    still possible to compile in this case using custom arguments to setup.py.
65    """
66    include_dir = os.environ.get("GEOS_INCLUDE_PATH")
67    library_dir = os.environ.get("GEOS_LIBRARY_PATH")
68    if include_dir and library_dir:
69        return {
70            "include_dirs": ["./src", include_dir],
71            "library_dirs": [library_dir],
72            "libraries": ["geos_c"],
73        }
74
75    geos_version = get_geos_config("--version")
76    if not geos_version:
77        log.warning(
78            "Could not find geos-config executable. Either append the path to geos-config"
79            " to PATH or manually provide the include_dirs, library_dirs, libraries and "
80            "other link args for compiling against a GEOS version >=%s.",
81            MIN_GEOS_VERSION,
82        )
83        return {}
84
85    if LooseVersion(geos_version) < LooseVersion(MIN_GEOS_VERSION):
86        raise ImportError(
87            "GEOS version should be >={}, found {}".format(
88                MIN_GEOS_VERSION, geos_version
89            )
90        )
91
92    libraries = []
93    library_dirs = []
94    include_dirs = ["./src"]
95    extra_link_args = []
96    for item in get_geos_config("--cflags").split():
97        if item.startswith("-I"):
98            include_dirs.extend(item[2:].split(":"))
99
100    for item in get_geos_config("--clibs").split():
101        if item.startswith("-L"):
102            library_dirs.extend(item[2:].split(":"))
103        elif item.startswith("-l"):
104            libraries.append(item[2:])
105        else:
106            extra_link_args.append(item)
107
108    return {
109        "include_dirs": include_dirs,
110        "library_dirs": library_dirs,
111        "libraries": libraries,
112        "extra_link_args": extra_link_args,
113    }
114
115
116class build_ext(_build_ext):
117    def finalize_options(self):
118        _build_ext.finalize_options(self)
119
120        # Add numpy include dirs without importing numpy on module level.
121        # derived from scikit-hep:
122        # https://github.com/scikit-hep/root_numpy/pull/292
123
124        # Prevent numpy from thinking it is still in its setup process:
125        try:
126            del builtins.__NUMPY_SETUP__
127        except AttributeError:
128            pass
129
130        import numpy
131
132        self.include_dirs.append(numpy.get_include())
133
134
135ext_modules = []
136
137if "clean" in sys.argv:
138    # delete any previously Cythonized or compiled files in pygeos
139    p = Path(".")
140    for pattern in [
141        "build/lib.*/pygeos/*.so",
142        "pygeos/*.c",
143        "pygeos/*.so",
144        "pygeos/*.pyd",
145    ]:
146        for filename in p.glob(pattern):
147            print("removing '{}'".format(filename))
148            filename.unlink()
149elif "sdist" in sys.argv:
150    if Path("LICENSE_GEOS").exists() or Path("LICENSE_win32").exists():
151        raise FileExistsError(
152            "Source distributions should not pack LICENSE_GEOS or LICENSE_win32. Please remove the files."
153        )
154else:
155    ext_options = get_geos_paths()
156
157    ext_modules = [
158        Extension(
159            "pygeos.lib",
160            sources=[
161                "src/c_api.c",
162                "src/coords.c",
163                "src/geos.c",
164                "src/lib.c",
165                "src/pygeom.c",
166                "src/strtree.c",
167                "src/ufuncs.c",
168                "src/vector.c",
169            ],
170            **ext_options,
171        )
172    ]
173
174    # Cython is required
175    if not cythonize:
176        sys.exit("ERROR: Cython is required to build pygeos from source.")
177
178    cython_modules = [
179        Extension(
180            "pygeos._geometry",
181            [
182                "pygeos/_geometry.pyx",
183            ],
184            **ext_options,
185        ),
186        Extension(
187            "pygeos._geos",
188            [
189                "pygeos/_geos.pyx",
190            ],
191            **ext_options,
192        ),
193    ]
194
195    ext_modules += cythonize(
196        cython_modules,
197        compiler_directives={"language_level": "3"},
198        # enable once Cython >= 0.3 is released
199        # define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")],
200    )
201
202
203try:
204    descr = open(os.path.join(os.path.dirname(__file__), "README.rst")).read()
205except IOError:
206    descr = ""
207
208
209version = versioneer.get_version()
210cmdclass = versioneer.get_cmdclass()
211cmdclass["build_ext"] = build_ext
212
213
214setup(
215    name="pygeos",
216    version=version,
217    description="GEOS wrapped in numpy ufuncs",
218    long_description=descr,
219    url="https://github.com/pygeos/pygeos",
220    author="Casper van der Wel",
221    author_email="caspervdw@gmail.com",
222    license="BSD 3-Clause",
223    packages=find_packages(include=["pygeos", "pygeos.*"]),
224    install_requires=["numpy>=1.13"],
225    extras_require={
226        "test": ["pytest"],
227        "docs": ["sphinx", "numpydoc"],
228    },
229    python_requires=">=3.6",
230    include_package_data=True,
231    ext_modules=ext_modules,
232    classifiers=[
233        "Programming Language :: Python :: 3",
234        "Intended Audience :: Science/Research",
235        "Intended Audience :: Developers",
236        "Development Status :: 4 - Beta",
237        "Topic :: Scientific/Engineering",
238        "Topic :: Software Development",
239        "Operating System :: Unix",
240        "Operating System :: MacOS",
241        "Operating System :: Microsoft :: Windows",
242    ],
243    cmdclass=cmdclass,
244)
245