diff --git a/.clang-format b/.clang-format index fdabb87..290997e 100644 --- a/.clang-format +++ b/.clang-format @@ -1,10 +1,17 @@ --- Language: Cpp BasedOnStyle: Google +ColumnLimit: 100 TabWidth: 4 IndentWidth: 4 PointerAlignment: Left ReferenceAlignment: Pointer IndentAccessModifiers: false AccessModifierOffset: -4 +BinPackArguments: false +BinPackParameters: false +ExperimentalAutoDetectBinPacking: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortLambdasOnASingleLine: Inline ... diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..286f2e5 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,39 @@ +on: + push: + branches: [main] + pull_request: + branches: [main] + +name: Lint + +jobs: + lint: + name: mypy + runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Setup micromamba + uses: mamba-org/provision-with-micromamba@v15 + with: + environment-file: ci/environment.yml + environment-name: spherely-dev + extra-specs: | + python=3.11 + + - name: Build and install spherely + run: | + python -m pip install . -v --no-build-isolation + + - name: Install mypy + run: | + python -m pip install 'mypy<0.990' + + - name: Run mypy + run: | + python -m mypy --install-types --non-interactive diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 6e1453d..9084edc 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -10,6 +10,9 @@ jobs: test: name: ${{ matrix.os }}, ${{ matrix.python-version }}, ${{ matrix.env }} runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash -l {0} strategy: fail-fast: false matrix: @@ -36,41 +39,20 @@ jobs: - name: Checkout repo uses: actions/checkout@v3 - - name: Setup mambaforge - uses: conda-incubator/setup-miniconda@v2 - with: - miniforge-variant: Mambaforge - miniforge-version: latest - activate-environment: spherely-dev - use-mamba: true - python-version: ${{ matrix.python-version }} - environment-file: ${{ matrix.env }} - - name: Get Date id: get-date - shell: bash -l {0} # cache will last one day run: echo "::set-output name=today::$(/bin/date -u '+%Y%m%d')" - - name: Cache environment - uses: actions/cache@v2 + - name: Setup micromamba + uses: mamba-org/provision-with-micromamba@v15 with: - path: ${{ env.CONDA }}/envs - key: ${{ runner.os }}-{{ matrix.python-version }}-conda-${{ hashFiles( matrix.env ) }}-${{ steps.get-date.outputs.today }}-${{ env.CACHE_NUMBER }} - env: - # Increase this value to reset cache if ci/environment.yml has not changed - CACHE_NUMBER: 0 - id: conda-cache - - - name: Update environment - run: mamba env update -n spherely-dev -f ${{ matrix.env }} - if: steps.conda-cache.outputs.cache-hit != 'true' - - - name: Conda info - shell: bash -l {0} - run: | - conda info - conda list + environment-file: ${{ matrix.env }} + environment-name: spherely-dev + cache-env: true + cache-env-key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ steps.get-date.outputs.today }}-${{ hashFiles( matrix.env) }}" + extra-specs: | + python=${{ matrix.python-version }} - name: Fetch s2geography uses: actions/checkout@v3 @@ -80,11 +62,9 @@ jobs: path: deps/s2geography fetch-depth: 0 if: | - matrix.dev == true && - steps.conda-cache.outputs.cache-hit != 'true' + matrix.dev == true - name: Configure, build & install s2geography (unix) - shell: bash -l {0} run: | cd deps/s2geography cmake -S . -B build \ @@ -96,11 +76,9 @@ jobs: cmake --install build if: | matrix.dev == true && - (steps.conda-cache.outputs.cache-hit != 'true' && - (runner.os == 'Linux' || runner.os == 'macOS')) + (runner.os == 'Linux' || runner.os == 'macOS') - name: Configure, build & install s2geography (win) - shell: bash -l {0} run: | cd deps/s2geography cmake -S . -B build \ @@ -111,15 +89,27 @@ jobs: cmake --build build --config Release cmake --install build if: | - matrix.dev == true && - (steps.conda-cache.outputs.cache-hit != 'true' && - runner.os == 'Windows') + matrix.dev == true && runner.os == 'Windows' - name: Build and install spherely - shell: bash -l {0} - run: python -m pip install . -v --no-build-isolation + run: | + python -m pip install . -v --no-build-isolation --config-settings cmake.define.SPHERELY_CODE_COVERAGE=ON --config-settings build-dir=_skbuild - name: Run tests - shell: bash -l {0} run: | pytest . -vv + + - name: Generate coverage report + run: | + python -m pip install gcovr + gcovr --exclude-unreachable-branches --print-summary -x -o coverage.xml + if: | + runner.os == 'Linux' && matrix.python-version == '3.11' && matrix.dev == false + + - name: Upload coverage report + uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + verbose: true + if: | + runner.os == 'Linux' && matrix.python-version == '3.11' && matrix.dev == false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cae9cbf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: mixed-line-ending + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + args: [--safe, --quiet] + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v15.0.7 + hooks: + - id: clang-format + args: [--style=file] + +ci: + autofix_prs: false diff --git a/CMakeLists.txt b/CMakeLists.txt index 27c4a01..fd4c256 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ project( set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard to build with") set(CMAKE_CXX_STANDARD_REQUIRED ON) +option(SPHERELY_CODE_COVERAGE "Enable coverage reporting" OFF) + # Dependencies list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/third_party/cmake") @@ -25,6 +27,11 @@ set(S2GEOGRAPHY_ROOT_DIR "$ENV{CONDA_PREFIX}") find_package(s2 REQUIRED) find_package(s2geography REQUIRED) +if(SPHERELY_CODE_COVERAGE) + message(STATUS "Building spherely with coverage enabled") + add_library(coverage_config INTERFACE) +endif() + # Compile definitions and flags if (MSVC) @@ -72,6 +79,12 @@ endif() set_target_properties(spherely PROPERTIES CXX_VISIBILITY_PRESET "hidden") +if (SPHERELY_CODE_COVERAGE) + target_compile_options(coverage_config INTERFACE -O0 -g --coverage) + target_link_options(coverage_config INTERFACE --coverage) + target_link_libraries(spherely PUBLIC coverage_config) +endif() + # Install install(TARGETS spherely LIBRARY DESTINATION .) diff --git a/README.md b/README.md index 425d87f..ac55f25 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ![Tests](https://github.com/benbovy/spherely/actions/workflows/run-tests.yaml/badge.svg) [![Docs](https://readthedocs.org/projects/spherely/badge/?version=latest)](https://spherely.readthedocs.io) +[![Coverage](https://codecov.io/gh/benbovy/spherely/branch/main/graph/badge.svg)](https://app.codecov.io/gh/benbovy/spherely?branch=main) *Python library for manipulation and analysis of geometric objects on the sphere.* @@ -67,7 +68,17 @@ Run the tests: $ pytest . -v ``` -## Using the latest s2geography +Spherely also uses [pre-commit](https://pre-commit.com/) for code +auto-formatting and linting at every commit. After installing it, you can enable +pre-commit hooks with the following command: + +``` +$ pre-commit install +``` + +(Note: you can skip the pre-commit checks with `git commit --no-verify`) + +## Using the latest s2geography version If you want to compile spherely against the latest version of s2geography, use: diff --git a/docs/api.rst b/docs/api.rst index aa5cb98..a8a9d2d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -54,4 +54,4 @@ Predicates intersects contains within - disjoint + disjoint diff --git a/pyproject.toml b/pyproject.toml index 880aedc..9bf28a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,3 +27,8 @@ Repository = "https://github.com/benbovy/spherely" [project.optional-dependencies] test = ["pytest>=6.0"] + +[tool.mypy] +files = ["tests", "src/spherely.pyi"] +show_error_codes = true +warn_unused_ignores = true diff --git a/src/geography.cpp b/src/geography.cpp index 92d58cf..a165cde 100644 --- a/src/geography.cpp +++ b/src/geography.cpp @@ -42,7 +42,9 @@ S2Point to_s2point(const std::pair &vertex) { return S2LatLng::FromDegrees(vertex.first, vertex.second).ToPoint(); } -S2Point to_s2point(const Point *vertex) { return vertex->s2point(); } +S2Point to_s2point(const Point *vertex) { + return vertex->s2point(); +} /* ** Helper to create Geography object wrappers. @@ -59,8 +61,7 @@ std::unique_ptr make_geography(S &&s2_obj) { class PointFactory { public: - static std::unique_ptr FromLatLonDegrees(double lat_degrees, - double lon_degrees) { + static std::unique_ptr FromLatLonDegrees(double lat_degrees, double lon_degrees) { auto latlng = S2LatLng::FromDegrees(lat_degrees, lon_degrees); return make_geography(S2Point(latlng)); @@ -70,26 +71,25 @@ class PointFactory { }; template -static std::unique_ptr create_linestring( - const std::vector &coords) { +static std::unique_ptr create_linestring(const std::vector &coords) { std::vector pts(coords.size()); - std::transform(coords.begin(), coords.end(), pts.begin(), - [](const V &vertex) { return to_s2point(vertex); }); + std::transform(coords.begin(), coords.end(), pts.begin(), [](const V &vertex) { + return to_s2point(vertex); + }); auto polyline_ptr = std::make_unique(pts); - return make_geography( - std::move(polyline_ptr)); + return make_geography(std::move(polyline_ptr)); } template -static std::unique_ptr create_polygon( - const std::vector &shell) { +static std::unique_ptr create_polygon(const std::vector &shell) { std::vector shell_pts(shell.size()); - std::transform(shell.begin(), shell.end(), shell_pts.begin(), - [](const V &vertex) { return to_s2point(vertex); }); + std::transform(shell.begin(), shell.end(), shell_pts.begin(), [](const V &vertex) { + return to_s2point(vertex); + }); auto shell_loop_ptr = std::make_unique(); // TODO: maybe add an option to skip validity checks @@ -116,8 +116,7 @@ static std::unique_ptr create_polygon( polygon_ptr->set_s2debug_override(S2Debug::DISABLE); polygon_ptr->InitOriented(std::move(loops)); - return make_geography( - std::move(polygon_ptr)); + return make_geography(std::move(polygon_ptr)); } /* @@ -139,8 +138,7 @@ py::array_t num_shapes(const py::array_t geographies) { return result; } -py::array_t create(py::array_t xs, - py::array_t ys) { +py::array_t create(py::array_t xs, py::array_t ys) { py::buffer_info xbuf = xs.request(), ybuf = ys.request(); if (xbuf.ndim != 1 || ybuf.ndim != 1) { throw std::runtime_error("Number of dimensions must be one"); @@ -181,7 +179,9 @@ int get_dimensions(PyObjectGeography obj) { ** Geography utils */ -bool is_geography(PyObjectGeography obj) { return obj.is_geog_ptr(); } +bool is_geography(PyObjectGeography obj) { + return obj.is_geog_ptr(); +} /* ** Geography creation @@ -205,8 +205,7 @@ PyObjectGeography destroy_prepared(PyObjectGeography obj) { void init_geography(py::module &m) { // Geography types - auto pygeography_types = - py::enum_(m, "GeographyType", R"pbdoc( + auto pygeography_types = py::enum_(m, "GeographyType", R"pbdoc( The enumeration of Geography types )pbdoc"); @@ -224,7 +223,8 @@ void init_geography(py::module &m) { )pbdoc"); - pygeography.def_property_readonly("dimensions", &Geography::dimension, + pygeography.def_property_readonly("dimensions", + &Geography::dimension, R"pbdoc( Returns the inherent dimensionality of a geometry. @@ -259,11 +259,9 @@ void init_geography(py::module &m) { )pbdoc"); - pypoint.def(py::init(&PointFactory::FromLatLonDegrees), py::arg("lat"), - py::arg("lon")); + pypoint.def(py::init(&PointFactory::FromLatLonDegrees), py::arg("lat"), py::arg("lon")); - auto pylinestring = - py::class_(m, "LineString", R"pbdoc( + auto pylinestring = py::class_(m, "LineString", R"pbdoc( A geography type composed of one or more arc segments. A LineString is a one-dimensional feature and has a non-zero length but @@ -280,11 +278,9 @@ void init_geography(py::module &m) { pylinestring.def(py::init(&create_linestring>), py::arg("coordinates")); - pylinestring.def(py::init(&create_linestring), - py::arg("coordinates")); + pylinestring.def(py::init(&create_linestring), py::arg("coordinates")); - auto pypolygon = - py::class_(m, "Polygon", R"pbdoc( + auto pypolygon = py::class_(m, "Polygon", R"pbdoc( A geography type representing an area that is enclosed by a linear ring. A polygon is a two-dimensional feature and has a non-zero area. @@ -297,8 +293,7 @@ void init_geography(py::module &m) { )pbdoc"); - pypolygon.def(py::init(&create_polygon>), - py::arg("shell")); + pypolygon.def(py::init(&create_polygon>), py::arg("shell")); pypolygon.def(py::init(&create_polygon), py::arg("shell")); // Temp test @@ -308,7 +303,9 @@ void init_geography(py::module &m) { // Geography properties - m.def("get_type_id", py::vectorize(&get_type_id), py::arg("geography"), + m.def("get_type_id", + py::vectorize(&get_type_id), + py::arg("geography"), R"pbdoc( Returns the type ID of a geography. @@ -323,8 +320,7 @@ void init_geography(py::module &m) { )pbdoc"); - m.def("get_dimensions", py::vectorize(&get_dimensions), - py::arg("geography"), R"pbdoc( + m.def("get_dimensions", py::vectorize(&get_dimensions), py::arg("geography"), R"pbdoc( Returns the inherent dimensionality of a geography. The inherent dimension is 0 for points, 1 for linestrings and 2 for @@ -340,7 +336,9 @@ void init_geography(py::module &m) { // Geography utils - m.def("is_geography", py::vectorize(&is_geography), py::arg("obj"), + m.def("is_geography", + py::vectorize(&is_geography), + py::arg("obj"), R"pbdoc( Returns True if the object is a :py:class:`Geography`, False otherwise. @@ -353,7 +351,9 @@ void init_geography(py::module &m) { // Geography creation - m.def("is_prepared", py::vectorize(&is_prepared), py::arg("geography"), + m.def("is_prepared", + py::vectorize(&is_prepared), + py::arg("geography"), R"pbdoc( Returns True if the geography object is "prepared", False otherwise. @@ -376,7 +376,9 @@ void init_geography(py::module &m) { )pbdoc"); - m.def("prepare", py::vectorize(&prepare), py::arg("geography"), + m.def("prepare", + py::vectorize(&prepare), + py::arg("geography"), R"pbdoc( Prepare a geography, improving performance of other operations. @@ -400,7 +402,8 @@ void init_geography(py::module &m) { )pbdoc"); - m.def("destroy_prepared", py::vectorize(&destroy_prepared), + m.def("destroy_prepared", + py::vectorize(&destroy_prepared), py::arg("geography"), R"pbdoc( Destroy the prepared part of a geography, freeing up memory. diff --git a/src/geography.hpp b/src/geography.hpp index f4bb60f..f7ba645 100644 --- a/src/geography.hpp +++ b/src/geography.hpp @@ -14,12 +14,7 @@ using S2GeographyIndexPtr = std::unique_ptr; /* ** The registered Geography types */ -enum class GeographyType : std::int8_t { - None = -1, - Point, - LineString, - Polygon -}; +enum class GeographyType : std::int8_t { None = -1, Point, LineString, Polygon }; /* ** Thin wrapper around s2geography::Geography. @@ -37,8 +32,7 @@ class Geography { // std::cout << "Geography move constructor called: " << this << // std::endl; } - Geography(S2GeographyPtr&& s2geog_ptr) - : m_s2geog_ptr(std::move(s2geog_ptr)) {} + Geography(S2GeographyPtr&& s2geog_ptr) : m_s2geog_ptr(std::move(s2geog_ptr)) {} ~Geography() { // std::cout << "Geography destructor called: " << this << std::endl; @@ -56,21 +50,30 @@ class Geography { return GeographyType::None; } - inline const s2geog::Geography& geog() const { return *m_s2geog_ptr; } + inline const s2geog::Geography& geog() const { + return *m_s2geog_ptr; + } inline const s2geog::ShapeIndexGeography& geog_index() { if (!m_s2geog_index_ptr) { - m_s2geog_index_ptr = - std::make_unique(geog()); + m_s2geog_index_ptr = std::make_unique(geog()); } return *m_s2geog_index_ptr; } - void reset_index() { m_s2geog_index_ptr.reset(); } - bool has_index() { return m_s2geog_index_ptr != nullptr; } + void reset_index() { + m_s2geog_index_ptr.reset(); + } + bool has_index() { + return m_s2geog_index_ptr != nullptr; + } - int dimension() const { return m_s2geog_ptr->dimension(); } - int num_shapes() const { return m_s2geog_ptr->num_shapes(); } + int dimension() const { + return m_s2geog_ptr->dimension(); + } + int num_shapes() const { + return m_s2geog_ptr->num_shapes(); + } private: S2GeographyPtr m_s2geog_ptr; @@ -86,8 +89,7 @@ class Point : public Geography { } inline const S2Point& s2point() const { - const auto& points = - static_cast(geog()).Points(); + const auto& points = static_cast(geog()).Points(); // TODO: does not work for empty point geography return points[0]; } diff --git a/src/predicates.cpp b/src/predicates.cpp index 4f8275b..933b43a 100644 --- a/src/predicates.cpp +++ b/src/predicates.cpp @@ -34,8 +34,10 @@ class Predicate { }; void init_predicates(py::module& m) { - m.def("intersects", py::vectorize(Predicate(s2geog::s2_intersects)), - py::arg("a"), py::arg("b"), + m.def("intersects", + py::vectorize(Predicate(s2geog::s2_intersects)), + py::arg("a"), + py::arg("b"), R"pbdoc( Returns True if A and B share any portion of space. @@ -48,7 +50,9 @@ void init_predicates(py::module& m) { )pbdoc"); - m.def("equals", py::vectorize(Predicate(s2geog::s2_equals)), py::arg("a"), + m.def("equals", + py::vectorize(Predicate(s2geog::s2_equals)), + py::arg("a"), py::arg("b"), R"pbdoc( Returns True if A and B are spatially equal. @@ -63,8 +67,10 @@ void init_predicates(py::module& m) { )pbdoc"); - m.def("contains", py::vectorize(Predicate(s2geog::s2_contains)), - py::arg("a"), py::arg("b"), + m.def("contains", + py::vectorize(Predicate(s2geog::s2_contains)), + py::arg("a"), + py::arg("b"), R"pbdoc( Returns True if B is completely inside A. @@ -75,15 +81,15 @@ void init_predicates(py::module& m) { )pbdoc"); - m.def( - "within", - py::vectorize(Predicate([](const s2geog::ShapeIndexGeography& a_index, - const s2geog::ShapeIndexGeography& b_index, - const S2BooleanOperation::Options& options) { - return s2geog::s2_contains(b_index, a_index, options); - })), - py::arg("a"), py::arg("b"), - R"pbdoc( + m.def("within", + py::vectorize(Predicate([](const s2geog::ShapeIndexGeography& a_index, + const s2geog::ShapeIndexGeography& b_index, + const S2BooleanOperation::Options& options) { + return s2geog::s2_contains(b_index, a_index, options); + })), + py::arg("a"), + py::arg("b"), + R"pbdoc( Returns True if A is completely inside B. Parameters @@ -93,15 +99,15 @@ void init_predicates(py::module& m) { )pbdoc"); - m.def( - "disjoint", - py::vectorize(Predicate([](const s2geog::ShapeIndexGeography& a_index, - const s2geog::ShapeIndexGeography& b_index, - const S2BooleanOperation::Options& options) { - return !s2geog::s2_intersects(a_index, b_index, options); - })), - py::arg("a"), py::arg("b"), - R"pbdoc( + m.def("disjoint", + py::vectorize(Predicate([](const s2geog::ShapeIndexGeography& a_index, + const s2geog::ShapeIndexGeography& b_index, + const S2BooleanOperation::Options& options) { + return !s2geog::s2_intersects(a_index, b_index, options); + })), + py::arg("a"), + py::arg("b"), + R"pbdoc( Returns True if A boundaries and interior does not intersect at all with those of B. diff --git a/src/pybind11.hpp b/src/pybind11.hpp index f48b2f3..afd95bc 100644 --- a/src/pybind11.hpp +++ b/src/pybind11.hpp @@ -84,15 +84,16 @@ class PyObjectGeography : public py::object { // Note: pybind11's `type_caster>` implements // move semantics (Python takes ownership). // - template ::value, - bool> = true> + template ::value, bool> = true> static py::object as_py_object(std::unique_ptr geog_ptr) { return py::cast(std::move(geog_ptr)); } // Just check whether the object is a Geography // - bool is_geog_ptr() const { return check_type(false); } + bool is_geog_ptr() const { + return check_type(false); + } }; } // namespace spherely @@ -122,9 +123,7 @@ struct vectorize_arg { static constexpr bool vectorize = true; // Accept this type: an array for vectorized types, otherwise the type // as-is: - using type = - conditional_t, array::forcecast>, T>; + using type = conditional_t, array::forcecast>, T>; }; // Register PyObjectGeography as a valid numpy dtype (numpy.object alias) @@ -154,9 +153,9 @@ struct handle_type_name> { // // Allows using PyObjectGeography as return type of vectorized functions. // -template ::value, int> = 0> +template < + typename T, + typename detail::enable_if_t::value, int> = 0> object cast(T &&value) { return value; } diff --git a/tests/test_geography.py b/tests/test_geography.py index 3770a91..07fdc70 100644 --- a/tests/test_geography.py +++ b/tests/test_geography.py @@ -29,7 +29,12 @@ def test_linestring(coords) -> None: "coords", [ [(0, 0), (0, 2), (2, 2), (2, 0)], - [spherely.Point(0, 0), spherely.Point(0, 2), spherely.Point(2, 2), spherely.Point(2, 0)], + [ + spherely.Point(0, 0), + spherely.Point(0, 2), + spherely.Point(2, 2), + spherely.Point(2, 0), + ], ], ) def test_polygon(coords) -> None: diff --git a/tests/test_predicates.py b/tests/test_predicates.py index 57c78c7..ecf40d4 100644 --- a/tests/test_predicates.py +++ b/tests/test_predicates.py @@ -61,10 +61,11 @@ def test_contains(): a2 = spherely.LineString([(50, 8), (60, 8)]) b2 = spherely.Point(50, 8) assert spherely.contains(a2, b2) - + + def test_within(): # test array + scalar - a = spherely.Point(40, 8) + a = spherely.Point(40, 8) b = np.array( [ spherely.LineString([(40, 8), (60, 8)]), @@ -80,10 +81,10 @@ def test_within(): a2 = spherely.Point(50, 8) b2 = spherely.LineString([(50, 8), (60, 8)]) assert spherely.within(a2, b2) - + + def test_disjoint(): - - a = spherely.Point(40, 9) + a = spherely.Point(40, 9) b = np.array( [ spherely.LineString([(40, 8), (60, 8)]), @@ -99,4 +100,3 @@ def test_disjoint(): a2 = spherely.Point(50, 9) b2 = spherely.LineString([(50, 8), (60, 8)]) assert spherely.disjoint(a2, b2) - \ No newline at end of file