diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..d2e91c6 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +ignore = E501,W503,E203 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist +max-line-length = 90 diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..c61977e --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,8 @@ +comment: off + +coverage: + status: + project: + default: + target: auto + threshold: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36f5f02..9c02bf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,59 +11,129 @@ on: jobs: tests: - runs-on: ubuntu-latest + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + include: + - {name: Windows, python: '3.10', os: windows-latest} + - {name: Mac, python: '3.10', os: macos-latest} + - {name: 'Ubuntu', python: '3.10', os: ubuntu-latest} + - {name: '3.13', python: '3.13', os: ubuntu-latest} + - {name: '3.12', python: '3.12', os: ubuntu-latest} + - {name: '3.11', python: '3.11', os: ubuntu-latest} + - {name: '3.9', python: '3.9', os: ubuntu-latest} + - {name: '3.8', python: '3.8', os: ubuntu-latest} steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python }} + - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox git submodule update --init + python -m pip install -e .["test"] - # Run tox using the version of Python in `PATH` - - name: Run Tox - run: tox -e py + - name: Run pre-commit + if: ${{ matrix.name == 'Ubuntu' }} + run: | + python -m pip install pre-commit + pre-commit run --all-files + + - name: Run Tests + run: | + python -m pytest --cov vtzero --cov-report xml --cov-report term-missing - publish: + - name: Upload Results + if: ${{ matrix.name == 'Ubuntu' }} + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: unittests + name: ${{ matrix.python }} + fail_ci_if_error: false + + ####################################################################################### + # Deploy + ####################################################################################### + build_wheels: needs: [tests] - runs-on: ubuntu-latest if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + + # Used to host cibuildwheel + - uses: actions/setup-python@v4 with: - python-version: "3.x" + python-version: '3.10' + + - name: Install cibuildwheel + run: python -m pip install cibuildwheel==2.22.0 - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install tox setuptools cython>=0.29.23 git submodule update --init - python setup.py build_ext --inplace - - name: Set tag version - id: tag - # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions - run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} + - name: Build wheels + run: python -m cibuildwheel --output-dir wheelhouse + env: + CIBW_SKIP: 'pp*' + CIBW_ARCHS_MACOS: auto universal2 - - name: Set module version - id: module - # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions - run: echo ::set-output name=version::$(python setup.py --version) + - uses: actions/upload-artifact@v4 + with: + name: vtzero-wheels-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl - - name: Build and publish - if: steps.tag.outputs.tag == steps.module.outputs.version - env: - TOXENV: release - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: tox + build_sdist: + needs: [tests] + if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + name: Install Python + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install numpy Cython + git submodule update --init + + - name: Build sdist + run: python setup.py sdist + + - uses: actions/upload-artifact@v4 + with: + name: vtzero-sdist + path: dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + pattern: vtzero-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: ${{ secrets.PYPI_USERNAME }} + password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..08d9bbb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + language_version: python + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.5 + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.9.0 + hooks: + - id: mypy + language_version: python + # No reason to run if only tests have changed. They intentionally break typing. + exclude: tests/.* diff --git a/CHANGELOG.md b/CHANGELOG.md index d380906..9849b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## Release Notes +### 0.0.1b5 - 2025-01-20 + +- add python 3.13 support + +### 0.0.1b4 - 2022-10-25 + +- add python 3.12 support +- remove support for python <3.8 + +### 0.0.1b3 - 2022-10-25 + +- add python 3.9, 3.10, 3.11 support +- add python wheels +- update vendor submodules ### 0.0.1b2 - 2021-06-16 diff --git a/README.md b/README.md index 6eed9c3..798deac 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Experimental Python wrapper of [vtzero](https://github.com/mapbox/vtzero) a mini ## Requirements -- Python >= 3.5 +- Python >= 3.8 - gcc/clang++ >= 4.5 (C++11) ## Install @@ -16,7 +16,7 @@ Experimental Python wrapper of [vtzero](https://github.com/mapbox/vtzero) a mini You can install python-vtzero using pip ```bash -$ pip install vtzero +$ python -m pip install vtzero ``` or install from source @@ -30,7 +30,7 @@ $ git submodule update --init # Compile Cython module $ python setup.py build_ext --inplace -$ pip install -e . +$ python -m pip install -e . ``` ## Example diff --git a/example/__init__.py b/example/__init__.py index f65ef81..4baf8e1 100644 --- a/example/__init__.py +++ b/example/__init__.py @@ -1,17 +1,19 @@ -from vtzero.tile import VectorTile, Tile, Layer, Point, Polygon, Linestring +"""Example of python-vtzero usage.""" + +from vtzero.tile import Layer, Linestring, Point, Polygon, Tile, VectorTile # Create MVT tile = Tile() # Add a layer -layer = Layer(tile, b'my_layer') +layer = Layer(tile, b"my_layer") # Add a point feature = Point(layer) feature.add_points(1) feature.set_point(10, 10) -feature.add_property(b'foo', b'bar') -feature.add_property(b'x', b'y') +feature.add_property(b"foo", b"bar") +feature.add_property(b"x", b"y") feature.commit() # Add a polygon @@ -47,4 +49,4 @@ if f.geometry_type == 0: break features.append(f) -print(f"Nb Features: {len(features)}") \ No newline at end of file +print(f"Nb Features: {len(features)}") diff --git a/pyproject.toml b/pyproject.toml index 6cee20f..20d9517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,29 @@ requires = ["setuptools>=40.8.0", "wheel", "Cython>=0.29.23"] build-backend = "setuptools.build_meta" -[tool.flake8] -exclude = ".git,__pycache__,docs/source/conf.py,old,build,dist" -max-line-length = 90 +[tool.isort] +profile = "black" +known_first_party = ["vtzero"] +default_section = "THIRDPARTY" + +[tool.mypy] +no_strict_optional = "True" + +[tool.ruff] +line-length = 90 + +[tool.ruff.lint] +select = [ + "D1", # pydocstyle errors + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # flake8 + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "B905", # ignore zip() without an explicit strict= parameter, only support with python >3.10 + "B028", +] diff --git a/setup.py b/setup.py index cdf2263..3a8ebc4 100644 --- a/setup.py +++ b/setup.py @@ -1,47 +1,48 @@ """python-vtzero setup.""" +from Cython.Build import cythonize from setuptools import setup from setuptools.extension import Extension -from Cython.Build import cythonize with open("README.md") as f: long_description = f.read() ext_options = { - 'include_dirs': ['./vendor/vtzero/include', './vendor/protozero/include'], - 'extra_compile_args': ['-O2', '-std=c++11'] + "include_dirs": ["./vendor/vtzero/include", "./vendor/protozero/include"], + "extra_compile_args": ["-O2", "-std=c++11"], } -ext_modules = cythonize([ - Extension('vtzero.tile', ['vtzero/tile.pyx'], language="c++", **ext_options) -]) +ext_modules = cythonize( + [Extension("vtzero.tile", ["vtzero/tile.pyx"], language="c++", **ext_options)] +) extra_reqs = { - "test": ["pytest"], + "test": ["pytest", "pytest-cov"], } setup( - name='vtzero', - description='Python wrapper for vtzero C++ library.', + name="vtzero", + description="Python wrapper for vtzero C++ library.", long_description=long_description, long_description_content_type="text/markdown", classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 3', - 'Operating System :: POSIX', - 'Environment :: Web Environment', - 'Development Status :: 2 - Pre-Alpha', - 'Topic :: Scientific/Engineering :: GIS' + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Operating System :: POSIX", + "Environment :: Web Environment", + "Development Status :: 2 - Pre-Alpha", + "Topic :: Scientific/Engineering :: GIS", ], - keywords='mvt mapbox vector tile gis', - platforms=['POSIX'], - author='Yohan Boniface', - author_email='yohan.boniface@data.gouv.fr', - license='MIT', - packages=['vtzero'], + keywords="mvt mapbox vector tile gis", + platforms=["POSIX"], + author="Yohan Boniface", + author_email="yohan.boniface@data.gouv.fr", + license="MIT", + packages=["vtzero"], ext_modules=ext_modules, - provides=['vtzero'], + provides=["vtzero"], include_package_data=True, extras_require=extra_reqs, + python_requires=">=3.8", ) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 77c56ce..ab74894 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,11 +1,13 @@ +"""fixtures.""" + import json from pathlib import Path -from vtzero.tile import VectorTile - import pytest -ROOT = Path(__file__).parent.parent / 'vendor/mvt-fixtures/fixtures' +from vtzero.tile import VectorTile + +ROOT = Path(__file__).parent.parent / "vendor/mvt-fixtures/fixtures" # Magic: Generate tests from yml. # def pytest_generate_tests(metafunc): @@ -26,23 +28,27 @@ @pytest.fixture def fixture(): + """data fixtures.""" + def _(id): path = ROOT / id - with (path / 'info.json').open('r') as f: + with (path / "info.json").open("r") as f: info = json.loads(f.read()) - with (path / 'tile.json').open('r') as f: + with (path / "tile.json").open("r") as f: data = json.loads(f.read()) - with (path / 'tile.mvt').open('rb') as f: + with (path / "tile.mvt").open("rb") as f: mvt = f.read() return info, data, mvt + return _ def first_feature(tile): + """check and return feature.""" assert not tile.empty() assert len(tile) == 1 layer = next(tile) - assert layer.name == b'hello' + assert layer.name == b"hello" assert layer.version == 2 assert layer.extent == 4096 assert len(layer) == 1 @@ -50,14 +56,16 @@ def first_feature(tile): def test_empty_tile(fixture): - info, data, mvt = fixture('001') + """empty tile fixture.""" + info, data, mvt = fixture("001") tile = VectorTile(mvt) assert tile.empty() assert len(tile) == 0 def test_single_point_without_id(fixture): - info, data, mvt = fixture('002') + """Point.""" + info, data, mvt = fixture("002") tile = VectorTile(mvt) feature = first_feature(tile) assert not feature.has_id() diff --git a/tests/test_tile.py b/tests/test_tile.py index 54904ca..15affe8 100644 --- a/tests/test_tile.py +++ b/tests/test_tile.py @@ -1,23 +1,28 @@ -from vtzero.tile import Tile, Layer, Point, Polygon, Linestring +"""Test tile encoding.""" + +from vtzero.tile import Layer, Linestring, Point, Polygon, Tile def test_point_encoding(): """Test creation of point feature.""" tile = Tile() - points = Layer(tile, b'points') + points = Layer(tile, b"points") feature = Point(points) feature.add_points(1) feature.set_point(10, 10) - feature.add_property(b'foo', b'bar') - feature.add_property(b'x', b'y') + feature.add_property(b"foo", b"bar") + feature.add_property(b"x", b"y") feature.commit() - assert tile.serialize() == b'\x1a0x\x02\n\x06points(\x80 \x12\r\x18\x01"\x03\t\x14\x14\x12\x04\x00\x00\x01\x01\x1a\x03foo\x1a\x01x"\x05\n\x03bar"\x03\n\x01y' # noqa + assert ( + tile.serialize() + == b'\x1a0x\x02\n\x06points(\x80 \x12\r\x18\x01"\x03\t\x14\x14\x12\x04\x00\x00\x01\x01\x1a\x03foo\x1a\x01x"\x05\n\x03bar"\x03\n\x01y' + ) # noqa def test_polygon_encoding(): """Test creation of polygon feature.""" tile = Tile() - poly = Layer(tile, b'polygon') + poly = Layer(tile, b"polygon") feature = Polygon(poly) feature.add_ring(5) feature.set_point(0, 0) @@ -25,15 +30,18 @@ def test_polygon_encoding(): feature.set_point(10, 10) feature.set_point(0, 10) feature.set_point(0, 0) - feature.add_property(b'foo', b'bar') + feature.add_property(b"foo", b"bar") feature.commit() - assert tile.serialize() == b'\x1a/x\x02\n\x07polygon(\x80 \x12\x13\x18\x03"\x0b\t\x00\x00\x1a\x14\x00\x00\x14\x13\x00\x0f\x12\x02\x00\x00\x1a\x03foo"\x05\n\x03bar' # noqa + assert ( + tile.serialize() + == b'\x1a/x\x02\n\x07polygon(\x80 \x12\x13\x18\x03"\x0b\t\x00\x00\x1a\x14\x00\x00\x14\x13\x00\x0f\x12\x02\x00\x00\x1a\x03foo"\x05\n\x03bar' + ) # noqa def test_polygon_encoding_close_ring(): """Test creation of polygon feature with 'close_ring' method.""" tile = Tile() - poly = Layer(tile, b'polygon') + poly = Layer(tile, b"polygon") feature = Polygon(poly) feature.add_ring(5) feature.set_point(0, 0) @@ -41,40 +49,49 @@ def test_polygon_encoding_close_ring(): feature.set_point(10, 10) feature.set_point(0, 10) feature.close_ring() - feature.add_property(b'foo', b'bar') + feature.add_property(b"foo", b"bar") feature.commit() - assert tile.serialize() == b'\x1a/x\x02\n\x07polygon(\x80 \x12\x13\x18\x03"\x0b\t\x00\x00\x1a\x14\x00\x00\x14\x13\x00\x0f\x12\x02\x00\x00\x1a\x03foo"\x05\n\x03bar' # noqa + assert ( + tile.serialize() + == b'\x1a/x\x02\n\x07polygon(\x80 \x12\x13\x18\x03"\x0b\t\x00\x00\x1a\x14\x00\x00\x14\x13\x00\x0f\x12\x02\x00\x00\x1a\x03foo"\x05\n\x03bar' + ) # noqa def test_linestring_encoding(): """Test creation of linestring feature.""" tile = Tile() - line = Layer(tile, b'linestring') + line = Layer(tile, b"linestring") feature = Linestring(line) feature.add_linestring(3) feature.set_point(0, 0) feature.set_point(10, 10) feature.set_point(20, 20) - feature.add_property(b'foo', b'bar') + feature.add_property(b"foo", b"bar") feature.commit() - assert tile.serialize() == b'\x1a/x\x02\n\nlinestring(\x80 \x12\x10\x18\x02"\x08\t\x00\x00\x12\x14\x14\x14\x14\x12\x02\x00\x00\x1a\x03foo"\x05\n\x03bar' # noqa + assert ( + tile.serialize() + == b'\x1a/x\x02\n\nlinestring(\x80 \x12\x10\x18\x02"\x08\t\x00\x00\x12\x14\x14\x14\x14\x12\x02\x00\x00\x1a\x03foo"\x05\n\x03bar' + ) # noqa def test_set_id_valid(): """Test set_id method.""" tile = Tile() - points = Layer(tile, b'points') + points = Layer(tile, b"points") feature = Point(points) feature.set_id(1) feature.add_points(1) feature.set_point(10, 10) - feature.add_property(b'foo', b'bar') - feature.add_property(b'x', b'y') + feature.add_property(b"foo", b"bar") + feature.add_property(b"x", b"y") feature.commit() - assert tile.serialize() == b'\x1a2x\x02\n\x06points(\x80 \x12\x0f\x18\x01\x08\x01"\x03\t\x14\x14\x12\x04\x00\x00\x01\x01\x1a\x03foo\x1a\x01x"\x05\n\x03bar"\x03\n\x01y' # noqa + assert ( + tile.serialize() + == b'\x1a2x\x02\n\x06points(\x80 \x12\x0f\x18\x01\x08\x01"\x03\t\x14\x14\x12\x04\x00\x00\x01\x01\x1a\x03foo\x1a\x01x"\x05\n\x03bar"\x03\n\x01y' + ) # noqa tile = Tile() - poly = Layer(tile, b'polygon') + poly = Layer(tile, b"polygon") feature = Polygon(poly) feature.set_id(1) feature.add_ring(5) @@ -84,10 +101,13 @@ def test_set_id_valid(): feature.set_point(0, 10) feature.set_point(0, 0) feature.commit() - assert tile.serialize() == b'\x1a!x\x02\n\x07polygon(\x80 \x12\x11\x18\x03\x08\x01"\x0b\t\x00\x00\x1a\x14\x00\x00\x14\x13\x00\x0f' # noqa + assert ( + tile.serialize() + == b'\x1a!x\x02\n\x07polygon(\x80 \x12\x11\x18\x03\x08\x01"\x0b\t\x00\x00\x1a\x14\x00\x00\x14\x13\x00\x0f' + ) # noqa tile = Tile() - line = Layer(tile, b'linestring') + line = Layer(tile, b"linestring") feature = Linestring(line) feature.set_id(1) feature.add_linestring(3) @@ -95,4 +115,7 @@ def test_set_id_valid(): feature.set_point(10, 10) feature.set_point(20, 20) feature.commit() - assert tile.serialize() == b'\x1a!x\x02\n\nlinestring(\x80 \x12\x0e\x18\x02\x08\x01"\x08\t\x00\x00\x12\x14\x14\x14\x14' # noqa + assert ( + tile.serialize() + == b'\x1a!x\x02\n\nlinestring(\x80 \x12\x0e\x18\x02\x08\x01"\x08\t\x00\x00\x12\x14\x14\x14\x14' + ) # noqa diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 6109c75..0000000 --- a/tox.ini +++ /dev/null @@ -1,43 +0,0 @@ -[tox] -envlist = py35,py36,py37,py38,py39 -isolated_build = true - -[testenv] -extras = test -deps = - setuptools - cython>=0.29.23 -commands= - python setup.py build_ext --inplace - pip install . --no-deps --no-build-isolation - python -m pytest - -[testenv:wheel] -deps = - wheel - setuptools - cython>=0.29.23 -skip_install = true -extras = -commands = - pip wheel -w wheelhouse --no-deps . - -# Release -[testenv:build] -basepython = python3 -skip_install = true -deps = - wheel - setuptools -commands = - python setup.py sdist - -[testenv:release] -basepython = python3 -skip_install = true -deps = - {[testenv:build]deps} - twine >= 1.5.0 -commands = - {[testenv:build]commands} - twine upload --skip-existing dist/* diff --git a/vendor/mvt-fixtures b/vendor/mvt-fixtures index aeeb6ff..e7591fb 160000 --- a/vendor/mvt-fixtures +++ b/vendor/mvt-fixtures @@ -1 +1 @@ -Subproject commit aeeb6ff59fcd09a152d99506a2f87b353ec65dc8 +Subproject commit e7591fbf674fa305443c5f9584d215b116f5b118 diff --git a/vendor/protozero b/vendor/protozero index aa8b304..542fcf7 160000 --- a/vendor/protozero +++ b/vendor/protozero @@ -1 +1 @@ -Subproject commit aa8b304cf63831589f52c254b5af2c688bdc2fc4 +Subproject commit 542fcf7dd228672b775eb283d18ccd94c4e682d9 diff --git a/vendor/vtzero b/vendor/vtzero index 5e70144..67f934e 160000 --- a/vendor/vtzero +++ b/vendor/vtzero @@ -1 +1 @@ -Subproject commit 5e70144a3a2ea1231b6e271ec75b6d49198eaa67 +Subproject commit 67f934ee9aa44b923b5d0bc2bc87448900051a57 diff --git a/vtzero/cvtzero.pxd b/vtzero/cvtzero.pxd index 75a42af..c8c1629 100644 --- a/vtzero/cvtzero.pxd +++ b/vtzero/cvtzero.pxd @@ -1,8 +1,8 @@ # cython: language_level=3 -from libc.stdint cimport uint32_t, uint64_t, int32_t -from libcpp.string cimport string +from libc.stdint cimport int32_t, uint32_t, uint64_t from libcpp cimport bool +from libcpp.string cimport string cdef extern from 'protozero/pbf_reader.hpp' namespace 'protozero': diff --git a/vtzero/tile.pyx b/vtzero/tile.pyx index 9ff278c..0b1645a 100644 --- a/vtzero/tile.pyx +++ b/vtzero/tile.pyx @@ -2,9 +2,10 @@ """vtzero.tile module.""" -from vtzero cimport cvtzero -from libcpp.string cimport string from libc.stdint cimport uint32_t +from libcpp.string cimport string + +from vtzero cimport cvtzero cdef class VectorTile: diff --git a/vtzero/version.py b/vtzero/version.py index 189f372..545c080 100644 --- a/vtzero/version.py +++ b/vtzero/version.py @@ -1,3 +1,3 @@ """vtzero version.""" -__version__ = '0.0.1b2' +__version__ = "0.0.1b5"